Compare commits
	
		
			1 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f42cb94872 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| .ansible* | .ansible_vault | ||||||
| .bitwarden | .bitwarden | ||||||
| environments | environments | ||||||
| *.log | *.log | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
									
									
									
									
								
							| @@ -1,9 +1,7 @@ | |||||||
| SHELL := /bin/bash |  | ||||||
|  |  | ||||||
| all: vagrant | all: vagrant | ||||||
|  |  | ||||||
| vagrant: | vagrant: | ||||||
| 	set -o pipefail; vagrant up --no-destroy-on-error --no-color | tee ./vagrantup.log | 	vagrant up --no-destroy-on-error --no-color | tee ./vagrantup.log | ||||||
| 	./scripts/forward-ssh.sh | 	./scripts/forward-ssh.sh | ||||||
|  |  | ||||||
| clean: | clean: | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,78 +1,49 @@ | |||||||
| # Free I.T. Athen's Infrastructure | # Free I.T. Athen's Infrastructure | ||||||
|  |  | ||||||
| This project is used to develop Ansible for deploying and maintaining websites | This project is used to develop Ansible for deploying and maintaining websites | ||||||
| and services operated by Free I.T. Athens (FRITA). | and services operated by Free I.T. Athens (FRITA). | ||||||
|  |  | ||||||
| - Requires GNU Make, Ansible, and Vagrant on the host | - Requires GNU Make, Ansible, and Vagrant on the host | ||||||
|  |  | ||||||
| ## Quick Start | ## Quick Start | ||||||
|  |  | ||||||
| 1. Clone this project | 1. Clone this project | ||||||
| 2. Run `make` to provision a Rocky 9 base box | 2. Run `make` to provision a Debian 11 base box | ||||||
| 3. Go to | 3. Go to | ||||||
|    - [Traefik Dashboard](https://traefik.local.freeitathens.org:9443/dashboard/#/) |     - [Traefik Dashboard](https://traefik.local.freeitathens.org:8443/dashboard/#/) | ||||||
|    - [WordPress](https://www.local.freeitathens.org) |     - [WordPress](https://www.local.freeitathens.org) | ||||||
|    - [Nextcloud](https://cloud.local.freeitathens.org) |     - [Nextcloud](https://cloud.local.freeitathens.org) | ||||||
|    - [Mediawiki](https://wiki.local.freeitathens.org) |  | ||||||
| 4. Click through the HTTPS security warning | 4. Click through the HTTPS security warning | ||||||
|  |  | ||||||
| ## Production | ## Production | ||||||
|  | 1. Clone [production-env](https://github.com/freeitathens/production-env/) to `./environments` | ||||||
|  |  | ||||||
| 1. Clone [production-env](https://github.com/freeitathens/production-env/) to |     ``` | ||||||
|    `./environments` |     mkdir -p environments | ||||||
|  |     git clone git@github.com:freeitathens/production-env.git ./environments | ||||||
|  |     ``` | ||||||
|  |  | ||||||
|    ``` | 2. Run `./scripts/vault-key.sh` from the root of the project to obtain the Ansible Vault password | ||||||
|    mkdir -p environments |  | ||||||
|    git clone git@github.com:freeitathens/production-env.git ./environments |  | ||||||
|    ``` |  | ||||||
|  |  | ||||||
| 2. Run `./scripts/vault-key.sh` from the root of the project to obtain the |  | ||||||
|    Ansible Vault password |  | ||||||
| 3. Enter the Bitwarden Master Password | 3. Enter the Bitwarden Master Password | ||||||
| 4. Run `ansible-playbook` against the production servers, e.g., | 4. Run `ansible-playbook` against the production servers, e.g., | ||||||
|  |  | ||||||
|    ``` |     ``` | ||||||
|    ansible-playbook -u root -i environments/production --vault-pass-file ./.ansible_vault webserver.yml --diff --check |     ansible-playbook -u root -i environments/production --vault-pass-file ./.ansible_vault webserver.yml --check | ||||||
|    ``` |     ``` | ||||||
|  |  | ||||||
| 5. Delete the `.ansible_vault` file when you are done | 5. Delete the `.ansible_vault` file when you are done | ||||||
|  |  | ||||||
| ### Using Ansible Vault to add or rotate values |  | ||||||
|  |  | ||||||
| Do not submit ciphertext into Ansible Vault with the indention formatting.<br /> |  | ||||||
| To submit, press `CTRL+d` twice. |  | ||||||
|  |  | ||||||
| - Decrypt Ansible Vault values |  | ||||||
|  |  | ||||||
|   ``` |  | ||||||
|   ansible-vault decrypt --vault-pass-file .ansible_vault |  | ||||||
|   ``` |  | ||||||
|  |  | ||||||
| - Encrypt new Ansible Vault values |  | ||||||
|  |  | ||||||
|   ``` |  | ||||||
|   ansible-vault encrypt --vault-pass-file .ansible_vault |  | ||||||
|   ``` |  | ||||||
|  |  | ||||||
|   - e.g., |  | ||||||
|     `pwgen -s 100 1 | ansible-vault encrypt --vault-pass-file .ansible_vault` |  | ||||||
|  |  | ||||||
| ## Authors | ## Authors | ||||||
|  | * **Kris Lamoureux** - *Project Founder* - [@krislamo](https://github.com/krislamo) | ||||||
| - **Kris Lamoureux** - _Project Founder_ - |  | ||||||
|   [@krislamo](https://github.com/krislamo) |  | ||||||
|  |  | ||||||
| ## Copyrights and Licenses | ## Copyrights and Licenses | ||||||
|  | Copyright (C) 2019, 2020, 2022  Free I.T. Athens | ||||||
| Copyright (C) 2019, 2020, 2022, 2023, 2025 Free I.T. Athens |  | ||||||
|  |  | ||||||
| This program is free software: you can redistribute it and/or modify it under | This program is free software: you can redistribute it and/or modify it under | ||||||
| the terms of the GNU General Public License as published by the Free Software | the terms of the GNU General Public License as published by the Free Software | ||||||
| Foundation, version 3 of the License. | Foundation, version 3 of the License. | ||||||
|  |  | ||||||
| This program is distributed in the hope that it will be useful, but WITHOUT ANY | This program is distributed in the hope that it will be useful, but WITHOUT | ||||||
| WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | ||||||
| PARTICULAR PURPOSE. See the GNU General Public License for more details. | FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details. | ||||||
|  |  | ||||||
| You should have received a copy of the GNU General Public License along with | You should have received a copy of the GNU General Public License along with | ||||||
| this program. If not, see <https://www.gnu.org/licenses/>. | this program. If not, see <https://www.gnu.org/licenses/>. | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							| @@ -14,13 +14,9 @@ else | |||||||
|   File.write(".playbook", PLAYBOOK) |   File.write(".playbook", PLAYBOOK) | ||||||
| end | end | ||||||
|  |  | ||||||
| # Optionally allow more verbosity in Ansible | # Debian 11 | ||||||
| VAGRANT_ANSIBLE_VERBOSE=ENV["VAGRANT_ANSIBLE_VERBOSE"] || false |  | ||||||
|  |  | ||||||
| Vagrant.configure("2") do |config| | Vagrant.configure("2") do |config| | ||||||
|   config.vm.box = "rockylinux/9" |   config.vm.box = "debian/bullseye64" | ||||||
|   config.vm.hostname = "fritadev" |  | ||||||
|   config.vm.disk :disk, size: "100GB", primary: true |  | ||||||
|   config.vm.synced_folder ".", "/vagrant", disabled: true |   config.vm.synced_folder ".", "/vagrant", disabled: true | ||||||
|   config.vm.network "private_network", type: "dhcp" |   config.vm.network "private_network", type: "dhcp" | ||||||
|  |  | ||||||
| @@ -33,7 +29,6 @@ Vagrant.configure("2") do |config| | |||||||
|     libvirt.cpus = 2 |     libvirt.cpus = 2 | ||||||
|     libvirt.memory = 4096 |     libvirt.memory = 4096 | ||||||
|     libvirt.default_prefix = "" |     libvirt.default_prefix = "" | ||||||
|     libvirt.machine_virtual_size = 100 |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   # Set VirtualBox settings |   # Set VirtualBox settings | ||||||
| @@ -42,24 +37,11 @@ Vagrant.configure("2") do |config| | |||||||
|     vbox.memory = 4096 |     vbox.memory = 4096 | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   # Expand XFS rootfs |  | ||||||
|   config.vm.provision "shell", inline: <<-SHELL |  | ||||||
|     set -xe |  | ||||||
|     df -h / |  | ||||||
|     dnf install -y cloud-utils-growpart |  | ||||||
|     PART="$(findmnt -n -o SOURCE /)" |  | ||||||
|     DISK="$(lsblk -n -o PKNAME "$PART")" |  | ||||||
|     NUM="$(lsblk -n -o KNAME "$PART" | sed 's/.*[^0-9]//')" |  | ||||||
|     growpart "/dev/$DISK" "$NUM" && \ |  | ||||||
|     xfs_growfs / |  | ||||||
|     df -h / |  | ||||||
|   SHELL |  | ||||||
|  |  | ||||||
|   # Provision with Ansible |   # Provision with Ansible | ||||||
|   config.vm.provision "ansible" do |ansible| |   config.vm.provision "ansible" do |ansible| | ||||||
|     ENV['ANSIBLE_ROLES_PATH'] = File.dirname(__FILE__) + "/roles" |     ENV['ANSIBLE_ROLES_PATH'] = File.dirname(__FILE__) + "/roles" | ||||||
|     ansible.compatibility_mode = "2.0" |     ansible.compatibility_mode = "2.0" | ||||||
|     ansible.playbook = "dev/" + PLAYBOOK + ".yml" |     ansible.playbook = "dev/" + PLAYBOOK + ".yml" | ||||||
|     ansible.verbose = VAGRANT_ANSIBLE_VERBOSE |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
| end | end | ||||||
|   | |||||||
| @@ -3,4 +3,4 @@ inventory = ./environments/development | |||||||
| interpreter_python = /usr/bin/python3 | interpreter_python = /usr/bin/python3 | ||||||
|  |  | ||||||
| [ssh_connection] | [ssh_connection] | ||||||
| pipelining = True | pipelining=True | ||||||
|   | |||||||
| @@ -7,17 +7,12 @@ secret: | |||||||
|   WORDPRESS_DB_PASSWORD: WPpa55w0rd! |   WORDPRESS_DB_PASSWORD: WPpa55w0rd! | ||||||
|   NEXTCLOUD_MYSQL_PASSWORD: NCdbpa55w0rd! |   NEXTCLOUD_MYSQL_PASSWORD: NCdbpa55w0rd! | ||||||
|   NEXTCLOUD_ADMIN_PASSWORD: NCadm1npa55w0rd! |   NEXTCLOUD_ADMIN_PASSWORD: NCadm1npa55w0rd! | ||||||
|   MEDIAWIKI_MYSQL_PASSWORD: MWdbpa55w0rd! |  | ||||||
|  |  | ||||||
| ############## | ############## | ||||||
| ### Common ### | ### Docker ### | ||||||
| ############## | ############## | ||||||
| users: | docker_users: | ||||||
|   oci: |   - vagrant | ||||||
|     uid: 2000 |  | ||||||
|     gid: 2000 |  | ||||||
|     home: true |  | ||||||
|     ansible_temp: true |  | ||||||
|  |  | ||||||
| ################ | ################ | ||||||
| #### MariaDB ### | #### MariaDB ### | ||||||
| @@ -27,8 +22,6 @@ databases: | |||||||
|     pass: "{{ secret.WORDPRESS_DB_PASSWORD }}" |     pass: "{{ secret.WORDPRESS_DB_PASSWORD }}" | ||||||
|   - name: nextcloud |   - name: nextcloud | ||||||
|     pass: "{{ secret.NEXTCLOUD_MYSQL_PASSWORD }}" |     pass: "{{ secret.NEXTCLOUD_MYSQL_PASSWORD }}" | ||||||
|   - name: mediawiki |  | ||||||
|     pass: "{{ secret.MEDIAWIKI_MYSQL_PASSWORD }}" |  | ||||||
|  |  | ||||||
| ####################### | ####################### | ||||||
| ### Webserver Stack ### | ### Webserver Stack ### | ||||||
| @@ -37,12 +30,12 @@ webserver: | |||||||
|   ############### |   ############### | ||||||
|   ### Traefik ### |   ### Traefik ### | ||||||
|   ############### |   ############### | ||||||
|   # TRAEFIK_VERSION: latest |   #TRAEFIK_VERSION: latest | ||||||
|   # TRAEFIK_ROOT_DOMAIN: local.freeitathens.org |   #TRAEFIK_ROOT_DOMAIN: local.freeitathens.org | ||||||
|   # TRAEFIK_DOMAIN: traefik.local.freeitathens.org |   #TRAEFIK_DOMAIN: traefik.local.freeitathens.org | ||||||
|   # TRAEFIK_DASHBOARD: true |   #TRAEFIK_DASHBOARD: true | ||||||
|   # TRAEFIK_EXPOSED_DEFAULT: false |   #TRAEFIK_EXPOSED_DEFAULT: false | ||||||
|   # TRAEFIK_WEB_ENABLED: true |   #TRAEFIK_WEB_ENABLED: true | ||||||
|   TRAEFIK_DEBUG: true |   TRAEFIK_DEBUG: true | ||||||
|   TRAEFIK_ACME_PROVIDER: dreamhost |   TRAEFIK_ACME_PROVIDER: dreamhost | ||||||
|   TRAEFIK_ACME_CASERVER: https://localhost/directory |   TRAEFIK_ACME_CASERVER: https://localhost/directory | ||||||
| @@ -52,29 +45,23 @@ webserver: | |||||||
|   ################# |   ################# | ||||||
|   ### WordPress ### |   ### WordPress ### | ||||||
|   ################# |   ################# | ||||||
|   # WORDPRESS_VERSION: latest |   #WORDPRESS_VERSION: latest | ||||||
|   # WORDPRESS_DOMAIN: www.local.freeitathens.org |   #WORDPRESS_DOMAIN: www.local.freeitathens.org | ||||||
|   # WORDPRESS_DB_HOST: host.docker.internal |   #WORDPRESS_DB_HOST: host.docker.internal | ||||||
|   # WORDPRESS_DB_NAME: wordpress |   #WORDPRESS_DB_NAME: wordpress | ||||||
|   # WORDPRESS_DB_USER: wordpress |   #WORDPRESS_DB_USER: wordpress | ||||||
|   # WORDPRESS_WEB_ENABLED: true |   #WORDPRESS_WEB_ENABLED: true | ||||||
|   WORDPRESS_DB_PASSWORD: "{{ secret.WORDPRESS_DB_PASSWORD }}" |   WORDPRESS_DB_PASSWORD: "{{ secret.WORDPRESS_DB_PASSWORD }}" | ||||||
|  |  | ||||||
|   ################# |   ################# | ||||||
|   ### Nextcloud ### |   ### Nextcloud ### | ||||||
|   ################# |   ################# | ||||||
|   # NEXTCLOUD_VERSION: stable |   #NEXTCLOUD_VERSION: stable | ||||||
|   # NEXTCLOUD_DOMAIN: cloud.local.freeitathens.org |   #NEXTCLOUD_DOMAIN: cloud.local.freeitathens.org | ||||||
|   # NEXTCLOUD_MYSQL_HOST: host.docker.internal |   #NEXTCLOUD_MYSQL_HOST: host.docker.internal | ||||||
|   # NEXTCLOUD_MYSQL_DATABASE: nextcloud |   #NEXTCLOUD_MYSQL_DATABASE: nextcloud | ||||||
|   # NEXTCLOUD_MYSQL_USER: nextcloud |   #NEXTCLOUD_MYSQL_USER: nextcloud | ||||||
|   # NEXTCLOUD_WEB_ENABLED: true |   #NEXTCLOUD_WEB_ENABLED: true | ||||||
|   # NEXTCLOUD_ADMIN: admin |   #NEXTCLOUD_ADMIN: admin | ||||||
|   NEXTCLOUD_ADMIN_PASSWORD: "{{ secret.NEXTCLOUD_ADMIN_PASSWORD }}" |   NEXTCLOUD_ADMIN_PASSWORD: "{{ secret.NEXTCLOUD_ADMIN_PASSWORD }}" | ||||||
|   NEXTCLOUD_MYSQL_PASSWORD: "{{ secret.NEXTCLOUD_MYSQL_PASSWORD }}" |   NEXTCLOUD_MYSQL_PASSWORD: "{{ secret.NEXTCLOUD_MYSQL_PASSWORD }}" | ||||||
|  |  | ||||||
|   ################# |  | ||||||
|   ### MediaWiki ### |  | ||||||
|   ################# |  | ||||||
|   # MEDIAWIKI_VERSION: stable |  | ||||||
|   # MEDIAWIKI_DOMAIN: wiki.local.freeitathens.org |  | ||||||
|   | |||||||
| @@ -5,5 +5,5 @@ | |||||||
|     - vars/webserver.yml |     - vars/webserver.yml | ||||||
|   roles: |   roles: | ||||||
|     - common |     - common | ||||||
|     - podman |     - docker | ||||||
|     - webserver |     - webserver | ||||||
|   | |||||||
| @@ -1,5 +1,2 @@ | |||||||
| common_packages: | packages: | ||||||
|   - dnsutils |   - dnsutils | ||||||
|   - ncdu |  | ||||||
|   - tree |  | ||||||
|   - vim |  | ||||||
|   | |||||||
| @@ -2,87 +2,35 @@ | |||||||
|   ansible.builtin.file: |   ansible.builtin.file: | ||||||
|     path: "~/.ansible/tmp" |     path: "~/.ansible/tmp" | ||||||
|     state: directory |     state: directory | ||||||
|     mode: "755" |     mode: 0700 | ||||||
|  |  | ||||||
| - name: Create system user groups |  | ||||||
|   ansible.builtin.group: |  | ||||||
|     name: "{{ item.key }}" |  | ||||||
|     gid: "{{ item.value.gid }}" |  | ||||||
|     state: present |  | ||||||
|   loop: "{{ users | dict2items }}" |  | ||||||
|   loop_control: |  | ||||||
|     label: "{{ item.key }}" |  | ||||||
|   when: users is defined |  | ||||||
|  |  | ||||||
| - name: Create system users |  | ||||||
|   ansible.builtin.user: |  | ||||||
|     name: "{{ item.key }}" |  | ||||||
|     state: present |  | ||||||
|     uid: "{{ item.value.uid }}" |  | ||||||
|     group: "{{ item.value.gid }}" |  | ||||||
|     groups: "{{ item.value.groups | default([]) }}" |  | ||||||
|     shell: "{{ item.value.shell | default('/bin/bash') }}" |  | ||||||
|     create_home: "{{ item.value.home | default(false) }}" |  | ||||||
|     home: "{{ item.value.homedir | default('/home/' + item.key) }}" |  | ||||||
|     system: "{{ item.value.system | default(false) }}" |  | ||||||
|   loop: "{{ users | dict2items }}" |  | ||||||
|   loop_control: |  | ||||||
|     label: "{{ item.key }}" |  | ||||||
|   when: users is defined |  | ||||||
|  |  | ||||||
| - name: Create Ansible's temporary remote directory for users |  | ||||||
|   ansible.builtin.file: |  | ||||||
|     path: "{{ item.value.homedir | default('/home/' + item.key) }}/.ansible/tmp" |  | ||||||
|     state: directory |  | ||||||
|     mode: "755" |  | ||||||
|     owner: "{{ item.key }}" |  | ||||||
|     group: "{{ item.value.gid }}" |  | ||||||
|   loop: "{{ users | dict2items }}" |  | ||||||
|   loop_control: |  | ||||||
|     label: "{{ item.key }}" |  | ||||||
|   when: |  | ||||||
|     - users is defined |  | ||||||
|     - item.value.ansible_temp | default(false) |  | ||||||
|  |  | ||||||
| - name: Install EPEL repository |  | ||||||
|   ansible.builtin.dnf: |  | ||||||
|     name: epel-release |  | ||||||
|     state: present |  | ||||||
|     update_cache: true |  | ||||||
|  |  | ||||||
| - name: Install useful software | - name: Install useful software | ||||||
|   ansible.builtin.dnf: |   ansible.builtin.apt: | ||||||
|     name: "{{ common_packages }}" |     name: "{{ packages }}" | ||||||
|     state: present |     state: present | ||||||
|     update_cache: true |     update_cache: true | ||||||
|  |  | ||||||
| - name: Install firewalld | - name: Install the Uncomplicated Firewall | ||||||
|   ansible.builtin.dnf: |   ansible.builtin.apt: | ||||||
|     name: firewalld |     name: ufw | ||||||
|     state: present |     state: present | ||||||
|  |     update_cache: true | ||||||
|  |  | ||||||
| - name: Start and enable firewalld service | - name: Deny incoming traffic by default | ||||||
|   ansible.builtin.systemd: |   community.general.ufw: | ||||||
|     name: firewalld |     default: deny | ||||||
|     state: started |     direction: incoming | ||||||
|     enabled: true |  | ||||||
|  |  | ||||||
| - name: Set default zone to drop (deny incoming by default) | - name: Allow outgoing traffic by default | ||||||
|   ansible.posix.firewalld: |   community.general.ufw: | ||||||
|     zone: drop |     default: allow | ||||||
|  |     direction: outgoing | ||||||
|  |  | ||||||
|  | - name: Allow OpenSSH with rate limiting | ||||||
|  |   community.general.ufw: | ||||||
|  |     name: ssh | ||||||
|  |     rule: limit | ||||||
|  |  | ||||||
|  | - name: Enable firewall | ||||||
|  |   community.general.ufw: | ||||||
|     state: enabled |     state: enabled | ||||||
|     permanent: true |  | ||||||
|     immediate: true |  | ||||||
|  |  | ||||||
| - name: Allow SSH in drop zone with rate limiting via rich rule |  | ||||||
|   ansible.posix.firewalld: |  | ||||||
|     zone: drop |  | ||||||
|     rich_rule: 'rule service name="ssh" accept limit value="10/m"' |  | ||||||
|     permanent: true |  | ||||||
|     immediate: true |  | ||||||
|     state: enabled |  | ||||||
|  |  | ||||||
| - name: Set drop as the default zone |  | ||||||
|   ansible.builtin.command: |  | ||||||
|     cmd: firewall-cmd --set-default-zone=drop |  | ||||||
|   changed_when: false |  | ||||||
|   | |||||||
| @@ -1,4 +0,0 @@ | |||||||
| - name: Restart systemd-logind |  | ||||||
|   ansible.builtin.systemd: |  | ||||||
|     name: systemd-logind |  | ||||||
|     state: restarted |  | ||||||
| @@ -1,49 +0,0 @@ | |||||||
| - name: Install Podman |  | ||||||
|   ansible.builtin.dnf: |  | ||||||
|     name: ["podman", "podman-docker", "podman-compose"] |  | ||||||
|     state: present |  | ||||||
|  |  | ||||||
| - name: Create /etc/containers/nodocker to quiet CLI emulation notice |  | ||||||
|   ansible.builtin.file: |  | ||||||
|     path: /etc/containers/nodocker |  | ||||||
|     state: touch |  | ||||||
|     mode: "644" |  | ||||||
|  |  | ||||||
| - name: Create logind.conf.d directory |  | ||||||
|   ansible.builtin.file: |  | ||||||
|     path: /etc/systemd/logind.conf.d |  | ||||||
|     state: directory |  | ||||||
|     mode: "755" |  | ||||||
|  |  | ||||||
| - name: Create linger directory |  | ||||||
|   ansible.builtin.file: |  | ||||||
|     path: /var/lib/systemd/linger |  | ||||||
|     state: directory |  | ||||||
|     mode: "755" |  | ||||||
|  |  | ||||||
| - name: Enable lingering for oci user |  | ||||||
|   ansible.builtin.file: |  | ||||||
|     path: /var/lib/systemd/linger/oci |  | ||||||
|     state: touch |  | ||||||
|     mode: "644" |  | ||||||
|   notify: Restart systemd-logind |  | ||||||
|  |  | ||||||
| - name: Force handler execution for user lingering |  | ||||||
|   ansible.builtin.meta: flush_handlers |  | ||||||
|  |  | ||||||
| - name: Create user systemd directory |  | ||||||
|   ansible.builtin.file: |  | ||||||
|     path: "/home/oci/.config/systemd/user" |  | ||||||
|     state: directory |  | ||||||
|     mode: "755" |  | ||||||
|     owner: oci |  | ||||||
|     group: oci |  | ||||||
|  |  | ||||||
| - name: Enable oci's podman socket |  | ||||||
|   ansible.builtin.systemd: |  | ||||||
|     name: podman.socket |  | ||||||
|     enabled: true |  | ||||||
|     state: started |  | ||||||
|     scope: user |  | ||||||
|   become_user: oci |  | ||||||
|   become: true |  | ||||||
| @@ -1,18 +1,20 @@ | |||||||
|  | version: '3.5' | ||||||
|  |  | ||||||
| volumes: | volumes: | ||||||
|   wordpress: |   wordpress: | ||||||
|   nextcloud: |   nextcloud: | ||||||
|   mediawiki: |   postgres: | ||||||
|  |  | ||||||
| networks: | networks: | ||||||
|   traefik: |   traefik: | ||||||
|     name: traefik |     name: traefik | ||||||
|  |   postgres: | ||||||
|  |     name: postgres | ||||||
|  |  | ||||||
| services: | services: | ||||||
|   traefik: |   traefik: | ||||||
|     image: ${TRAEFIK_IMAGE:-docker.io/library/traefik}:${TRAEFIK_VERSION:-latest} |     image: traefik:${TRAEFIK_VERSION:-latest} | ||||||
|     restart: always |     restart: always | ||||||
|     security_opt: |  | ||||||
|       - label=type:container_runtime_t |  | ||||||
|     command: |     command: | ||||||
|       - --api.dashboard=${TRAEFIK_DASHBOARD:-true} |       - --api.dashboard=${TRAEFIK_DASHBOARD:-true} | ||||||
|       - --api.debug=${TRAEFIK_DEBUG:-false} |       - --api.debug=${TRAEFIK_DEBUG:-false} | ||||||
| @@ -21,7 +23,7 @@ services: | |||||||
|       - --providers.docker.exposedbydefault=${TRAEFIK_EXPOSED_DEFAULT:-false} |       - --providers.docker.exposedbydefault=${TRAEFIK_EXPOSED_DEFAULT:-false} | ||||||
|       - --entrypoints.web.address=:80 |       - --entrypoints.web.address=:80 | ||||||
|       - --entrypoints.websecure.address=:443 |       - --entrypoints.websecure.address=:443 | ||||||
|       - --entrypoints.local.address=:9443 |       - --entrypoints.local.address=:8443 | ||||||
|       - --entrypoints.web.http.redirections.entrypoint.to=websecure |       - --entrypoints.web.http.redirections.entrypoint.to=websecure | ||||||
|       - --entrypoints.web.http.redirections.entrypoint.scheme=https |       - --entrypoints.web.http.redirections.entrypoint.scheme=https | ||||||
|       - --entrypoints.web.http.redirections.entrypoint.permanent=true |       - --entrypoints.web.http.redirections.entrypoint.permanent=true | ||||||
| @@ -34,11 +36,11 @@ services: | |||||||
|     environment: |     environment: | ||||||
|       DREAMHOST_API_KEY: ${TRAEFIK_DREAMHOST_APIKEY} |       DREAMHOST_API_KEY: ${TRAEFIK_DREAMHOST_APIKEY} | ||||||
|     ports: |     ports: | ||||||
|       - "${ENTRYWEB:-127.0.0.1:8080}:80" |       - 80:80 | ||||||
|       - "${ENTRYSECURE:-127.0.0.1:8443}:443" |       - 443:443 | ||||||
|       - "${ENTRYLOCAL:-127.0.0.1:9443}:9443" |       - "127.0.0.1:8443:8443" | ||||||
|     volumes: |     volumes: | ||||||
|       - ${OCI_SOCK:-/run/user/2000/podman/podman.sock}:/var/run/docker.sock:ro,Z |       - /var/run/docker.sock:/var/run/docker.sock | ||||||
|       - ./.acme:/etc/letsencrypt |       - ./.acme:/etc/letsencrypt | ||||||
|     labels: |     labels: | ||||||
|       traefik.http.routers.api.rule: Host(`${TRAEFIK_DOMAIN:-traefik.local.freeitathens.org}`) |       traefik.http.routers.api.rule: Host(`${TRAEFIK_DOMAIN:-traefik.local.freeitathens.org}`) | ||||||
| @@ -53,7 +55,7 @@ services: | |||||||
|       - traefik |       - traefik | ||||||
|  |  | ||||||
|   wordpress: |   wordpress: | ||||||
|     image: ${WORDPRESS_IMAGE:-docker.io/library/wordpress}:${WORDPRESS_VERSION:-latest} |     image: wordpress:${WORDPRESS_VERSION:-latest} | ||||||
|     restart: always |     restart: always | ||||||
|     environment: |     environment: | ||||||
|       WORDPRESS_DB_HOST: ${WORDPRESS_DB_HOST:-host.docker.internal} |       WORDPRESS_DB_HOST: ${WORDPRESS_DB_HOST:-host.docker.internal} | ||||||
| @@ -61,9 +63,7 @@ services: | |||||||
|       WORDPRESS_DB_USER: ${WORDPRESS_DB_USER:-wordpress} |       WORDPRESS_DB_USER: ${WORDPRESS_DB_USER:-wordpress} | ||||||
|       WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD} |       WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD} | ||||||
|     labels: |     labels: | ||||||
|       traefik.http.routers.wordpress.rule: |       traefik.http.routers.wordpress.rule: Host(`${WORDPRESS_DOMAIN:-www.local.freeitathens.org}`,`${TRAEFIK_ACME_DOMAIN_MAIN:-local.freeitathens.org}`) | ||||||
|         Host(`${WORDPRESS_DOMAIN:-www.local.freeitathens.org}`) || |  | ||||||
|         Host(`${TRAEFIK_ACME_DOMAIN_MAIN:-local.freeitathens.org}`) |  | ||||||
|       traefik.http.routers.wordpress.entrypoints: websecure |       traefik.http.routers.wordpress.entrypoints: websecure | ||||||
|       traefik.http.routers.wordpress.middlewares: "wwwredirect" |       traefik.http.routers.wordpress.middlewares: "wwwredirect" | ||||||
|       traefik.http.routers.wordpress.tls: true |       traefik.http.routers.wordpress.tls: true | ||||||
| @@ -84,7 +84,7 @@ services: | |||||||
|       - host.docker.internal:host-gateway |       - host.docker.internal:host-gateway | ||||||
|  |  | ||||||
|   nextcloud: |   nextcloud: | ||||||
|     image: ${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud}:${NEXTCLOUD_VERSION:-stable} |     image: nextcloud:${NEXTCLOUD_VERSION:-stable} | ||||||
|     restart: always |     restart: always | ||||||
|     environment: |     environment: | ||||||
|       MYSQL_HOST: ${NEXTCLOUD_MYSQL_HOST:-host.docker.internal:3306} |       MYSQL_HOST: ${NEXTCLOUD_MYSQL_HOST:-host.docker.internal:3306} | ||||||
| @@ -112,23 +112,35 @@ services: | |||||||
|     extra_hosts: |     extra_hosts: | ||||||
|       - host.docker.internal:host-gateway |       - host.docker.internal:host-gateway | ||||||
|  |  | ||||||
|   mediawiki: |   timetrex: | ||||||
|     image: ${MEDIAWIKI_IMAGE:-docker.io/library/mediawiki}:${MEDIAWIKI_VERSION:-stable} |     image: freeitathens/timetrex:${TIMETREX_VERSION:-latest} | ||||||
|     restart: always |     restart: always | ||||||
|  |     environment: | ||||||
|  |       POSTGRES_PASSWORD: password | ||||||
|  |       POSTGRES_HOST: postgres | ||||||
|  |     links: | ||||||
|  |       - postgres | ||||||
|     labels: |     labels: | ||||||
|       traefik.http.routers.mediawiki.rule: "Host(`${MEDIAWIKI_DOMAIN:-wiki.local.freeitathens.org}`)" |       traefik.http.routers.timetrex.rule: "Host(`${TIMETREX_DOMAIN:-time.local.freeitathens.org}`)" | ||||||
|       traefik.http.routers.mediawiki.entrypoints: websecure |       traefik.http.routers.timetrex.entrypoints: websecure | ||||||
|       traefik.http.routers.mediawiki.tls: true |       traefik.http.routers.timetrex.tls: true | ||||||
|       traefik.http.routers.mediawiki.tls.certresolver: letsencrypt |       traefik.http.routers.timetrex.tls.certresolver: letsencrypt | ||||||
|       traefik.http.routers.mediawiki.tls.domains[0].main: ${TRAEFIK_ACME_DOMAIN_MAIN:-local.freeitathens.org} |       traefik.http.routers.timetrex.tls.domains[0].main: ${TRAEFIK_ACME_DOMAIN_MAIN:-local.freeitathens.org} | ||||||
|       traefik.http.routers.mediawiki.tls.domains[0].sans: "${TRAEFIK_ACME_DOMAIN_SANS:-*.local.freeitathens.org}" |       traefik.http.routers.timetrex.tls.domains[0].sans: "${TRAEFIK_ACME_DOMAIN_SANS:-*.local.freeitathens.org}" | ||||||
|       traefik.http.services.mediawiki.loadbalancer.server.port: 80 |       traefik.http.services.timetrex.loadbalancer.server.port: 80 | ||||||
|       traefik.docker.network: traefik |       traefik.docker.network: traefik | ||||||
|       traefik.enable: ${MEDIAWIKI_WEB_ENABLED:-true} |       traefik.enable: ${NEXTCLOUD_WEB_ENABLED:-true} | ||||||
|     volumes: |  | ||||||
|       - ./LocalSettings.php:/var/www/html/LocalSettings.php:ro,Z |  | ||||||
|       - mediawiki:/var/www/html/images |  | ||||||
|     networks: |     networks: | ||||||
|  |       - postgres | ||||||
|       - traefik |       - traefik | ||||||
|     extra_hosts: |  | ||||||
|       - host.docker.internal:host-gateway |   postgres: | ||||||
|  |     image: postgres:13-bullseye | ||||||
|  |     volumes: | ||||||
|  |       - postgres:/var/lib/postgresql/data | ||||||
|  |     environment: | ||||||
|  |       POSTGRES_DB: timetrex | ||||||
|  |       POSTGRES_USER: timetrex | ||||||
|  |       POSTGRES_PASSWORD: password | ||||||
|  |     networks: | ||||||
|  |       - postgres | ||||||
|   | |||||||
| @@ -1,22 +0,0 @@ | |||||||
| user nginx; |  | ||||||
| worker_processes auto; |  | ||||||
| error_log /var/log/nginx/error.log; |  | ||||||
| pid /run/nginx.pid; |  | ||||||
|  |  | ||||||
| include /usr/share/nginx/modules/*.conf; |  | ||||||
|  |  | ||||||
| events { |  | ||||||
|     worker_connections 1024; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| stream { |  | ||||||
|     server { |  | ||||||
|         listen 80; |  | ||||||
|         proxy_pass 127.0.0.1:8080; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     server { |  | ||||||
|         listen 443; |  | ||||||
|         proxy_pass 127.0.0.1:8443; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,38 +1,14 @@ | |||||||
| - name: Restart nginx |  | ||||||
|   ansible.builtin.systemd: |  | ||||||
|     name: nginx |  | ||||||
|     state: restarted |  | ||||||
|  |  | ||||||
| - name: Restart MariaDB | - name: Restart MariaDB | ||||||
|   ansible.builtin.service: |   ansible.builtin.service: | ||||||
|     name: mariadb |     name: mariadb | ||||||
|     state: restarted |     state: restarted | ||||||
|   listen: restart_mariadb |   listen: restart_mariadb | ||||||
|  |  | ||||||
| - name: Start podman compose project | - name: Compose up on webserver stack | ||||||
|   ansible.builtin.command: |   ansible.builtin.command: "docker-compose up -d" | ||||||
|     cmd: podman compose up -d |   args: | ||||||
|     chdir: "/home/oci/webserver" |     chdir: "{{ webserver_root }}" | ||||||
|   notify: Generate systemd service files |   listen: composeup_webserver | ||||||
|   changed_when: false |  | ||||||
|   become_user: oci |  | ||||||
|   become: true |  | ||||||
|  |  | ||||||
| - name: Reload systemd user daemon |  | ||||||
|   ansible.builtin.systemd: |  | ||||||
|     daemon_reload: true |  | ||||||
|     scope: user |  | ||||||
|   notify: Enable systemd user service |  | ||||||
|   become_user: oci |  | ||||||
|   become: true |  | ||||||
|  |  | ||||||
| - name: Enable systemd user service |  | ||||||
|   ansible.builtin.systemd: |  | ||||||
|     name: webserver |  | ||||||
|     enabled: true |  | ||||||
|     scope: user |  | ||||||
|   become_user: oci |  | ||||||
|   become: true |  | ||||||
|  |  | ||||||
| - name: Grab Nextcloud container information | - name: Grab Nextcloud container information | ||||||
|   community.docker.docker_container_info: |   community.docker.docker_container_info: | ||||||
| @@ -47,9 +23,8 @@ | |||||||
|   listen: composeup_webserver |   listen: composeup_webserver | ||||||
|  |  | ||||||
| - name: Check Nextcloud status | - name: Check Nextcloud status | ||||||
|   ansible.builtin.command: |   ansible.builtin.command: "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1 | ||||||
|     "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1 |             php occ status" | ||||||
|     php occ status" |  | ||||||
|   listen: composeup_webserver |   listen: composeup_webserver | ||||||
|   register: nextcloud_status |   register: nextcloud_status | ||||||
|  |  | ||||||
| @@ -59,12 +34,3 @@ | |||||||
|   when: |   when: | ||||||
|     - nextcloud_status.stderr[:26] == "Nextcloud is not installed" |     - nextcloud_status.stderr[:26] == "Nextcloud is not installed" | ||||||
|     - nextcloud_autoinstall |     - nextcloud_autoinstall | ||||||
|  |  | ||||||
| - name: Install webserver docker-compose.yml |  | ||||||
|   ansible.builtin.copy: |  | ||||||
|     src: docker-compose.yml |  | ||||||
|     dest: /home/oci/webserver/compose.yml |  | ||||||
|     mode: "600" |  | ||||||
|     owner: oci |  | ||||||
|     group: oci |  | ||||||
|   notify: Generate systemd service files |  | ||||||
|   | |||||||
| @@ -24,15 +24,6 @@ | |||||||
|   listen: composeup_webserver |   listen: composeup_webserver | ||||||
|   when: nextcloud_install.changed |   when: nextcloud_install.changed | ||||||
|  |  | ||||||
| - name: Install Nextcloud background jobs cron |  | ||||||
|   ansible.builtin.cron: |  | ||||||
|     name: Nextcloud background job |  | ||||||
|     minute: "*/5" |  | ||||||
|     job: "/usr/bin/docker exec -u www-data webserver_nextcloud_1 /usr/local/bin/php -f /var/www/html/cron.php" |  | ||||||
|     user: root |  | ||||||
|   listen: composeup_webserver |  | ||||||
|   when: nextcloud_install.changed |  | ||||||
|  |  | ||||||
| - name: Preform Nextcloud database maintenance | - name: Preform Nextcloud database maintenance | ||||||
|   ansible.builtin.command: "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1 {{ item }}" |   ansible.builtin.command: "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1 {{ item }}" | ||||||
|   loop: |   loop: | ||||||
| @@ -41,4 +32,4 @@ | |||||||
|     - "php occ db:convert-filecache-bigint" |     - "php occ db:convert-filecache-bigint" | ||||||
|     - "php occ maintenance:mode --off" |     - "php occ maintenance:mode --off" | ||||||
|   listen: composeup_webserver |   listen: composeup_webserver | ||||||
|   when: "'  - needsDbUpgrade: true' in nextcloud_status.stdout_lines or nextcloud_install.changed" |   when: "'  - needsDbUpgrade: true' in nextcloud_status.stdout_lines" | ||||||
|   | |||||||
| @@ -1,102 +1,72 @@ | |||||||
| - name: Install MariaDB Server | - name: Install MariaDB Server | ||||||
|   ansible.builtin.dnf: |   ansible.builtin.apt: | ||||||
|     name: mariadb-server |     name: mariadb-server | ||||||
|     state: present |     state: present | ||||||
|  |  | ||||||
| - name: Change the bind-address to allow Docker | - name: Change the bind-address to allow Docker | ||||||
|   ansible.builtin.lineinfile: |   ansible.builtin.lineinfile: | ||||||
|     path: /etc/my.cnf.d/mariadb-server.cnf |     path: /etc/mysql/mariadb.conf.d/50-server.cnf | ||||||
|     regex: "^bind-address" |     regex: "^bind-address" | ||||||
|     line: "bind-address            = 0.0.0.0" |     line: "bind-address            = 0.0.0.0" | ||||||
|   notify: restart_mariadb |   notify: restart_mariadb | ||||||
|  |  | ||||||
| - name: Start and enable MariaDB service |  | ||||||
|   ansible.builtin.systemd: |  | ||||||
|     name: mariadb |  | ||||||
|     state: started |  | ||||||
|     enabled: true |  | ||||||
|  |  | ||||||
| - name: Install MySQL Support for Python 3 | - name: Install MySQL Support for Python 3 | ||||||
|   ansible.builtin.dnf: |   ansible.builtin.apt: | ||||||
|     name: python3-PyMySQL |     name: python3-pymysql | ||||||
|     state: present |     state: present | ||||||
|  |  | ||||||
| - name: Create MariaDB databases | - name: Create MariaDB databases | ||||||
|   community.mysql.mysql_db: |   community.mysql.mysql_db: | ||||||
|     name: "{{ item.name }}" |     name: "{{ item.name }}" | ||||||
|     state: present |     state: present | ||||||
|     login_unix_socket: /var/lib/mysql/mysql.sock |     login_unix_socket: /var/run/mysqld/mysqld.sock | ||||||
|   loop: "{{ databases }}" |   loop: "{{ databases }}" | ||||||
|   no_log: true |   no_log: "{{ item.pass is defined }}" | ||||||
|  |  | ||||||
| - name: Create MariaDB users | - name: Create MariaDB users | ||||||
|   community.mysql.mysql_user: |   community.mysql.mysql_user: | ||||||
|     name: "{{ item.name }}" |     name: "{{ item.name }}" | ||||||
|     password: "{{ item.pass }}" |     password: "{{ item.pass }}" | ||||||
|     host: "%" |     host: '%' | ||||||
|     state: present |     state: present | ||||||
|     priv: "{{ item.name }}.*:ALL" |     priv: "{{ item.name }}.*:ALL" | ||||||
|     login_unix_socket: /var/lib/mysql/mysql.sock |     login_unix_socket: /var/run/mysqld/mysqld.sock | ||||||
|   loop: "{{ databases }}" |   loop: "{{ databases }}" | ||||||
|   no_log: true |   no_log: "{{ item.pass is defined }}" | ||||||
|  |  | ||||||
| - name: Create webserver stack directory | - name: Create webserver docker-compose directory | ||||||
|   ansible.builtin.file: |   ansible.builtin.file: | ||||||
|     path: /home/oci/webserver |     path: "{{ webserver_root }}" | ||||||
|     state: directory |     state: directory | ||||||
|     mode: "700" |     mode: 0600 | ||||||
|     owner: oci |  | ||||||
|     group: oci |  | ||||||
|  |  | ||||||
| - name: Install webserver compose file | - name: Install webserver docker-compose.yml | ||||||
|   ansible.builtin.copy: |   ansible.builtin.copy: | ||||||
|     src: docker-compose.yml |     src: docker-compose.yml | ||||||
|     dest: /home/oci/webserver/compose.yml |     dest: "{{ webserver_root }}/docker-compose.yml" | ||||||
|     mode: "600" |     mode: 0600 | ||||||
|     owner: oci |   notify: composeup_webserver | ||||||
|     group: oci |  | ||||||
|   notify: Start podman compose project |  | ||||||
|  |  | ||||||
| - name: Generate webserver environment configuration | - name: Install docker-compose .env | ||||||
|   ansible.builtin.template: |   ansible.builtin.template: | ||||||
|     src: compose-env.j2 |     src: compose-env.j2 | ||||||
|     dest: /home/oci/webserver/.env |     dest: "{{ webserver_root }}/.env" | ||||||
|     mode: "400" |     mode: 0600 | ||||||
|     owner: oci |   notify: composeup_webserver | ||||||
|     group: oci |  | ||||||
|   notify: Start podman compose project |  | ||||||
|  |  | ||||||
| - name: Install nginx | - name: Allow MariaDB database connections | ||||||
|   ansible.builtin.dnf: |   community.general.ufw: | ||||||
|     name: ["nginx", "nginx-mod-stream"] |     rule: allow | ||||||
|     state: present |     port: 3306 | ||||||
|     update_cache: true |     proto: tcp | ||||||
|  |     src: "{{ item }}" | ||||||
|  |   loop: "{{ mariadb_trust }}" | ||||||
|  |  | ||||||
| - name: Allow nginx to make network connections | - name: Add HTTP and HTTPS firewall rule | ||||||
|   ansible.posix.seboolean: |   community.general.ufw: | ||||||
|     name: httpd_can_network_connect |     rule: allow | ||||||
|     state: true |     port: "{{ item }}" | ||||||
|     persistent: true |     proto: tcp | ||||||
|  |  | ||||||
| - name: Deploy nginx proxy config |  | ||||||
|   ansible.builtin.copy: |  | ||||||
|     src: nginx.conf |  | ||||||
|     dest: /etc/nginx/nginx.conf |  | ||||||
|     mode: "644" |  | ||||||
|   notify: Restart nginx |  | ||||||
|  |  | ||||||
| - name: Allow HTTP and HTTPS in firewall |  | ||||||
|   ansible.posix.firewalld: |  | ||||||
|     service: "{{ item }}" |  | ||||||
|     permanent: true |  | ||||||
|     state: enabled |  | ||||||
|     immediate: true |  | ||||||
|   loop: |   loop: | ||||||
|     - http |     - "80" | ||||||
|     - https |     - "443" | ||||||
|  |  | ||||||
| - name: Start and enable nginx |  | ||||||
|   ansible.builtin.systemd: |  | ||||||
|     name: nginx |  | ||||||
|     state: started |  | ||||||
|     enabled: true |  | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ MATCH_PATTERN="ssh -fNT -i ${PRIVATE_KEY}.*vagrant@" | |||||||
|  |  | ||||||
| function ssh_connect { | function ssh_connect { | ||||||
|   sudo ssh -fNT -i "$PRIVATE_KEY" \ |   sudo ssh -fNT -i "$PRIVATE_KEY" \ | ||||||
|     -L 9443:localhost:9443 \ |     -L 8443:localhost:8443 \ | ||||||
|     -L 80:localhost:80 \ |     -L 80:localhost:80 \ | ||||||
|     -L 443:localhost:443 \ |     -L 443:localhost:443 \ | ||||||
|     -o UserKnownHostsFile=/dev/null \ |     -o UserKnownHostsFile=/dev/null \ | ||||||
|   | |||||||
| @@ -3,5 +3,5 @@ | |||||||
|   become: true |   become: true | ||||||
|   roles: |   roles: | ||||||
|     - common |     - common | ||||||
|     - podman |     - docker | ||||||
|     - webserver |     - webserver | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user