feat: setup servers on hetzner cloud

master
Sven Ketelsen 5 years ago
parent c006d4b017
commit 7eefe6b28f

@ -16,3 +16,9 @@ Install ansible role for managing hetzner cloud servers.
Create/Start servers for stage-dev
ansible-playbook -i stage-dev provisioning.yml --vault-password-file ~/vault-pass
ansible-playbook -i stage-dev start.yml --vault-password-file ~/vault-pass
ansible-playbook -i stage-dev stop.yml --vault-password-file ~/vault-pass
# Provisioning
ansible-playbook -i stage-dev setup.yml --vault-password-file ~/vault-pass -u root

@ -1,6 +1,49 @@
---
send_status_messages: true
use_ssl: true
service_prefix: ''
service_suffix: ''
service_name: "{{ inventory_hostname }}"
stage_server_hostname: "{{ inventory_hostname }}"
domain: smardigo.digital
service_url: "{{ service_name }}.{{ domain }}"
ansible_ssh_host: "{{ inventory_hostname }}.{{ domain }}"
admin_user: "administrator"
sudo_groups: [
{
id: "CentOS",
sudo_group: "wheel",
},
{
id: "RedHat",
sudo_group: "wheel",
},
{
id: "Ubuntu",
sudo_group: "sudo",
},
]
sudo_group: "{{ sudo_groups
| selectattr('id', 'match', '' + ansible_distribution + '' )
| map(attribute='sudo_group')
| list
| first
| replace('.','-') }}"
default_plattform_users:
- 'nobody'
- 'vagrant'
- 'administrator'
- '{{ admin_user }}'
smardigo_plattform_users:
- 'sven.ketelsen'
hetzner_server_type: cx11
hetzner_server_image: ubuntu-20.04
@ -8,5 +51,6 @@ hetzner_server_image: ubuntu-20.04
hetzner_ssh_keys:
- sven.ketelsen@arxes-tolina.de
#mattermost_hook_smardigo: "< see vault >"
#hetzner_authentication_token: "< see vault >"
#digitalocean_authentication_token: "< see vault >"

@ -1,15 +1,20 @@
$ANSIBLE_VAULT;1.1;AES256
62366361333863393564663466393361633166613434303036363563306634316161326432336262
3331653631666639623366326238323465333736653532660a333335643632353633303037663631
37636163613537313035633433313439326134303532346434373533643865343466336433643837
3764666639343265630a393463306363653962333561353161336264306664656163386232333438
39396232303938393961393065306433643232343766356235363562623431623437613134353135
38633433643365613434636531616134303835626661643835633437343262646534346562663165
39393762333565336339663130383461383931643165386635376532316137366165356336353964
65656235626362353937373065386131386139663334653438376138353436613434343639646134
62663936323033366265316361343039383531376230396466366331383632383163646433316631
62356364303662366630396535626232613566336430616536623561623333643333393434613863
62336632333465366363303164373331336436393830636133366263383163336362343366653762
39643762393864626366383731626366643831653238303532663964363537393031663836343338
34643735306335313030343664313361356361316633613530353361346232326261366239383662
6163326466643334646436383366363531633066313335323336
36356561396566663330633733666665333231653532633630393364633161613966643163343361
3063643237303932343339643464316363633334306431630a383632646263323365643835623932
30316336663265366337623834393134363761313035343263386233626533316634663238323636
3137353262353531370a613036643361346430656537636363666462363633306364316435373638
37346661666330323763323164653264613138663239323136326666666133623061663134636136
35343636316131646336636363326265646261336438623834656562613534616166306461666662
61656638396136376331633639386638336563666264333062346633376566303136313037346438
63353931633831616464343565393839323334313338663838336465663565633165633937306435
35306338643039306137663437383563316465346532366361633864383661396461626433326133
39626135666132613261353437613835633137656430663630333331636536376365666265633536
31353032303930366131636434646132633137376262653439316632643535356538656265333734
37346632386433326435646638326166653236633163663162633433353032373734643165313235
30333635313766663531383830633864326230363836623465386262396165386365346438356333
66346335346331393939386264633730383461663662343039303936653863346130343964613431
32663037323833666633393238663835653138323336656336616639656436623961313064366438
35363438616361653634303131653132393263303964373830336463393930353562363836343331
35333331633336356166396630653834373030313333613666383335613032376163353562613530
31633964336430366432633664356463333336376563383761343666663362633864656437356462
396232323765356262626366343266316136

