initial commit
commit
961498e32b
|
@ -0,0 +1 @@
|
|||
.vscode
|
|
@ -0,0 +1,45 @@
|
|||
openssh_port: "12322"
|
||||
|
||||
smtp_accounts:
|
||||
- name: ahoy
|
||||
host: mail.infomaniak.ch
|
||||
port: 587
|
||||
from: ahoy@pirateparty.be
|
||||
password: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
62633164383764376333643063363263356461613164663066623836613931306437633033633134
|
||||
3632326164663564653962613437376265333234313032360a313935303230393938356632356231
|
||||
34613661383736313232613131313262616261323464653936393634653464323631333839353030
|
||||
3230396536663635650a633537633633623365346563323334616338333436633537623831313165
|
||||
38343766346437626332313230346537663735313937643765353465356236633134
|
||||
smtp_default_account: ahoy
|
||||
smtp_default_contact: it@pirateparty.be
|
||||
|
||||
node_exporter_password: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
35333237666635633862336433303264613133376230346461333332636231643563636466356630
|
||||
3235623237303366626562393065353436663632306438370a333132653432636632643134326130
|
||||
66396666626631373637373065613137393232383361346438633763396266636264663364663238
|
||||
3363666332633562360a323532666664333266333761343136306133336138623137316234653939
|
||||
37643239613631383165656138633134663736393238343939336135303732333838336538373531
|
||||
3635396265643061356339333035393836313936316633623662
|
||||
|
||||
backup_targets:
|
||||
- host: storage.pirateparty.be
|
||||
ssh:
|
||||
port: 23
|
||||
username: "{{ storage_box_username }}"
|
||||
key_file: storage-box
|
||||
- host: batato.be
|
||||
ssh:
|
||||
key_file: batato
|
||||
|
||||
users:
|
||||
- name: hgo
|
||||
ssh_keys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOK8Y3OEq1j3rR8EOLpVPYZeA5qC0PTsctza9c2qhbU hadrien@terry
|
||||
- name: tierce
|
||||
ssh_keys:
|
||||
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC75IeAMEg6RwvbO6oLEQOpYSASGx9A3JD15gtA7D3NJFz+pZ7hBjBSjTxZrHDQLg1OFs0XRGS5DATRMnj6jSRAc25C71DewbNY9fWOH1/dAuo45zBllO3/pol17uYVqUbaPVjnqQFfikLCf7HjBbjt7JEVffJ3nkalE2q0TqjGK0JrltrL9dePE/R3ZNzVSDXvkgMsu18Wov9if6ftsKYgNTW+oOc9xoN1GSHYEnzv68+YNt3zKGTiwhU87cLyHJBu9o/wFDNOLdQcOtKa3IUPZvOgDlLrAm8a4Z9/A9DCJS/8kFmyNOazF1rupPAojn7k9mIBvVPxc5zqg+qrKbxR tierce@q
|
||||
|
||||
acme_email: it@pirateparty.be
|
|
@ -0,0 +1,41 @@
|
|||
alertmanager_smtp:
|
||||
from: ahoy@pirateparty.be
|
||||
smarthost: mail.infomaniak.ch:587
|
||||
auth_username: ahoy@pirateparty.be
|
||||
auth_password: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
61643536623562333434653364623535633331653539356132653863313965313030333163313637
|
||||
6161333463653839383265323937376630336134633531650a313132326536346530353764656465
|
||||
63323737643034353532333363303363616261363335333365663133626537653961323133626433
|
||||
6566656236383864610a323262393562663836343162326131336630363939356333313934326436
|
||||
6261
|
||||
|
||||
alertmanager_route:
|
||||
receiver: 'default-receiver'
|
||||
group_wait: 30s
|
||||
group_interval: 5m
|
||||
repeat_interval: 3h
|
||||
|
||||
alertmanager_receivers:
|
||||
- name: default-receiver
|
||||
email_configs:
|
||||
- to: hadrien@pirateparty.be
|
||||
|
||||
alertmanager_password: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
63313035633034643636326230383162666666626539623934303631366236656432616238356362
|
||||
6665626364643666343737623532616133303539356133300a396530643865323334313564363762
|
||||
31646562306232356437636537383732626664663166656331303630303537383064663565323235
|
||||
3962313936613039320a656337356131363636643366393233613462313361323639373363643134
|
||||
32383436313035323032656266376664383166633631663438316165313930373937636436633962
|
||||
6131336262343531643264346362343433373165386266323439
|
||||
prometheus_password: "{{ alertmanager_password }}"
|
||||
grafana_admin_user: ppbe
|
||||
grafana_admin_password: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
39626461326636633230343536613564643537376464336537353661636638303238303966383030
|
||||
3133663938623334396435333761306265373064353462610a356531326130396566386638653533
|
||||
36323833663030663466356538353237376137313135656534383439613935623234373065376530
|
||||
3864366438626135300a333664313339343964306538343366306639393631666366323537313734
|
||||
36613731626439646537653565646436323839383930363131653431306431396638613665616464
|
||||
3435313137313964636139366439336365663564326639303234
|
|
@ -0,0 +1,46 @@
|
|||
umurmur_version: 0.2.17
|
||||
umurmur_domain: mumble.parley.be
|
||||
|
||||
umurmur_welcome_text:
|
||||
- Welcome to Parley Talk!
|
||||
- You can talk to the people in the room you joined.
|
||||
- You start in the Welcome room, to join another channel right click the room name and choose "Join Channel"
|
||||
umurmur_admin_password: wC7yZ4vV2ocb7AkBfQ2RwuhRqYVyiwY42Rjpw3pfJ
|
||||
umurmur_max_users: 100
|
||||
|
||||
umurmur_channels:
|
||||
- name: Parley
|
||||
description: Main Parley Talk channel. No entry.
|
||||
noenter: yes
|
||||
- name: Welcome
|
||||
parent: Parley
|
||||
description: Welcome channel
|
||||
position: 0
|
||||
- name: Silent
|
||||
parent: Parley
|
||||
description: Silent channel
|
||||
silent: yes
|
||||
position: 1
|
||||
- name: Mary Read's territory
|
||||
description: The channel dedicated to Mary Read. She was a Caribbean pirate. Dressed as a man, Mary went to sea and later joined the British army, fighting in the War Of The Spanish Succession. She married and settled down as a woman, but dressed back as a man following the death of her husband, later boarding a ship bound for the West Indies.
|
||||
parent: Welcome
|
||||
- name: Anne Bonny crew
|
||||
description: The channel dedicated to Anne Bonny. She was one the most famous female pirates. She operated in the Caribbean. She discovered that one of her crew companion, Mark Read, was secretly a woman (Mary Read) and the two became very close.
|
||||
parent: Welcome
|
||||
- name: Mary Cricket's ship
|
||||
description: The channel dedicated to Mary Cricket. Toghether with 5 other prisoners Mary Crickett escaped and overpowered the two-man crew of the sloop John and Elizabeth on 12 May 1729. She held the prisoners in the ship's hold, sitting on the hatch to prevent their escape. The pair was released a few days later. The pirates sailed into Chesapeake Bay but before they could raid any other ships, they were captured by HMS Shoreham. Returned to Virginia, they were tried in August 1729, convicted of piracy, and sentenced to hang.
|
||||
parent: Welcome
|
||||
- name: Flora Burn's island
|
||||
description: The channel dedicated to Flora Burn. She began her pirate career in 1741 and operated mainly on the East Coast of North America.
|
||||
parent: Welcome
|
||||
- name: Sayyida al Hurra
|
||||
description: She was a Moroccan pirate from the 16th century, and controlled the Mediterranean Sea together with Barbarossa. Her name means "noble lady who is free and independent"
|
||||
parent: Welcome
|
||||
- name: Ching Shih
|
||||
description: She was a Chinese pirate from the 19th century. She is considered to be the pirate with the largest crew ever assembled (between 20.000 and 40.000 pirates) and died peacefully as a free woman
|
||||
parent: Welcome
|
||||
|
||||
umurmur_default_channel: Welcome
|
||||
|
||||
mumble_web_domain: talk.parley.be
|
||||
mumble_web_version: c03b78d096eae69e1cec82e148f65e7a8541bd68
|
|
@ -0,0 +1 @@
|
|||
ansible_user: admin
|
|
@ -0,0 +1,8 @@
|
|||
storage_box_username: u212275-sub5
|
||||
storage_box_password: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
36313662333062323531613966386365373339663566303133653562663838316632613830613264
|
||||
6564333736343830623061313534313630313534316231390a666662633861383563333562356561
|
||||
64616534313266323833383331313334333761323965383634666635663430366461353437616465
|
||||
6337363536643738310a656530663837386537336434633037376463336165613239323265366234
|
||||
64663863333763356430616635323061396663373264343666323831646664646430
|
|
@ -0,0 +1,11 @@
|
|||
[web]
|
||||
mastodon.pirateparty.be
|
||||
pirateparty.be
|
||||
wiki.pirateparty.be
|
||||
talk.parley.be
|
||||
|
||||
[monitoring]
|
||||
status.pirateparty.be
|
||||
|
||||
[mumble]
|
||||
talk.parley.be
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
- hosts: all
|
||||
become: yes
|
||||
|
||||
pre_tasks:
|
||||
- name: Update apt cache
|
||||
apt:
|
||||
update_cache: yes
|
||||
cache_valid_time: 3600
|
||||
|
||||
roles:
|
||||
- common
|
|
@ -0,0 +1,4 @@
|
|||
- hosts: monitoring
|
||||
roles:
|
||||
- common
|
||||
- monitoring
|
|
@ -0,0 +1,10 @@
|
|||
- hosts: mumble
|
||||
become: yes
|
||||
|
||||
vars:
|
||||
acme_domains:
|
||||
- "{{ umurmur_domain }}"
|
||||
- "{{ mumble_web_domain }}"
|
||||
roles:
|
||||
- acme
|
||||
- mumble
|
|
@ -0,0 +1,9 @@
|
|||
acme_directory: https://acme-v02.api.letsencrypt.org/directory
|
||||
acme_config_dir: /etc/ssl
|
||||
acme_keys_dir: "{{ acme_config_dir }}/private"
|
||||
acme_csr_dir: "{{ acme_config_dir }}/csr"
|
||||
acme_certs_dir: "{{ acme_config_dir }}/certs"
|
||||
acme_accounts_dir: "{{ acme_config_dir }}/accounts"
|
||||
acme_account_key: "acme_account.key"
|
||||
acme_ssl_group: ssl-cert
|
||||
acme_challenge_dir: /var/www/acme
|
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import josepy as jose
|
||||
from OpenSSL import crypto
|
||||
from acme import client, messages, challenges, crypto_util
|
||||
|
||||
SSL_CONFIG_DIR="/etc/ssl"
|
||||
OPENSSL_DATE_FORMAT = '%Y%m%d%H%M%SZ'
|
||||
USER_AGENT="python-acme"
|
||||
|
||||
def select_http01_challenge(order):
|
||||
for auth in order.authorizations:
|
||||
for challenge in auth.body.challenges:
|
||||
if isinstance(challenge.chall, challenges.HTTP01):
|
||||
return challenge
|
||||
raise Exception("HTTP-01 challenge was not offered by the CA server.")
|
||||
|
||||
def perform_http01_challenge(client_acme, challenge, order, challenge_dir):
|
||||
response, validation = challenge.response_and_validation(client_acme.net.key)
|
||||
validation_filename, validation_content = validation.split('.')
|
||||
|
||||
challenge_path = os.path.join(challenge_dir, validation_filename)
|
||||
with open(challenge_path, 'w') as f_out:
|
||||
f_out.write(validation_content)
|
||||
|
||||
client_acme.answer_challenge(challenge, response)
|
||||
|
||||
fullchain_pem = client_acme.poll_and_finalize(order).fullchain_pem
|
||||
|
||||
os.remove(challenge_path)
|
||||
|
||||
return fullchain_pem
|
||||
|
||||
def renew_cert(domain,
|
||||
account_key_path,
|
||||
privkey_path,
|
||||
csr_path,
|
||||
certs_dir,
|
||||
challenge_dir,
|
||||
directory_url,
|
||||
days_before_renewal,
|
||||
force = False):
|
||||
logging.info(f"Checking {domain} certificate's expiration date…")
|
||||
now = datetime.now()
|
||||
cert_expiration_date = now
|
||||
|
||||
fullchain_path = os.path.join(certs_dir, "fullchain.pem")
|
||||
try:
|
||||
with open(fullchain_path, 'rb') as pem_in:
|
||||
fullchain_pem = crypto.load_certificate(crypto.FILETYPE_PEM, pem_in.read())
|
||||
cert_expiration_date = datetime.strptime(fullchain_pem.get_notAfter().decode(), OPENSSL_DATE_FORMAT)
|
||||
except IOError as e:
|
||||
logging.warning(f"Unable to load {domain} certificate: {e}")
|
||||
|
||||
cert_expiration_days = (cert_expiration_date - now).days
|
||||
if not force and cert_expiration_days >= days_before_renewal:
|
||||
logging.info(f"Certificate expires in {cert_expiration_days} days. Nothing to do.")
|
||||
return
|
||||
|
||||
logging.info(f"Certificate expires in {cert_expiration_days} days! Renewing certificate…")
|
||||
|
||||
logging.info(f"Loading account key from {account_key_path}…")
|
||||
with open(account_key_path, 'rb') as pem_in:
|
||||
account_pem = crypto.load_privatekey(crypto.FILETYPE_PEM, pem_in.read())
|
||||
account_jwk = jose.JWKRSA(key=account_pem.to_cryptography_key())
|
||||
account_jwk_pub = account_jwk.public_key()
|
||||
|
||||
logging.info(f"Loading {domain} CSR file from {csr_path}…")
|
||||
try:
|
||||
with open(csr_path, 'r') as pem_in:
|
||||
csr_pem = pem_in.read()
|
||||
except IOError as e:
|
||||
logging.warning(f"Unable to load CSR file: {e}")
|
||||
logging.info(f"Loading {domain} private key from {privkey_path}…")
|
||||
with open(privkey_path, 'r') as pem_in:
|
||||
privkey_pem = pem_in.read()
|
||||
|
||||
csr_pem = crypto_util.make_csr(privkey_pem, [domain])
|
||||
|
||||
client_network = client.ClientNetwork(account_jwk, user_agent=USER_AGENT)
|
||||
directory = messages.Directory.from_json(client_network.get(directory_url).json())
|
||||
client_acme = client.ClientV2(directory, net=client_network)
|
||||
|
||||
logging.info("Registering with ACME account…")
|
||||
# Here we assume we already have an account
|
||||
registration_message = messages.NewRegistration(
|
||||
key=account_jwk_pub,
|
||||
only_return_existing=True)
|
||||
registration_response = client_acme._post(directory["newAccount"], registration_message)
|
||||
account = client_acme._regr_from_response(registration_response)
|
||||
client_acme.net.account = account
|
||||
|
||||
logging.info("Ordering ACME challenge…")
|
||||
order = client_acme.new_order(csr_pem)
|
||||
|
||||
logging.info("Selecting HTTP-01 ACME challenge…")
|
||||
challenge = select_http01_challenge(order)
|
||||
|
||||
logging.info("Performing HTTP-01 ACME challenge…")
|
||||
fullchain_pem = perform_http01_challenge(client_acme, challenge, order, challenge_dir)
|
||||
|
||||
logging.info(f"Writing {domain} certificates into {certs_dir}…")
|
||||
certs_pem = []
|
||||
for line in fullchain_pem.split('\n'):
|
||||
if 'BEGIN CERTIFICATE' in line:
|
||||
cert_pem = ''
|
||||
|
||||
cert_pem += line + '\n'
|
||||
|
||||
if 'END CERTIFICATE' in line:
|
||||
certs_pem.append(cert_pem)
|
||||
|
||||
cert_path = os.path.join(certs_dir, "cert.pem")
|
||||
with open(cert_path, 'w') as pem_out:
|
||||
pem_out.write(certs_pem[0])
|
||||
|
||||
chain_path = os.path.join(certs_dir, "chain.pem")
|
||||
with open(chain_path, 'w') as pem_out:
|
||||
pem_out.write(''.join(certs_pem[1:]))
|
||||
|
||||
with open(fullchain_path, 'w') as pem_out:
|
||||
pem_out.write(fullchain_pem)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Renew ACME certificates for a list of domains. This script assumes you already have an account on the CA server and a private key for each certificate.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("domains", nargs='+',
|
||||
help="List of domain names for which to renew the certificate.")
|
||||
parser.add_argument("--account_key", "-a",
|
||||
default=os.path.join(SSL_CONFIG_DIR, "accounts", "acme_account.key"),
|
||||
help="Path to the account key.")
|
||||
parser.add_argument("--privkey", "-p",
|
||||
default=os.path.join(SSL_CONFIG_DIR, "private", "{domain}.pem"),
|
||||
help="Path to the private certificate.")
|
||||
parser.add_argument("--csr", "-r",
|
||||
default=os.path.join(SSL_CONFIG_DIR, "csr", "{domain}.pem"),
|
||||
help="Path to the CSR file. If the file doesn't exist, it will be generated from the private key and the domain name.")
|
||||
parser.add_argument("--certs", "-o",
|
||||
default=os.path.join(SSL_CONFIG_DIR, "certs", "{domain}.d"),
|
||||
help="Path to the certificates directory.")
|
||||
parser.add_argument("--challenge", "-c",
|
||||
default="/var/www/html/.well-known/acme-challenge",
|
||||
help="Path to the challenge directory.")
|
||||
parser.add_argument("--directory_url", "-u",
|
||||
default="https://acme-v02.api.letsencrypt.org/directory",
|
||||
help="Directory URL on which performing ACME challenges. Only ACME v2 is supported.")
|
||||
parser.add_argument("--days", "-d", type=int,
|
||||
default=30,
|
||||
help="Days before attempting to renew the certificates.")
|
||||
parser.add_argument("--quiet", "-q",
|
||||
action="store_true",
|
||||
help="Quiet mode.")
|
||||
parser.add_argument("--force", "-f",
|
||||
action="store_true",
|
||||
help="Force certificates renewal without checking their expiration date.")
|
||||
args = parser.parse_args()
|
||||
|
||||
log_level = logging.WARN if args.quiet else logging.INFO
|
||||
logging.basicConfig(stream=sys.stdout, level=log_level, format="%(levelname)s:%(message)s")
|
||||
|
||||
for domain in args.domains:
|
||||
account_key = args.account_key.format(domain=domain)
|
||||
privkey = args.privkey.format(domain=domain)
|
||||
csr = args.csr.format(domain=domain)
|
||||
certs_dir = args.certs.format(domain=domain)
|
||||
challenge_dir = args.challenge.format(domain=domain)
|
||||
|
||||
renew_cert(domain,
|
||||
account_key_path=account_key,
|
||||
privkey_path=privkey,
|
||||
csr_path=csr,
|
||||
certs_dir=certs_dir,
|
||||
challenge_dir=challenge_dir,
|
||||
directory_url=args.directory_url,
|
||||
days_before_renewal=args.days,
|
||||
force=args.force)
|
|
@ -0,0 +1,76 @@
|
|||
- name: Install ACME dependencies
|
||||
apt:
|
||||
name: python3-acme
|
||||
state: present
|
||||
tags: acme_install
|
||||
|
||||
- name: Create Let's Encrypt config directories
|
||||
file:
|
||||
path: "{{ config_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: "{{ acme_ssl_group }}"
|
||||
mode: "711"
|
||||
loop:
|
||||
- "{{ acme_config_dir }}"
|
||||
- "{{ acme_keys_dir }}"
|
||||
- "{{ acme_accounts_dir }}"
|
||||
- "{{ acme_csr_dir }}"
|
||||
loop_control:
|
||||
loop_var: config_dir
|
||||
tags: acme_install
|
||||
|
||||
- name: Create challenge directory
|
||||
file:
|
||||
path: "{{ acme_challenge_dir }}/.well-known/acme-challenge"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "755"
|
||||
tags: acme_install
|
||||
|
||||
- name: Perform ACME challenge for each domain
|
||||
include_tasks:
|
||||
file: challenge.yml
|
||||
apply:
|
||||
tags: acme_challenge
|
||||
loop: "{{ acme_domains | unique }}"
|
||||
loop_control:
|
||||
loop_var: domain_name
|
||||
tags: acme_challenge
|
||||
|
||||
- name: Create directory for certificate renewal tool
|
||||
file:
|
||||
path: /opt/acme
|
||||
owner: root
|
||||
group: root
|
||||
mode: "755"
|
||||
state: directory
|
||||
tags: acme_renew
|
||||
|
||||
- name: Copy script to renew ACME certificates
|
||||
copy:
|
||||
src: acme_renew_cert.py
|
||||
dest: /opt/acme/acme_renew_cert.py
|
||||
owner: root
|
||||
group: root
|
||||
mode: "755"
|
||||
tags: acme_renew
|
||||
|
||||
- name: Setup cron job for ACME certificates renewal of {{ domain_name }}
|
||||
cron:
|
||||
name: acme renew {{ domain_name }} cert
|
||||
job: >-
|
||||
sleep $((RANDOM % 3600)) && /opt/acme/acme_renew_cert.py {{ domain_name }} -q
|
||||
-a {{ (acme_accounts_dir + '/' + acme_account_key) | quote }}
|
||||
-p {{ acme_keys_dir | quote }}/{domain}.pem
|
||||
-r {{ acme_csr_dir | quote }}/{domain}.csr
|
||||
-o {{ acme_certs_dir | quote }}/{domain}.d
|
||||
-c {{ acme_challenge_dir | quote }}/.well-known/acme-challenge
|
||||
minute: "30"
|
||||
hour: "2"
|
||||
state: present
|
||||
loop: "{{ acme_domains | unique }}"
|
||||
loop_control:
|
||||
loop_var: domain_name
|
||||
tags: acme_renew
|
|
@ -0,0 +1,91 @@
|
|||
- name: Create {{ domain_name }} certificates directory
|
||||
file:
|
||||
path: "{{ acme_certs_dir }}/{{ domain_name }}.d"
|
||||
state: directory
|
||||
owner: root
|
||||
group: "{{ acme_ssl_group }}"
|
||||
mode: "755"
|
||||
tags: acme_install
|
||||
|
||||
- name: Generate Let's Encrypt account key
|
||||
openssl_privatekey:
|
||||
path: "{{ acme_accounts_dir }}/{{ acme_account_key }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "600"
|
||||
type: RSA
|
||||
size: 4096
|
||||
tags: acme_account
|
||||
|
||||
- name: Generate Let's Encrypt private key for {{ domain_name }}
|
||||
openssl_privatekey:
|
||||
path: "{{ acme_keys_dir }}/{{ domain_name }}.pem"
|
||||
owner: root
|
||||
group: "{{ acme_ssl_group }}"
|
||||
mode: "640"
|
||||
type: RSA
|
||||
size: 4096
|
||||
|
||||
- name: Generate Let's Encrypt CSR for {{ domain_name }}
|
||||
openssl_csr:
|
||||
path: "{{ acme_csr_dir }}/{{ domain_name }}.csr"
|
||||
owner: root
|
||||
group: "{{ acme_ssl_group }}"
|
||||
mode: "644"
|
||||
privatekey_path: "{{ acme_keys_dir }}/{{ domain_name }}.pem"
|
||||
common_name: "{{ domain_name }}"
|
||||
|
||||
# - name: Check if Let's Encrypt certificate already exists for {{ domain_name }}
|
||||
# stat:
|
||||
# path: "{{ acme_certs_dir }}/{{ domain_name }}.d/cert.pem"
|
||||
# register: _acme_cert_file
|
||||
|
||||
# - name: Check Let's Encrypt certificate expiration date for {{ domain_name }}
|
||||
# openssl_certificate_info:
|
||||
# path: "{{ acme_certs_dir }}/{{ domain_name }}.d/cert.pem"
|
||||
# valid_at:
|
||||
# thirty_days: "+30d"
|
||||
# register: _acme_cert_validity
|
||||
# when: _acme_cert_file.stat.isreg is defined and _acme_cert_file.stat.isreg
|
||||
|
||||
- name: Begin Let's Encrypt challenges for {{ domain_name }}
|
||||
acme_certificate:
|
||||
acme_directory: "{{ acme_directory }}"
|
||||
acme_version: "{{ acme_version }}"
|
||||
account_key_src: "{{ acme_accounts_dir }}/{{ acme_account_key }}"
|
||||
account_email: "{{ acme_email }}"
|
||||
terms_agreed: yes
|
||||
challenge: http-01
|
||||
csr: "{{ acme_csr_dir }}/{{ domain_name }}.csr"
|
||||
dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/cert.pem"
|
||||
fullchain_dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/fullchain.pem"
|
||||
remaining_days: 30
|
||||
register: _acme_challenge
|
||||
# when: _acme_cert_validity is skipped or not _acme_cert_validity.valid_at.thirty_days
|
||||
|
||||
- debug:
|
||||
var: _acme_challenge
|
||||
|
||||
# - name: Implement and complete Let's Encrypt challenge for {{ domain_name }}
|
||||
# when: _acme_challenge is not skipped
|
||||
# block:
|
||||
# - name: Implement http-01 challenge files for {{ domain_name }}
|
||||
# copy:
|
||||
# content: "{{ _acme_challenge.challenge_data[domain_name]['http-01'].resource_value }}"
|
||||
# dest: "{{ acme_challenge_dir }}/{{ _acme_challenge.challenge_data[domain_name]['http-01'].resource }}"
|
||||
# owner: root
|
||||
# group: root
|
||||
# mode: "644"
|
||||
|
||||
# - name: Complete Let's Encrypt challenges for {{ domain_name }}
|
||||
# acme_certificate:
|
||||
# acme_directory: "{{ acme_directory }}"
|
||||
# acme_version: "{{ acme_version }}"
|
||||
# account_key_src: "{{ acme_accounts_dir }}/{{ acme_account_key }}"
|
||||
# account_email: "{{ acme_email }}"
|
||||
# challenge: http-01
|
||||
# csr: "{{ acme_csr_dir }}/{{ domain_name }}.csr"
|
||||
# dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/cert.pem"
|
||||
# chain_dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/chain.pem"
|
||||
# fullchain_dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/fullchain.pem"
|
||||
# data: "{{ _acme_challenge }}"
|
|
@ -0,0 +1,2 @@
|
|||
- import_tasks: acme.yml
|
||||
tags: acme
|
|
@ -0,0 +1 @@
|
|||
acme_version: 2
|
|
@ -0,0 +1,38 @@
|
|||
Role Name
|
||||
=========
|
||||
|
||||
A brief description of the role goes here.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||
|
||||
Role Variables
|
||||
--------------
|
||||
|
||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||
|
||||
Example Playbook
|
||||
----------------
|
||||
|
||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||
|
||||
- hosts: servers
|
||||
roles:
|
||||
- { role: username.rolename, x: 42 }
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
BSD
|
||||
|
||||
Author Information
|
||||
------------------
|
||||
|
||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
# defaults file for common
|
||||
node_exporter_path: /
|
||||
node_exporter_port: 9100
|
||||
node_exporter_public_port: "9180"
|
||||
|
||||
nginx_config_dir: /etc/nginx/conf.d
|
||||
nginx_ssl_dir: /etc/nginx/ssl
|
||||
|
||||
ssh_config_dir: ~/.ssh
|
||||
backup_targets: []
|
|
@ -0,0 +1,8 @@
|
|||
- name: restart openssh
|
||||
service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
|
||||
- name: reload nginx
|
||||
include_tasks: ../handlers/nginx.yml
|
||||
when: nginx_started is not changed
|
|
@ -0,0 +1,7 @@
|
|||
- name: Validate Nginx config
|
||||
command: nginx -t
|
||||
|
||||
- name: Reload Nginx server
|
||||
service:
|
||||
name: nginx
|
||||
state: reloaded
|
|
@ -0,0 +1,16 @@
|
|||
- import_tasks: repos.yml
|
||||
tags: repos
|
||||
- import_tasks: users.yml
|
||||
tags: users
|
||||
- import_tasks: openssh.yml
|
||||
tags: openssh
|
||||
- import_tasks: ufw.yml
|
||||
tags: firewall
|
||||
- import_tasks: msmtp.yml
|
||||
tags: smtp
|
||||
- import_tasks: nginx.yml
|
||||
tags: nginx
|
||||
- import_tasks: node_exporter.yml
|
||||
tags: node_exporter
|
||||
#- import_tasks: backup.yml
|
||||
# tags: backup
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
# Install and configure SMTP relay
|
||||
- name: Install msmtp
|
||||
apt:
|
||||
name:
|
||||
- msmtp
|
||||
- msmtp-mta
|
||||
state: present
|
||||
|
||||
- name: Copy msmtp configuration
|
||||
template:
|
||||
src: msmtp/msmtprc.j2
|
||||
dest: /etc/msmtprc
|
||||
|
||||
- name: Copy aliases
|
||||
template:
|
||||
src: msmtp/aliases.j2
|
||||
dest: /etc/aliases
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
# Install and configure Nginx
|
||||
- name: Install htpasswd dependencies
|
||||
apt:
|
||||
name: python-passlib
|
||||
state: present
|
||||
|
||||
- name: Install SSL dependencies
|
||||
apt:
|
||||
name: ssl-cert
|
||||
state: present
|
||||
|
||||
- name: Install Nginx
|
||||
apt:
|
||||
name: nginx-full
|
||||
state: present
|
||||
|
||||
- name: Create Nginx configuration directories
|
||||
file:
|
||||
path: "{{ config_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: www-data
|
||||
mode: "755"
|
||||
loop:
|
||||
- "{{ nginx_config_dir }}"
|
||||
- "{{ nginx_ssl_dir }}"
|
||||
loop_control:
|
||||
loop_var: config_dir
|
||||
|
||||
- name: Generate Diffie-Hellman parameters
|
||||
# This can take a long time... So we are doing it in async mode
|
||||
openssl_dhparam:
|
||||
path: "{{ nginx_ssl_dir }}/dhparam.pem"
|
||||
size: 3072
|
||||
owner: root
|
||||
group: www-data
|
||||
async: 3600
|
||||
poll: 0
|
||||
register: nginx_dh
|
||||
|
||||
- name: Use snakoil cert key as Nginx's default private key
|
||||
file:
|
||||
src: "/etc/ssl/private/ssl-cert-snakeoil.key"
|
||||
path: "{{ nginx_ssl_dir }}/nginx.key"
|
||||
state: link
|
||||
owner: root
|
||||
group: www-data
|
||||
mode: "750"
|
||||
force: yes
|
||||
|
||||
- name: Use snakoil cert as Nginx's default certificate
|
||||
file:
|
||||
src: "/etc/ssl/certs/ssl-cert-snakeoil.pem"
|
||||
path: "{{ nginx_ssl_dir }}/nginx.crt"
|
||||
state: link
|
||||
owner: root
|
||||
group: www-data
|
||||
mode: "755"
|
||||
force: yes
|
||||
|
||||
- name: Copy default Nginx config
|
||||
template:
|
||||
src: nginx/default.conf.j2
|
||||
dest: /etc/nginx/sites-available/default
|
||||
owner: root
|
||||
group: www-data
|
||||
mode: "755"
|
||||
notify: reload nginx
|
||||
|
||||
- name: Enable default Nginx config
|
||||
file:
|
||||
src: /etc/nginx/sites-available/default
|
||||
dest: /etc/nginx/sites-enabled/default
|
||||
owner: root
|
||||
group: www-data
|
||||
mode: "755"
|
||||
notify: reload nginx
|
||||
|
||||
- name: Allow default Nginx ports
|
||||
ufw:
|
||||
rule: allow
|
||||
name: "Nginx Full"
|
||||
|
||||
- name: Waiting for Diffie-Hellman task to complete…
|
||||
async_status:
|
||||
jid: "{{ nginx_dh.ansible_job_id }}"
|
||||
register: nginx_dh_job
|
||||
retries: 60
|
||||
delay: 30 # will retry every 30s for 30min (60 retries)
|
||||
until: nginx_dh_job.finished
|
||||
|
||||
- name: Start Nginx server
|
||||
service:
|
||||
name: nginx
|
||||
state: started
|
||||
enabled: yes
|
||||
register: nginx_started
|
||||
|
||||
- name: "Trigger Nginx handlers"
|
||||
meta: flush_handlers
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
# Install and configure node-exporter
|
||||
- name: Include role for installing node-exporter
|
||||
include_role:
|
||||
name: cloudalchemy.node-exporter
|
||||
public: yes
|
||||
vars:
|
||||
node_exporter_web_listen_address: "0.0.0.0:{{ node_exporter_port }}"
|
||||
|
||||
- name: Configure Nginx for node-exporter
|
||||
import_role:
|
||||
name: nginx
|
||||
vars:
|
||||
nginx_config_file: node-exporter.conf
|
||||
nginx_server:
|
||||
name: "{{ inventory_hostname }}"
|
||||
port: "{{ node_exporter_public_port }}"
|
||||
locations:
|
||||
- path: "{{ node_exporter_path }}"
|
||||
basic_auth:
|
||||
file: .htpasswd.node-exporter
|
||||
password: "{{ node_exporter_password }}"
|
||||
proxy_pass:
|
||||
port: "{{ node_exporter_port }}"
|
||||
path: /metrics
|
||||
|
||||
- name: Allow node-exporter port {{ node_exporter_public_port }}
|
||||
ufw:
|
||||
rule: allow
|
||||
port: "{{ node_exporter_public_port }}"
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
# Configure OpenSSH server
|
||||
- name: Configure OpenSSH server
|
||||
template:
|
||||
src: openssh/sshd_config.j2
|
||||
dest: /etc/ssh/sshd_config
|
||||
backup: yes
|
||||
owner: "0"
|
||||
group: "0"
|
||||
mode: "0644"
|
||||
validate: '/usr/sbin/sshd -T -f %s'
|
||||
notify: restart openssh
|
||||
|
||||
- name: Trigger Ansible handlers
|
||||
meta: flush_handlers
|
||||
|
||||
- name: Change Ansible SSH port to {{ openssh_port }}
|
||||
set_fact:
|
||||
ansible_port: "{{ openssh_port }}"
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
# Configure APT repositories and automatic upgrades
|
||||
- name: Safely upgrade the server
|
||||
apt:
|
||||
upgrade: safe
|
||||
update_cache: yes
|
||||
cache_valid_time: "3600"
|
||||
|
||||
- name: Install unattend-upgrades for automatic upgrades
|
||||
apt:
|
||||
name: unattended-upgrades
|
||||
state: present
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
# Install and configure UFW, the uncomplicated firewall
|
||||
- name: Install UFW, the uncomplicated firewall
|
||||
apt:
|
||||
name: ufw
|
||||
state: present
|
||||
|
||||
- name: Allow OpenSSH port {{ openssh_port }}
|
||||
ufw:
|
||||
rule: allow
|
||||
port: "{{ openssh_port }}"
|
||||
|
||||
- name: Enable UFW config
|
||||
ufw:
|
||||
state: enabled
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
# Create an user and add their SSH public keys
|
||||
- name: Create user {{ user.name }} with no password
|
||||
user:
|
||||
name: "{{ user.name }}"
|
||||
shell: /bin/bash
|
||||
# See https://unix.stackexchange.com/questions/193066/how-to-unlock-account-for-public-key-ssh-authorization-but-not-for-password-aut/193131#193131
|
||||
password: '*'
|
||||
groups:
|
||||
- sudo
|
||||
append: yes
|
||||
state: present
|
||||
update_password: on_create
|
||||
|
||||
- name: Add SSH public keys for user {{ user.name }}
|
||||
authorized_key:
|
||||
user: "{{ user.name }}"
|
||||
state: present
|
||||
# we can pass multiple SSH keys, but they must be separated by newlines
|
||||
key: "{{ user.ssh_keys | join('\n') }}"
|
||||
# remove obsolete keys
|
||||
exclusive: yes
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
# Create users and add their SSH public keys
|
||||
- name: Install sudo package
|
||||
package:
|
||||
name: sudo
|
||||
state: present
|
||||
tags: sudo
|
||||
|
||||
- name: Remove password to become root with sudo
|
||||
lineinfile:
|
||||
path: /etc/sudoers
|
||||
state: present
|
||||
regexp: '^%sudo'
|
||||
line: '%sudo ALL=(ALL) NOPASSWD: ALL'
|
||||
validate: 'visudo -cf %s'
|
||||
tags: sudo
|
||||
|
||||
- name: Remove password for root user
|
||||
user:
|
||||
name: root
|
||||
shell: /bin/bash
|
||||
state: present
|
||||
|
||||
- name: Create users and add their SSH public keys
|
||||
include_tasks: user.yml
|
||||
loop: "{{ users }}"
|
||||
loop_control:
|
||||
loop_var: user
|
|
@ -0,0 +1 @@
|
|||
default: {{ smtp_default_contact }}
|
|
@ -0,0 +1,19 @@
|
|||
defaults
|
||||
auth on
|
||||
tls on
|
||||
tls_trust_file /etc/ssl/certs/ca-certificates.crt
|
||||
logfile /var/log/msmtp.log
|
||||
|
||||
{% for account in smtp_accounts %}
|
||||
account {{ account.name }}
|
||||
host {{ account.host }}
|
||||
port 587
|
||||
from {{ account.from }}
|
||||
user {{ account.user | default(account.from) }}
|
||||
password {{ account.password }}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
account default : {{ smtp_default_account }}
|
||||
|
||||
aliases /etc/aliases
|
|
@ -0,0 +1,62 @@
|
|||
{{ ansible_managed | comment }}
|
||||
# Default server configuration
|
||||
#
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server ipv6only=on;
|
||||
|
||||
root /var/www/default;
|
||||
|
||||
server_name _;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
root /var/www/acme;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location = /favicon.ico {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /robots.txt {
|
||||
allow all;
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location ~* ^.+.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js)$ {
|
||||
expires 7d;
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
location ~ /nginx_status {
|
||||
stub_status on;
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 default_server;
|
||||
listen [::]:443 default_server;
|
||||
|
||||
server_name _;
|
||||
|
||||
include snippets/snakeoil.conf;
|
||||
|
||||
return 444;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{{ ansible_managed | comment }}
|
||||
|
||||
{% if nginx_server_name is defined %}
|
||||
server {
|
||||
listen {{ nginx_port }};
|
||||
|
||||
server_name {{ nginx_server_name }};
|
||||
{% endif %}
|
||||
|
||||
{% for location in nginx_locations %}
|
||||
location {{ location }} {
|
||||
{% if location.proxy_pass is defined %}
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_redirect off;
|
||||
{% endif %}
|
||||
|
||||
{% if location.basic_auth_file is defined %}
|
||||
auth_basic "Authentication required";
|
||||
auth_basic_user_file /etc/nginx/{{ location.basic_auth_file }};
|
||||
{% endif %}
|
||||
|
||||
{% if location.proxy_pass is defined %}
|
||||
proxy_pass http://localhost:{{ location.proxy_pass.port | default('80') }}{{ location.proxy_pass.path }};
|
||||
{% endif %}
|
||||
}
|
||||
{% endif %}
|
||||
{% if nginx_server_name is defined %}
|
||||
}
|
||||
{% endif %}
|
|
@ -0,0 +1,17 @@
|
|||
# {{ ansible_managed }}
|
||||
|
||||
Port {{ openssh_port }}
|
||||
|
||||
PermitRootLogin no
|
||||
|
||||
ChallengeResponseAuthentication no
|
||||
|
||||
UsePAM yes
|
||||
|
||||
X11Forwarding yes
|
||||
|
||||
PrintMotd no
|
||||
|
||||
AcceptEnv LANG LC_*
|
||||
|
||||
Subsystem sftp /usr/lib/openssh/sftp-server
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
# vars file for common
|
|
@ -0,0 +1,38 @@
|
|||
Role Name
|
||||
=========
|
||||
|
||||
A brief description of the role goes here.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||
|
||||
Role Variables
|
||||
--------------
|
||||
|
||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||
|
||||
Example Playbook
|
||||
----------------
|
||||
|
||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||
|
||||
- hosts: servers
|
||||
roles:
|
||||
- { role: username.rolename, x: 42 }
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
BSD
|
||||
|
||||
Author Information
|
||||
------------------
|
||||
|
||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
# defaults file for roles/docker
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
# handlers file for roles/docker
|
|
@ -0,0 +1,53 @@
|
|||
galaxy_info:
|
||||
author: your name
|
||||
description: your description
|
||||
company: your company (optional)
|
||||
|
||||
# If the issue tracker for your role is not on github, uncomment the
|
||||
# next line and provide a value
|
||||
# issue_tracker_url: http://example.com/issue/tracker
|
||||
|
||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||
# - BSD-3-Clause (default)
|
||||
# - MIT
|
||||
# - GPL-2.0-or-later
|
||||
# - GPL-3.0-only
|
||||
# - Apache-2.0
|
||||
# - CC-BY-4.0
|
||||
license: license (GPL-2.0-or-later, MIT, etc)
|
||||
|
||||
min_ansible_version: 2.4
|
||||
|
||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||
# min_ansible_container_version:
|
||||
|
||||
#
|
||||
# Provide a list of supported platforms, and for each platform a list of versions.
|
||||
# If you don't wish to enumerate all versions for a particular platform, use 'all'.
|
||||
# To view available platforms and versions (or releases), visit:
|
||||
# https://galaxy.ansible.com/api/v1/platforms/
|
||||
#
|
||||
# platforms:
|
||||
# - name: Fedora
|
||||
# versions:
|
||||
# - all
|
||||
# - 25
|
||||
# - name: SomePlatform
|
||||
# versions:
|
||||
# - all
|
||||
# - 1.0
|
||||
# - 7
|
||||
# - 99.99
|
||||
|
||||
galaxy_tags: []
|
||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||
# remove the '[]' above, if you add tags to this list.
|
||||
#
|
||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||
# Maximum 20 tags per role.
|
||||
|
||||
dependencies: []
|
||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||
# if you add dependencies to this list.
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
# tasks file for roles/docker
|
||||
- name: install required system packages
|
||||
apt:
|
||||
names:
|
||||
- apt-transport-https
|
||||
- 'ca-certificates'
|
||||
- 'curl'
|
||||
- 'software-properties-common'
|
||||
- 'python3-pip'
|
||||
- 'virtualenv'
|
||||
- 'python3-setuptools'
|
||||
state: present
|
||||
|
||||
- name: add Docker GPG apt Key
|
||||
apt_key:
|
||||
url: https://download.docker.com/linux/ubuntu/gpg
|
||||
state: present
|
||||
|
||||
- name: add Docker Repository
|
||||
apt_repository:
|
||||
repo: deb https://download.docker.com/linux/ubuntu bionic stable
|
||||
state: present
|
||||
register: docker_repo
|
||||
|
||||
- name: update apt cache
|
||||
apt:
|
||||
update_cache: yes
|
||||
cache_valid_time: 3600
|
||||
when: docker_repo is changed
|
||||
|
||||
- name: install docker-ce package
|
||||
apt:
|
||||
name: docker-ce
|
||||
state: present
|
||||
|
||||
- name: enable docker service
|
||||
systemd:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: yes
|
||||
|
||||
- name: install Docker Module for Python
|
||||
pip:
|
||||
name: docker
|
|
@ -0,0 +1,2 @@
|
|||
localhost
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- hosts: localhost
|
||||
remote_user: root
|
||||
roles:
|
||||
- roles/docker
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
# vars file for roles/docker
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
# defaults file for roles/prometheus
|
||||
grafana_admin_user: admin
|
||||
grafana_domain: "{{ inventory_hostname }}"
|
||||
grafana_web_path: /grafana
|
||||
grafana_protocol: http
|
||||
grafana_port: 3000
|
||||
|
||||
prometheus_domain: "{{ inventory_hostname }}"
|
||||
prometheus_web_path: /prometheus
|
||||
prometheus_port: 9090
|
||||
|
||||
nginx_default_path: "{{ grafana_web_path }}"
|
||||
|
||||
alertmanager_domain: "{{ inventory_hostname }}"
|
||||
alertmanager_web_path: /alertmanager
|
||||
alertmanager_port: 9093
|
|
@ -0,0 +1,43 @@
|
|||
groups:
|
||||
- name: Hardware alerts
|
||||
rules:
|
||||
- alert: Instancedown
|
||||
expr: up == 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
title: Instance {{ $labels.instance }} is down
|
||||
description: Failed to scrape {{ $labels.job }} on {{ $labels.instance }} for more than 5 minute. Instance seems down.
|
||||
- alert: LowRootDiskSpace
|
||||
expr: (node_filesystem_free_bytes{device =~ "/dev/.+"} / node_filesystem_size_bytes{device =~ "/dev/.+"} * 100) < 10
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
title: Low free root space on {{ $labels.instance }}
|
||||
description: On {{ $labels.instance }} device {{ $labels.device }} mounted on {{ $labels.mountpoint }} has low free space of {{ $value }}%
|
||||
- alert: LowDataDiskSpace
|
||||
expr: (node_filesystem_free_bytes{device !~ "/dev/.+", fstype !~ "tmpfs|.*lxcfs"} / node_filesystem_size_bytes{device !~ "/dev/.+", fstype !~ "tmpfs|.*lxcfs"} * 100) < 10
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
title: Low free data space on {{ $labels.instance }}
|
||||
description: On {{ $labels.instance }} device {{ $labels.device }} mounted on {{ $labels.mountpoint }} has low free space of {{ $value }}%
|
||||
- alert: HighCPULoad
|
||||
expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
title: High CPU load on instance {{ $labels.instance }}
|
||||
description: Instance {{ $labels.instance }} has high CPU load.
|
||||
- alert: HighMemoryUsage
|
||||
expr: (1 - ((node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) / node_memory_MemTotal_bytes)) * 100 > 90
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Instance {{ $labels.instance }} has high memory usage
|
||||
summary: High memory usage on {{ $labels.instance }}
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
# handlers file for roles/prometheus
|
||||
- name: restart grafana
|
||||
service:
|
||||
name: grafana-server
|
||||
state: restarted
|
||||
when: not grafana_service.changed
|
||||
|
||||
- name: restart prometheus
|
||||
service:
|
||||
name: prometheus
|
||||
state: restarted
|
||||
when: not prometheus_service.changed
|
|
@ -0,0 +1,24 @@
|
|||
- name: Install Alertmanager
|
||||
import_role:
|
||||
name: cloudalchemy.alertmanager
|
||||
public: yes
|
||||
vars:
|
||||
alertmanager_web_external_url: "http://{{ alertmanager_domain }}{{ alertmanager_web_path }}"
|
||||
alertmanager_config_flags_extra:
|
||||
web.route-prefix: /
|
||||
alertmanager_web_listen_address: "0.0.0.0:{{ alertmanager_port }}"
|
||||
|
||||
- name: Configure Nginx for Alertmaneger
|
||||
import_role:
|
||||
name: nginx
|
||||
vars:
|
||||
nginx_config_file: "{{ inventory_hostname }}.d/alertmanager.conf"
|
||||
nginx_server:
|
||||
locations:
|
||||
- path: "{{ alertmanager_web_path }}/"
|
||||
basic_auth:
|
||||
file: .htpasswd.alertmanager
|
||||
password: "{{ alertmanager_password }}"
|
||||
proxy_pass:
|
||||
port: "{{ alertmanager_port }}"
|
||||
path: /
|
|
@ -0,0 +1,71 @@
|
|||
- name: add grafana GPG apt key
|
||||
apt_key:
|
||||
url: https://packages.grafana.com/gpg.key
|
||||
state: present
|
||||
|
||||
- block:
|
||||
- name: add grafana repository
|
||||
apt_repository:
|
||||
repo: deb https://packages.grafana.com/oss/deb stable main
|
||||
state: present
|
||||
register: grafana_repo
|
||||
notify: restart grafana
|
||||
|
||||
- name: update apt cache
|
||||
apt:
|
||||
update_cache: yes
|
||||
cache_valid_time: 3600
|
||||
when: grafana_repo is changed
|
||||
|
||||
- name: install grafana package
|
||||
apt:
|
||||
pkg: grafana
|
||||
state: present
|
||||
notify: restart grafana
|
||||
|
||||
- name: copy grafana config
|
||||
template:
|
||||
src: grafana.ini.j2
|
||||
dest: /etc/grafana/grafana.ini
|
||||
become: yes
|
||||
notify: restart grafana
|
||||
|
||||
- name: enable grafana service
|
||||
systemd:
|
||||
name: grafana-server
|
||||
state: started
|
||||
enabled: yes
|
||||
register: grafana_service
|
||||
|
||||
- name: Configure Nginx for Grafana
|
||||
import_role:
|
||||
name: nginx
|
||||
vars:
|
||||
nginx_config_file: "{{ inventory_hostname }}.conf"
|
||||
nginx_server:
|
||||
name: "{{ inventory_hostname }}"
|
||||
port: 80
|
||||
locations:
|
||||
- path: "= /"
|
||||
return:
|
||||
code: 301
|
||||
url: "http://{{ inventory_hostname }}{{ grafana_web_path }}"
|
||||
includes:
|
||||
- "{{ nginx_config_dir }}/{{ inventory_hostname }}.d/*.conf"
|
||||
|
||||
- name: ensure nginx config directory exists
|
||||
file:
|
||||
path: "{{ nginx_config_dir }}/{{ inventory_hostname }}.d"
|
||||
state: directory
|
||||
|
||||
- include_role:
|
||||
name: nginx
|
||||
tasks_from: configure
|
||||
vars:
|
||||
nginx_config_file: "{{ inventory_hostname }}.d/grafana.conf"
|
||||
nginx_server:
|
||||
locations:
|
||||
- path: "{{ grafana_web_path }}/"
|
||||
proxy_pass:
|
||||
port: "{{ grafana_port }}"
|
||||
path: /
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
- import_tasks: grafana.yml
|
||||
become: yes
|
||||
tags: grafana
|
||||
- import_tasks: alertmanager.yml
|
||||
tags: alertmanager
|
||||
- import_tasks: prometheus.yml
|
||||
tags: prometheus
|
|
@ -0,0 +1,26 @@
|
|||
- name: Install Prometheus
|
||||
import_role:
|
||||
name: cloudalchemy.prometheus
|
||||
public: yes
|
||||
vars:
|
||||
prometheus_config_file: "prometheus_custom.yml.j2"
|
||||
prometheus_alert_rules: []
|
||||
prometheus_web_external_url: "http://{{ prometheus_domain }}{{ prometheus_web_path }}"
|
||||
prometheus_config_flags_extra:
|
||||
web.route-prefix: /
|
||||
prometheus_web_listen_address: "0.0.0.0:{{ prometheus_port }}"
|
||||
|
||||
- name: Configure Nginx for Prometheus
|
||||
import_role:
|
||||
name: nginx
|
||||
vars:
|
||||
nginx_config_file: "{{ inventory_hostname }}.d/prometheus.conf"
|
||||
nginx_server:
|
||||
locations:
|
||||
- path: "{{ prometheus_web_path }}/"
|
||||
basic_auth:
|
||||
file: .htpasswd.prometheus
|
||||
password: "{{ prometheus_password }}"
|
||||
proxy_pass:
|
||||
port: "{{ prometheus_port }}"
|
||||
path: /
|
|
@ -0,0 +1,627 @@
|
|||
[server]
|
||||
|
||||
[security]
|
||||
|
||||
##################### Grafana Configuration Example #####################
|
||||
#
|
||||
# Everything has defaults so you only need to uncomment things you want to
|
||||
# change
|
||||
|
||||
# possible values : production, development
|
||||
;app_mode = production
|
||||
|
||||
# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
|
||||
;instance_name = ${HOSTNAME}
|
||||
|
||||
#################################### Paths ####################################
|
||||
[paths]
|
||||
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
|
||||
;data = /var/lib/grafana
|
||||
|
||||
# Temporary files in `data` directory older than given duration will be removed
|
||||
;temp_data_lifetime = 24h
|
||||
|
||||