@ -5,3 +5,7 @@ hetzner_server_type: cx21
hetzner_ssh_keys:
- stefan@curow.de
- sven.ketelsen@arxes-tolina.de
smardigo_plattform_users:
- 'stefan.curow'
- 'sven.ketelsen'

@ -5,3 +5,7 @@ hetzner_server_type: cx21
hetzner_ssh_keys:
- stefan@curow.de
- sven.ketelsen@arxes-tolina.de
smardigo_plattform_users:
- 'stefan.curow'
- 'sven.ketelsen'

@ -5,3 +5,7 @@ hetzner_server_type: cx21
hetzner_ssh_keys:
- stefan@curow.de
- sven.ketelsen@arxes-tolina.de
smardigo_plattform_users:
- 'stefan.curow'
- 'sven.ketelsen'

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2017 Jeff Geerling
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,97 @@
# Ansible Role: Docker
[![Build Status](https://travis-ci.org/geerlingguy/ansible-role-docker.svg?branch=master)](https://travis-ci.org/geerlingguy/ansible-role-docker)
An Ansible Role that installs [Docker](https://www.docker.com) on Linux.
## Requirements
None.
## Role Variables
Available variables are listed below, along with default values (see `defaults/main.yml`):
# Edition can be one of: 'ce' (Community Edition) or 'ee' (Enterprise Edition).
docker_edition: 'ce'
docker_package: "docker-{{ docker_edition }}"
docker_package_state: present
The `docker_edition` should be either `ce` (Community Edition) or `ee` (Enterprise Edition). You can also specify a specific version of Docker to install using the distribution-specific format: Red Hat/CentOS: `docker-{{ docker_edition }}-<VERSION>`; Debian/Ubuntu: `docker-{{ docker_edition }}=<VERSION>`.
You can control whether the package is installed, uninstalled, or at the latest version by setting `docker_package_state` to `present`, `absent`, or `latest`, respectively. Note that the Docker daemon will be automatically restarted if the Docker package is updated. This is a side effect of flushing all handlers (running any of the handlers that have been notified by this and any other role up to this point in the play).
docker_service_state: started
docker_service_enabled: true
docker_restart_handler_state: restarted
Variables to control the state of the `docker` service, and whether it should start on boot. If you're installing Docker inside a Docker container without systemd or sysvinit, you should set these to `stopped` and set the enabled variable to `no`.
docker_install_compose: true
docker_compose_version: "1.26.0"
docker_compose_path: /usr/local/bin/docker-compose
Docker Compose installation options.
docker_apt_release_channel: stable
docker_apt_arch: amd64
docker_apt_repository: "deb [arch={{ docker_apt_arch }}] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} {{ docker_apt_release_channel }}"
docker_apt_ignore_key_error: True
docker_apt_gpg_key: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
(Used only for Debian/Ubuntu.) You can switch the channel to `edge` if you want to use the Edge release.
You can change `docker_apt_gpg_key` to a different url if you are behind a firewall or provide a trustworthy mirror.
Usually in combination with changing `docker_apt_repository` as well.
docker_yum_repo_url: https://download.docker.com/linux/centos/docker-{{ docker_edition }}.repo
docker_yum_repo_enable_edge: '0'
docker_yum_repo_enable_test: '0'
docker_yum_gpg_key: https://download.docker.com/linux/centos/gpg
(Used only for RedHat/CentOS.) You can enable the Edge or Test repo by setting the respective vars to `1`.
You can change `docker_yum_gpg_key` to a different url if you are behind a firewall or provide a trustworthy mirror.
Usually in combination with changing `docker_yum_repository` as well.
docker_users:
- user1
- user2
A list of system users to be added to the `docker` group (so they can use Docker on the server).
## Use with Ansible (and `docker` Python library)
Many users of this role wish to also use Ansible to then _build_ Docker images and manage Docker containers on the server where Docker is installed. In this case, you can easily add in the `docker` Python library using the `geerlingguy.pip` role:
```yaml
- hosts: all
vars:
pip_install_packages:
- name: docker
roles:
- geerlingguy.pip
- geerlingguy.docker
```
## Dependencies
None.
## Example Playbook
```yaml
- hosts: all
roles:
- geerlingguy.docker
```
## License
MIT / BSD
## Author Information
This role was created in 2017 by [Jeff Geerling](https://www.jeffgeerling.com/), author of [Ansible for DevOps](https://www.ansiblefordevops.com/).

@ -0,0 +1,31 @@
---
# Edition can be one of: 'ce' (Community Edition) or 'ee' (Enterprise Edition).
docker_edition: 'ce'
docker_package: "docker-{{ docker_edition }}"
docker_package_state: present
# Service options.
docker_service_state: started
docker_service_enabled: true
docker_restart_handler_state: restarted
# Docker Compose options.
docker_install_compose: true
docker_compose_version: "1.26.0"
docker_compose_path: /usr/local/bin/docker-compose
# Used only for Debian/Ubuntu. Switch 'stable' to 'edge' if needed.
docker_apt_release_channel: stable
docker_apt_arch: amd64
docker_apt_repository: "deb [arch={{ docker_apt_arch }}] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} {{ docker_apt_release_channel }}"
docker_apt_ignore_key_error: true
docker_apt_gpg_key: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
# Used only for RedHat/CentOS/Fedora.
docker_yum_repo_url: https://download.docker.com/linux/{{ (ansible_distribution == "Fedora") | ternary("fedora","centos") }}/docker-{{ docker_edition }}.repo
docker_yum_repo_enable_edge: '0'
docker_yum_repo_enable_test: '0'
docker_yum_gpg_key: https://download.docker.com/linux/centos/gpg
# A list of users who will be added to the docker group.
docker_users: []

@ -0,0 +1,3 @@
---
- name: restart docker
service: "name=docker state={{ docker_restart_handler_state }}"

@ -0,0 +1,35 @@
---
dependencies: []
galaxy_info:
role_name: docker
author: geerlingguy
description: Docker for Linux.
company: "Midwestern Mac, LLC"
license: "license (BSD, MIT)"
min_ansible_version: 2.4
platforms:
- name: EL
versions:
- 7
- 8
- name: Fedora
versions:
- all
- name: Debian
versions:
- stretch
- buster
- name: Ubuntu
versions:
- xenial
- bionic
- focal
galaxy_tags:
- web
- system
- containers
- docker
- orchestration
- compose
- server

@ -0,0 +1,24 @@
---
- name: Converge
hosts: all
become: true
pre_tasks:
- name: Update apt cache.
apt: update_cache=yes cache_valid_time=600
when: ansible_os_family == 'Debian'
- name: Wait for systemd to complete initialization. # noqa 303
command: systemctl is-system-running
register: systemctl_status
until: >
'running' in systemctl_status.stdout or
'degraded' in systemctl_status.stdout
retries: 30
delay: 5
when: ansible_service_mgr == 'systemd'
changed_when: false
failed_when: systemctl_status.rc > 1
roles:
- role: geerlingguy.docker

@ -0,0 +1,21 @@
---
dependency:
name: galaxy
driver:
name: docker
lint: |
set -e
yamllint .
ansible-lint
platforms:
- name: instance
image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible:latest"
command: ${MOLECULE_DOCKER_COMMAND:-""}
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
privileged: true
pre_build_image: true
provisioner:
name: ansible
playbooks:
converge: ${MOLECULE_PLAYBOOK:-converge.yml}

@ -0,0 +1,20 @@
---
- name: Check current docker-compose version.
command: docker-compose --version
register: docker_compose_current_version
changed_when: false
failed_when: false
- name: Delete existing docker-compose version if it's different.
file:
path: "{{ docker_compose_path }}"
state: absent
when: >
docker_compose_current_version.stdout is defined
and docker_compose_version not in docker_compose_current_version.stdout
- name: Install Docker Compose (if configured).
get_url:
url: https://github.com/docker/compose/releases/download/{{ docker_compose_version }}/docker-compose-Linux-x86_64
dest: "{{ docker_compose_path }}"
mode: 0755

@ -0,0 +1,7 @@
---
- name: Ensure docker users are added to the docker group.
user:
name: "{{ item }}"
groups: docker
append: true
with_items: "{{ docker_users }}"

@ -0,0 +1,27 @@
---
- include_tasks: setup-RedHat.yml
when: ansible_os_family == 'RedHat'
- include_tasks: setup-Debian.yml
when: ansible_os_family == 'Debian'
- name: Install Docker.
package:
name: "{{ docker_package }}"
state: "{{ docker_package_state }}"
notify: restart docker
- name: Ensure Docker is started and enabled at boot.
service:
name: docker
state: "{{ docker_service_state }}"
enabled: "{{ docker_service_enabled }}"
- name: Ensure handlers are notified now to avoid firewall conflicts.
meta: flush_handlers
- include_tasks: docker-compose.yml
when: docker_install_compose | bool
- include_tasks: docker-users.yml
when: docker_users | length > 0

@ -0,0 +1,40 @@
---
- name: Ensure old versions of Docker are not installed.
package:
name:
- docker
- docker-engine
state: absent
- name: Ensure dependencies are installed.
apt:
name:
- apt-transport-https
- ca-certificates
- gnupg2
state: present
- name: Add Docker apt key.
apt_key:
url: "{{ docker_apt_gpg_key }}"
id: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
state: present
register: add_repository_key
ignore_errors: "{{ docker_apt_ignore_key_error }}"
- name: Ensure curl is present (on older systems without SNI).
package: name=curl state=present
when: add_repository_key is failed
- name: Add Docker apt key (alternative for older systems without SNI).
shell: >
curl -sSL {{ docker_apt_gpg_key }} | sudo apt-key add -
args:
warn: false
when: add_repository_key is failed
- name: Add Docker repository.
apt_repository:
repo: "{{ docker_apt_repository }}"
state: present
update_cache: true

@ -0,0 +1,54 @@
---
- name: Ensure old versions of Docker are not installed.
package:
name:
- docker
- docker-common
- docker-engine
state: absent
- name: Add Docker GPG key.
rpm_key:
key: "{{ docker_yum_gpg_key }}"
state: present
- name: Add Docker repository.
get_url:
url: "{{ docker_yum_repo_url }}"
dest: '/etc/yum.repos.d/docker-{{ docker_edition }}.repo'
owner: root
group: root
mode: 0644
- name: Configure Docker Edge repo.
ini_file:
dest: '/etc/yum.repos.d/docker-{{ docker_edition }}.repo'
section: 'docker-{{ docker_edition }}-edge'
option: enabled
value: '{{ docker_yum_repo_enable_edge }}'
mode: 0644
- name: Configure Docker Test repo.
ini_file:
dest: '/etc/yum.repos.d/docker-{{ docker_edition }}.repo'
section: 'docker-{{ docker_edition }}-test'
option: enabled
value: '{{ docker_yum_repo_enable_test }}'
mode: 0644
- name: Configure containerd on RHEL 8.
block:
- name: Ensure container-selinux is installed.
package:
name: container-selinux
state: present
- name: Disable container-tools module.
command: dnf -y module disable container-tools
changed_when: false
- name: Ensure containerd.io is installed.
package:
name: containerd.io
state: present
when: ansible_distribution_major_version | int == 8

@ -0,0 +1,6 @@
{
"auths": {
},
"HttpHeaders": {
}
}

@ -0,0 +1,8 @@
{
"log-driver": "json-file",
"log-opts": {
"max-size": "1m",
"max-file": "5",
"compress": "true"
}
}

@ -0,0 +1,18 @@
---
- name: restart ntp
service:
name=ntpd
state=restarted
- name: restart ssh
service:
name=sshd
state=restarted
- name: reload NetworkManager
service:
name: NetworkManager
state: reloaded

@ -0,0 +1,248 @@
---
# This playbook contains common plays that will be run on all nodes.
### tags:
### local_ssh_config
### users
### install
### config
- name: "Send mattermost messsge"
uri:
url: "{{ mattermost_hook_smardigo }}"
method: POST
body: "{{ lookup('template','mattermost-deploy-start.json.j2') }}"
body_format: json
headers:
Content-Type: "application/json"
delegate_to: 127.0.0.1
become: false
when:
- send_status_messages
- name: Gather current server infos
hcloud_server_info:
api_token: "{{ hetzner_authentication_token }}"
register: hetzner_server_infos
delegate_to: 127.0.0.1
become: false
- name: Save current server infos as variable (fact)
set_fact:
hetzner_server_infos_json: "{{ hetzner_server_infos.hcloud_server_info }}"
delegate_to: 127.0.0.1
become: false
- name: Read ip for {{ inventory_hostname }}
set_fact:
stage_server_ip: "{{ item.ipv4_address }}"
when: item.name == inventory_hostname
with_items: "{{ hetzner_server_infos_json }}"
delegate_to: 127.0.0.1
become: false
- name: 'Insert/Update ssh config in ~/.ssh/config'
blockinfile:
marker: '# {mark} managed by ansible (ssh config for {{ inventory_hostname }})'
path: '~/.ssh/config'
create: yes
block: |
Host {{ inventory_hostname }}
HostName {{ stage_server_ip }}
delegate_to: 127.0.0.1
become: false
throttle: 1
tags:
- local_ssh_config
- name: "Set hostname to <{{ stage_server_hostname }}>"
hostname:
name: "{{ stage_server_hostname }}"
- name: Add hostname to /etc/hosts file
lineinfile:
dest: /etc/hosts
regexp: '^127\.0\.1\.1'
line: "127.0.1.1 {{ stage_server_hostname }}"
state: present
when: ansible_facts['distribution'] == "Ubuntu"
- name: "Read current users"
shell: "getent passwd | awk -F: '$3 > 999 {print $1}'"
register: current_users
tags:
- users
- name: "Remove outdated users"
user: name={{item}} state=absent remove=yes
with_items: "{{ current_users.stdout_lines }}"
when: not ((item in default_plattform_users) or (item in smardigo_plattform_users))
tags:
- users
- name: "Create users"
user:
name: '{{ item }}'
groups: '{{ sudo_group }}'
shell: '/bin/bash'
state: present
append: yes
loop: '{{ smardigo_plattform_users }}'
loop_control:
index_var: index
tags:
- users
# TODO check usage of key_options "no-agent-forwarding, no-agent-forwarding, no-X11-forwarding"
- name: "Set up authorized keys"
authorized_key:
user: '{{ item }}'
state: present
exclusive: true
key: "{{ lookup('file', '{{ inventory_dir }}/keys/{{ item }}/id_rsa.pub') }}"
loop: '{{ smardigo_plattform_users }}'
tags:
- users
#- name: "Set up authorized keys as administrator"
# authorized_key:
# user: administrator
# state: present
# key: "{{ lookup('file', '{{ inventory_dir }}/keys/{{ item }}/id_rsa.pub') }}"
# loop: '{{ smardigo_plattform_users }}'
# tags:
# - users
- name: "Ensure docker configuration directory exists"
file:
path: '/home/{{ item }}/.docker/'
state: directory
owner: '{{ item }}'
group: '{{ item }}'
loop: '{{ smardigo_plattform_users }}'
tags:
- users
- name: "Insert/Update docker configuration"
template:
src: 'configs/docker/config.json.j2'
dest: '/home/{{ item }}/.docker/config.json'
owner: '{{ item }}'
group: '{{ item }}'
mode: 0600
loop: '{{ smardigo_plattform_users }}'
tags:
- users
- name: "Install common dependencies"
apt:
name: [
'mc',
'vim',
'zip',
'curl',
'htop',
'net-tools',
]
state: 'present'
when: ansible_distribution == "Ubuntu"
tags:
- install
- name: "Upgrade all packages"
apt:
name: '*'
state: latest
tags:
- install
when: ansible_distribution == "Ubuntu"
- name: "Ensure docker configuration directory exists"
file:
path: '/root/.docker/'
state: directory
owner: 'root'
group: 'root'
tags:
- config
- name: "Insert/Update docker configuration"
template:
src: 'configs/docker/config.json.j2'
dest: '/root/.docker/config.json'
owner: 'root'
group: 'root'
mode: 0600
tags:
- config
- name: "Insert/Update docker daemon configuration"
template:
src: 'configs/docker/daemon.json.j2'
dest: '/etc/docker/daemon.json'
owner: 'root'
group: 'root'
mode: 0600
tags:
- config
#- name: "Make sure line 'dns=none' is set in /etc/NetworkManager/NetworkManager.conf"
# ini_file:
# path: /etc/NetworkManager/NetworkManager.conf
# state: present
# no_extra_spaces: yes
# section: main
# option: dns
# value: none
# owner: root
# group: root
# mode: 0644
# notify:
# - reload NetworkManager
# tags:
# - config
#- name: "Deploy resolv.conf template"
# template:
# src: resolv.conf.j2
# dest: /etc/resolv.conf
# owner: root
# group: root
# mode: 0644
# notify:
# - reload NetworkManager
# tags:
# - config
# elasticsearch production mode requirements
- name: "Set vm.max_map_count"
sysctl:
name: vm.max_map_count
value: '262144'
sysctl_set: yes
state: present
tags:
- config
# elasticsearch production mode requirements
- name: "Set fs.file-max"
sysctl:
name: fs.file-max
value: '65536'
sysctl_set: yes
state: present
tags:
- config
- name: "Send mattermost messsge"
uri:
url: "{{ mattermost_hook_smardigo }}"
method: POST
body: "{{ lookup('template','mattermost-deploy-end.json.j2') }}"
body_format: json
headers:
Content-Type: "application/json"
delegate_to: 127.0.0.1
become: false
when:
- send_status_messages

@ -0,0 +1,41 @@
---
- name: 'apply setup to {{ host | default("all") }}'
hosts: '{{ host | default("all") }}'
serial: "{{ serial_number | default(5) }}"
become: yes
pre_tasks:
- name: "Check if ansible version is at least 2.10.x"
assert:
that:
- ansible_version.major >= 2
- ansible_version.minor >= 10
msg: "The ansible version has to be at least ({{ ansible_version.full }})"
- name: Remove outdated dependencies
apt:
name: [
'docker',
'docker-client',
'docker-client-latest',
'docker-common',
'docker-latest',
'docker-latest-logrotate',
'docker-logrotate',
'docker-engine',
'smartmontools',
]
state: 'absent'
when: ansible_distribution == "Ubuntu"
tags:
- install
roles:
- role: ansible-role-docker
vars:
docker_compose_version: '1.25.5'
docker_compose_path: '/usr/bin/docker-compose'
docker_users: '{{ smardigo_plattform_users }}'
- role: common
tags:
- common

@ -0,0 +1,3 @@
{
"text": "Role role {{ role_name }} on <{{ service_name }}> finished successfully."
}

@ -0,0 +1,3 @@
{
"text": "Start role {{ role_name }} on <{{ service_name }}>."
}

@ -0,0 +1,3 @@
{
"text": "Removed {{ service_name }} on {{ stage_server_url_host }} successfully."
}

@ -0,0 +1,3 @@
{
"text": "Removing {{ service_name }} on {{ stage_server_url_host }}."
}
Loading…
Cancel
Save