commit
101fc6e791
@ -0,0 +1,68 @@ |
||||
- hosts: all |
||||
gather_facts: no |
||||
serial: "{{ hosts_per_batch | d(1) | int }}" |
||||
strategy: "{{ hosts_strategy | d('linear') }}" |
||||
tasks: |
||||
- name: get primary role |
||||
set_fact: |
||||
host_primary_role: "{%- if primary_role is defined -%}{{ primary_role }}\ |
||||
{%- elif hostvars[inventory_hostname]['primary_role'] is defined -%}{{ hostvars[inventory_hostname]['primary_role'] }}\ |
||||
{%- else -%}{{ inventory_hostname }}\ |
||||
{%- endif -%}" |
||||
|
||||
|
||||
- name: import role mappings |
||||
import_tasks: mappings.yml |
||||
|
||||
|
||||
- name: fail if mappings are missing |
||||
fail: |
||||
msg: role mappings are missing or invalid |
||||
when: (common_roles is not defined) or (common_roles | type_debug != 'list') |
||||
|
||||
|
||||
- name: warn if current role mapping is missing |
||||
debug: |
||||
msg: "mapping for role \"{{ host_primary_role }}\" is missing - using default role mapping at stage 6" |
||||
when: (extra_roles | d({}))[host_primary_role] is not defined |
||||
|
||||
|
||||
- name: build role mapping |
||||
set_fact: |
||||
role_mapping: "{{ (((extra_roles | d({}))[host_primary_role] | d([{ 'stage': 6, 'role': host_primary_role }])) + |
||||
([] if host_primary_role in (no_common_roles | d([])) else common_roles) + |
||||
([{ 'stage': 1, 'role': 'container' }] if 'containers' in group_names else []) + |
||||
([{ 'stage': 3, 'role': 'postgres', 'function': 'integrate' }] if host_primary_role in (database_roles | d([])) else []) |
||||
) | sort(attribute='stage') }}" |
||||
|
||||
|
||||
- name: remember selected stages |
||||
set_fact: |
||||
selected_stages: "{%- if stage is defined and ((stage | string) is search(',')) -%}{{ stage | string | split(',') | list | map('int') | list }}\ |
||||
{%- elif (stage is not defined) or ((stage | int) == 0) -%}{{ [1,2,3,4,5,6,7,8,9] }}\ |
||||
{%- else -%}{{ [stage | int] }}\ |
||||
{%- endif -%}" |
||||
no_log: yes |
||||
|
||||
|
||||
- name: show deployment info |
||||
debug: |
||||
msg: "deploying primary role \"{{ host_primary_role }}\" on host \"{{ inventory_hostname }}\", {{ |
||||
(('stages ' if (selected_stages | length > 1) else 'stage ') ~ (selected_stages | join(', '))) |
||||
if ([1,2,3,4,5,6,7,8,9] | symmetric_difference(selected_stages) | list | length > 0) else 'all stages' }}" |
||||
|
||||
|
||||
- name: run pre_tasks |
||||
include_tasks: tasks/pre_tasks.yml |
||||
|
||||
|
||||
- name: run stages |
||||
include_tasks: tasks/includes/stage.yml |
||||
loop: "{{ selected_stages }}" |
||||
loop_control: |
||||
loop_var: this_stage |
||||
|
||||
|
||||
- name: show deployment info |
||||
debug: |
||||
msg: "ok: deployment completed on host \"{{ inventory_hostname }}\"" |
@ -0,0 +1,13 @@ |
||||
[defaults] |
||||
interpreter_python = auto_silent |
||||
stdout_callback = debug |
||||
use_persistent_connections = true |
||||
forks = 6 |
||||
internal_poll_interval = 0.01 |
||||
jinja2_native = true |
||||
|
||||
[ssh_connection] |
||||
pipelining = true |
||||
transfer_method = scp |
||||
scp_if_ssh = smart |
||||
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s -o PreferredAuthentications=publickey,password |
@ -0,0 +1,36 @@ |
||||
ansible_user: root |
||||
ansible_dir: /etc/ansible |
||||
ansible_key_dir: keys |
||||
alpine_version: "3.16" |
||||
|
||||
mac_prefix: 02:FF |
||||
|
||||
default_container_hardware: |
||||
cores: 1 |
||||
cpus: 1 |
||||
cpuunits: 1024 |
||||
memory: 128 |
||||
swap: 128 |
||||
disk: 0.4 |
||||
|
||||
known_external_ca: |
||||
- url: letsencrypt.org |
||||
wildcard: no |
||||
validation_methods: |
||||
- dns-01 |
||||
- url: ';' |
||||
wildcard: yes |
||||
|
||||
bogons: |
||||
- 0.0.0.0/8 |
||||
- 127.0.0.0/8 |
||||
- 169.254.0.0/16 |
||||
- 192.0.0.0/24 |
||||
- 192.0.2.0/24 |
||||
- 198.18.0.0/15 |
||||
- 198.51.100.0/24 |
||||
- 203.0.113.0/24 |
||||
- 240.0.0.0/4 |
||||
|
||||
services: {} |
||||
mail_server: {} |
@ -0,0 +1,126 @@ |
||||
timezone: Europe/Kirov |
||||
org: Organization Name |
||||
org_localized: Название организации |
||||
tld: org.local |
||||
int_net: 10.0.0.0/8 |
||||
|
||||
int_tld: "corp.{{ tld }}" |
||||
maintainer_email: "admin@{{ tld }}" |
||||
|
||||
timezone_win: Russian Standard Time |
||||
|
||||
container_default_nameserver: 10.40.0.1 |
||||
|
||||
networks: |
||||
srv: |
||||
gw: 10.41.0.1/16 |
||||
tag: 11 |
||||
priv: |
||||
gw: 10.42.0.1/16 |
||||
tag: 12 |
||||
dmz: |
||||
gw: 10.43.0.1/16 |
||||
tag: 13 |
||||
|
||||
|
||||
services: |
||||
db: |
||||
hostname: postgres |
||||
vault: |
||||
hostname: vault |
||||
backup: |
||||
hostname: rest-server |
||||
port: 443 |
||||
internal_ns: |
||||
hostname: ns |
||||
recursive_ns: |
||||
hostname: ns-rec |
||||
filtering_ns: |
||||
- hostname: blocky1 |
||||
- hostname: blocky2 |
||||
acme_dns: |
||||
hostname: acme-dns |
||||
rest_server: |
||||
hostname: rest-server |
||||
mariadb: |
||||
hostname: mariadb |
||||
smb: |
||||
hostname: smb |
||||
|
||||
use_alternative_apk_repo: yes |
||||
|
||||
mail_server: |
||||
tld: "{{ tld }}" |
||||
max_mail_size_bytes: 75000000 |
||||
admin_email: "admin@{{ tld }}" |
||||
|
||||
db_server_hostname: postgres |
||||
db_name: mail |
||||
db_user: mail |
||||
db_pass: pass |
||||
|
||||
mta_hostname: postfix |
||||
mua_hostname: dovecot |
||||
rspamd_hostname: rspamd |
||||
webmail_hostname: mail |
||||
clamav_hostname: clamav |
||||
|
||||
mua_lmtp_port: 11001 |
||||
mua_quota_port: 11002 |
||||
mua_auth_port: 11003 |
||||
mua_managesieve_port: 4190 |
||||
rspamd_port: 11332 |
||||
mta_sts_port: 11000 |
||||
clamav_port: 7357 |
||||
|
||||
mta_actual_hostname: smtp |
||||
mua_actual_hostname: imap |
||||
|
||||
allowed_spf: |
||||
- 1.1.1.1 |
||||
|
||||
domains: |
||||
- "{{ tld }}" |
||||
|
||||
aliases: |
||||
- { source: 'postmaster', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" } |
||||
- { source: 'hostmaster', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" } |
||||
- { source: 'webmaster', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" } |
||||
- { source: 'abuse', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" } |
||||
- { source: 'caa-report', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" } |
||||
- { source: 'dkim-report', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" } |
||||
- { source: 'dmarc-report', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" } |
||||
- { source: 'smtp-tls-report', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" } |
||||
|
||||
|
||||
|
||||
|
||||
acme_preferred_chain: ISRG Root X1 |
||||
|
||||
winrm_remote_user: remote-admin |
||||
winrm_bootstrap_password: bootstrap123 |
||||
|
||||
|
||||
backup_filters: |
||||
none: |
||||
- "*" |
||||
- "!*/" |
||||
|
||||
office: |
||||
- "!*.doc" |
||||
- "!*.docx" |
||||
- "!*.xls" |
||||
- "!*.xlsx" |
||||
- "!*.ppt" |
||||
- "!*.pptx" |
||||
- "!*.txt" |
||||
- "!*.ods" |
||||
- "!*.odt" |
||||
- "!*.odp" |
||||
- "!*.pdf" |
||||
|
||||
images: |
||||
- "!*.jpg" |
||||
- "!*.jpeg" |
||||
- "!*.png" |
||||
- "!*.tiff" |
@ -0,0 +1,10 @@ |
||||
is_windows: true |
||||
|
||||
ansible_connection: winrm |
||||
ansible_user: "{{ winrm_remote_user }}" |
||||
|
||||
ansible_winrm_transport: credssp |
||||
ansible_winrm_scheme: http |
||||
ansible_port: 5985 |
||||
|
||||
primary_role: workstation |
@ -0,0 +1,36 @@ |
||||
all: |
||||
children: |
||||
containers: |
||||
hosts: |
||||
ansible: |
||||
ansible_host: 10.0.0.3 |
||||
ansible_ssh_private_key_file: /etc/ansible/keys/ansible |
||||
container_password: --- |
||||
container_id: 100 |
||||
container_network: srv |
||||
database: {user: 'test', name: 'test', pass: 'test'} |
||||
|
||||
|
||||
nodes: |
||||
hosts: |
||||
node1: |
||||
ansible_host: 10.0.0.2 |
||||
ansible_password: --- |
||||
ansible_ssh_extra_args: -o StrictHostKeyChecking=no |
||||
external_ipv4: 1.1.1.1 |
||||
primary_role: proxmox |
||||
container_mtu: 1390 |
||||
|
||||
|
||||
windows: |
||||
children: |
||||
workstations: |
||||
|
||||
|
||||
infra: |
||||
vars: |
||||
ansible_group_priority: 1000 |
||||
children: |
||||
containers: |
||||
nodes: |
||||
windows: |
@ -0,0 +1,138 @@ |
||||
- name: define role list |
||||
set_fact: |
||||
# common roles for all primary roles |
||||
common_roles: |
||||
- {stage: 2, role: 'common'} |
||||
- {stage: 3, role: 'ns', function: 'add_records'} |
||||
- {stage: 5, role: 'mail-user'} |
||||
- {stage: 8, role: 'iptables'} |
||||
- {stage: 9, role: 'backup', function: 'setup'} |
||||
|
||||
# these primary roles do not inherit common roles |
||||
no_common_roles: |
||||
- mikrotik |
||||
- workstation |
||||
|
||||
# these primary roles will always inherit postgres integration |
||||
database_roles: |
||||
- acme-dns |
||||
- asterisk |
||||
- gitea |
||||
- roundcube |
||||
- shop |
||||
- wikijs |
||||
- vault |
||||
|
||||
# additional roles for specific primary roles |
||||
extra_roles: |
||||
ca: |
||||
- {stage: 2, role: 'ca', function: 'install'} |
||||
coredns: |
||||
- {stage: 2, role: 'coredns', function: 'install'} |
||||
- {stage: 4, role: 'coredns', function: 'install_tls'} |
||||
mariadb: |
||||
- {stage: 4, role: 'mariadb', function: 'install'} |
||||
mikrotik: |
||||
- {stage: 3, role: 'ns', function: 'add_records'} |
||||
- {stage: 5, role: 'mikrotik'} |
||||
nsd: |
||||
- {stage: 4, role: 'nsd', function: 'install'} |
||||
- {stage: 4, role: 'nsd', function: 'populate'} |
||||
- {stage: 5, role: 'nsd', function: 'install_dnssec'} |
||||
- {stage: 5, role: 'nsd', function: 'install_tls'} |
||||
postfix: |
||||
- {stage: 3, role: 'mail-db'} |
||||
- {stage: 4, role: 'postfix'} |
||||
postgres: |
||||
- {stage: 2, role: 'postgres', function: 'install'} |
||||
- {stage: 3, role: 'postgres', function: 'install_tls'} |
||||
powerdns: |
||||
- {stage: 2, role: 'postgres', function: 'integrate'} |
||||
- {stage: 2, role: 'powerdns', function: 'install'} |
||||
- {stage: 3, role: 'ca', function: 'certs'} |
||||
proxmox: |
||||
- {stage: 1, role: 'common'} |
||||
- {stage: 1, role: 'proxmox', function: 'install'} |
||||
- {stage: 5, role: 'mail-user'} |
||||
- {stage: 5, role: 'proxmox', function: 'tls'} |
||||
- {stage: 6, role: 'proxmox', function: 'mail'} |
||||
rest-server: |
||||
- {stage: 6, role: 'rest-server', function: 'install'} |
||||
workstation: |
||||
- {stage: 3, role: 'ns', function: 'add_records'} |
||||
- {stage: 5, role: 'workstation'} |
||||
|
||||
# recommended hardware parameters for each primary role |
||||
role_hardware: |
||||
acme-dns: {cores: 2, memory: 96, swap: 64, disk: 0.15} |
||||
ansible: {cores: 4, memory: 256, swap: 384, disk: 1.5} |
||||
asterisk: {cores: 4, memory: 192, swap: 96, disk: 0.6, cpuunits: 2048} |
||||
blocky: {cores: 4, memory: 384, swap: 128, disk: 0.15} |
||||
ca: {cores: 2, memory: 128, swap: 64, disk: 0.15, cpuunits: 512} |
||||
clamav: {cores: 4, memory: 2048, swap: 256, disk: 0.75} |
||||
coredns: {cores: 4, memory: 128, swap: 64, disk: 0.15} |
||||
crl: {cores: 2, memory: 128, swap: 48, disk: 0.15} |
||||
dovecot: {cores: 4, memory: 256, swap: 64, disk: 0.15} |
||||
gitea: {cores: 4, memory: 512, swap: 256, disk: 1} |
||||
grafana: {cores: 4, memory: 512, swap: 256, disk: 0.4} |
||||
mariadb: {cores: 4, memory: 256, swap: 128, disk: 0.4} |
||||
mc: {cores: 4, memory: 2048, swap: 512, disk: 0.5} |
||||
nsd: {cores: 2, memory: 256, swap: 256, disk: 0.15} |
||||
ntp: {cores: 2, memory: 64, swap: 128, disk: 0.15} |
||||
postfix: {cores: 4, memory: 256, swap: 48, disk: 0.15} |
||||
postgres: {cores: 4, memory: 256, swap: 256, disk: 0.5} |
||||
powerdns: {cores: 2, memory: 96, swap: 64, disk: 0.15} |
||||
prometheus: {cores: 4, memory: 512, swap: 256, disk: 0.3} |
||||
rclone: {cores: 4, memory: 192, swap: 96, disk: 0.2, cpuunits: 768} |
||||
rest-server: {cores: 4, memory: 256, swap: 192, disk: 0.2, cpuunits: 512} |
||||
roundcube: {cores: 4, memory: 384, swap: 256, disk: 0.5} |
||||
rspamd: {cores: 4, memory: 768, swap: 128, disk: 0.3} |
||||
seafile: {cores: 4, memory: 1024, swap: 1024, disk: 5} |
||||
shop: {cores: 4, memory: 192, swap: 128, disk: 0.4} |
||||
smb: {cores: 2, memory: 128, swap: 64, disk: 0.15} |
||||
strongswan: {cores: 4, memory: 128, swap: 48, disk: 0.15} |
||||
unbound: {cores: 2, memory: 128, swap: 64, disk: 0.15} |
||||
uptime-kuma: {cores: 4, memory: 384, swap: 128, disk: 0.5} |
||||
vault: {cores: 4, memory: 128, swap: 64, disk: 0.3} |
||||
web: {cores: 4, memory: 128, swap: 64, disk: 0.2} |
||||
wikijs: {cores: 4, memory: 256, swap: 256, disk: 0.75} |
||||
|
||||
# role dependency table |
||||
# 0 - DNS ok |
||||
# 1 - DB ok |
||||
role_dependency: |
||||
acme-dns: 0 |
||||
ansible: 0 |
||||
asterisk: 2 |
||||
blocky: 0 |
||||
ca: 0 |
||||
clamav: 1 |
||||
coredns: 0 |
||||
crl: 1 |
||||
dovecot: 2 |
||||
gitea: 2 |
||||
grafana: 2 |
||||
mariadb: 0 |
||||
mc: 3 |
||||
nsd: 0 |
||||
ntp: 0 |
||||
postfix: 2 |
||||
postgres: 0 |
||||
powerdns: 1 |
||||
prometheus: 1 |
||||
rclone: 1 |
||||
rest-server: 0 |
||||
roundcube: 2 |
||||
rspamd: 2 |
||||
seafile: 3 |
||||
shop: 2 |
||||
smb: 1 |
||||
strongswan: 1 |
||||
unbound: 0 |
||||
uptime-kuma: 3 |
||||
vault: 2 |
||||
web: 1 |
||||
wikijs: 3 |
||||
|
||||
run_once: yes |
||||
no_log: yes |
@ -0,0 +1,44 @@ |
||||
acme_dns_user: acmedns |
||||
acme_dns_group: acmedns |
||||
acme_dns_dir: /opt/acmedns |
||||
|
||||
acme_dns_tld: "acme-dns.{{ acme_tld | d(tld) }}" |
||||
acme_dns_ns: "ns.acme-dns.{{ acme_tld | d(tld) }}" |
||||
acme_dns_admin: "{{ maintainer_email | d('admin@' ~ (acme_tld | d(tld))) }}" |
||||
|
||||
acme_dns_api_port: 8080 |
||||
|
||||
|
||||
acme_dns_default_config: |
||||
general: |
||||
listen: ":53" |
||||
protocol: both4 |
||||
domain: "{{ acme_dns_tld }}" |
||||
nsname: "{{ acme_dns_ns | d(acme_dns_tld) }}" |
||||
nsadmin: "{{ acme_dns_admin | replace('@', '.') }}" |
||||
|
||||
records: |
||||
- "{{ acme_dns_tld ~ '. A ' ~ acme_dns_external_ipv4 }}" |
||||
- "{{ (acme_dns_ns | d(acme_dns_tld)) ~ '. A ' ~ acme_dns_external_ipv4 }}" |
||||
- "{{ acme_dns_tld ~ '. NS ' ~ (acme_dns_ns | d(acme_dns_tld)) ~ '.' }}" |
||||
|
||||
database: |
||||
engine: postgres |
||||
connection: "{{ 'postgresql://' ~ database_user ~ ':' ~ database_pass ~ '@' ~ database_host ~ '/' ~ database_name ~ '?sslmode=disable' }}" |
||||
|
||||
api: |
||||
ip: "0.0.0.0" |
||||
autocert_port: 80 |
||||
port: "{{ acme_dns_api_port }}" |
||||
disable_registration: no |
||||
tls: none |
||||
use_header: no |
||||
|
||||
notification_email: "{{ letsencrypt_email | d(maintainer_email) }}" |
||||
corsorigins: |
||||
- "*" |
||||
|
||||
logconfig: |
||||
loglevel: debug |
||||
logtype: stdout |
||||
logformat: text |
@ -0,0 +1,5 @@ |
||||
- name: restart acme-dns |
||||
service: |
||||
name: acme-dns |
||||
state: restarted |
||||
|
@ -0,0 +1,113 @@ |
||||
- name: set acme_dns_cfg |
||||
set_fact: |
||||
acme_dns_cfg: "{{ acme_dns_default_config | d({}) | combine(acme_dns_config | d({}), recursive=true) }}" |
||||
|
||||
|
||||
- name: install dependencies |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- libcap |
||||
|
||||
|
||||
- name: create user and group |
||||
include_tasks: tasks/create_user.yml |
||||
vars: |
||||
user: |
||||
name: "{{ acme_dns_user }}" |
||||
group: "{{ acme_dns_group }}" |
||||
dir: "{{ acme_dns_dir }}" |
||||
|
||||
|
||||
- name: get and extract latest version of acme-dns |
||||
include_tasks: tasks/get_lastversion.yml |
||||
vars: |
||||
package: |
||||
name: fritterhoff/acme-dns |
||||
location: github |
||||
assets: yes |
||||
asset_filter: 'Linux_amd64.tar.gz$' |
||||
file: "{{ acme_dns_dir }}/last_version" |
||||
extract: "{{ acme_dns_dir }}" |
||||
user: "{{ acme_dns_user }}" |
||||
group: "{{ acme_dns_group }}" |
||||
notify: restart acme-dns |
||||
|
||||
|
||||
- name: delete unnecessary files |
||||
file: |
||||
path: "{{ acme_dns_dir }}/{{ item }}" |
||||
state: absent |
||||
loop: |
||||
- CHANGELOG.md |
||||
- LICENSE |
||||
- README.md |
||||
|
||||
|
||||
- name: template acme-dns config |
||||
template: |
||||
src: config.j2 |
||||
dest: "{{ acme_dns_dir }}/config.cfg" |
||||
force: yes |
||||
mode: 0400 |
||||
owner: "{{ acme_dns_user }}" |
||||
group: "{{ acme_dns_group }}" |
||||
lstrip_blocks: yes |
||||
notify: restart acme-dns |
||||
|
||||
|
||||
- name: template init script |
||||
template: |
||||
src: init.j2 |
||||
dest: /etc/init.d/acme-dns |
||||
force: yes |
||||
mode: "+x" |
||||
notify: restart acme-dns |
||||
|
||||
|
||||
- name: ensure acme-dns binary has executable bit set |
||||
file: |
||||
path: "{{ acme_dns_dir }}/acme-dns" |
||||
mode: "+x" |
||||
|
||||
|
||||
- name: add cap_net_bind_service to acme-dns executable |
||||
community.general.capabilities: |
||||
path: "{{ acme_dns_dir }}/acme-dns" |
||||
capability: cap_net_bind_service+ep |
||||
changed_when: no |
||||
|
||||
|
||||
- name: set acme server address |
||||
set_fact: |
||||
acme_server: "http://127.0.0.1:{{ acme_dns_api_port }}" |
||||
|
||||
|
||||
- name: install and configure nginx |
||||
include_role: |
||||
name: nginx |
||||
vars: |
||||
nginx: |
||||
servers: |
||||
- conf: nginx_server |
||||
certs: "{{ host_tls }}" |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ acme_dns_dir }}" |
||||
|
||||
|
||||
- name: enable and start acme-dns |
||||
service: |
||||
name: acme-dns |
||||
state: started |
||||
enabled: yes |
@ -0,0 +1,26 @@ |
||||
{% macro acme_dns_option(option) -%} |
||||
{% if option.value is boolean -%} |
||||
{{ option.key }} = {{ 'true' if option.value else 'false' }} |
||||
{% elif option.value | type_debug == 'list' -%} |
||||
{{ option.key }} = [ |
||||
{%- for s in option.value -%} |
||||
"{{- s -}}", |
||||
{%- endfor -%} |
||||
] |
||||
{% elif option.value != None -%} |
||||
{{ option.key }} = "{{ option.value }}" |
||||
{% endif -%} |
||||
{% endmacro -%} |
||||
|
||||
|
||||
|
||||
{% for section in (acme_dns_cfg | d({}) | dict2items) -%} |
||||
[{{ section.key | lower }}] |
||||
{% for option in (section.value | d({}) | dict2items) -%} |
||||
{{ acme_dns_option(option) -}} |
||||
{% endfor %} |
||||
|
||||
{%- if not loop.last %} |
||||
|
||||
{% endif -%} |
||||
{% endfor %} |
@ -0,0 +1,18 @@ |
||||
#!/sbin/openrc-run |
||||
|
||||
name="$SVCNAME" |
||||
command="{{ acme_dns_dir }}/$SVCNAME" |
||||
directory="{{ acme_dns_dir }}" |
||||
command_user="{{ acme_dns_user }}:{{ acme_dns_group }}" |
||||
pidfile="/var/run/$SVCNAME.pid" |
||||
command_background=true |
||||
start_stop_daemon_args="--stdout-logger logger --stderr-logger logger" |
||||
|
||||
depend() { |
||||
need net |
||||
use dns |
||||
} |
||||
|
||||
start_pre() { |
||||
setcap 'cap_net_bind_service=+ep' {{ acme_dns_dir }}/$SVCNAME |
||||
} |
@ -0,0 +1,8 @@ |
||||
location / { |
||||
proxy_pass http://127.0.0.1:{{ acme_dns_api_port }}; |
||||
proxy_http_version 1.1; |
||||
proxy_set_header Host $host; |
||||
proxy_set_header X-Real-IP $remote_addr; |
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
||||
proxy_set_header X-Forwarded-Proto $scheme; |
||||
} |
@ -0,0 +1,2 @@ |
||||
acme_directory: /etc/letsencrypt |
||||
acme_max_log_backups: 5 |
@ -0,0 +1,202 @@ |
||||
- name: install certbot |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: certbot |
||||
|
||||
|
||||
- name: create certbot directories |
||||
file: |
||||
path: "{{ item }}" |
||||
state: directory |
||||
loop: |
||||
- "{{ acme_directory }}" |
||||
- "{{ acme_directory }}/cron" |
||||
|
||||
|
||||
- name: change certbot directory permissions |
||||
file: |
||||
path: "{{ acme_directory ~ '/' ~ item }}" |
||||
state: directory |
||||
mode: "g+rx,o+rx" |
||||
loop: |
||||
- archive |
||||
- live |
||||
|
||||
|
||||
- name: check if acme-dns auth hook already exists |
||||
stat: |
||||
path: "{{ acme_directory }}/acme-dns-auth.py" |
||||
register: result |
||||
|
||||
|
||||
- name: download acme-dns auth hook |
||||
get_url: |
||||
url: "https://raw.githubusercontent.com/RangeForce/acme-dns-certbot-joohoi/master/acme-dns-auth.py" |
||||
dest: "{{ acme_directory }}/acme-dns-auth.py" |
||||
force: no |
||||
mode: "+x" |
||||
when: result.stat.exists == false |
||||
|
||||
|
||||
- name: update python interpreter in acme-dns-auth to python3 |
||||
lineinfile: |
||||
path: "{{ acme_directory }}/acme-dns-auth.py" |
||||
regexp: '^#!\/usr\/bin\/env python\s*$' |
||||
line: '#!/usr/bin/env python3' |
||||
|
||||
|
||||
- name: clear acme fqdn list |
||||
set_fact: |
||||
acme_domain_list: "{{ [] }}" |
||||
|
||||
|
||||
- name: build acme fqdn list |
||||
set_fact: |
||||
acme_domain_list: "{{ (acme_domain_list | d([])) + |
||||
([item.fqdn | d((item.hostname | d(host_name)) ~ '.' ~ (item.tld | d(host_tld)))] if item is mapping else |
||||
[item]) }}" |
||||
loop: "{{ acme_hosts if (acme_hosts | type_debug == 'list') else [] }}" |
||||
|
||||
|
||||
- name: build single acme fqdn |
||||
set_fact: |
||||
acme_domain_list: "{%- if acme_fqdn is defined and acme_fqdn != None -%}\ |
||||
{{ [ acme_fqdn ] }}\ |
||||
{%- elif (acme_hostname is defined and acme_hostname != None) or (acme_tld is defined and acme_tld != None) -%}\ |
||||
{{ [((acme_hostname is defined and acme_hostname != None) | ternary(acme_hostname, host_name)) ~ '.' ~ |
||||
((acme_tld is defined and acme_tld != None) | ternary(acme_tld, host_tld))] }}\ |
||||
{%- else -%}\ |
||||
{{ [ host_fqdn ] }}\ |
||||
{%- endif -%}" |
||||
when: (acme_hosts is not defined) or (acme_hosts | type_debug != 'list') |
||||
|
||||
|
||||
- name: set acme parameters |
||||
set_fact: |
||||
acme_cert_name: "{{ acme_id if (acme_id is defined) and (acme_id != None) else (host_name ~ ('-ecc' if (acme_ecc | d(false) == true) else '')) }}" |
||||
acme_target_server: "{%- if (acme_server is defined) and (acme_server != None) -%}\ |
||||
{{ acme_server }}\ |
||||
{%- else -%}\ |
||||
{{ (services.acme_dns.protocol | d('https')) ~ '://' ~ services.acme_dns.hostname ~ '.' ~ (services.acme_dns.tld | d(int_tld)) ~ |
||||
((':' ~ services.acme_dns.port) if services.acme_dns.port is defined else '') }}\ |
||||
{%- endif -%}" |
||||
|
||||
|
||||
- name: set certbot parameters |
||||
set_fact: |
||||
acme_params: "{{ ['--manual', |
||||
'--manual-auth-hook ' ~ ((acme_directory ~ '/acme-dns-auth.py') | quote), |
||||
'--preferred-challenges dns', |
||||
'--debug-challenges', |
||||
('--key-type ecdsa' if (acme_ecc | d(false) == true) else ''), |
||||
('--staging' if (acme_staging | d(false) == true) else ''), |
||||
('--force-renewal' if (acme_force | d(false) == true) else ''), |
||||
('--must-staple' if (acme_stapling | d(false) == true) else ''), |
||||
'--cert-name ' ~ (acme_cert_name | quote), |
||||
'--non-interactive', |
||||
'--agree-tos', |
||||
'--email ' ~ ((acme_email | d(maintainer_email)) | quote), |
||||
'--no-eff-email', |
||||
(('--preferred-chain ' ~ (acme_preferred_chain | quote)) if acme_preferred_chain is defined else ''), |
||||
'--max-log-backups ' ~ (acme_max_log_backups | quote) |
||||
] | select() | list | join(' ') }}" |
||||
|
||||
|
||||
- block: |
||||
- name: issue cert with dns mode |
||||
shell: |
||||
cmd: "certbot certonly {{ acme_params }} -d {{ acme_domain_list | map('quote') | join(' -d ') }}" |
||||
chdir: /usr/bin |
||||
environment: |
||||
ACMEDNS_URL: "{{ acme_target_server }}" |
||||
register: result |
||||
changed_when: ('Successfully received certificate' in result.stdout) |
||||
notify: "{{ acme_notify if (acme_notify is defined) and (acme_notify != None) else omit }}" |
||||
|
||||
rescue: |
||||
- name: wait for user interaction (CNAME record must be set manually) |
||||
pause: |
||||
prompt: "{{ result.stdout }}" |
||||
|
||||
- name: try again to issue cert with dns mode |
||||
shell: |
||||
cmd: "certbot certonly {{ acme_params }} -d {{ acme_domain_list | map('quote') | join(' -d ') }}" |
||||
chdir: /usr/bin |
||||
environment: |
||||
ACMEDNS_URL: "{{ acme_target_server }}" |
||||
register: result |
||||
changed_when: ('Successfully received certificate' in result.stdout) |
||||
notify: "{{ acme_notify if (acme_notify is defined) and (acme_notify != None) else omit }}" |
||||
|
||||
|
||||
- name: create symlinks |
||||
file: |
||||
path: "{{ item.dest }}" |
||||
src: "{{ acme_directory ~ '/live/' ~ acme_cert_name ~ '/' ~ item.src }}" |
||||
state: link |
||||
force: yes |
||||
when: (item.dest is string) and (item.dest | length > 0) and (acme_use_symlinks | d(true) == true) |
||||
loop: |
||||
- { src: 'fullchain.pem', dest: "{{ acme_cert | d(None) }}" } |
||||
- { src: 'privkey.pem', dest: "{{ acme_key | d(None) }}" } |
||||
- { src: 'cert.pem', dest: "{{ acme_cert_single | d(None) }}" } |
||||
- { src: 'chain.pem', dest: "{{ acme_chain | d(None) }}" } |
||||
notify: "{{ acme_notify if (acme_notify is defined) and (acme_notify != None) else omit }}" |
||||
|
||||
|
||||
- name: fix ownership on archive dir |
||||
file: |
||||
path: "{{ acme_directory ~ '/archive/' ~ acme_cert_name }}" |
||||
follow: no |
||||
recurse: yes |
||||
owner: "{{ acme_owner if (acme_owner is defined) and (acme_owner != None) else omit }}" |
||||
group: "{{ acme_group if (acme_group is defined) and (acme_group != None) else omit }}" |
||||
|
||||
|
||||
- name: copy certs |
||||
copy: |
||||
src: "{{ acme_directory ~ '/live/' ~ acme_cert_name ~ '/' ~ item.src }}" |
||||
dest: "{{ item.dest }}" |
||||
remote_src: yes |
||||
mode: 0600 |
||||
owner: "{{ acme_owner if (acme_owner is defined) and (acme_owner != None) else omit }}" |
||||
group: "{{ acme_group if (acme_group is defined) and (acme_group != None) else omit }}" |
||||
when: (item.dest is string) and (item.dest | length > 0) and (acme_use_symlinks | d(true) == false) |
||||
loop: |
||||
- { src: 'fullchain.pem', dest: "{{ acme_cert | d(None) }}" } |
||||
- { src: 'privkey.pem', dest: "{{ acme_key | d(None) }}" } |
||||
- { src: 'cert.pem', dest: "{{ acme_cert_single | d(None) }}" } |
||||
- { src: 'chain.pem', dest: "{{ acme_chain | d(None) }}" } |
||||
notify: "{{ acme_notify | d(omit) }}" |
||||
|
||||
|
||||
- name: edit renewal file |
||||
lineinfile: |
||||
path: "{{ acme_directory ~ '/renewal/' ~ acme_cert_name ~ '.conf' }}" |
||||
regexp: '^{{ item.name | regex_escape }}(\s+)=' |
||||
line: '{{ item.name }} = {{ item.value }}' |
||||
insertafter: '^\[renewalparams\]' |
||||
create: no |
||||
firstmatch: yes |
||||
when: (item.value is string) and (item.value | length > 0) and |
||||
((item.extra_condition is not defined) or (item.extra_condition | d(true))) |
||||
loop: |
||||
- { name: 'renew_hook', value: "{{ acme_directory ~ '/cron/' ~ acme_cert_name ~ '.sh' }}" } |
||||
|
||||
|
||||
- name: create custom renewal hook file |
||||
template: |
||||
src: renewal.j2 |
||||
dest: "{{ acme_directory ~ '/cron/' ~ acme_cert_name ~ '.sh' }}" |
||||
force: yes |
||||
mode: 0500 |
||||
lstrip_blocks: yes |
||||
|
||||
|
||||
- name: add certbot to crontab |
||||
cron: |
||||
name: "certbot renewal ({{ acme_cert_name ~ ' on ' ~ acme_target_server }})" |
||||
job: "ACMEDNS_URL={{ acme_target_server | quote }} \ |
||||
/usr/bin/certbot renew --cert-name {{ acme_cert_name | quote }} --max-log-backups {{ acme_max_log_backups | quote }}" |
||||
hour: "{{ 4 | random(start=1, seed=(host_name ~ acme_cert_name)) }}" |
||||
minute: "{{ 59 | random(seed=(host_name ~ acme_cert_name)) }}" |
@ -0,0 +1,49 @@ |
||||
#!/bin/sh |
||||
|
||||
{% if (acme_owner is string) and (acme_group is string) and (acme_owner | length > 0) and (acme_group | length > 0) and (acme_use_symlinks | d(true) == true) -%} |
||||
chown -R {{ acme_owner ~ ':' ~ acme_group }} {{ (acme_directory ~ '/archive/' ~ acme_cert_name ~ '/') | quote }} |
||||
{% endif -%} |
||||
|
||||
|
||||
{{ acme_before_copy_hook | d('') }} |
||||
|
||||
|
||||
{% if (acme_cert is string) and (acme_cert | length > 0) and (acme_use_symlinks | d(true) == false) -%} |
||||
cp -fpT {{ (acme_directory ~ '/live/' ~ acme_cert_name ~ '/fullchain.pem') | quote }} {{ acme_cert | quote }} |
||||
{% if (acme_owner is not string) and (acme_group is string) -%} |
||||
chgrp -f {{ acme_group }} {{ acme_cert | quote }} |
||||
{% elif acme_owner is defined -%} |
||||
chown -f {{ acme_owner ~ ((':' ~ acme_group) if acme_group is string else '') }} {{ acme_cert | quote }} |
||||
{% endif -%} |
||||
{% endif -%} |
||||
|
||||
{% if (acme_key is string) and (acme_key | length > 0) and (acme_use_symlinks | d(true) == false) -%} |
||||
cp -fpT {{ (acme_directory ~ '/live/' ~ acme_cert_name ~ '/privkey.pem') | quote }} {{ acme_key | quote }} |
||||
{% if (acme_owner is not string) and (acme_group is string) -%} |
||||
chgrp -f {{ acme_group }} {{ acme_key | quote }} |
||||
{% elif acme_owner is defined -%} |
||||
chown -f {{ acme_owner ~ ((':' ~ acme_group) if acme_group is string else '') }} {{ acme_key | quote }} |
||||
{% endif -%} |
||||
{% endif -%} |
||||
|
||||
{% if (acme_cert_single is string) and (acme_cert_single | length > 0) and (acme_use_symlinks | d(true) == false) -%} |
||||
cp -fpT {{ (acme_directory ~ '/live/' ~ acme_cert_name ~ '/cert.pem') | quote }} {{ acme_cert_single | quote }} |
||||
{% if (acme_owner is not string) and (acme_group is string) -%} |
||||
chgrp -f {{ acme_group }} {{ acme_cert_single | quote }} |
||||
{% elif acme_owner is defined -%} |
||||
chown -f {{ acme_owner ~ ((':' ~ acme_group) if acme_group is string else '') }} {{ acme_cert_single | quote }} |
||||
{% endif -%} |
||||
{% endif -%} |
||||
|
||||
{% if (acme_chain is string) and (acme_chain | length > 0) and (acme_use_symlinks | d(true) == false) -%} |
||||
cp -fpT {{ (acme_directory ~ '/live/' ~ acme_cert_name ~ '/chain.pem') | quote }} {{ acme_chain | quote }} |
||||
{% if (acme_owner is not string) and (acme_group is string) -%} |
||||
chgrp -f {{ acme_group }} {{ acme_chain | quote }} |
||||
{% elif acme_owner is defined -%} |
||||
chown -f {{ acme_owner ~ ((':' ~ acme_group) if acme_group is string else '') }} {{ acme_chain | quote }} |
||||
{% endif -%} |
||||
{% endif -%} |
||||
|
||||
|
||||
{{ (acme_post_hook ~ ' &>/dev/null &') if acme_post_hook is defined else '' }} |
||||
|
@ -0,0 +1 @@ |
||||
ansible_dir: /etc/ansible |
@ -0,0 +1,31 @@ |
||||
- name: install ansible and dependencies |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- ansible |
||||
- py3-lxml |
||||
- py3-pip |
||||
- py3-requests |
||||
- py3-netaddr |
||||
|
||||
|
||||
- name: install python dependencies |
||||
pip: |
||||
name: |
||||
- pywinrm |
||||
- pywinrm[credssp] |
||||
|
||||
|
||||
- name: create ansible directory |
||||
file: |
||||
path: "{{ ansible_dir }}" |
||||
state: directory |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ ansible_dir }}" |
@ -0,0 +1,620 @@ |
||||
asterisk_user: asterisk |
||||
asterisk_group: asterisk |
||||
|
||||
asterisk_dir: /var/lib/asterisk |
||||
asterisk_conf_dir: /etc/asterisk |
||||
asterisk_tls_dir: "{{ asterisk_conf_dir }}/tls" |
||||
asterisk_recordings_dir: /opt/recordings |
||||
asterisk_data_dir: "{{ asterisk_dir }}" |
||||
|
||||
asterisk_users: {} |
||||
asterisk_trunks: {} |
||||
|
||||
asterisk_language: ru |
||||
|
||||
asterisk_pjsip_ciphers: |
||||
- ECDHE-ECDSA-CHACHA20-POLY1305 |
||||
- ECDHE-ECDSA-AES256-GCM-SHA384 |
||||
- ECDHE-ECDSA-AES128-GCM-SHA256 |
||||
- ECDHE-RSA-CHACHA20-POLY1305 |
||||
- ECDHE-RSA-AES256-GCM-SHA384 |
||||
- ECDHE-RSA-AES128-GCM-SHA256 |
||||
- DHE-RSA-AES128-SHA256 |
||||
|
||||
|
||||
# meta definitions: |
||||
# __template__ (bool) (section): this section is a template |
||||
# __template_from__ (string/list) (section): templates to inherit from |
||||
# __comment__ (string) (section): specify a comment before the section definition |
||||
# __inner_objects__ (boolean) (config/section): use object syntax when enumerating section members |
||||
|
||||
asterisk_default_config: |
||||
acl: |
||||
acl_lan_clients: |
||||
deny: |
||||
- 0.0.0.0/0.0.0.0 |
||||
permit: |
||||
- "{{ int_net | ansible.utils.ipaddr('network') }}/{{ int_net | ansible.utils.ipaddr('netmask') }}" |
||||
acl_inet_clients: |
||||
deny: |
||||
- "{{ int_net | ansible.utils.ipaddr('network') }}/{{ int_net | ansible.utils.ipaddr('netmask') }}" |
||||
permit: |
||||
- 0.0.0.0/0.0.0.0 |
||||
|
||||
asterisk: |
||||
directories: |
||||
__template__: yes |
||||
__inner_objects__: yes |
||||
astetcdir: "{{ asterisk_conf_dir }}" |
||||
astvarlibdir: "{{ asterisk_dir }}" |
||||
astdatadir: "{{ asterisk_data_dir }}" |
||||
astdbdir: "{{ asterisk_db_dir | d(asterisk_dir) }}" |
||||
astkeydir: "{{ asterisk_key_dir | d(asterisk_dir) }}" |
||||
astagidir: "{{ asterisk_agi_dir | d(asterisk_dir ~ '/agi-bin') }}" |
||||
astspooldir: "{{ asterisk_spool_dir | d('/var/spool/asterisk') }}" |
||||
astrundir: "{{ asterisk_run_dir | d('/var/run/asterisk') }}" |
||||
astlogdir: "{{ asterisk_log_dir | d('/var/log/asterisk') }}" |
||||
astsbindir: /usr/sbin |
||||
astmoddir: /usr/lib/asterisk/modules |
||||
|
||||
options: |
||||
verbose: 0 |
||||
debug: no |
||||
trace: 0 |
||||
|
||||
execincludes: no |
||||
highpriority: yes |
||||
initcrypto: yes |
||||
nocolor: yes |
||||
dumpcore: no |
||||
runuser: "{{ asterisk_user }}" |
||||
rungroup: "{{ asterisk_group }}" |
||||
autosystemname: yes |
||||
maxcalls: 200 |
||||
maxload: "100.0" |
||||
minmemfree: 1 |
||||
languageprefix: yes |
||||
transmit_silence: no |
||||
|
||||
defaultlanguage: en |
||||
documentation_language: en_US |
||||
|
||||
ccss: |
||||
general: |
||||
cc_max_requests: 15 |
||||
|
||||
cdr: |
||||
general: |
||||
enable: yes |
||||
unanswered: yes |
||||
congestion: yes |
||||
|
||||
cel: |
||||
general: |
||||
enable: no |
||||
|
||||
cdr_pgsql: |
||||
global: |
||||
hostname: "{{ database_host }}" |
||||
port: 5432 |
||||
user: "{{ database_user | d('cdr') }}" |
||||
dbname: "{{ database_name | d('cdr') }}" |
||||
table: "{{ database_table | d('cdr') }}" |
||||
password: "{{ database_pass }}" |
||||
encoding: UNICODE |
||||
|
||||
cli_aliases: |
||||
general: |
||||
template: friendly |
||||
friendly: |
||||
"hangup request": channel request hangup |
||||
"originate": channel originate |
||||
"help": core show help |
||||
"pri intense debug span": pri set debug intense span |
||||
"reload": module reload |
||||
"pjsip reload": module reload res_pjsip.so res_pjsip_authenticator_digest.so res_pjsip_endpoint_identifier_ip.so res_pjsip_mwi.so res_pjsip_notify.so res_pjsip_outbound_publish.so res_pjsip_publish_asterisk.so res_pjsip_outbound_registration.so |
||||
|
||||
cli_permissions: |
||||
general: |
||||
default_perm: permit |
||||
|
||||
codecs: |
||||
plc: |
||||
__inner_objects__: yes |
||||
genericplc: "true" |
||||
genericplc_on_equal_codecs: "false" |
||||
opus: |
||||
type: opus |
||||
packet_loss: 2 |
||||
signal: voice |
||||
|
||||
confbridge: |
||||
default_user: |
||||
type: user |
||||
dsp_drop_silence: yes |
||||
jitterbuffer: yes |
||||
default_bridge: |
||||
type: bridge |
||||
max_members: 30 |
||||
language: "{{ asterisk_language }}" |
||||
|
||||
features: |
||||
__inner_objects__: yes |
||||
featuremap: |
||||
blindxfer: "**" |
||||
atxfer: "*#" |
||||
applicationmap: |
||||
volume-up-tx: "#1,self/caller,Gosub(volume-up-tx,s,1)" |
||||
volume-up-rx: "#2,self/caller,Gosub(volume-up-rx,s,1)" |
||||
volume-down-tx: "#3,self/caller,Gosub(volume-down-tx,s,1)" |
||||
volume-down-rx: "#4,self/caller,Gosub(volume-down-rx,s,1)" |
||||
volume-increase-all: "#5,self/caller,Gosub(volume-increase-all,s,1)" |
||||
call-controls: |
||||
volume-up-tx: "" |
||||
volume-up-rx: "" |
||||
volume-down-tx: "" |
||||
volume-down-rx: "" |
||||
volume-increase-all: "" |
||||
|
||||
followme: |
||||
__inner_objects__: yes |
||||
general: |
||||
featuredigittimeout: 3500 |
||||
enable_callee_prompt: "true" |
||||
takecall: 1 |
||||
declinecall: 2 |
||||
call_from_prompt: followme/call-from |
||||
norecording_prompt: followme/no-recording |
||||
options_prompt: followme/options |
||||
pls_hold_prompt: followme/pls-hold-while-try |
||||
status_prompt: followme/status |
||||
sorry_prompt: followme/sorry |
||||
connecting_prompt: "" |
||||
default: |
||||
musicclass: default |
||||
context: default |
||||
enable_callee_prompt: "true" |
||||
takecall: 1 |
||||
declinecall: 2 |
||||
call_from_prompt: followme/call-from |
||||
norecording_prompt: followme/no-recording |
||||
options_prompt: followme/options |
||||
pls_hold_prompt: followme/pls-hold-while-try |
||||
status_prompt: followme/status |
||||
sorry_prompt: followme/sorry |
||||
connecting_prompt: "" |
||||
|
||||
indications: |
||||
general: |
||||
country: ru |
||||
ru: |
||||
description: Russian Federation / ex Soviet Union |
||||
ringcadence: "1000,4000" |
||||
dial: "425" |
||||
busy: "425/350,0/350" |
||||
ring: "425/1000,0/4000" |
||||
congestion: "425/175,0/175" |
||||
callwaiting: "425/200,0/5000" |
||||
record: "1400/400,0/15000" |
||||
info: "950/330,1400/330,1800/330,0/1000" |
||||
dialrecall: "425/400,0/40" |
||||
stutter: "!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425" |
||||
|
||||
logger: |
||||
general: |
||||
queue_log: no |
||||
logfiles: |
||||
__inner_objects__: yes |
||||
console: notice,warning,error,verbose,dtmf |
||||
"syslog.local0": "[plain]notice,warning,error" |
||||
|
||||
manager: |
||||
general: |
||||
enabled: yes |
||||
webenabled: no |
||||
port: 5038 |
||||
bindaddr: 0.0.0.0 |
||||
debug: "off" |
||||
allowmultiplelogin: yes |
||||
displayconnects: yes |
||||
timestampevents: yes |
||||
authtimeout: 10 |
||||
|
||||
musiconhold: |
||||
default: |
||||
mode: files |
||||
directory: moh |
||||
|
||||
pjproject: |
||||
startup: |
||||
cache_pools: yes |
||||
|
||||
|
||||
pjsip: |
||||
system: |
||||
type: system |
||||
threadpool_auto_increment: 3 |
||||
timer_t1: 250 |
||||
timer_b: 16000 |
||||
global: |
||||
type: global |
||||
max_forwards: 40 |
||||
keep_alive_interval: 15 |
||||
user_agent: "{{ org }} Asterisk PBX" |
||||
endpoint_identifier_order: username,ip |
||||
default_from_user: pbx |
||||
default_realm: "{{ host_fqdn }}" |
||||
|
||||
transport-common: |
||||
__template__: yes |
||||
type: transport |
||||
tos: cs3 |
||||
cos: 3 |
||||
allow_reload: no |
||||
local_net: "{{ int_net | ansible.utils.ipaddr('network') }}/{{ int_net | ansible.utils.ipaddr('netmask') }}" |
||||
|
||||
transport-ext: |
||||
__template__: yes |
||||
__template_from__: transport-common |
||||
external_media_address: "{{ asterisk_external_ipv4 | d(hostvars[selected_node]['external_ipv4']) }}" |
||||
external_signaling_address: "{{ asterisk_external_ipv4 | d(hostvars[selected_node]['external_ipv4']) }}" |
||||
|
||||
transport-udp: |
||||
__template__: yes |
||||
__template_from__: transport-common |
||||
protocol: udp |
||||
|
||||
transport-tcp: |
||||
__template__: yes |
||||
__template_from__: transport-common |
||||
protocol: tcp |
||||
|
||||
transport-lan: |
||||
__template_from__: transport-udp |
||||
bind: 0.0.0.0:5060 |
||||
|
||||
transport-lan-tcp: |
||||
__template_from__: transport-tcp |
||||
bind: 0.0.0.0:5060 |
||||
|
||||
transport-lan-tls: |
||||
__template_from__: transport-common |
||||
protocol: tls |
||||
bind: 0.0.0.0:5061 |
||||
cert_file: "{{ asterisk_tls_dir }}/asterisk.crt" |
||||
priv_key_file: "{{ asterisk_tls_dir }}/asterisk.key" |
||||
cipher: "{{ asterisk_pjsip_ciphers | join(',') }}" |
||||
method: tlsv1_2 |
||||
require_client_cert: no |
||||
verify_client: no |
||||
verify_server: no |
||||
|
||||
endpoint-common: |
||||
__template__: yes |
||||
type: endpoint |
||||
allow: "!all,opus,g722,alaw,ulaw,g726,ilbc,gsm" |
||||
allow_overlap: no |
||||
send_connected_line: yes |
||||
trust_connected_line: yes |
||||
direct_media: no |
||||
dtmf_mode: auto_info |
||||
force_rport: yes |
||||
ice_support: no |
||||
identify_by: username |
||||
rewrite_contact: yes |
||||
rtp_symmetric: yes |
||||
send_diversion: yes |
||||
send_history_info: yes |
||||
send_pai: no |
||||
send_rpid: no |
||||
use_ptime: yes |
||||
t38_udptl: no |
||||
tone_zone: ru |
||||
language: ru |
||||
tos_audio: ef |
||||
cos_audio: 5 |
||||
rtp_keepalive: 5 |
||||
rtp_timeout: 360 |
||||
rtp_timeout_hold: 720 |
||||
rtcp_mux: yes |
||||
max_video_streams: 0 |
||||
max_audio_streams: 1 |
||||
bundle: no |
||||
sdp_session: "{{ org }} Asterisk PBX" |
||||
sdp_owner: PBX |
||||
suppress_q850_reason_headers: yes |
||||
|
||||
endpoint-trunk: |
||||
__template__: yes |
||||
__template_from__: endpoint-common |
||||
identify_by: ip,username |
||||
trust_id_inbound: yes |
||||
acl: acl_inet_clients |
||||
contact_acl: acl_inet_clients |
||||
|
||||
endpoint-lan: |
||||
__template__: yes |
||||
__template_from__: endpoint-common |
||||
identify_by: username |
||||
trust_id_inbound: no |
||||
trust_id_outbound: yes |
||||
acl: acl_lan_clients |
||||
contact_acl: acl_lan_clients |
||||
context: outbound |
||||
allow_subscribe: yes |
||||
device_state_busy_at: 1 |
||||
sub_min_expiry: 15 |
||||
media_encryption: sdes |
||||
media_encryption_optimistic: yes |
||||
|
||||
auth-common: |
||||
__template__: yes |
||||
type: auth |
||||
auth_type: userpass |
||||
|
||||
registration-common: |
||||
__template__: yes |
||||
type: registration |
||||
expiration: 1800 |
||||
auth_rejection_permanent: no |
||||
max_retries: 10000 |
||||
retry_interval: 20 |
||||
forbidden_retry_interval: 60 |
||||
fatal_retry_interval: 60 |
||||
|
||||
aor-common: |
||||
__template__: yes |
||||
type: aor |
||||
qualify_frequency: 30 |
||||
max_contacts: 2 # https://asterisk.org/pjsip-mis-configuration-can-cause-loss-sip-registrations |
||||
|
||||
__include__: custom_pjsip.conf |
||||
|
||||
|
||||
pjsip_notify: |
||||
__inner_objects__: yes |
||||
clear-mwi: |
||||
Event: message-summary |
||||
Content-type: application/simple-message-summary |
||||
Content: |
||||
- "Messages-Waiting: no" |
||||
- "Message-Account: sip:asterisk@127.0.0.1" |
||||
- "Voice-Message: 0/0 (0/0)" |
||||
- "" |
||||
polycom-check-cfg: |
||||
Event: check-sync |
||||
yealink-reboot: |
||||
Event: check-sync |
||||
|
||||
queues: |
||||
general: |
||||
persistentmembers: no |
||||
autofill: yes |
||||
monitor-type: MixMonitor |
||||
updatecdr: yes |
||||
log_membername_as_agent: yes |
||||
shared_lastcall: yes |
||||
|
||||
queue-template: |
||||
__template__: yes |
||||
musicclass: default |
||||
strategy: ringall |
||||
servicelevel: 30 |
||||
maxlen: 128 |
||||
timeoutpriority: conf |
||||
timeout: 300 |
||||
wrapuptime: 5 |
||||
announce-frequency: 0 |
||||
periodic-announce-frequency: 0 |
||||
announce-position: no |
||||
autopause: yes |
||||
autopausedelay: 60 |
||||
autopausebusy: yes |
||||
joinempty: unavailable |
||||
leavewhenempty: unavailable |
||||
ringinuse: no |
||||
|
||||
queue-single: |
||||
__template__: yes |
||||
__template_from__: queue-template |
||||
weight: 1 |
||||
autopause: no |
||||
context: inbound-queued-inqueue-busy |
||||
|
||||
queue-le: |
||||
__template__: yes |
||||
__template_from__: queue-template |
||||
weight: 1 |
||||
autopause: no |
||||
|
||||
__include__: custom_queues.conf |
||||
|
||||
queuerules: |
||||
general: |
||||
|
||||
rtp: |
||||
general: |
||||
rtpstart: 15000 |
||||
rtpend: 19000 |
||||
strictrtp: yes |
||||
icesupport: "false" |
||||
|
||||
udptl: |
||||
general: |
||||
|
||||
modules: |
||||
modules: |
||||
autoload: no |
||||
load: |
||||
- app_attended_transfer.so |
||||
- app_blind_transfer.so |
||||
- app_bridgeaddchan.so |
||||
- app_bridgewait.so |
||||
- app_cdr.so |
||||
- app_celgenuserevent.so |
||||
- app_chanisavail.so |
||||
- app_channelredirect.so |
||||
- app_chanspy.so |
||||
- app_confbridge.so |
||||
- app_controlplayback.so |
||||
- app_dial.so |
||||
- app_directed_pickup.so |
||||
- app_dumpchan.so |
||||
- app_echo.so |
||||
- app_exec.so |
||||
- app_followme.so |
||||
- app_forkcdr.so |
||||
- app_mixmonitor.so |
||||
- app_originate.so |
||||
- app_playback.so |
||||
- app_queue.so |
||||
- app_read.so |
||||
- app_readexten.so |
||||
- app_senddtmf.so |
||||
- app_softhangup.so |
||||
- app_stack.so |
||||
- app_stream_echo.so |
||||
- app_talkdetect.so |
||||
- app_transfer.so |
||||
- app_verbose.so |
||||
- app_waitforring.so |
||||
- app_waitforsilence.so |
||||
- app_waituntil.so |
||||
- app_while.so |
||||
|
||||
- bridge_builtin_features.so |
||||
- bridge_builtin_interval_features.so |
||||
- bridge_holding.so |
||||
- bridge_native_rtp.so |
||||
- bridge_simple.so |
||||
- bridge_softmix.so |
||||
|
||||
- cdr_pgsql.so |
||||
|
||||
- chan_bridge_media.so |
||||
- chan_pjsip.so |
||||
- chan_rtp.so |
||||
|
||||
- codec_a_mu.so |
||||
- codec_adpcm.so |
||||
- codec_alaw.so |
||||
- codec_g722.so |
||||
- codec_g726.so |
||||
- codec_gsm.so |
||||
- codec_ilbc.so |
||||
- codec_opus_open_source.so |
||||
- codec_resample.so |
||||
- codec_ulaw.so |
||||
|
||||
- format_g719.so |
||||
- format_g723.so |
||||
- format_g726.so |
||||
- format_gsm.so |
||||
- format_ilbc.so |
||||
- format_pcm.so |
||||
- format_sln.so |
||||
- format_vox.so |
||||
- format_wav.so |
||||
- format_wav_gsm.so |
||||
|
||||
- func_blacklist.so |
||||
- func_callcompletion.so |
||||
- func_callerid.so |
||||
- func_cdr.so |
||||
- func_channel.so |
||||
- func_config.so |
||||
- func_cut.so |
||||
- func_devstate.so |
||||
- func_dialplan.so |
||||
- func_global.so |
||||
- func_hangupcause.so |
||||
- func_holdintercept.so |
||||
- func_jitterbuffer.so |
||||
- func_logic.so |
||||
- func_module.so |
||||
- func_pjsip_aor.so |
||||
- func_pjsip_contact.so |
||||
- func_pjsip_endpoint.so |
||||
- func_rand.so |
||||
- func_sorcery.so |
||||
- func_strings.so |
||||
- func_talkdetect.so |
||||
- func_timeout.so |
||||
- func_volume.so |
||||
|
||||
- pbx_config.so |
||||
- pbx_loopback.so |
||||
- pbx_realtime.so |
||||
- pbx_spool.so |
||||
|
||||
- res_audiosocket.so |
||||
- res_clialiases.so |
||||
- res_clioriginate.so |
||||
- res_convert.so |
||||
- res_crypto.so |
||||
- res_format_attr_celt.so |
||||
- res_format_attr_g729.so |
||||
- res_format_attr_ilbc.so |
||||
- res_format_attr_opus.so |
||||
- res_format_attr_silk.so |
||||
- res_format_attr_siren14.so |
||||
- res_format_attr_siren7.so |
||||
- res_musiconhold.so |
||||
- res_mutestream.so |
||||
- res_pjproject.so |
||||
|
||||
- res_pjsip.so |
||||
- res_pjsip_acl.so |
||||
- res_pjsip_authenticator_digest.so |
||||
- res_pjsip_caller_id.so |
||||
- res_pjsip_dialog_info_body_generator.so |
||||
- res_pjsip_diversion.so |
||||
- res_pjsip_dlg_options.so |
||||
- res_pjsip_dtmf_info.so |
||||
- res_pjsip_empty_info.so |
||||
- res_pjsip_endpoint_identifier_ip.so |
||||
- res_pjsip_endpoint_identifier_user.so |
||||
- res_pjsip_exten_state.so |
||||
- res_pjsip_header_funcs.so |
||||
- res_pjsip_history.so |
||||
- res_pjsip_logger.so |
||||
- res_pjsip_messaging.so |
||||
- res_pjsip_mwi.so |
||||
- res_pjsip_mwi_body_generator.so |
||||
- res_pjsip_nat.so |
||||
- res_pjsip_notify.so |
||||
- res_pjsip_outbound_authenticator_digest.so |
||||
- res_pjsip_outbound_publish.so |
||||
- res_pjsip_outbound_registration.so |
||||
- res_pjsip_path.so |
||||
- res_pjsip_pidf_body_generator.so |
||||
- res_pjsip_publish_asterisk.so |
||||
- res_pjsip_pubsub.so |
||||
- res_pjsip_refer.so |
||||
- res_pjsip_registrar.so |
||||
- res_pjsip_rfc3326.so |
||||
- res_pjsip_sdp_rtp.so |
||||
- res_pjsip_send_to_voicemail.so |
||||
- res_pjsip_session.so |
||||
- res_pjsip_sips_contact.so |
||||
- res_pjsip_xpidf_body_generator.so |
||||
|
||||
- res_rtp_asterisk.so |
||||
- res_rtp_multicast.so |
||||
- res_security_log.so |
||||
- res_sorcery_astdb.so |
||||
- res_sorcery_config.so |
||||
- res_sorcery_memory.so |
||||
- res_sorcery_memory_cache.so |
||||
- res_srtp.so |
||||
- res_stasis.so |
||||
- res_stasis_answer.so |
||||
- res_stasis_device_state.so |
||||
- res_stasis_playback.so |
||||
- res_stasis_recording.so |
||||
- res_timing_pthread.so |
||||
- res_timing_timerfd.so |
||||
|
||||
- res_pjsip_header_funcs.so |
||||
- res_pjsip_history.so |
||||
- res_pjsip_sdp_rtp.so |
@ -0,0 +1,8 @@ |
||||
- name: handle config change |
||||
import_tasks: asterisk_handlers.yml |
||||
|
||||
|
||||
- name: restart asterisk |
||||
service: |
||||
name: asterisk |
||||
state: restarted |
@ -0,0 +1,19 @@ |
||||
- block: |
||||
- name: restart asterisk |
||||
service: |
||||
name: asterisk |
||||
state: restarted |
||||
when: item.item.action is not defined |
||||
|
||||
|
||||
- name: reload dialplan |
||||
command: |
||||
cmd: 'asterisk -rx "dialplan reload"' |
||||
when: item.item.action == 'reload dialplan' |
||||
|
||||
|
||||
- name: reload configs |
||||
command: |
||||
cmd: 'asterisk -rx "core reload"' |
||||
when: item.item.action == 'reload configs' |
||||
when: item is defined |
@ -0,0 +1,194 @@ |
||||
- name: set asterisk_cfg |
||||
set_fact: |
||||
asterisk_cfg: "{{ asterisk_default_config | d({}) | combine(asterisk_config | d({}), recursive=true) }}" |
||||
|
||||
|
||||
- name: install dependencies |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- asterisk |
||||
- asterisk-pgsql |
||||
- asterisk-openrc |
||||
- asterisk-opus |
||||
- asterisk-srtp |
||||
- tar |
||||
- vorbis-tools |
||||
|
||||
|
||||
- name: create user and group |
||||
include_tasks: tasks/create_user.yml |
||||
vars: |
||||
user: |
||||
name: "{{ asterisk_user }}" |
||||
group: "{{ asterisk_group }}" |
||||
dir: "{{ asterisk_dir }}" |
||||
|
||||
|
||||
- name: ensure asterisk directories exist |
||||
file: |
||||
path: "{{ item }}" |
||||
state: directory |
||||
owner: "{{ asterisk_user }}" |
||||
group: "{{ asterisk_group }}" |
||||
loop: |
||||
- "{{ asterisk_dir }}" |
||||
- "{{ asterisk_conf_dir }}" |
||||
- "{{ asterisk_tls_dir }}" |
||||
- "{{ asterisk_data_dir }}" |
||||
- "{{ asterisk_data_dir }}/moh" |
||||
- "{{ asterisk_data_dir }}/sounds" |
||||
- "{{ asterisk_data_dir }}/sounds/{{ asterisk_language }}" |
||||
- "{{ asterisk_data_dir }}/sounds/{{ asterisk_language }}/custom" |
||||
- "{{ asterisk_recordings_dir }}" |
||||
|
||||
|
||||
- name: template custom asterisk configs |
||||
template: |
||||
src: "{{ item }}.j2" |
||||
dest: "{{ asterisk_conf_dir }}/{{ item }}.conf" |
||||
force: yes |
||||
mode: 0400 |
||||
owner: "{{ asterisk_user }}" |
||||
group: "{{ asterisk_group }}" |
||||
lstrip_blocks: yes |
||||
notify: restart asterisk |
||||
loop: |
||||
- custom_pjsip |
||||
- custom_queues |
||||
- ext_ivr |
||||
- ext_utils |
||||
- extensions |
||||
|
||||
|
||||
- name: template asterisk configs |
||||
template: |
||||
src: "{{ 'config' if item is string else (item.config | d('config')) }}.j2" |
||||
dest: "{{ asterisk_conf_dir }}/{{ item if item is string else (item.dest | d(item.config) | d(item.name)) }}.conf" |
||||
force: yes |
||||
mode: 0400 |
||||
owner: "{{ asterisk_user }}" |
||||
group: "{{ asterisk_group }}" |
||||
lstrip_blocks: yes |
||||
notify: restart asterisk |
||||
loop: |
||||
- acl |
||||
- asterisk |
||||
- ccss |
||||
- cdr |
||||
- cdr_pgsql |
||||
- cli_aliases |
||||
- cli_permissions |
||||
- codecs |
||||
- confbridge |
||||
- features |
||||
- followme |
||||
- indications |
||||
- logger |
||||
- manager |
||||
- musiconhold |
||||
- pjproject |
||||
- pjsip |
||||
- pjsip_notify |
||||
- queues |
||||
- rtp |
||||
- modules |
||||
- queuerules |
||||
- cel |
||||
- udptl |
||||
|
||||
|
||||
- name: edit service config |
||||
lineinfile: |
||||
path: /etc/conf.d/asterisk |
||||
regexp: "^{{ item.name | upper }}=" |
||||
line: "{{ item.name | upper }}=\"{{ item.value }}\"" |
||||
when: item.when | d(true) |
||||
notify: restart asterisk |
||||
loop: |
||||
- name: asterisk_opts |
||||
value: "-C {{ (asterisk_conf_dir ~ '/asterisk.conf') | quote }}" |
||||
when: "{{ asterisk_conf_dir != '/etc/asterisk' }}" |
||||
- name: asterisk_user |
||||
value: "{{ asterisk_user }}" |
||||
- name: asterisk_nice |
||||
value: "{{ asterisk_niceness | d(None) }}" |
||||
when: "{{ asterisk_niceness is defined }}" |
||||
|
||||
|
||||
- name: download asterisk sound pack |
||||
get_url: |
||||
url: "https://downloads.asterisk.org/pub/telephony/sounds/asterisk-core-sounds-{{ asterisk_language }}-{{ item }}-current.tar.gz" |
||||
dest: "{{ asterisk_data_dir }}/{{ asterisk_language }}-{{ item }}.tar.gz" |
||||
owner: "{{ asterisk_user }}" |
||||
group: "{{ asterisk_group }}" |
||||
register: result |
||||
loop: |
||||
- sln16 |
||||
- wav |
||||
|
||||
|
||||
- name: extract sound pack |
||||
unarchive: |
||||
src: "{{ item }}" |
||||
dest: "{{ asterisk_data_dir }}/sounds/{{ asterisk_language }}" |
||||
remote_src: yes |
||||
owner: "{{ asterisk_user }}" |
||||
group: "{{ asterisk_group }}" |
||||
loop: "{{ result.results | d([]) | selectattr('dest', 'defined') | selectattr('changed', 'defined') | selectattr('changed', 'equalto', true) | map(attribute='dest') | list }}" |
||||
|
||||
|
||||
- name: deploy RSA cert for SIP TLS |
||||
include_role: |
||||
name: certs |
||||
vars: |
||||
certs: |
||||
id: ast-tls |
||||
cert: "{{ asterisk_tls_dir }}/asterisk.crt" |
||||
key: "{{ asterisk_tls_dir }}/asterisk.key" |
||||
chain: "{{ asterisk_tls_dir }}/chain.crt" |
||||
owner: "{{ asterisk_user }}" |
||||
group: "{{ asterisk_group }}" |
||||
post_hook: service asterisk restart |
||||
notify: restart asterisk |
||||
|
||||
|
||||
- name: install and configure cdr |
||||
include_role: |
||||
name: cdr |
||||
vars: |
||||
cdr_group: "{{ asterisk_group }}" |
||||
cdr_config: |
||||
db_host: "{{ asterisk_cfg.cdr_pgsql.global.hostname }}" |
||||
db_user: "{{ asterisk_cfg.cdr_pgsql.global.user }}" |
||||
db_pass: "{{ asterisk_cfg.cdr_pgsql.global.password }}" |
||||
db_database: "{{ asterisk_cfg.cdr_pgsql.global.dbname }}" |
||||
db_table: "{{ asterisk_cfg.cdr_pgsql.global.table }}" |
||||
record_dir: "{{ asterisk_recordings_dir }}" |
||||
ami_user: "{{ asterisk_ami_cdr_user }}" |
||||
ami_pass: "{{ asterisk_ami_cdr_secret }}" |
||||
when: asterisk_use_cdr | d(true) == true |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ asterisk_conf_dir }}" |
||||
- "{{ asterisk_tls_dir }}" |
||||
- "{{ asterisk_data_dir }}/moh" |
||||
- "{{ asterisk_data_dir }}/sounds/{{ asterisk_language }}/custom" |
||||
- "{{ asterisk_dir }}/astdb.sqlite3" |
||||
|
||||
|
||||
- name: enable and start asterisk |
||||
service: |
||||
name: asterisk |
||||
enabled: yes |
||||
state: started |
@ -0,0 +1,85 @@ |
||||
{% macro config_template(config_name, asterisk_cfg) -%} |
||||
{% set ns = namespace(objects=false) -%} |
||||
|
||||
{% if config_name is string and asterisk_cfg[config_name] is mapping -%} |
||||
{% for section in (asterisk_cfg[config_name] | dict2items) -%} |
||||
{% if section.value is mapping -%} |
||||
{% set template_parts = [] -%} |
||||
|
||||
{% if (section.value['__template__'] is boolean) and (section.value['__template__'] == true) -%} |
||||
{% set template_parts = template_parts + ['!'] -%} |
||||
{% endif -%} |
||||
|
||||
{% if section.value['__template_from__'] is string -%} |
||||
{% set template_parts = template_parts + [section.value['__template_from__']] -%} |
||||
{% elif section.value['__template_from__'] | type_debug == 'list' -%} |
||||
{% set template_parts = template_parts + section.value['__template_from__'] -%} |
||||
{% endif -%} |
||||
|
||||
{% if section.value['__comment__'] is string -%} |
||||
; {{ section.value['__comment__'] }} |
||||
{% endif -%} |
||||
|
||||
|
||||
{% if template_parts | length == 0 -%} |
||||
[{{ section.key }}] |
||||
{% else -%} |
||||
[{{ section.key }}]({{ template_parts | join(',') }}) |
||||
{% endif -%} |
||||
|
||||
{% set ns.objects = (section.value['__inner_objects__'] | d(asterisk_cfg[config_name]['__inner_objects__'] | d(false))) -%} |
||||
|
||||
{% for option in (section.value | dict2items) -%} |
||||
{% if not option.key.startswith('__') and not option.key.endswith('__') -%} |
||||
|
||||
{% if option.value | type_debug == 'list' -%} |
||||
{% if option.value | length == 0 -%} |
||||
{{ option.key }} => |
||||
{% else -%} |
||||
{% for option_element in option.value -%} |
||||
{% set option_value = 'yes' if (option_element is boolean and option_element == true) else ('no' if (option_element is boolean and option_element == false) else option_element ) -%} |
||||
{{ option.key }} => {{ option_value }} |
||||
{% endfor -%} |
||||
{% endif -%} |
||||
{% elif option.value is mapping -%} |
||||
{% set option_is_object = option.value['__inner_objects__'] | d(ns.objects) -%} |
||||
|
||||
{% if option.value['__comment__'] is string -%} |
||||
; {{ option.value['__comment__'] }} |
||||
{% endif -%} |
||||
{% if option.value['__include_before__'] is string -%} |
||||
#include {{ option.value['__include_before__'] }} |
||||
{% endif -%} |
||||
{% if option.value['__try_include_before__'] is string -%} |
||||
#tryinclude {{ option.value['__try_include_before__'] }} |
||||
{% endif -%} |
||||
|
||||
{% set option_value = 'yes' if (option.value['__value__'] is boolean and option.value['__value__'] == true) else ('no' if (option.value['__value__'] is boolean and option.value['__value__'] == false) else option.value['__value__'] ) -%} |
||||
{{ option.key }} {{ '=>' if option_is_object else '=' }} {{ option_value }} |
||||
|
||||
{% if option.value['__include_after__'] is string -%} |
||||
#include {{ option.value['__include_after__'] }} |
||||
{% endif -%} |
||||
{% if option.value['__try_include_after__'] is string -%} |
||||
#tryinclude {{ option.value['__try_include_after__'] }} |
||||
{% endif -%} |
||||
{% else -%} |
||||
{% set option_value = 'yes' if (option.value is boolean and option.value == true) else ('no' if (option.value is boolean and option.value == false) else option.value ) -%} |
||||
{{ option.key }} {{ '=>' if ns.objects else '=' }} {{ option_value }} |
||||
{% endif -%} |
||||
|
||||
{% endif -%} |
||||
{% endfor -%} |
||||
{% if not loop.last %} |
||||
|
||||
{% endif -%} |
||||
{% elif (section.key == '__include__') and (section.value is string) -%} |
||||
#include {{ section.value }} |
||||
{% elif (section.key == '__try_include__') and (section.value is string) -%} |
||||
#tryinclude {{ section.value }} |
||||
{% endif -%} |
||||
{% endfor -%} |
||||
{% endif -%} |
||||
{% endmacro -%} |
||||
|
||||
|
@ -0,0 +1,3 @@ |
||||
{%- from '_macros.j2' import config_template -%} |
||||
|
||||
{{- config_template(item if (item is string) else (item.config | d(item.name)), asterisk_cfg) -}} |
@ -0,0 +1,75 @@ |
||||
{% macro trunk_options(opts) -%} |
||||
{% for opt in (opts | d({}) | dict2items) -%} |
||||
{{ opt.key }} = {{ 'yes' if (opt.value is boolean and opt.value == true) else ('no' if (opt.value is boolean and opt.value == false) else opt.value ) }} |
||||
{% endfor -%} |
||||
{% endmacro -%} |
||||
|
||||
|
||||
|
||||
{% for user in asterisk_users | d({}) | dict2items -%} |
||||
{% if user.value is mapping -%} |
||||
{% if user.value['__comment__'] is string -%} |
||||
; {{ user.value['__comment__'] }} |
||||
{% endif -%} |
||||
|
||||
[auth-{{ user.key }}](auth-common) |
||||
username = {{ user.value['login'] | d(user.key) }} |
||||
password = {{ user.value['password'] }} |
||||
|
||||
[{{ user.key }}](aor-common) |
||||
|
||||
[{{ user.key }}](endpoint-lan) |
||||
auth = auth-{{ user.key }} |
||||
aors = {{ user.key }} |
||||
callerid = {{ user.value['callerid'] | d(user.key) }} <{{ user.key }}> |
||||
|
||||
|
||||
{% endif -%} |
||||
{% endfor %} |
||||
|
||||
{% for trunk in asterisk_trunks | d({}) | dict2items -%} |
||||
{% if trunk.value is mapping -%} |
||||
{% if trunk.value['__comment__'] is string -%} |
||||
; {{ trunk.value['__comment__'] }} |
||||
{% endif -%} |
||||
|
||||
[transport-{{ trunk.key }}](transport-udp,transport-ext) |
||||
{{ trunk_options(trunk.value['transport']) }} |
||||
{# #} |
||||
[registration-{{ trunk.key }}](registration-common) |
||||
outbound_auth = auth-{{ trunk.key }} |
||||
endpoint = endpoint-{{ trunk.key }} |
||||
transport = transport-{{ trunk.key }} |
||||
{{ trunk_options(trunk.value['registration']) }} |
||||
{# #} |
||||
[auth-{{ trunk.key }}](auth-common) |
||||
{{ trunk_options(trunk.value['auth']) }} |
||||
{# #} |
||||
[aor-{{ trunk.key }}](aor-common) |
||||
{{ trunk_options(trunk.value['aor']) }} |
||||
{# #} |
||||
[endpoint-{{ trunk.key }}](endpoint-trunk) |
||||
transport = transport-{{ trunk.key }} |
||||
context = inbound-{{ trunk.key }} |
||||
outbound_auth = auth-{{ trunk.key }} |
||||
aors = aor-{{ trunk.key }} |
||||
{{ trunk_options(trunk.value['endpoint']) }} |
||||
{# #} |
||||
[identify-{{ trunk.key }}] |
||||
type = identify |
||||
endpoint = endpoint-{{ trunk.key }} |
||||
{{ trunk_options(trunk.value['identify']) }} |
||||
|
||||
{%- if not loop.last %} |
||||
|
||||
|
||||
{% endif -%} |
||||
{% endif -%} |
||||
{% endfor %} |
||||
|
||||
|
||||
|
||||
[reslist-all] |
||||
type=resource_list |
||||
event=presence |
||||
list_item={{ asterisk_users | d({}) | dict2items | map(attribute='key') | list | join(',') }} |
@ -0,0 +1,26 @@ |
||||
{% for user in asterisk_users | d({}) | dict2items -%} |
||||
{% if user.value is mapping -%} |
||||
[queue-{{ user.key }}]({{ user.value['self_queue_type'] | d('queue-single') }}) |
||||
member => PJSIP/{{ user.key }},0,{{ user.value['callerid'] | d(user.key) }} |
||||
{% endif -%} |
||||
{% endfor %} |
||||
|
||||
|
||||
{% set defined_queues = (asterisk_users | d({}) | dict2items | map(attribute='value') | list | selectattr('queues', 'defined') | map(attribute='queues') | list | flatten | unique | list) -%} |
||||
{% set auto_queues = (asterisk_users | d({}) | dict2items | rejectattr('value.queues', 'defined') | map(attribute='key') | list) -%} |
||||
{% set all_queues = ((defined_queues | d([])) + (auto_queues | d([])) | unique | list) -%} |
||||
|
||||
{% for queue in defined_queues -%} |
||||
{% if asterisk_users[queue] is not defined -%} |
||||
{% set queue_users = (asterisk_users | d({}) | dict2items | selectattr('value.queues', 'defined') | selectattr('value.queues', 'contains', queue) | list) -%} |
||||
{% if queue_users | length > 1 -%} |
||||
[queue-{{ queue }}](queue-template) |
||||
{% for user in queue_users -%} |
||||
member => PJSIP/{{ user.key }},0,{{ user.value['callerid'] | d(user.key) }} |
||||
{% endfor -%} |
||||
{%- if not loop.last %} |
||||
|
||||
{% endif -%} |
||||
{% endif -%} |
||||
{% endif -%} |
||||
{% endfor -%} |
@ -0,0 +1,58 @@ |
||||
; IVR |
||||
; 1 - went to IVR |
||||
; 2 - pressed a button |
||||
; 3 - did not press anything |
||||
|
||||
|
||||
[ivr-dial] |
||||
exten => s,1,Set(CDR(ivr)=2) |
||||
same => n,Gosub(inbound-queued,s,1(${ARG1})) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
[ivr-dial-all] |
||||
exten => s,1,Set(CDR(ivr)=3) |
||||
same => n,Queue(queue-all,inrt,,,,,,pre-call) |
||||
|
||||
|
||||
[ivr-select] |
||||
exten => 1,1,Gosub(ivr-dial,s,1(1)) |
||||
exten => 2,1,Gosub(ivr-dial,s,1(3)) |
||||
exten => 3,1,Gosub(ivr-dial,s,1(2)) |
||||
exten => 4,1,Gosub(ivr-dial,s,1(11)) |
||||
exten => 5,1,Gosub(ivr-dial,s,1(9)) |
||||
|
||||
|
||||
[ivr] |
||||
exten => s,1,Answer(250) |
||||
same => n,Set(CDR(ivr)=1) |
||||
same => n,Set(TIMEOUT(digit)=3) |
||||
same => n,Set(TIMEOUT(response)=3) |
||||
same => n,Background(custom/ivr-intro-12-2021,m,,ivr-select) |
||||
same => n,WaitExten(3) |
||||
same => n,Gosub(ivr-dial-all,s,1) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[ivr-select-spb] |
||||
exten => 1,1,Gosub(ivr-dial,s,1(6)) |
||||
exten => 2,1,Gosub(ivr-dial,s,1(8)) |
||||
|
||||
[ivr-dial-all-spb] |
||||
exten => s,1,Set(CDR(ivr)=3) |
||||
same => n,Queue(queue-spb,inrt,,,,,,pre-call) |
||||
|
||||
[ivr-spb] |
||||
exten => s,1,Answer(250) |
||||
same => n,Set(CDR(ivr)=1) |
||||
same => n,Set(TIMEOUT(digit)=3) |
||||
same => n,Set(TIMEOUT(response)=3) |
||||
same => n,Background(custom/ivr-intro-spb,m,,ivr-select-spb) |
||||
same => n,WaitExten(3) |
||||
same => n,Gosub(ivr-dial-all-spb,s,1) |
||||
same => n,Hangup() |
@ -0,0 +1,88 @@ |
||||
; Extension utilities |
||||
|
||||
|
||||
[record-start] |
||||
exten => s,1,ExecIf($["${IS_RECORDING}"="1"]?Return()) |
||||
same => n,Set(UID=${UNIQUEID}.${RAND(1,100000)}) |
||||
same => n,Set(CDR(actualuniqueid)=${UID}) |
||||
same => n,MixMonitor({{ asterisk_recordings_dir }}/${UID}.wav,b,oggenc -q 5 -o {{ asterisk_recordings_dir }}/${UID}.ogg {{ asterisk_recordings_dir }}/${UID}.wav && rm {{ asterisk_recordings_dir }}/${UID}.wav) |
||||
same => n,Set(__IS_RECORDING=1) |
||||
same => n,Return() |
||||
|
||||
[record-stop] |
||||
exten => s,1,StopMixMonitor() |
||||
same => n,Return() |
||||
|
||||
|
||||
; Filtering CallerID |
||||
[clear-callerid] |
||||
exten => s,1,Verbose(Filtering CallerID) |
||||
same => n,Set(CALLERID(num)=${FILTER(0-9,${CALLERID(num)})}) |
||||
same => n,Set(CALLERID(name)=) |
||||
same => n,Return() |
||||
|
||||
|
||||
; Setting up volume control |
||||
[volume-setup] |
||||
exten => s,1,Set(CURRENT_VOLUME_TX=1) |
||||
same => n,Set(CURRENT_VOLUME_RX=1) |
||||
same => n,Set(__DYNAMIC_FEATURES=call-controls) |
||||
same => n,Return() |
||||
|
||||
[volume-up-tx] |
||||
exten => s,1,Set(CURRENT_VOLUME_TX=$[${CURRENT_VOLUME_TX}*1.25]) |
||||
same => n,Set(VOLUME(TX)=${CURRENT_VOLUME_TX}) |
||||
same => n,Return() |
||||
|
||||
[volume-up-rx] |
||||
exten => s,1,Set(CURRENT_VOLUME_RX=$[${CURRENT_VOLUME_RX}*1.25]) |
||||
same => n,Set(VOLUME(RX)=${CURRENT_VOLUME_RX}) |
||||
same => n,Return() |
||||
|
||||
[volume-down-tx] |
||||
exten => s,1,Set(CURRENT_VOLUME_TX=$[${CURRENT_VOLUME_TX}*0.75]) |
||||
same => n,Set(VOLUME(TX)=${CURRENT_VOLUME_TX}) |
||||
same => n,Return() |
||||
|
||||
[volume-down-rx] |
||||
exten => s,1,Set(CURRENT_VOLUME_RX=$[${CURRENT_VOLUME_RX}*0.75]) |
||||
same => n,Set(VOLUME(RX)=${CURRENT_VOLUME_RX}) |
||||
same => n,Return() |
||||
|
||||
[volume-increase-all] |
||||
exten => s,1,Set(CURRENT_VOLUME_RX=2) |
||||
same => n,Set(CURRENT_VOLUME_TX=2) |
||||
same => n,Set(VOLUME(RX)=2) |
||||
same => n,Set(VOLUME(TX)=2) |
||||
same => n,Return() |
||||
|
||||
|
||||
; An invalid extension has been dialed |
||||
[invalid-ext] |
||||
exten => s,1,Answer(250) |
||||
same => n,Playback(custom/invalid-ext) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
; An extension has been dialed, but it is currently offline |
||||
[offline-ext] |
||||
exten => s,1,Answer(250) |
||||
same => n,Playback(custom/this-offline) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
|
||||
|
||||
; Output "Busy" signal |
||||
[busy] |
||||
exten => s,1,Busy(10) |
||||
same => n,Wait(1) |
||||
same => n,Hangup() |
||||
|
||||
; Output "Congestion" signal |
||||
[congestion] |
||||
exten => s,1,Congestion(10) |
||||
same => n,Wait(1) |
||||
same => n,Hangup() |
@ -0,0 +1,303 @@ |
||||
[general] |
||||
static=yes ; never rewrite this file |
||||
writeprotect=yes |
||||
autofallthrough=yes ; hang up if end of dialplan is reached |
||||
clearglobalvars=yes ; clear global vars on dialplan reload |
||||
|
||||
|
||||
[globals] |
||||
#include ext_utils.conf ; include utilities |
||||
#include ext_ivr.conf ; include IVR |
||||
|
||||
TRANSFER_CONTEXT=transfer |
||||
|
||||
|
||||
|
||||
[transfer] |
||||
exten => 0,1,Verbose(TRANSFER IVR) |
||||
same => n,Set(__IS_RECORDING=0) |
||||
same => n,StopMixMonitor() |
||||
same => n,ForkCDR(erv) |
||||
same => n,Gosub(pre-any,s,1(IVR,TRANSFER)) |
||||
same => n,Gosub(ivr,s,1) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
exten => _Z,1,Verbose(TRANSFER) |
||||
same => n,Set(__IS_RECORDING=0) |
||||
same => n,StopMixMonitor() |
||||
same => n,ForkCDR(erv) |
||||
same => n,Gosub(pre-any,s,1(${EXTEN},TRANSFER)) |
||||
same => n,Gosub(inbound-queued,s,1(${EXTEN})) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
include => catchall |
||||
|
||||
|
||||
|
||||
[pre-any] |
||||
exten => s,1,Gosub(clear-callerid,s,1) |
||||
same => n,Set(__CALLER=${CALLERID(num)}) |
||||
same => n,Set(__CALLEE=${ARG1}) |
||||
same => n,Set(__CALL_OPERATION=${ARG2}) |
||||
same => n,Set(CDR(actualsrc)=${CALLER}) |
||||
same => n,Set(CDR(actualdst)=${CALLEE}) |
||||
same => n,Set(CDR(realcall)=1) |
||||
same => n,Verbose(${CALL_OPERATION}: ${CALLER} -> ${CALLEE}) |
||||
same => n,Set(LIMIT_PLAYAUDIO_CALLER=no,LIMIT_PLAYAUDIO_CALLEE=yes) |
||||
same => n,Set(LIMIT_TIMEOUT_FILE=custom/call-expired) |
||||
same => n,Set(LIMIT_WARNING_FILE=custom/call-expiring-soon) |
||||
same => n,Return() |
||||
|
||||
|
||||
[pre-call] |
||||
exten => s,1,Gosub(volume-setup,s,1) |
||||
same => n,Gosub(record-start,s,1) |
||||
same => n,Set(CDR(realcall)=2) |
||||
same => n,Set(CDR(startedat)=${EPOCH}) |
||||
same => n,Set(CDR(actualdisposition)=ANSWERED) |
||||
same => n,Set(CDR(actualdst2)=${CALLERID(num)}) |
||||
same => n,Return() |
||||
|
||||
|
||||
[pre-out-call] |
||||
exten => s,1,Gosub(volume-setup,s,1) |
||||
same => n,Gosub(record-start,s,1) |
||||
same => n,Set(CDR(realcall)=2) |
||||
same => n,Set(CDR(startedat)=${EPOCH}) |
||||
same => n,Return() |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
; 1.1. Place an inbound call into a single queue |
||||
[inbound-queued] |
||||
exten => s,1,Gosub(pre-any,s,1(${ARG1},INBOUND-QUEUED)) |
||||
same => n,Verbose(DS ${DEVICE_STATE(PJSIP/${CALLEE})}) |
||||
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "BUSY"]?inbound-queued-busy,s,1) |
||||
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "INUSE"]?inbound-queued-busy,s,1) |
||||
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "RINGINUSE"]?inbound-queued-busy,s,1) |
||||
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "RINGING"]?inbound-queued-busy,s,1) |
||||
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "UNAVAILABLE"]?inbound-queued-unavail,s,1) |
||||
same => n,Queue(queue-${CALLEE},inrt,,,,,,pre-call) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
; 1.2. Callee is busy, play a message (if appropriate) and place it into a single queue |
||||
[inbound-queued-busy] |
||||
exten => s,1,Verbose(QUEUED BUSY) |
||||
same => n,GotoIf($["${CALLEE}"="9"]?busy-le) |
||||
same => n,GotoIf($[ $["${CALLEE}"="6"] | $["${CALLEE}"="8"] | $["${CALLEE}"="10"] | $["${CALLEE}"="12"] ]?busy-spb) |
||||
same => n,Background(custom/this-busy-ask-redirect,m,,inbound-queued-select-busy)) |
||||
same => n,Queue(queue-${CALLEE},inrt,,,,,,pre-call) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
same => n(busy-le),Playback(custom/this-busy-le) |
||||
same => n,Queue(queue-${CALLEE},inrt,,,,,,pre-call) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
same => n(busy-spb),Playback(custom/this-busy-spb) |
||||
same => n,Queue(queue-${CALLEE},inrt,,,,,,pre-call) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
|
||||
; 1.25. Callee is not available, play a message and place it into a single queue |
||||
;same => n,GosubIf($[ $["${CALLEE}"="6"] | $["${CALLEE}"="8"] | $["${CALLEE}"="10"] | $["${CALLEE}"="12"] ]?inbound-queued-unavail-spb,s,1) |
||||
|
||||
[inbound-queued-unavail] |
||||
exten => s,1,Verbose(QUEUED UNAVAIL) |
||||
same => n,GosubIf($["${CALLEE}"="9"]?inbound-queued-unavail-le,s,1) |
||||
same => n,Playback(custom/this-unavail-will-redirect) |
||||
same => n,Set(CDR(ivr)=3) |
||||
same => n,Queue(queue-some-${CALLEE},inrt,,,,,,pre-call) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
; 1.25.1 LE callee is not available, play a message and hang up |
||||
[inbound-queued-unavail-le] |
||||
exten => s,1,Playback(custom/this-unavail-le) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
; 1.25.2 SPB callee is not available, play a message and hang up |
||||
;[inbound-queued-unavail-spb] |
||||
;exten => s,1,Playback(custom/this-unavail-spb) |
||||
; same => n,Wait(0.5) |
||||
; same => n,Hangup() |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
; 1.3. Caller has requested to join a "some" queue, place it there |
||||
[inbound-queued-to-some] |
||||
exten => s,1,Verbose(QUEUE TO SOME) |
||||
same => n,Set(CDR(ivr)=3) |
||||
same => n,Queue(queue-some-${CALLEE},inrt,,,,,,pre-call) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
; 1.1.1. Allow callers to exit from a background playback to dial some |
||||
[inbound-queued-select-busy] |
||||
exten => 1,1,Gosub(inbound-queued-to-some,s,1) |
||||
|
||||
|
||||
; 1.2.1. Allow callers to exit from a queue to dial some |
||||
; Invalid DTMF keypresses get redirected back to inbound queue |
||||
[inbound-queued-inqueue-busy] |
||||
exten => 1,1,Gosub(inbound-queued-to-some,s,1) |
||||
exten => i,1,Gosub(inbound-queued,s,1(${CALLEE})) |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
; Inbound calls from Multifon trunk to LE endpoint (9) |
||||
[inbound-multifon] |
||||
exten => _Z.,1,Gosub(inbound-queued,s,1(9)) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
; Inbound calls from Dom.ru 222003 endpoint directly to ext 1 |
||||
[inbound-domru-3] |
||||
exten => _Z.,1,Gosub(inbound-queued,s,1(1)) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
; Inbound calls from Dom.ru 222004 endpoint to IVR |
||||
[inbound-domru-4] |
||||
exten => _Z.,1,Gosub(pre-any,s,1(IVR,INBOUND)) |
||||
same => n,Gosub(ivr,s,1) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
; Inbound calls from Smart SPB trunk to SPB IVR |
||||
[inbound-smart-spb] |
||||
exten => _Z.,1,Gosub(pre-any,s,1(IVR,INBOUND)) |
||||
same => n,Gosub(ivr-spb,s,1) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
; Outbound calls from all local endpoints |
||||
[outbound] |
||||
exten => _Z,hint,PJSIP/${EXTEN} |
||||
exten => _ZX,hint,PJSIP/${EXTEN} |
||||
exten => _Z,1,Gosub(outbound-internal,s,1(${EXTEN})) |
||||
exten => _ZX,1,Gosub(outbound-internal,s,1(${EXTEN})) |
||||
|
||||
|
||||
exten => _7XXXXXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(8${EXTEN:1}):outbound-external,s,1(+${EXTEN})) |
||||
exten => _8XXXXXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(${EXTEN}):outbound-external,s,1(+7${EXTEN:1})) |
||||
exten => _+7XXXXXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(8${EXTEN:2}):outbound-external,s,1(${EXTEN})) |
||||
exten => _9XXXXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(8${EXTEN}):outbound-external,s,1(+7${EXTEN})) |
||||
exten => _XXXXXX,1,Gosub(outbound-external,s,1(+78332${EXTEN})) |
||||
exten => _XXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(${EXTEN})) |
||||
include => service |
||||
include => catchall |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
; Internal calls |
||||
[outbound-internal] |
||||
exten => s,1,Gosub(pre-any,s,1(${ARG1},INTERNAL)) |
||||
|
||||
same => n,GotoIf($["${CALLERID(number)}" = "${ARG1}"]?busy,s,1) ; dialing the same extension as caller |
||||
same => n,GotoIf($["${DEVICE_STATE(PJSIP/${ARG1})}" = "INVALID"]?invalid-ext,s,1) ; extension is invalid |
||||
same => n,GotoIf($["${DEVICE_STATE(PJSIP/${ARG1})}" = "UNAVAILABLE"]?offline-ext,s,1) ; extension is valid but offline |
||||
|
||||
same => n,Dial(PJSIP/${ARG1},900,girtTL(3600000:60000)U(pre-out-call)) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
|
||||
[outbound-external] |
||||
exten => s,1,Gosub(pre-any,s,1(${ARG1},OUTBOUND)) |
||||
|
||||
same => n,GosubIf($[${CALLERID(num)} = 9]?outbound-multifon,s,1(${ARG1})) |
||||
same => n,GosubIf($[${CALLERID(num)} = 6]?outbound-smart-spb,s,1(${ARG1})) |
||||
same => n,GosubIf($[${CALLERID(num)} = 8]?outbound-smart-spb,s,1(${ARG1})) |
||||
same => n,GosubIf($[${CALLERID(num)} = 10]?outbound-smart-spb,s,1(${ARG1})) |
||||
same => n,GosubIf($[${CALLERID(num)} = 12]?outbound-smart-spb,s,1(${ARG1})) |
||||
same => n,Gosub(outbound-domru,s,1(${ARG1})) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
|
||||
[outbound-multifon] |
||||
exten => s,1,Dial(PJSIP/${ARG1}@endpoint-multifon,900,irTL(3600000:60000)U(pre-out-call)) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
[outbound-domru] |
||||
exten => s,1,Dial(PJSIP/${ARG1}@endpoint-domru-4,900,irTL(3600000:60000)U(pre-out-call)) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
[outbound-smart-spb] |
||||
exten => s,1,Dial(PJSIP/${ARG1}@endpoint-smart-spb,900,irTL(3600000:60000)U(pre-out-call)) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
|
||||
[service] |
||||
; Simple ring test |
||||
exten => 001,1,Ringing() |
||||
same => n,Wait(20) |
||||
same => n,Hangup() |
||||
|
||||
; Hello World playback |
||||
exten => 002,1,Answer(250) |
||||
same => n,Playback(hello-world) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
; Echo test |
||||
exten => 003,1,Answer(250) |
||||
same => n,Playback(demo-echotest) |
||||
same => n,Echo |
||||
same => n,Playback(demo-echodone) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
; Internal IVR |
||||
exten => 004,1,Answer(250) |
||||
same => n,Gosub(ivr,s,1) |
||||
same => n,Wait(0.5) |
||||
same => n,Hangup() |
||||
|
||||
; Congestion test |
||||
exten => 005,1,Congestion() |
||||
same => n,Wait(20) |
||||
same => n,Hangup() |
||||
|
||||
|
||||
|
||||
|
||||
[catchall] |
||||
exten => _X.,1,Gosub(invalid-ext,s,1) ; go to invalid extension macro on all extensions |
||||
exten => i,1,Gosub(invalid-ext,s,1) ; same, but with invalid extensions |
@ -0,0 +1,5 @@ |
||||
- name: add backup dirs to collected backup dirs |
||||
set_fact: |
||||
collected_backup_dirs: "{{ (collected_backup_dirs | d([])) + |
||||
([backup_items] if backup_items is string else backup_items) }}" |
||||
when: backup_items is defined and ((backup_items | type_debug == 'list') or backup_items is string) |
@ -0,0 +1,8 @@ |
||||
- name: add to backup plan |
||||
include_tasks: add.yml |
||||
when: function is defined and function == 'add' |
||||
|
||||
|
||||
- name: setup backups |
||||
include_tasks: setup.yml |
||||
when: function is defined and function == 'setup' |
@ -0,0 +1,31 @@ |
||||
- name: notify that backups are not supported |
||||
debug: |
||||
msg: backup host is missing, will not set up backups |
||||
when: services.backup is not mapping |
||||
|
||||
|
||||
- name: install restic with custom configuration |
||||
block: |
||||
- include_role: |
||||
name: restic |
||||
vars: |
||||
backup: "{{ backup_cfg }}" |
||||
|
||||
when: services.backup is mapping and backup_cfg is mapping |
||||
|
||||
|
||||
- name: install restic with default configuration |
||||
block: |
||||
- include_role: |
||||
name: restic |
||||
vars: |
||||
backup: |
||||
dirs: "{{ collected_backup_dirs }}" |
||||
password: "{{ backup_password }}" |
||||
tags: automated |
||||
filter: |
||||
- "*.log" |
||||
- "node_modules" |
||||
- ".npm" |
||||
|
||||
when: services.backup is mapping and backup_cfg is not defined and backup_password is defined |
@ -0,0 +1,52 @@ |
||||
blocky_user: blocky |
||||
blocky_group: blocky |
||||
blocky_dir: /opt/blocky |
||||
blocky_conf_dir: /etc/blocky |
||||
blocky_conf_file: "{{ blocky_conf_dir }}/blocky.yml" |
||||
|
||||
blocky_tls_ecc384_cert: "{{ blocky_conf_dir }}/ecc384.crt" |
||||
blocky_tls_ecc384_key: "{{ blocky_conf_dir }}/ecc384.key" |
||||
|
||||
blocky_port: 9000 |
||||
blocky_enable_dot: yes |
||||
|
||||
blocky_default_groups: |
||||
- selector: default |
||||
groups: |
||||
- all |
||||
|
||||
blocky_default_config: |
||||
port: 53 |
||||
bootstrapDns: 1.1.1.1 |
||||
logLevel: warn |
||||
logTimestamp: no |
||||
upstreamTimeout: 4s |
||||
|
||||
httpPort: "127.0.0.1:{{ blocky_port }}" |
||||
|
||||
prometheus: |
||||
enable: "{{ host_metrics }}" |
||||
|
||||
caching: |
||||
maxTime: 8h |
||||
maxItemsCount: 15000 |
||||
prefetchMaxItemsCount: 1000 |
||||
|
||||
upstream: |
||||
default: |
||||
- tcp-tls:anycast.censurfridns.dk:853 |
||||
- tcp-tls:dns.quad9.net:853 |
||||
- tcp-tls:one.one.one.one:853 |
||||
- tcp-tls:dns.digitale-gesellschaft.ch:853 |
||||
|
||||
blocking: |
||||
blackLists: |
||||
all: |
||||
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts |
||||
- https://block.energized.pro/extensions/regional/formats/hosts |
||||
- https://block.energized.pro/bluGo/formats/hosts |
||||
whiteLists: |
||||
all: |
||||
- https://raw.githubusercontent.com/anudeepND/whitelist/master/domains/whitelist.txt |
||||
refreshPeriod: 8h |
||||
blockTTL: 5m |
@ -0,0 +1,4 @@ |
||||
- name: restart blocky |
||||
service: |
||||
name: blocky |
||||
state: restarted |
@ -0,0 +1,185 @@ |
||||
- name: import internal tld resolver vars if internal nameserver is present |
||||
include_vars: |
||||
file: internal.yml |
||||
when: services.internal_ns is defined |
||||
|
||||
|
||||
- name: import ipv6 disable snippet |
||||
include_vars: |
||||
file: disable_ipv6.yml |
||||
hash_behaviour: merge |
||||
when: blocky_disable_ipv6 | d(false) == true |
||||
|
||||
|
||||
- name: import tls support |
||||
include_vars: |
||||
file: tls.yml |
||||
hash_behaviour: merge |
||||
when: host_tls and blocky_enable_dot |
||||
|
||||
|
||||
- name: set blocky_cfg |
||||
set_fact: |
||||
blocky_cfg: "{{ blocky_default_config | d({}) | combine(blocky_config | d({}), recursive=true) }}" |
||||
|
||||
|
||||
- name: install dependencies |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- libcap |
||||
- libc6-compat |
||||
|
||||
|
||||
- name: create user and group |
||||
include_tasks: tasks/create_user.yml |
||||
vars: |
||||
user: |
||||
name: "{{ blocky_user }}" |
||||
group: "{{ blocky_group }}" |
||||
dir: "{{ blocky_dir }}" |
||||
notify: restart blocky |
||||
|
||||
|
||||
- name: create directories |
||||
file: |
||||
path: "{{ item }}" |
||||
state: directory |
||||
mode: 0755 |
||||
owner: "{{ blocky_user }}" |
||||
group: "{{ blocky_group }}" |
||||
notify: restart blocky |
||||
loop: |
||||
- "{{ blocky_conf_dir }}" |
||||
- "{{ blocky_dir }}" |
||||
|
||||
|
||||
- name: get and extract latest version of blocky |
||||
include_tasks: tasks/get_lastversion.yml |
||||
vars: |
||||
package: |
||||
name: 0xERR0R/blocky |
||||
location: github |
||||
assets: yes |
||||
asset_filter: 'Linux_x86_64.tar.gz$' |
||||
file: "{{ blocky_dir }}/last_version" |
||||
extract: "{{ blocky_dir }}" |
||||
user: "{{ blocky_user }}" |
||||
group: "{{ blocky_group }}" |
||||
notify: restart blocky |
||||
|
||||
|
||||
- name: template config file |
||||
template: |
||||
src: blocky.j2 |
||||
dest: "{{ blocky_conf_file }}" |
||||
force: yes |
||||
mode: 0400 |
||||
owner: "{{ blocky_user }}" |
||||
group: "{{ blocky_group }}" |
||||
lstrip_blocks: yes |
||||
notify: restart blocky |
||||
|
||||
|
||||
- name: template init script |
||||
template: |
||||
src: init.j2 |
||||
dest: /etc/init.d/blocky |
||||
force: yes |
||||
mode: "+x" |
||||
notify: restart blocky |
||||
|
||||
|
||||
- name: ensure blocky binary has executable bit set |
||||
file: |
||||
path: "{{ blocky_dir }}/blocky" |
||||
mode: "+x" |
||||
|
||||
|
||||
- name: add cap_net_bind_service to blocky executable |
||||
community.general.capabilities: |
||||
path: "{{ blocky_dir }}/blocky" |
||||
capability: cap_net_bind_service+ep |
||||
changed_when: no |
||||
|
||||
|
||||
- name: install and configure nginx |
||||
include_role: |
||||
name: nginx |
||||
vars: |
||||
nginx: |
||||
servers: |
||||
- conf: nginx_server |
||||
certs: "{{ host_tls }}" |
||||
external_tld: "{{ host_tld }}" |
||||
|
||||
|
||||
- block: |
||||
- name: get certificate file type |
||||
stat: |
||||
path: /etc/nginx/tls/ecc384.crt |
||||
register: stat |
||||
|
||||
|
||||
- name: copy nginx ecc384 certificate to blocky dir |
||||
copy: |
||||
src: "/etc/nginx/tls/{{ item.src }}" |
||||
dest: "{{ item.dest }}" |
||||
force: yes |
||||
mode: 0400 |
||||
owner: "{{ blocky_user }}" |
||||
group: "{{ blocky_group }}" |
||||
remote_src: yes |
||||
loop: |
||||
- src: ecc384.crt |
||||
dest: "{{ blocky_tls_ecc384_cert }}" |
||||
- src: ecc384.key |
||||
dest: "{{ blocky_tls_ecc384_key }}" |
||||
when: not (stat.stat.islnk is defined and stat.stat.islnk) |
||||
|
||||
|
||||
- name: create symlinks |
||||
file: |
||||
path: "{{ item.dest }}" |
||||
src: "/etc/nginx/tls/{{ item.src }}" |
||||
state: link |
||||
force: yes |
||||
loop: |
||||
- src: ecc384.crt |
||||
dest: "{{ blocky_tls_ecc384_cert }}" |
||||
- src: ecc384.key |
||||
dest: "{{ blocky_tls_ecc384_key }}" |
||||
when: stat.stat.islnk is defined and stat.stat.islnk |
||||
|
||||
when: host_tls and blocky_enable_dot |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ blocky_conf_dir }}" |
||||
|
||||
|
||||
- name: add prometheus metric target |
||||
include_role: |
||||
name: prometheus |
||||
vars: |
||||
function: add_target |
||||
target: |
||||
name: blocky |
||||
scheme: "{{ host_protocol }}" |
||||
when: host_metrics |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: enable and start blocky |
||||
service: |
||||
name: blocky |
||||
enabled: yes |
||||
state: started |
@ -0,0 +1,7 @@ |
||||
{%- set mappings = blocky_default_mappings | items2dict(key_name='tld', value_name='resolver') -%} |
||||
{%- set conditional = { 'conditional': { 'mapping': mappings }} -%} |
||||
|
||||
{%- set groups = blocky_default_groups | items2dict(key_name='selector', value_name='groups') -%} |
||||
{%- set clientGroupsBlock = { 'blocking': { 'clientGroupsBlock': groups }} -%} |
||||
|
||||
{{- blocky_cfg | combine(clientGroupsBlock, recursive=true) | combine(conditional, recursive=true) | to_nice_yaml(indent=2, width=512) }} |
@ -0,0 +1,19 @@ |
||||
#!/sbin/openrc-run |
||||
|
||||
name="blocky" |
||||
command="{{ blocky_dir }}/blocky" |
||||
command_args="--config {{ blocky_conf_file | quote }}" |
||||
directory="{{ blocky_dir }}" |
||||
command_user="{{ blocky_user }}:{{ blocky_group }}" |
||||
pidfile="/var/run/blocky.pid" |
||||
command_background=true |
||||
start_stop_daemon_args="--stdout-logger logger --stderr-logger logger" |
||||
|
||||
depend() { |
||||
need net |
||||
use dns |
||||
} |
||||
|
||||
start_pre() { |
||||
setcap 'cap_net_bind_service=+ep' {{ (blocky_dir ~ '/blocky') | quote }} |
||||
} |
@ -0,0 +1,16 @@ |
||||
location / { |
||||
return 404; |
||||
} |
||||
|
||||
location /dns-query { |
||||
proxy_pass http://127.0.0.1:{{ blocky_port }}; |
||||
proxy_set_header Connection ""; |
||||
} |
||||
|
||||
{% if host_metrics -%} |
||||
location /metrics { |
||||
proxy_pass http://127.0.0.1:{{ blocky_port }}; |
||||
allow {{ int_net }}; |
||||
deny all; |
||||
} |
||||
{%- endif %} |
@ -0,0 +1,4 @@ |
||||
blocky_default_config: |
||||
filtering: |
||||
queryTypes: |
||||
- AAAA |
@ -0,0 +1,7 @@ |
||||
blocky_default_mappings: |
||||
- tld: "{{ int_tld }}" |
||||
resolver: "{%- if services.internal_ns is mapping -%}\ |
||||
{{- hostvars[services.internal_ns.hostname]['ansible_host'] -}}\ |
||||
{%- else -%}\ |
||||
{{- hostvars | dict2items | selectattr('key', 'in', services.internal_ns | map(attribute='hostname')) | map(attribute='value') | list | map(attribute='ansible_host') | list | join(',') -}}\ |
||||
{%- endif -%}" |
@ -0,0 +1,4 @@ |
||||
blocky_default_config: |
||||
tlsPort: 853 |
||||
certFile: "{{ blocky_tls_ecc384_cert }}" |
||||
keyFile: "{{ blocky_tls_ecc384_key }}" |
@ -0,0 +1,27 @@ |
||||
ca_key_types: |
||||
- { name: rsa2048, type: RSA, size: 2048 } |
||||
- { name: ecc384, type: ECC, curve: secp384r1, digest: sha384 } |
||||
|
||||
ca_key_names: "{{ ca_key_types | map(attribute='name') | list }}" |
||||
|
||||
ca_default_items: |
||||
- { type: ecc384 } |
||||
- { type: rsa2048 } |
||||
|
||||
ca_dir: /etc/ca |
||||
|
||||
ca_rp: root- |
||||
ca_ip: inter- |
||||
ca_crt_ext: crt |
||||
ca_key_ext: key |
||||
ca_csr_ext: csr |
||||
ca_pfx_ext: pfx |
||||
|
||||
# when to start to reissue certs |
||||
ca_reissue_period: 8w |
||||
|
||||
ca_options: {} |
||||
|
||||
crl_last_update_time: +8w |
||||
crl_next_update_time: +24w |
||||
crl_dir: /opt/crl |
@ -0,0 +1,227 @@ |
||||
- include_tasks: prepare_item.yml |
||||
|
||||
|
||||
- name: define combined options |
||||
set_fact: |
||||
ca_combined: "{{ ca_options | d({}) | combine(item) }}" |
||||
|
||||
|
||||
- name: define cert parameters |
||||
set_fact: |
||||
key_path: "{%- if item.key is defined -%}{{ item.key }}\ |
||||
{%- else -%}{{ ca_combined.path ~ '/' ~ kt.name ~ '.' ~ ca_key_ext }}\ |
||||
{%- endif -%}" |
||||
|
||||
cert_path: "{%- if item.cert is defined -%}{{ item.cert }}\ |
||||
{%- else -%}{{ ca_combined.path ~ '/' ~ kt.name ~ '.' ~ ca_crt_ext }}\ |
||||
{%- endif -%}" |
||||
|
||||
use_acme: "{{ ca_combined.acme | d(has_acme | d(false)) }}" |
||||
|
||||
|
||||
- name: define tld and presets |
||||
set_fact: |
||||
ca_tld: "{{ ca_combined.tld | d(host_tld) }}" |
||||
ca_presets: |
||||
web: |
||||
cn: FQDN |
||||
eku: ['clientAuth', 'serverAuth'] |
||||
ku: ['digitalSignature', 'keyEncipherment', 'keyAgreement'] |
||||
san: FQDN |
||||
psh: |
||||
cn: FQDN |
||||
eku: ['serverAuth'] |
||||
ku: ['digitalSignature', 'keyEncipherment', 'keyAgreement'] |
||||
san: FQDN |
||||
|
||||
|
||||
- name: select a preset |
||||
set_fact: |
||||
ca_preset: > |
||||
{% if item.preset is defined -%}{{ ca_presets[item.preset] }} |
||||
{%- elif ca_options.preset is defined -%}{{ ca_presets[ca_options.preset] }} |
||||
{%- else -%}{{ None }} |
||||
{%- endif %} |
||||
|
||||
|
||||
- name: generate private key |
||||
community.crypto.openssl_privatekey: |
||||
path: "{{ key_path }}" |
||||
size: "{{ kt.size | d(omit) }}" |
||||
curve: "{{ kt.curve | d(omit) }}" |
||||
type: "{{ kt.type }}" |
||||
backup: yes |
||||
force: no |
||||
format: pkcs8 |
||||
format_mismatch: convert |
||||
regenerate: never |
||||
mode: "{{ k_mode | d(omit) }}" |
||||
owner: "{{ k_owner | d(omit) }}" |
||||
group: "{{ k_group | d(omit) }}" |
||||
notify: "{{ ca_options.notify | d(omit) }}" |
||||
|
||||
|
||||
- name: generate in-memory csr request for private key |
||||
community.crypto.openssl_csr_pipe: |
||||
basic_constraints: |
||||
- 'CA:FALSE' |
||||
basic_constraints_critical: yes |
||||
digest: "{{ kt.digest | d(omit) }}" |
||||
key_usage_critical: yes |
||||
privatekey_path: "{{ key_path }}" |
||||
|
||||
common_name: "{%- if item.cn is defined -%}{{ item.cn }}\ |
||||
{%- elif ca_options.cn is defined -%}{{ ca_options.cn }}\ |
||||
{%- elif ca_preset.cn == 'FQDN' -%}{{ host_name ~ '.' ~ ca_tld }}\ |
||||
{%- elif ca_preset.cn is defined -%}{{ ca_preset.cn }}\ |
||||
{%- endif -%}" |
||||
|
||||
extended_key_usage: "{%- if item.eku is defined -%}{{ item.eku }}\ |
||||
{%- elif ca_options.eku is defined -%}{{ ca_options.eku }}\ |
||||
{%- elif ca_preset.eku is defined -%}{{ ca_preset.eku }}\ |
||||
{%- endif -%}" |
||||
|
||||
key_usage: "{%- if item.ku is defined -%}{{ item.ku }}\ |
||||
{%- elif ca_options.ku is defined -%}{{ ca_options.ku }}\ |
||||
{%- elif ca_preset.ku is defined -%}{{ ca_preset.ku }}\ |
||||
{%- else -%}{{ ['digitalSignature', 'keyEncipherment', 'keyAgreement'] }}\ |
||||
{%- endif -%}" |
||||
|
||||
subject_alt_name: "{%- if item.san is defined -%}{{ item.san }}\ |
||||
{%- elif ca_options.san is defined -%}{{ ca_options.san }}\ |
||||
{%- elif item.cn is defined -%}{{ ['DNS:' ~ item.cn] }}\ |
||||
{%- elif ca_options.cn is defined -%}{{ ['DNS:' ~ ca_options.cn] }}\ |
||||
{%- elif ca_preset.san == 'FQDN' -%}{{ ['DNS:' ~ host_name ~ '.' ~ ca_tld] }}\ |
||||
{%- elif ca_preset.san is defined -%}{{ ca_preset.san }}\ |
||||
{%- endif -%}" |
||||
|
||||
ocsp_must_staple: "{{ (has_acme | d(false)) and (ca_options.ocsp_must_staple | d(false)) }}" |
||||
register: csr |
||||
changed_when: no |
||||
|
||||
|
||||
- name: check if cert already exists |
||||
stat: |
||||
path: "{{ cert_path }}" |
||||
register: cert_exists |
||||
|
||||
|
||||
- name: slurp cert if exists |
||||
slurp: |
||||
src: "{{ cert_path }}" |
||||
when: cert_exists.stat.exists |
||||
register: cert |
||||
|
||||
|
||||
- name: check if the cert validity period is about to expire |
||||
community.crypto.x509_certificate_info: |
||||
content: "{{ cert.content | b64decode }}" |
||||
valid_at: |
||||
reissue_period: "+{%- if has_acme | d(false) == true -%}45d\ |
||||
{%- else -%}{{ ca_reissue_period | d('8w') }}\ |
||||
{%- endif -%}" |
||||
when: cert_exists.stat.exists |
||||
register: cert_info |
||||
|
||||
|
||||
- block: |
||||
- name: generate certificate on ca |
||||
community.crypto.x509_certificate_pipe: |
||||
content: "{{ (cert.content | b64decode) if cert_exists.stat.exists else omit }}" |
||||
csr_content: "{{ csr.csr }}" |
||||
provider: ownca |
||||
ownca_not_after: "{{ item.duration | d('+365d') }}" |
||||
ownca_not_before: -1d |
||||
ownca_digest: "{{ kt.digest | d(omit) }}" |
||||
ownca_path: "{{ ca_dir }}/{{ ca_ip }}{{ kt.name }}.{{ ca_crt_ext }}" |
||||
ownca_privatekey_path: "{{ ca_dir }}/{{ ca_ip }}{{ kt.name }}.{{ ca_key_ext }}" |
||||
ownca_privatekey_passphrase: "{{ ca_pk_inter_password }}" |
||||
force: "{{ cert_exists.stat.exists and not cert_info.valid_at.reissue_period }}" |
||||
register: cert |
||||
delegate_to: "{{ services.ca.hostname }}" |
||||
notify: "{{ ca_options.notify | d(omit) }}" |
||||
|
||||
|
||||
- name: save new cert if it was changed |
||||
copy: |
||||
dest: "{{ cert_path }}" |
||||
content: "{{ cert.certificate }}" |
||||
mode: "{{ k_mode | d(omit) }}" |
||||
owner: "{{ k_owner | d(omit) }}" |
||||
group: "{{ k_group | d(omit) }}" |
||||
follow: "{{ (ca_options | combine(item)).follow_symlinks | d(omit) }}" |
||||
when: cert is changed |
||||
notify: "{{ ca_options.notify | d(omit) }}" |
||||
|
||||
when: has_acme | d(false) == false |
||||
|
||||
|
||||
- name: generate acme certificate |
||||
include_tasks: gen_acme.yml |
||||
when: has_acme | d(false) == true |
||||
|
||||
|
||||
- block: |
||||
- name: slurp certificate |
||||
slurp: |
||||
src: "{{ cert_path }}" |
||||
register: cert |
||||
|
||||
- name: complete certificate chain |
||||
community.crypto.certificate_complete_chain: |
||||
input_chain: "{{ ((cert.content | b64decode).split('\n\n'))[0] }}" |
||||
root_certificates: /etc/ssl/certs |
||||
register: chain |
||||
|
||||
- name: save chain to file |
||||
copy: |
||||
dest: "{{ item.chain }}" |
||||
content: | |
||||
{% set result = chain.complete_chain %} |
||||
{% set _ = result.pop(0) %} |
||||
{{ result | join('') }} |
||||
mode: "{{ k_mode | d(omit) }}" |
||||
owner: "{{ k_owner | d(omit) }}" |
||||
group: "{{ k_group | d(omit) }}" |
||||
follow: "{{ (ca_options | combine(item)).follow_symlinks | d(omit) }}" |
||||
notify: "{{ ca_options.notify | d(omit) }}" |
||||
|
||||
when: item.chain is string |
||||
|
||||
|
||||
- block: |
||||
- name: slurp intermediate from ca |
||||
slurp: |
||||
src: "{{ ca_dir }}/{{ ca_ip }}{{ kt.name }}.{{ ca_crt_ext }}" |
||||
register: inter |
||||
delegate_to: "{{ services.ca.hostname }}" |
||||
|
||||
|
||||
- name: add intermediate cert if requested |
||||
blockinfile: |
||||
block: "{{ inter.content | b64decode }}" |
||||
insertafter: EOF |
||||
marker: "" |
||||
path: "{{ cert_path }}" |
||||
notify: "{{ ca_options.notify | d(omit) }}" |
||||
|
||||
when: (use_acme | d(false) == false) and (cert is changed) and ((ca_options | combine(item)).concat_inter | d(true) == true) |
||||
|
||||
|
||||
- block: |
||||
- name: slurp root from ca |
||||
slurp: |
||||
src: "{{ ca_dir }}/{{ ca_rp }}{{ kt.name }}.{{ ca_crt_ext }}" |
||||
register: root |
||||
delegate_to: "{{ services.ca.hostname }}" |
||||
|
||||
|
||||
- name: add root cert if requested |
||||
blockinfile: |
||||
block: "{{ root.content | b64decode }}" |
||||
insertafter: EOF |
||||
marker: "" |
||||
path: "{{ cert_path }}" |
||||
notify: "{{ ca_options.notify | d(omit) }}" |
||||
|
||||
when: (use_acme | d(false) == false) and (cert is changed) and ((ca_options | combine(item)).concat_root | d(false) == true) |
@ -0,0 +1,44 @@ |
||||
- include_tasks: prepare_item.yml |
||||
|
||||
|
||||
- name: slurp root from ca |
||||
slurp: |
||||
src: "{{ ca_dir }}/{{ ca_rp }}{{ kt.name }}.{{ ca_crt_ext }}" |
||||
register: root |
||||
delegate_to: "{{ services.ca.hostname }}" |
||||
|
||||
|
||||
- name: copy root to memory |
||||
set_fact: |
||||
"root_{{ kt.name }}": "{{ root.content | b64decode }}" |
||||
when: (ca_options | combine(item)).memory | d(false) == true |
||||
|
||||
|
||||
- name: copy root to remote node |
||||
copy: |
||||
dest: "{%- if item.path is defined -%}{{ item.path }}\ |
||||
{%- else -%}{{ ca_options.path ~ '/' ~ ca_rp ~ kt.name ~ '.' ~ ca_crt_ext }}\ |
||||
{%- endif -%}" |
||||
content: "{{ root.content | b64decode }}" |
||||
mode: "{{ k_mode | d(omit) }}" |
||||
owner: "{{ k_owner | d(omit) }}" |
||||
group: "{{ k_group | d(omit) }}" |
||||
when: (ca_options | combine(item)).path is defined |
||||
|
||||
|
||||
- name: copy root to system storage |
||||
block: |
||||
- name: ensure ca-certificates is installed |
||||
package: |
||||
name: ca-certificates |
||||
|
||||
- name: upload root cert to user cert storage |
||||
copy: |
||||
dest: "/usr/local/share/ca-certificates/{{ ca_rp }}{{ kt.name }}.{{ ca_crt_ext }}" |
||||
content: "{{ root.content | b64decode }}" |
||||
|
||||
- name: update ca certificates |
||||
command: /usr/sbin/update-ca-certificates |
||||
changed_when: no |
||||
|
||||
when: (ca_options | combine(item)).system | d(false) == true |
@ -0,0 +1,18 @@ |
||||
- block: |
||||
- name: check if acme main account exists |
||||
community.crypto.acme_account_info: |
||||
account_key_src: "{{ ca_dir ~ '/acme-main.' ~ ca_key_ext }}" |
||||
account_key_passphrase: "{{ ca_acme_account_key_password }}" |
||||
acme_directory: "{{ ca_acme_endpoint | d('https://acme-v02.api.letsencrypt.org/directory') }}" |
||||
acme_version: "{{ ca_acme_version | d(2) }}" |
||||
register: acme_info |
||||
delegate_to: "{{ services.ca.hostname }}" |
||||
|
||||
- name: determine acme support |
||||
set_fact: |
||||
has_acme: "{{ acme_info is defined and acme_info.exists and acme_info.account.status == 'valid' and (acme_disable | d(false) == false) }}" |
||||
|
||||
rescue: |
||||
- name: revert has_acme |
||||
set_fact: |
||||
has_acme: false |
@ -0,0 +1,86 @@ |
||||
- name: define some acme parameters |
||||
set_fact: |
||||
acme_staging: "{{ (ca_options | d({}) | combine(item)).acme_staging | d(false) }}" |
||||
acme_upgrade_int_ca: "{{ cert_info is defined and ((cert_info.ocsp_uri is not defined) or (cert_info.ocsp_uri == None)) }}" |
||||
|
||||
|
||||
- name: determine if acme cert generation will be forced |
||||
set_fact: |
||||
acme_forced: "{{ acme_upgrade_int_ca or (always_update_acme is defined) }}" |
||||
|
||||
|
||||
- name: slurp account key from ca |
||||
slurp: |
||||
src: "{{ ca_dir ~ '/acme-' ~ ('staging' if acme_staging == true else 'main') ~ '.' ~ ca_key_ext }}" |
||||
register: acme_account_key |
||||
delegate_to: "{{ services.ca.hostname }}" |
||||
|
||||
|
||||
- name: define args for acme certificate generation |
||||
set_fact: |
||||
acme_common_args: |
||||
account_key_content: "{{ acme_account_key.content | b64decode }}" |
||||
account_key_passphrase: "{{ ca_acme_account_key_password }}" |
||||
acme_directory: "{%- if (acme_staging == false) or (acme_staging == None) -%}{{ ca_acme_endpoint | d('https://acme-v02.api.letsencrypt.org/directory') }}\ |
||||
{%- else -%}{{ ca_acme_staging_endpoint | d('https://acme-staging-v02.api.letsencrypt.org/directory') }}\ |
||||
{%- endif -%}" |
||||
acme_version: "{{ ca_acme_version | d(2) }}" |
||||
acme_extra_args: |
||||
challenge: dns-01 |
||||
csr_content: "{{ csr.csr }}" |
||||
fullchain_dest: "{{ cert_path if ((ca_options | d({}) | combine(item)).concat_inter | d(true) == true) else omit }}" |
||||
dest: "{{ cert_path if ((ca_options | d({}) | combine(item)).concat_inter | d(true) == false) else omit }}" |
||||
modify_account: no |
||||
remaining_days: 45 |
||||
force: "{{ acme_forced }}" |
||||
terms_agreed: yes |
||||
|
||||
|
||||
- name: generate acme challenge request |
||||
community.crypto.acme_certificate: |
||||
args: "{{ acme_common_args | combine(acme_extra_args) }}" |
||||
register: challenge |
||||
changed_when: no |
||||
|
||||
|
||||
- block: |
||||
- name: unset challenge_records |
||||
set_fact: |
||||
challenge_records: "{{ [] }}" |
||||
|
||||
|
||||
- name: fill challenge records |
||||
set_fact: |
||||
challenge_records: "{{ challenge_records + [{ |
||||
'name': item2.key | regex_search('(.*).' ~ (tld | regex_escape()), '\\1') | first, |
||||
'type': 'TXT', |
||||
'value': item2.value[0] |
||||
}] }}" |
||||
loop: "{{ challenge['challenge_data_dns'] | dict2items }}" |
||||
loop_control: |
||||
loop_var: item2 |
||||
|
||||
|
||||
- include_tasks: gen_acme_include.yml |
||||
|
||||
|
||||
- block: |
||||
- name: revoke cert if it already exists |
||||
community.crypto.acme_certificate_revoke: |
||||
certificate: "{{ cert_path }}" |
||||
revoke_reason: 4 |
||||
args: "{{ acme_common_args }}" |
||||
when: (cert_exists is defined) and cert_exists.stat.exists and not acme_upgrade_int_ca |
||||
|
||||
rescue: |
||||
- debug: |
||||
msg: failed to revoke certificate, ignoring |
||||
|
||||
|
||||
- name: finalize acme challenge request |
||||
community.crypto.acme_certificate: |
||||
data: "{{ challenge }}" |
||||
args: "{{ acme_common_args | combine(acme_extra_args) }}" |
||||
notify: "{{ ca_options.notify | d(omit) }}" |
||||
|
||||
when: (challenge.cert_days is not defined) or (challenge.cert_days < 45) or acme_forced |
@ -0,0 +1,7 @@ |
||||
- name: add records to external ns |
||||
include_role: |
||||
name: external_ns |
||||
vars: |
||||
nse_items: "{{ challenge_records }}" |
||||
nse_function: add_records |
||||
nse_instant: true |
@ -0,0 +1,74 @@ |
||||
- name: define dh param dict |
||||
set_fact: |
||||
dh: "{{ {'remote_gen': true, 'size': 2048, 'backup': false} | combine(dh_params | d({})) }}" |
||||
|
||||
|
||||
- name: check if dhparam file exists |
||||
stat: |
||||
path: "{{ dh.path | mandatory }}" |
||||
register: res |
||||
|
||||
|
||||
- block: |
||||
- name: ensure cryptography toolkit is installed |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- alpine: py3-cryptography |
||||
debian: python3-cryptography |
||||
when: dh.remote_gen == false |
||||
|
||||
|
||||
- block: |
||||
- name: wait until ca becomes available |
||||
wait_for_connection: |
||||
timeout: 10 |
||||
|
||||
- name: create temporary file for dh params |
||||
tempfile: |
||||
state: file |
||||
register: tf |
||||
|
||||
delegate_to: "{{ services.ca.hostname }}" |
||||
when: dh.remote_gen == true |
||||
|
||||
|
||||
- name: generate dh params |
||||
community.crypto.openssl_dhparam: |
||||
path: "{%- if dh.remote_gen == false -%}{{ dh.path | mandatory }}\ |
||||
{%- else -%}{{ tf.path }}\ |
||||
{%- endif -%}" |
||||
size: "{{ dh.size }}" |
||||
backup: "{{ dh.backup }}" |
||||
mode: "{{ (dh.mode | d('0400')) if (dh.remote_gen == false) else '0400' }}" |
||||
owner: "{{ (dh.owner | d(omit)) if (dh.remote_gen == false) else omit }}" |
||||
group: "{{ (dh.group | d(omit)) if (dh.remote_gen == false) else omit }}" |
||||
return_content: "{{ dh.remote_gen == true }}" |
||||
delegate_to: "{{ inventory_hostname if (dh.remote_gen == false) else services.ca.hostname }}" |
||||
notify: "{{ dh.notify | d(omit) }}" |
||||
register: dh_result |
||||
|
||||
|
||||
- block: |
||||
- name: remove temporary file |
||||
file: |
||||
path: "{{ tf.path }}" |
||||
state: absent |
||||
delegate_to: "{{ services.ca.hostname }}" |
||||
|
||||
- name: copy dh result to remote node |
||||
copy: |
||||
content: "{{ dh_result.dhparams }}" |
||||
dest: "{{ dh.path | mandatory }}" |
||||
mode: "{{ dh.mode | d('0400') }}" |
||||
owner: "{{ dh.owner | d(omit) }}" |
||||
group: "{{ dh.group | d(omit) }}" |
||||
|
||||
when: dh.remote_gen == true |
||||
|
||||
when: (not res.stat.exists) or (dh.remote_gen == false) |
||||
|
||||
|
||||
- name: unset dh param dict |
||||
set_fact: |
||||
dh: "{{ {} }}" |
@ -0,0 +1,154 @@ |
||||
- name: ensure cryptography toolkit is installed |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- alpine: py3-cryptography |
||||
debian: python3-cryptography |
||||
|
||||
|
||||
- name: early check to ensure ca variables are defined |
||||
fail: |
||||
msg: "\"{{ item }}\" is not defined" |
||||
when: item is not defined |
||||
loop: |
||||
- ca_dir |
||||
- ca_key_types |
||||
- ca_rp |
||||
- ca_ip |
||||
- ca_crt_ext |
||||
- ca_csr_ext |
||||
- ca_key_ext |
||||
|
||||
|
||||
- name: create ca directories |
||||
file: |
||||
path: "{{ ca_dir }}" |
||||
state: directory |
||||
mode: 0700 |
||||
|
||||
|
||||
- name: generate root private keys |
||||
community.crypto.openssl_privatekey: |
||||
path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_key_ext }}" |
||||
size: "{{ item.size | d(omit) }}" |
||||
curve: "{{ item.curve | d(omit) }}" |
||||
type: "{{ item.type }}" |
||||
backup: yes |
||||
cipher: auto |
||||
force: no |
||||
format: pkcs8 |
||||
format_mismatch: convert |
||||
passphrase: "{{ ca_pk_password }}" |
||||
regenerate: never |
||||
mode: 0600 |
||||
loop: "{{ ca_key_types }}" |
||||
|
||||
|
||||
- name: generate csr requests for all root keys |
||||
community.crypto.openssl_csr: |
||||
path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_csr_ext }}" |
||||
basic_constraints: |
||||
- 'CA:TRUE' |
||||
basic_constraints_critical: yes |
||||
common_name: "{{ org }} Root CA ({{ item.type | upper }})" |
||||
digest: "{{ item.digest | d(omit) }}" |
||||
key_usage: |
||||
- keyCertSign |
||||
- cRLSign |
||||
key_usage_critical: yes |
||||
privatekey_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_key_ext }}" |
||||
privatekey_passphrase: "{{ ca_pk_password }}" |
||||
use_common_name_for_san: no |
||||
mode: 0600 |
||||
loop: "{{ ca_key_types }}" |
||||
|
||||
|
||||
- name: generate root certificates |
||||
community.crypto.x509_certificate: |
||||
path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_crt_ext }}" |
||||
csr_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_csr_ext }}" |
||||
privatekey_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_key_ext }}" |
||||
privatekey_passphrase: "{{ ca_pk_password }}" |
||||
provider: selfsigned |
||||
selfsigned_not_after: "{{ ca_root_valid_until | mandatory }}" |
||||
selfsigned_digest: "{{ item.digest | d(omit) }}" |
||||
mode: 0600 |
||||
loop: "{{ ca_key_types }}" |
||||
|
||||
|
||||
|
||||
|
||||
- name: generate inter private keys |
||||
community.crypto.openssl_privatekey: |
||||
path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_key_ext }}" |
||||
size: "{{ item.size | d(omit) }}" |
||||
curve: "{{ item.curve | d(omit) }}" |
||||
type: "{{ item.type }}" |
||||
backup: yes |
||||
cipher: auto |
||||
force: no |
||||
format: pkcs8 |
||||
format_mismatch: convert |
||||
passphrase: "{{ ca_pk_inter_password }}" |
||||
regenerate: never |
||||
mode: 0600 |
||||
loop: "{{ ca_key_types }}" |
||||
|
||||
|
||||
- name: generate csr requests for all inter keys |
||||
community.crypto.openssl_csr: |
||||
path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_csr_ext }}" |
||||
basic_constraints: |
||||
- 'CA:TRUE' |
||||
- 'pathlen:0' |
||||
basic_constraints_critical: yes |
||||
common_name: "{{ org }} Intermediate CA ({{ item.type | upper }})" |
||||
digest: "{{ item.digest | d(omit) }}" |
||||
key_usage: |
||||
- digitalSignature |
||||
- keyCertSign |
||||
- cRLSign |
||||
key_usage_critical: yes |
||||
privatekey_path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_key_ext }}" |
||||
privatekey_passphrase: "{{ ca_pk_inter_password }}" |
||||
use_common_name_for_san: no |
||||
|
||||
crl_distribution_points: |
||||
- full_name: "URI:http://crl.{{ int_tld }}/{{ item.name }}.crl" |
||||
crl_issuer: "URI:http://crl.{{ int_tld }}" |
||||
name_constraints_permitted: |
||||
- "DNS:{{ tld }}" |
||||
- "email:{{ tld }}" |
||||
name_constraints_excluded: |
||||
- "IP:0.0.0.0/0" |
||||
mode: 0600 |
||||
loop: "{{ ca_key_types }}" |
||||
|
||||
|
||||
- name: generate inter certificates |
||||
community.crypto.x509_certificate: |
||||
path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_crt_ext }}" |
||||
csr_path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_csr_ext }}" |
||||
privatekey_path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_key_ext }}" |
||||
privatekey_passphrase: "{{ ca_pk_inter_password }}" |
||||
provider: ownca |
||||
ownca_not_after: "{{ ca_inter_valid_until | mandatory }}" |
||||
ownca_digest: "{{ item.digest | d(omit) }}" |
||||
ownca_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_crt_ext }}" |
||||
ownca_privatekey_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_key_ext }}" |
||||
ownca_privatekey_passphrase: "{{ ca_pk_password }}" |
||||
mode: 0600 |
||||
loop: "{{ ca_key_types }}" |
||||
|
||||
|
||||
- name: install acme |
||||
include_tasks: install_acme.yml |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ ca_dir }}" |
@ -0,0 +1,39 @@ |
||||
- name: select key type for acme |
||||
set_fact: |
||||
kt: "{{ ca_key_types | selectattr('name', 'equalto', ca_acme_account_key_type | d('ecc384')) | list | first }}" |
||||
|
||||
|
||||
- name: generate acme account keys |
||||
community.crypto.openssl_privatekey: |
||||
path: "{{ ca_dir ~ '/acme-' ~ item ~ '.' ~ ca_key_ext }}" |
||||
size: "{{ kt.size | d(omit) }}" |
||||
curve: "{{ kt.curve | d(omit) }}" |
||||
type: "{{ kt.type }}" |
||||
backup: yes |
||||
cipher: auto |
||||
force: no |
||||
format: pkcs8 |
||||
format_mismatch: convert |
||||
passphrase: "{{ ca_acme_account_key_password }}" |
||||
regenerate: never |
||||
mode: 0600 |
||||
loop: |
||||
- main |
||||
- staging |
||||
|
||||
|
||||
- name: create acme accounts |
||||
community.crypto.acme_account: |
||||
account_key_src: "{{ ca_dir ~ '/acme-' ~ item ~ '.' ~ ca_key_ext }}" |
||||
account_key_passphrase: "{{ ca_acme_account_key_password }}" |
||||
acme_directory: "{%- if item == 'main' -%}{{ ca_acme_endpoint | d('https://acme-v02.api.letsencrypt.org/directory') }}\ |
||||
{%- else -%}{{ ca_acme_staging_endpoint | d('https://acme-staging-v02.api.letsencrypt.org/directory') }}\ |
||||
{%- endif -%}" |
||||
acme_version: "{{ ca_acme_version | d(2) }}" |
||||
contact: |
||||
- "mailto:{{ maintainer_email | d('admin@' ~ tld) }}" |
||||
state: present |
||||
terms_agreed: yes |
||||
loop: |
||||
- main |
||||
- staging |
@ -0,0 +1,51 @@ |
||||
- name: ca installation |
||||
include_tasks: install.yml |
||||
when: function == 'install' |
||||
|
||||
|
||||
- name: install roots |
||||
include_tasks: add_root.yml |
||||
loop: "{{ ca_default_items if (ca_roots is not defined) or (ca_roots == None) or ((ca_roots | length) == 0) else ca_roots }}" |
||||
when: function == 'roots' |
||||
|
||||
|
||||
- block: |
||||
- name: wait until ca becomes available |
||||
wait_for_connection: |
||||
timeout: 10 |
||||
delegate_to: "{{ services.ca.hostname }}" |
||||
|
||||
|
||||
- name: check if acme can be used |
||||
include_tasks: check_acme.yml |
||||
|
||||
|
||||
- name: process roots if no acme will be used |
||||
include_tasks: add_root.yml |
||||
loop: "{{ ca_default_items if (ca_roots is not defined) or (ca_roots == None) or ((ca_roots | length) == 0) else ca_roots }}" |
||||
when: not has_acme |
||||
|
||||
|
||||
- name: ensure cryptography toolkit is installed |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- alpine: py3-cryptography |
||||
debian: python3-cryptography |
||||
|
||||
|
||||
- name: process certificates |
||||
include_tasks: add_cert.yml |
||||
loop: "{{ ca_default_items if (ca_certs is not defined) or (ca_certs == None) or ((ca_certs | length) == 0) else ca_certs }}" |
||||
|
||||
when: function == 'certs' |
||||
|
||||
|
||||
- name: generate dhparams |
||||
include_tasks: gen_dhparam.yml |
||||
when: (function == 'dhparam' or function == 'dhparams') |
||||
|
||||
|
||||
- name: check acme availability |
||||
include_tasks: check_acme.yml |
||||
when: function == 'check_acme' |
@ -0,0 +1,17 @@ |
||||
- name: select key type |
||||
set_fact: |
||||
kt: "{{ ca_key_types | selectattr('name', 'equalto', item.type) | list | first }}" |
||||
|
||||
|
||||
- name: fail if key type is empty |
||||
fail: |
||||
msg: "key type must be one of: {{ ca_key_names | join(', ') }}" |
||||
when: (kt | length) == 0 |
||||
|
||||
|
||||
- name: set preferred mode, owner and group |
||||
set_fact: |
||||
k_mode: "{{ (ca_options | d({}) | combine(item)).mode | d(omit) }}" |
||||
k_owner: "{{ (ca_options | d({}) | combine(item)).owner | d(omit) }}" |
||||
k_group: "{{ (ca_options | d({}) | combine(item)).group | d(omit) }}" |
||||
k_none: yes |
@ -0,0 +1,24 @@ |
||||
cdr_user: cdr |
||||
cdr_group: cdr |
||||
cdr_dir: /opt/cdr |
||||
cdr_port: 3000 |
||||
|
||||
cdr_default_config: |
||||
port: "{{ cdr_port }}" |
||||
|
||||
db_type: pg |
||||
db_host: "{{ database_host }}" |
||||
db_user: "{{ database_user }}" |
||||
db_password: "{{ database_pass }}" |
||||
db_database: "{{ database_name }}" |
||||
db_table: cdr |
||||
|
||||
record_dir: /opt/recordings |
||||
record_pretty_names: yes |
||||
|
||||
ami_enable: yes |
||||
ami_host: 127.0.0.1 |
||||
|
||||
originate_enable: yes |
||||
originate_context: outbound |
||||
originate_timeout: 30 |
@ -0,0 +1,4 @@ |
||||
- name: restart cdr |
||||
service: |
||||
name: cdr |
||||
state: restarted |
@ -0,0 +1,141 @@ |
||||
- name: set cdr_cfg |
||||
set_fact: |
||||
cdr_cfg: "{{ cdr_default_config | d({}) | combine(cdr_config | d({}), recursive=true) }}" |
||||
|
||||
|
||||
- name: install dependencies |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- nodejs |
||||
- npm |
||||
|
||||
|
||||
- name: add extra cname record |
||||
include_role: |
||||
name: ns |
||||
vars: |
||||
function: add_records |
||||
ns_add_default_record: no |
||||
ns_records: |
||||
- name: cdr |
||||
type: CNAME |
||||
value: "{{ host_fqdn }}" |
||||
when: "inventory_hostname != 'cdr'" |
||||
|
||||
|
||||
- name: create user and group |
||||
include_tasks: tasks/create_user.yml |
||||
vars: |
||||
user: |
||||
name: "{{ cdr_user }}" |
||||
group: "{{ cdr_group }}" |
||||
dir: "{{ cdr_dir }}" |
||||
notify: restart cdr |
||||
|
||||
|
||||
- name: ensure cdr dir exists |
||||
file: |
||||
path: "{{ cdr_dir }}" |
||||
state: directory |
||||
owner: "{{ cdr_user }}" |
||||
group: "{{ cdr_group }}" |
||||
|
||||
|
||||
- name: ensure recordings dir exists |
||||
file: |
||||
path: "{{ cdr_cfg.record_dir }}" |
||||
state: directory |
||||
|
||||
|
||||
- name: get source-mark status |
||||
stat: |
||||
path: "{{ cdr_dir }}/source-mark" |
||||
register: source_mark |
||||
|
||||
|
||||
- name: pause if source-mark is missing |
||||
pause: |
||||
prompt: source-mark is missing, source code has to be manually uploaded |
||||
when: source_mark.stat.exists == false |
||||
|
||||
|
||||
- name: create source-mark |
||||
file: |
||||
path: "{{ cdr_dir }}/source-mark" |
||||
state: touch |
||||
modification_time: preserve |
||||
access_time: preserve |
||||
|
||||
|
||||
- name: template env file |
||||
template: |
||||
src: env.j2 |
||||
dest: "{{ cdr_dir }}/.env" |
||||
force: yes |
||||
owner: "{{ cdr_user }}" |
||||
group: "{{ cdr_group }}" |
||||
lstrip_blocks: yes |
||||
notify: restart cdr |
||||
|
||||
|
||||
- name: ensure app script has executable bit set |
||||
file: |
||||
path: "{{ cdr_dir }}/app.js" |
||||
mode: "+x" |
||||
|
||||
|
||||
- name: install npm dependencies |
||||
npm: |
||||
path: "{{ cdr_dir }}" |
||||
no_optional: yes |
||||
ignore_scripts: yes |
||||
production: yes |
||||
become: yes |
||||
become_user: "{{ cdr_user }}" |
||||
become_method: su |
||||
become_flags: '-s /bin/ash' |
||||
notify: restart cdr |
||||
changed_when: no |
||||
|
||||
|
||||
- name: template init script |
||||
template: |
||||
src: init.j2 |
||||
dest: /etc/init.d/cdr |
||||
force: yes |
||||
mode: "+x" |
||||
notify: restart cdr |
||||
|
||||
|
||||
- name: install and configure nginx |
||||
include_role: |
||||
name: nginx |
||||
vars: |
||||
nginx: |
||||
servers: |
||||
- conf: nginx_server |
||||
override_server_name: cdr |
||||
certs: "{{ host_tls }}" |
||||
domains: |
||||
- "cdr.{{ host_tld }}" |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ cdr_dir }}" |
||||
|
||||
|
||||
- name: enable and start cdr |
||||
service: |
||||
name: cdr |
||||
enabled: yes |
||||
state: started |
@ -0,0 +1,3 @@ |
||||
{% for option in (cdr_cfg | d({}) | dict2items) -%} |
||||
{{ option.key | upper }}={{ option.value | quote }} |
||||
{% endfor -%} |
@ -0,0 +1,14 @@ |
||||
#!/sbin/openrc-run |
||||
|
||||
name="$SVCNAME" |
||||
directory="{{ cdr_dir }}" |
||||
command="node {{ cdr_dir }}/app.js" |
||||
command_user="{{ cdr_user }}:{{ cdr_group }}" |
||||
pidfile="/var/run/$SVCNAME.pid" |
||||
supervisor="supervise-daemon" |
||||
respawn_max=0 |
||||
|
||||
depend() { |
||||
need net |
||||
use dns |
||||
} |
@ -0,0 +1,11 @@ |
||||
set_real_ip_from 10.0.0.0/8; |
||||
real_ip_header X-Real-IP; |
||||
real_ip_recursive on; |
||||
|
||||
location / { |
||||
proxy_pass http://127.0.0.1:{{ cdr_port }}; |
||||
proxy_http_version 1.1; |
||||
proxy_set_header X-Real-IP $remote_addr; |
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
||||
proxy_set_header X-Forwarded-Proto $scheme; |
||||
} |
@ -0,0 +1,24 @@ |
||||
- name: set combined cert info |
||||
set_fact: |
||||
combined: "{{ (common | d({})) | combine(cert | d({}), recursive=true) }}" |
||||
|
||||
|
||||
- name: generate certificate through acme-dns |
||||
include_role: |
||||
name: acme |
||||
vars: |
||||
acme_id: "{{ cert.id | d(host_name ~ ('-ecc' if (combined.ecc | d(false) == true) else '')) }}" |
||||
acme_cert: "{{ cert.cert }}" |
||||
acme_key: "{{ cert.key }}" |
||||
acme_chain: "{{ cert.chain | d(None) }}" |
||||
acme_cert_single: "{{ cert.cert_single | d(None) }}" |
||||
acme_ecc: "{{ combined.ecc | d(false) }}" |
||||
acme_stapling: no |
||||
acme_notify: "{{ combined.notify | d(None) }}" |
||||
acme_owner: "{{ combined.owner | d(None) }}" |
||||
acme_group: "{{ combined.group | d(None) }}" |
||||
acme_post_hook: "{{ combined.post_hook | d(None) }}" |
||||
acme_hostname: "{{ combined.hostname | d(None) }}" |
||||
acme_tld: "{{ combined.tld | d(None) }}" |
||||
acme_fqdn: "{{ combined.fqdn | d(None) }}" |
||||
acme_hosts: "{{ combined.hosts | d(None) }}" |
@ -0,0 +1,46 @@ |
||||
- name: set combined cert info |
||||
set_fact: |
||||
combined: "{{ cert | combine(common | d({}), recursive=true) }}" |
||||
|
||||
|
||||
- name: clear san list |
||||
set_fact: |
||||
san_list: "{{ [] }}" |
||||
|
||||
|
||||
- block: |
||||
- name: build san list |
||||
set_fact: |
||||
san_list: "{{ (san_list | d([])) + ['DNS:' ~ (item.fqdn | d((item.hostname | d(host_name)) ~ '.' ~ (item.tld | d(host_tld))))] }}" |
||||
loop: "{{ cert.hosts }}" |
||||
when: (cert.hosts is defined) and (cert.hosts | type_debug == 'list') |
||||
|
||||
|
||||
- name: generate certificate through external ns |
||||
include_role: |
||||
name: ca |
||||
vars: |
||||
function: certs |
||||
ca_options: |
||||
mode: '0400' |
||||
owner: "{{ combined.owner | d(None) }}" |
||||
group: "{{ combined.group | d(None) }}" |
||||
concat_inter: yes |
||||
preset: web |
||||
acme: yes |
||||
ocsp_must_staple: "{{ combined.stapling | d(false) }}" |
||||
notify: "{{ combined.notify | d(None) }}" |
||||
ca_certs: |
||||
- type: "{{ 'ecc384' if (combined.ecc | d(false) == true) else 'rsa2048' }}" |
||||
cert: "{{ cert.cert }}" |
||||
key: "{{ cert.key }}" |
||||
cn: "{% if cert.hosts is defined and cert.hosts | type_debug == 'list' -%}\ |
||||
{{ cert.hosts[0].fqdn | d((cert.hosts[0].hostname | d(host_name)) ~ '.' ~ (cert.hosts[0].tld | d(host_tld))) }}\ |
||||
{%- else -%}\ |
||||
{{ combined.fqdn | d((combined.hostname | d(host_name)) ~ '.' ~ (combined.tld | d(host_tld))) }}\ |
||||
{%- endif -%}" |
||||
san: "{% if san_list | length > 0 -%}\ |
||||
{{ san_list }}\ |
||||
{%- else -%}\ |
||||
{{ 'DNS:' ~ (combined.fqdn | d((combined.hostname | d(host_name)) ~ '.' ~ (combined.tld | d(host_tld)))) }}\ |
||||
{%- endif -%}" |
@ -0,0 +1,2 @@ |
||||
- fail: |
||||
msg: deployment of certs through internal CA is not implemented |
@ -0,0 +1,41 @@ |
||||
- name: validate cert parameter |
||||
fail: |
||||
msg: certs variable must be a dict or a list |
||||
when: (certs is not defined) or ((certs is not mapping) and (certs | type_debug != 'list')) |
||||
|
||||
|
||||
- name: validate common parameter |
||||
fail: |
||||
msg: common variable must be a dict |
||||
when: (common is defined) and (common is not mapping) |
||||
|
||||
|
||||
- name: validate certificates |
||||
include_tasks: validate.yml |
||||
loop: "{{ certs if (certs | type_debug == 'list') else [certs] }}" |
||||
loop_control: |
||||
loop_var: cert |
||||
|
||||
|
||||
- name: process certificates with acme dns |
||||
include_tasks: acme_dns.yml |
||||
loop: "{{ certs if (certs | type_debug == 'list') else [certs] }}" |
||||
loop_control: |
||||
loop_var: cert |
||||
when: services.acme_dns is defined |
||||
|
||||
|
||||
- name: process certificates with standalone dns |
||||
include_tasks: external_ns.yml |
||||
loop: "{{ certs if (certs | type_debug == 'list') else [certs] }}" |
||||
loop_control: |
||||
loop_var: cert |
||||
when: (services.external_ns is defined) and (services.acme_dns is not defined) |
||||
|
||||
|
||||
- name: process certificates with internal ca |
||||
include_tasks: internal_ca.yml |
||||
loop: "{{ certs if (certs | type_debug == 'list') else [certs] }}" |
||||
loop_control: |
||||
loop_var: cert |
||||
when: (services.ca is defined) and (services.external_ns is not defined) and (services.acme_dns is not defined) |
@ -0,0 +1,46 @@ |
||||
- name: validate mandatory parameters |
||||
fail: |
||||
msg: some mandatory parameters in cert variable are missing or invalid |
||||
when: (cert is not defined) or (cert is not mapping) or |
||||
(cert.key is not string) or (cert.cert is not string) |
||||
|
||||
|
||||
- name: validate optional parameters |
||||
fail: |
||||
msg: some optional parameters in cert variable are missing or invalid |
||||
when: ((cert.ca is defined) and (cert.ca is not string)) or |
||||
((cert.id is defined) and (cert.id is not string)) or |
||||
((cert.ecc is defined) and (cert.ecc is not boolean)) or |
||||
((cert.fqdn is defined) and (cert.fqdn is not string)) or |
||||
((cert.tld is defined) and (cert.tld is not string)) or |
||||
((cert.hostname is defined) and (cert.hostname is not string)) or |
||||
((cert.hosts is defined) and (cert.hosts | type_debug != 'list')) or |
||||
((cert.tld is defined) and (cert.tld is not string)) or |
||||
((cert.stapling is defined) and (cert.stapling is not boolean)) or |
||||
((cert.post_hook is defined) and (cert.post_hook is not string)) or |
||||
((cert.notify is defined) and (cert.notify is not string)) or |
||||
((cert.owner is defined) and (cert.owner is not string)) or |
||||
((cert.group is defined) and (cert.group is not string)) |
||||
|
||||
|
||||
- name: validate parameter combinations |
||||
fail: |
||||
msg: parameters are defined in an invalid combination |
||||
when: ((cert.fqdn is defined) and (cert.hosts is defined)) or |
||||
((cert.tld is defined) and (cert.hosts is defined)) or |
||||
((cert.hostname is defined) and (cert.hosts is defined)) or |
||||
((cert.fqdn is defined) and (cert.tld is defined)) or |
||||
((cert.fqdn is defined) and (cert.hostname is defined)) |
||||
|
||||
|
||||
- name: validate hosts |
||||
fail: |
||||
msg: host parameters are invalid or are defined in an invalid combination |
||||
when: ((host.fqdn is defined) and (host.fqdn is not string)) or |
||||
((host.tld is defined) and (host.tld is not string)) or |
||||
((host.hostname is defined) and (host.hostname is not string)) or |
||||
((host.fqdn is defined) and (host.tld is defined)) or |
||||
((host.fqdn is defined) and (host.hostname is defined)) |
||||
loop: "{{ cert.hosts }}" |
||||
loop_control: |
||||
loop_var: host |
@ -0,0 +1,72 @@ |
||||
clamav_user: clamav |
||||
clamav_group: clamav |
||||
|
||||
clamav_conf_dir: /etc/clamav |
||||
clamav_db_dir: /opt/clamav |
||||
|
||||
clamav_conf_file: "{{ clamav_conf_dir }}/clamd.conf" |
||||
clamav_freshclam_conf_file: "{{ clamav_conf_dir }}/freshclam.conf" |
||||
clamav_milter_conf_file: "{{ clamav_conf_dir }}/clamav-milter.conf" |
||||
|
||||
clamav_socket: /run/clamav/clamd.sock |
||||
|
||||
clamav_max_file_size: "{{ mail_server.max_mail_size_bytes | d('25M') }}" |
||||
|
||||
|
||||
clamav_default_config: |
||||
clamav: |
||||
log_syslog: yes |
||||
log_facility: LOG_LOCAL0 |
||||
extended_detection_info: yes |
||||
pid_file: /run/clamav/clamd.pid |
||||
database_directory: "{{ clamav_db_dir }}" |
||||
local_socket: "{{ clamav_socket }}" |
||||
local_socket_mode: 660 |
||||
stream_max_length: "{{ clamav_max_file_size }}" |
||||
self_check: 3600 |
||||
concurrent_database_reload: no |
||||
user: "{{ clamav_user }}" |
||||
detect_p_u_a: yes |
||||
heuristic_scan_precedence: no |
||||
alert_encrypted: yes |
||||
alert_encrypted_archive: yes |
||||
alert_encrypted_doc: yes |
||||
max_scan_time: 30000 |
||||
max_file_size: "{{ clamav_max_file_size }}" |
||||
max_recursion: 12 |
||||
alert_exceeds_max: yes |
||||
bytecode: yes |
||||
bytecode_security: Paranoid |
||||
|
||||
|
||||
freshclam: |
||||
log_syslog: yes |
||||
log_facility: LOG_LOCAL0 |
||||
pid_file: /run/clamav/freshclam.pid |
||||
database_directory: "{{ clamav_db_dir }}" |
||||
database_owner: "{{ clamav_user }}" |
||||
update_log_file: /dev/stdout |
||||
checks: 4 |
||||
test_databases: no |
||||
bytecode: yes |
||||
safe_browsing: yes |
||||
notify_clamd: "{{ clamav_conf_file }}" |
||||
scripted_updates: no |
||||
private_mirror: https://packages.microsoft.com/clamav |
||||
|
||||
|
||||
milter: |
||||
log_syslog: yes |
||||
log_facility: LOG_LOCAL0 |
||||
log_infected: Basic |
||||
log_clean: Basic |
||||
milter_socket: "inet:{{ mail_server.clamav_port | d(7357) }}" |
||||
user: "{{ clamav_user }}" |
||||
clamd_socket: "unix:{{ clamav_socket }}" |
||||
max_file_size: "{{ clamav_max_file_size }}" |
||||
on_infected: Reject |
||||
add_header: Add |
||||
report_hostname: "{{ (mail_server.mta_actual_hostname ~ '.' ~ mail_server.tld) if |
||||
(mail_server.mta_actual_hostname is defined) and (mail_server.tld is defined) else 'clamav' }}" |
||||
support_multiple_recipients: yes |
||||
foreground: yes |
@ -0,0 +1,16 @@ |
||||
- name: restart clamd |
||||
service: |
||||
name: clamd |
||||
state: restarted |
||||
|
||||
|
||||
- name: restart freshclam |
||||
service: |
||||
name: freshclam |
||||
state: restarted |
||||
|
||||
|
||||
- name: restart clamav milter |
||||
service: |
||||
name: clamav-milter |
||||
state: restarted |
@ -0,0 +1,97 @@ |
||||
- name: set clamav_cfg |
||||
set_fact: |
||||
clamav_cfg: "{{ clamav_default_config | d({}) | combine(clamav_config | d({}), recursive=true) }}" |
||||
|
||||
|
||||
- name: install dependencies |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- clamav-daemon |
||||
- alpine: clamav-daemon-openrc |
||||
- clamav-milter |
||||
|
||||
|
||||
- name: create user and group |
||||
include_tasks: tasks/create_user.yml |
||||
vars: |
||||
user: |
||||
name: "{{ clamav_user }}" |
||||
group: "{{ clamav_group }}" |
||||
|
||||
|
||||
- name: create directories |
||||
file: |
||||
path: "{{ item }}" |
||||
state: directory |
||||
mode: 0700 |
||||
owner: "{{ clamav_user }}" |
||||
group: "{{ clamav_group }}" |
||||
loop: |
||||
- "{{ clamav_conf_dir }}" |
||||
- "{{ clamav_db_dir }}" |
||||
|
||||
|
||||
- name: template clamav configs |
||||
template: |
||||
src: config.j2 |
||||
dest: "{{ item.dest }}" |
||||
force: yes |
||||
mode: 0400 |
||||
owner: "{{ clamav_user }}" |
||||
group: "{{ clamav_group }}" |
||||
lstrip_blocks: yes |
||||
notify: "{{ item.notify }}" |
||||
loop: |
||||
- { dest: "{{ clamav_conf_file }}", section: "clamav", notify: "restart clamd" } |
||||
- { dest: "{{ clamav_freshclam_conf_file }}", section: "freshclam", notify: "restart freshclam" } |
||||
- { dest: "{{ clamav_milter_conf_file }}", section: "milter", notify: "restart clamav milter" } |
||||
|
||||
|
||||
- name: edit init script for clamd |
||||
lineinfile: |
||||
path: /etc/init.d/clamd |
||||
regexp: '^CONF=' |
||||
line: 'CONF={{ clamav_conf_file | quote }}' |
||||
notify: restart clamd |
||||
|
||||
|
||||
- name: edit init script for freshclam |
||||
lineinfile: |
||||
path: /etc/init.d/freshclam |
||||
regexp: '^CONF=' |
||||
line: 'CONF={{ clamav_freshclam_conf_file | quote }}' |
||||
notify: restart freshclam |
||||
|
||||
|
||||
- name: template init script for clamav milter |
||||
template: |
||||
src: milter_init.j2 |
||||
dest: /etc/init.d/clamav-milter |
||||
force: yes |
||||
mode: "+x" |
||||
notify: restart clamav milter |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ clamav_conf_dir }}" |
||||
|
||||
|
||||
- name: enable and start services |
||||
service: |
||||
name: "{{ item }}" |
||||
enabled: yes |
||||
state: started |
||||
loop: |
||||
- clamd |
||||
- freshclam |
||||
- clamav-milter |
@ -0,0 +1,16 @@ |
||||
{% macro clamav_option(option) -%} |
||||
{% set key = option.key.split('_') | map('capitalize') | join('') -%} |
||||
|
||||
{% if option.value is boolean -%} |
||||
{{ key }} {{ 'yes' if option.value else 'no' }} |
||||
{% elif option.value != None -%} |
||||
{{ key }} {{ option.value }} |
||||
{% endif -%} |
||||
{% endmacro -%} |
||||
|
||||
|
||||
{% if clamav_cfg[item.section] is mapping -%} |
||||
{% for option in (clamav_cfg[item.section] | d({}) | dict2items) -%} |
||||
{{ clamav_option(option) }} |
||||
{%- endfor %} |
||||
{% endif -%} |
@ -0,0 +1,14 @@ |
||||
#!/sbin/openrc-run |
||||
|
||||
name="$SVCNAME" |
||||
directory="{{ clamav_db_dir }}" |
||||
command="/usr/sbin/clamav-milter" |
||||
command_user="{{ clamav_user ~ ':' ~ clamav_group }}" |
||||
pidfile="/var/run/$SVCNAME.pid" |
||||
supervisor="supervise-daemon" |
||||
|
||||
depend() { |
||||
need net |
||||
use dns |
||||
after clamd |
||||
} |
@ -0,0 +1 @@ |
||||
dropbear_dir: /etc/dropbear |
@ -0,0 +1,19 @@ |
||||
#!/sbin/openrc-run |
||||
|
||||
depend() { |
||||
use logger dns |
||||
need net |
||||
after firewall |
||||
} |
||||
|
||||
start() { |
||||
ebegin "Starting dropbear" |
||||
/usr/sbin/dropbear ${DROPBEAR_OPTS} |
||||
eend $? |
||||
} |
||||
|
||||
stop() { |
||||
ebegin "Stopping dropbear" |
||||
start-stop-daemon --stop --pidfile /var/run/dropbear.pid |
||||
eend $? |
||||
} |
@ -0,0 +1,16 @@ |
||||
- name: restart syslog |
||||
service: |
||||
name: syslog |
||||
state: restarted |
||||
|
||||
|
||||
- name: restart crond |
||||
service: |
||||
name: cron |
||||
state: restarted |
||||
|
||||
|
||||
- name: restart dropbear |
||||
service: |
||||
name: dropbear |
||||
state: restarted |
@ -0,0 +1,94 @@ |
||||
- name: setup timezone |
||||
shell: |
||||
cmd: PATH=$PATH:/sbin; /sbin/setup-timezone -z {{ timezone | quote }} |
||||
chdir: /sbin |
||||
creates: "{{ ('/etc/zoneinfo', timezone) | path_join }}" |
||||
notify: restart syslog |
||||
when: timezone is string |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: upgrade alpine version |
||||
replace: |
||||
path: /etc/apk/repositories |
||||
regexp: '/alpine/v\d+\.\d+/' |
||||
replace: '/alpine/v{{ alpine_version }}/' |
||||
|
||||
|
||||
- name: change apk repository |
||||
replace: |
||||
path: /etc/apk/repositories |
||||
regexp: '^https:\/\/dl-cdn\.alpinelinux\.org\/alpine\/' |
||||
replace: 'https://mirror.yandex.ru/mirrors/alpine/' |
||||
when: use_alternative_apk_repo | d(false) == true |
||||
|
||||
|
||||
- block: |
||||
- name: update repository index |
||||
community.general.apk: |
||||
update_cache: yes |
||||
changed_when: no |
||||
register: apk_result |
||||
|
||||
rescue: |
||||
- name: fix repository keys |
||||
command: |
||||
cmd: /sbin/apk fix --allow-untrusted alpine-keys |
||||
when: "'UNTRUSTED signature' in apk_result.stderr" |
||||
|
||||
- name: update repository index in untrusted mode |
||||
command: |
||||
cmd: /sbin/apk --allow-untrusted update |
||||
when: "'UNTRUSTED signature' in apk_result.stderr" |
||||
|
||||
- name: upgrade basic dependencies in untrusted mode |
||||
command: |
||||
cmd: /sbin/apk --allow-untrusted upgrade apk-tools alpine-keys |
||||
when: "'UNTRUSTED signature' in apk_result.stderr" |
||||
|
||||
- name: update repository index |
||||
community.general.apk: |
||||
update_cache: yes |
||||
changed_when: no |
||||
|
||||
|
||||
- name: check if there are any updates |
||||
command: |
||||
cmd: /sbin/apk list -u |
||||
register: updates_found |
||||
changed_when: no |
||||
|
||||
|
||||
- name: pause and confirm updates |
||||
pause: |
||||
prompt: "{{ updates_found.stdout }}" |
||||
when: (updates_found.stdout | length > 0) and (interactive | d(true) == true) and (hosts_strategy | d('') != 'free') |
||||
changed_when: updates_found.stdout | length > 0 |
||||
|
||||
|
||||
- name: upgrade all packages if updates are found |
||||
community.general.apk: |
||||
upgrade: yes |
||||
when: updates_found.stdout | length > 0 |
||||
|
||||
|
||||
- name: collect apk-new files |
||||
find: |
||||
paths: |
||||
- /etc |
||||
- /usr |
||||
- /var |
||||
patterns: "*.apk-new" |
||||
recurse: yes |
||||
depth: 8 |
||||
register: new_files |
||||
|
||||
|
||||
- name: remove apk-new files |
||||
file: |
||||
path: "{{ item.path }}" |
||||
state: absent |
||||
loop: "{{ new_files.files | flatten(levels=1) }}" |
@ -0,0 +1,52 @@ |
||||
- name: set timezone |
||||
community.general.timezone: |
||||
name: "{{ timezone }}" |
||||
notify: restart crond |
||||
when: timezone is defined |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: update repository index |
||||
apt: |
||||
force_apt_get: yes |
||||
update_cache: yes |
||||
changed_when: false |
||||
|
||||
|
||||
- name: ensure apt-show-versions is installed |
||||
apt: |
||||
force_apt_get: yes |
||||
name: apt-show-versions |
||||
state: latest |
||||
|
||||
|
||||
- name: get upgradeable packages |
||||
shell: |
||||
cmd: apt-show-versions --upgradeable |
||||
register: upgradeable |
||||
changed_when: false |
||||
|
||||
|
||||
- block: |
||||
- name: pause and confirm updates |
||||
pause: |
||||
prompt: "{{ upgradeable.stdout }}" |
||||
|
||||
|
||||
- name: upgrade all packages |
||||
apt: |
||||
force_apt_get: yes |
||||
install_recommends: no |
||||
upgrade: dist |
||||
|
||||
when: "(upgradeable.stdout_lines is defined) and (upgradeable.stdout_lines | length > 0)" |
||||
|
||||
|
||||
- name: clean repository cache |
||||
apt: |
||||
force_apt_get: yes |
||||
autoclean: yes |
||||
autoremove: yes |
@ -0,0 +1,147 @@ |
||||
- block: |
||||
- name: try to connect |
||||
wait_for_connection: |
||||
timeout: 10 |
||||
|
||||
- set_fact: |
||||
ssh_ok: yes |
||||
|
||||
rescue: |
||||
- name: save old ansible ssh args |
||||
set_fact: |
||||
old_ansible_ssh_extra_args: "{{ ansible_ssh_extra_args | d('') }}" |
||||
|
||||
- name: disable key checking and enable password login |
||||
set_fact: |
||||
ssh_ok: no |
||||
host_key_checking: no |
||||
ansible_password: "{{ container_password | d(host_password) }}" |
||||
ansible_ssh_extra_args: "{{ ansible_ssh_extra_args | d('') }} -o StrictHostKeyChecking=no" |
||||
|
||||
- name: try to connect without key checking |
||||
wait_for_connection: |
||||
timeout: 10 |
||||
|
||||
|
||||
- name: gather facts |
||||
setup: |
||||
gather_subset: |
||||
- min |
||||
- distribution |
||||
|
||||
|
||||
- name: generate host ssh key |
||||
include_tasks: gen_ssh_key.yml |
||||
when: (use_ssh_keys | d(true) == true) and ('containers' not in group_names) |
||||
|
||||
|
||||
- block: |
||||
- name: remove default dropbear keys |
||||
file: |
||||
path: "{{ (dropbear_dir, item) | path_join }}" |
||||
state: absent |
||||
loop: |
||||
- dropbear_dss_host_key |
||||
- dropbear_rsa_host_key |
||||
- dropbear_ecdsa_host_key |
||||
notify: restart dropbear |
||||
|
||||
|
||||
- name: generate ed25519 dropbear key if missing |
||||
command: |
||||
cmd: "dropbearkey -t ed25519 -f {{ (dropbear_dir, 'dropbear_ed25519_host_key') | path_join | quote }}" |
||||
creates: "{{ (dropbear_dir, 'dropbear_ed25519_host_key') | path_join }}" |
||||
notify: restart dropbear |
||||
|
||||
|
||||
- name: get remote host public key |
||||
command: |
||||
cmd: "dropbearkey -y -f {{ (dropbear_dir, 'dropbear_ed25519_host_key') | path_join | quote }}" |
||||
register: pubkey |
||||
changed_when: no |
||||
|
||||
|
||||
- name: get actual public key |
||||
set_fact: |
||||
host_ssh_pubkey: "{{ pubkey.stdout_lines | map('regex_search', '^ssh-ed25519.*$') | select('string') | first }}" |
||||
|
||||
|
||||
- name: fail if public key is missing |
||||
fail: |
||||
msg: "remote host ssh public key is missing" |
||||
when: host_ssh_pubkey | length == 0 |
||||
|
||||
|
||||
- name: add public key to known_hosts on ansible controller |
||||
known_hosts: |
||||
key: "{{ ansible_host }} {{ host_ssh_pubkey }}" |
||||
name: "{{ ansible_host }}" |
||||
delegate_to: localhost |
||||
|
||||
|
||||
- name: edit dropbear conf file |
||||
lineinfile: |
||||
path: /etc/conf.d/dropbear |
||||
regexp: '^DROPBEAR_OPTS=.*$' |
||||
line: "DROPBEAR_OPTS=\"-r {{ (dropbear_dir, 'dropbear_ed25519_host_key') | path_join | quote }} -jk -T 5 -K 360 -I 7200\"" |
||||
notify: restart dropbear |
||||
|
||||
|
||||
- name: copy dropbear init file |
||||
copy: |
||||
src: dropbear_init |
||||
dest: /etc/init.d/dropbear |
||||
force: yes |
||||
notify: restart dropbear |
||||
|
||||
|
||||
- name: ensure remote host has ansible key in authorized_keys file |
||||
lineinfile: |
||||
path: /root/.ssh/authorized_keys |
||||
line: "{{ container_key.public_key }}" |
||||
create: yes |
||||
mode: 0400 |
||||
when: container_key is defined and container_key.public_key is defined |
||||
|
||||
when: ansible_distribution == 'Alpine' |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: if key checking was disabled |
||||
block: |
||||
- name: set it back on |
||||
set_fact: |
||||
host_key_checking: yes |
||||
ansible_ssh_extra_args: "{{ old_ansible_ssh_extra_args }}" |
||||
ansible_password: "{{ None }}" |
||||
|
||||
- name: try to connect |
||||
wait_for_connection: |
||||
timeout: 10 |
||||
|
||||
- set_fact: |
||||
ssh_ok: true |
||||
|
||||
when: not ssh_ok |
||||
|
||||
|
||||
- name: add etc directory to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- /etc |
||||
|
||||
|
||||
- name: alpine setup |
||||
include_tasks: alpine.yml |
||||
when: ansible_distribution == 'Alpine' |
||||
|
||||
|
||||
- name: debian setup |
||||
include_tasks: debian.yml |
||||
when: ansible_distribution == 'Debian' |
@ -0,0 +1,7 @@ |
||||
container_description: Managed by Ansible |
||||
container_pool: production |
||||
|
||||
container_distro: alpine |
||||
container_template: |
||||
alpine: alpine-3.15-default_20211202_amd64.tar.xz |
||||
debian: debian-11-standard_11.3-1_amd64.tar.zst |
@ -0,0 +1,186 @@ |
||||
- name: specify connection parameters |
||||
set_fact: |
||||
pm_api_host: "{{ hostvars[selected_node]['ansible_host'] | mandatory }}" |
||||
pm_api_user: "{{ hostvars[selected_node]['api_user'] | d('root@pam') }}" |
||||
pm_api_password: "{{ hostvars[selected_node]['api_password'] | d(hostvars[selected_node]['ansible_password']) }}" |
||||
pm_lxc_storage: "{{ hostvars[selected_node]['lxc_storage'] | d('local-zfs') }}" |
||||
no_log: yes |
||||
|
||||
|
||||
- name: validate template and distribution parameters |
||||
fail: |
||||
msg: some container parameters are missing or invalid |
||||
when: (container_distro is not defined) or (container_template is not mapping) or |
||||
(container_template[container_distro] is not defined) or |
||||
(container_id is not defined) or (container_password is not defined) |
||||
|
||||
|
||||
- name: ensure pool exists on cluster node |
||||
command: |
||||
cmd: "pveum pool add {{ container_pool | quote }}" |
||||
register: pool_res |
||||
changed_when: pool_res.rc == 0 |
||||
failed_when: (pool_res.rc != 0) and not ((pool_res.rc == 255) and ('already exists' in pool_res.stderr)) |
||||
when: container_pool is defined |
||||
delegate_to: "{{ selected_node }}" |
||||
|
||||
|
||||
- block: |
||||
- name: ensure pip3 is installed on local node |
||||
package: |
||||
name: py3-pip |
||||
run_once: yes |
||||
|
||||
|
||||
- name: ensure proxmoxer is installed on local node |
||||
pip: |
||||
name: proxmoxer |
||||
run_once: yes |
||||
|
||||
|
||||
- name: generate host ssh key |
||||
include_tasks: gen_ssh_key.yml |
||||
when: use_ssh_keys | d(true) == true |
||||
|
||||
|
||||
- name: ensure there is a container template |
||||
community.general.proxmox_template: |
||||
node: "{{ selected_node }}" |
||||
api_host: "{{ pm_api_host }}" |
||||
api_user: "{{ pm_api_user }}" |
||||
api_password: "{{ pm_api_password }}" |
||||
content_type: vztmpl |
||||
template: "{{ container_template[container_distro] }}" |
||||
validate_certs: no |
||||
timeout: 20 |
||||
|
||||
|
||||
- name: create container if not exists |
||||
community.general.proxmox: |
||||
node: "{{ selected_node }}" |
||||
api_host: "{{ pm_api_host }}" |
||||
api_user: "{{ pm_api_user }}" |
||||
api_password: "{{ pm_api_password }}" |
||||
|
||||
cores: "{{ hardware.cores }}" |
||||
cpus: "{{ hardware.cpus }}" |
||||
cpuunits: "{{ hardware.cpuunits }}" |
||||
disk: "{{ hardware.disk | string }}" |
||||
memory: "{{ hardware.memory }}" |
||||
swap: "{{ hardware.swap }}" |
||||
|
||||
description: "{{ container_description | d(omit) }}" |
||||
hostname: "{{ inventory_hostname }}" |
||||
pool: "{{ container_pool | d(omit) }}" |
||||
vmid: "{{ container_id }}" |
||||
|
||||
password: "{{ container_password }}" |
||||
pubkey: "{{ (container_key | d({})).public_key | d(omit) }}" |
||||
|
||||
ostemplate: "local:vztmpl/{{ container_template[container_distro] }}" |
||||
netif: "{\"net0\":\ |
||||
\"name=eth0,hwaddr={{ container_mac | d(mac_prefix | community.general.random_mac(seed=inventory_hostname)) }},\ |
||||
ip={{ ansible_host }}/{{ networks[container_network].gw | ansible.utils.ipaddr('prefix') }},\ |
||||
gw={{ networks[container_network].gw | ansible.utils.ipaddr('address') }},\ |
||||
bridge=vmbr0,\ |
||||
firewall=0,\ |
||||
tag={{ networks[container_network].tag }},\ |
||||
type=veth,\ |
||||
mtu={{ container_mtu | d(hostvars[selected_node]['container_mtu'] | d(1500)) }}\"}" |
||||
nameserver: "{%- if container_nameserver is defined -%}\ |
||||
{{ hostvars[container_nameserver]['ansible_host'] }}\ |
||||
{%- elif services.filtering_ns is defined -%}\ |
||||
{%- if services.filtering_ns | type_debug == 'list' -%} |
||||
{{ hostvars[services.filtering_ns[0].hostname]['ansible_host'] }}\ |
||||
{%- else -%} |
||||
{{ hostvars[services.filtering_ns.hostname]['ansible_host'] }}\ |
||||
{%- endif -%} |
||||
{%- elif container_default_nameserver is defined -%}\ |
||||
{{ container_default_nameserver }}\ |
||||
{%- else -%}\ |
||||
{{ omit }}\ |
||||
{%- endif -%}" |
||||
onboot: yes |
||||
proxmox_default_behavior: no_defaults |
||||
storage: "{{ pm_lxc_storage }}" |
||||
unprivileged: yes |
||||
timeout: 240 |
||||
|
||||
mounts: >- |
||||
{ {%- for item in (container_mounts | d([])) -%} |
||||
"{{ item.id }}":"{{ pm_lxc_storage }}:{{ item.size | mandatory }},mp={{ item.mp | mandatory }}{% if item.readonly is defined and item.readonly %},ro=1{% endif %}", |
||||
{%- endfor -%} } |
||||
|
||||
|
||||
- block: |
||||
- name: add features to lxc config |
||||
lineinfile: |
||||
path: "/etc/pve/lxc/{{ container_id }}.conf" |
||||
line: "features: {{ container_features | join(',') }}" |
||||
when: container_features | d([]) | length > 0 |
||||
|
||||
|
||||
- name: check that lxc config is correct |
||||
lineinfile: |
||||
path: "/etc/pve/lxc/{{ container_id }}.conf" |
||||
regexp: "^{{ item.name }}:(\\s*).*$" |
||||
line: "{{ item.name | mandatory }}:\\g<1>{{ item.value | mandatory }}" |
||||
backrefs: yes |
||||
loop: |
||||
- { name: cpus, value: "{{ hardware.cpus }}" } |
||||
- { name: cores, value: "{{ [hardware.cores, hostvars[selected_node]['max_cores'] | d(hardware.cores)] | min }}" } |
||||
- { name: cpuunits, value: "{{ hardware.cpuunits }}" } |
||||
- { name: memory, value: "{{ hardware.memory }}" } |
||||
- { name: swap, value: "{{ hardware.swap }}" } |
||||
- { name: onboot, value: "{{ '1' if (container_active | d(true) == true) else '0' }}" } |
||||
|
||||
|
||||
- name: set startup order and delay |
||||
lineinfile: |
||||
path: "/etc/pve/lxc/{{ container_id }}.conf" |
||||
regexp: '^startup:.*$' |
||||
line: "startup: {{ 'order=' ~ (container_order | d(role_dependency[host_primary_role] | d('0'))) ~ ((',up=' ~ container_startup_delay) if container_startup_delay is defined else '') }}" |
||||
insertbefore: '^[^\#]' |
||||
firstmatch: yes |
||||
when: (container_order is defined) or (role_dependency[host_primary_role] is defined) or (container_startup_delay is defined) |
||||
|
||||
|
||||
- name: ensure that cpulimit is not set |
||||
lineinfile: |
||||
path: "/etc/pve/lxc/{{ container_id }}.conf" |
||||
regexp: '^cpulimit:.*$' |
||||
state: absent |
||||
|
||||
delegate_to: "{{ selected_node }}" |
||||
|
||||
|
||||
- name: start/stop container |
||||
community.general.proxmox: |
||||
node: "{{ selected_node }}" |
||||
api_host: "{{ pm_api_host }}" |
||||
api_user: "{{ pm_api_user }}" |
||||
api_password: "{{ pm_api_password }}" |
||||
vmid: "{{ container_id }}" |
||||
proxmox_default_behavior: no_defaults |
||||
state: "{{ 'started' if (container_active | d(true) == true) else 'stopped' }}" |
||||
|
||||
|
||||
- name: end playbook for current host if container is set to inactive |
||||
meta: end_host |
||||
when: container_active | d(true) == false |
||||
|
||||
|
||||
- name: wait until networking is avaliable |
||||
command: |
||||
cmd: "ping -c1 -W1 {{ ansible_host | quote }}" |
||||
register: ping_result |
||||
until: ping_result.rc == 0 |
||||
retries: 5 |
||||
delay: 2 |
||||
changed_when: no |
||||
|
||||
delegate_to: 127.0.0.1 |
||||
|
||||
|
||||
- name: preconfigure container |
||||
include_tasks: preconf.yml |
@ -0,0 +1,66 @@ |
||||
- block: |
||||
- name: install basic dependencies |
||||
include_tasks: tasks/pct_command.yml |
||||
vars: |
||||
pct_command: "{{ item.pct_command }}" |
||||
chg_substr: "{{ item.chg_substr | d(omit) }}" |
||||
loop: |
||||
- pct_command: apk update |
||||
- pct_command: apk add python3 |
||||
chg_substr: Installing |
||||
- pct_command: apk add dropbear |
||||
chg_substr: Installing |
||||
- pct_command: rc-update add dropbear |
||||
chg_substr: added to runlevel |
||||
|
||||
- name: install dropbear-scp if this is not an ansible controller |
||||
include_tasks: tasks/pct_command.yml |
||||
vars: |
||||
pct_command: apk add dropbear-scp |
||||
chg_substr: Installing |
||||
when: (inventory_hostname != 'ansible') and ((primary_role is not defined) or (primary_role != 'ansible')) |
||||
and alpine_version is version('3.15', '<=') |
||||
|
||||
- name: install openssh-sftp-server due to openssh 9 scp deprecation |
||||
include_tasks: tasks/pct_command.yml |
||||
vars: |
||||
pct_command: apk add openssh-sftp-server |
||||
chg_substr: Installing |
||||
when: alpine_version is version('3.16', '>=') |
||||
|
||||
- name: start dropbear |
||||
include_tasks: tasks/pct_command.yml |
||||
vars: |
||||
pct_command: service dropbear start |
||||
chg_substr: \* Starting dropbear ... [ ok ] |
||||
|
||||
when: (container_distro | lower) == 'alpine' |
||||
|
||||
|
||||
- block: |
||||
- name: install basic dependencies |
||||
include_tasks: tasks/pct_command.yml |
||||
vars: |
||||
pct_command: "{{ item.pct_command }}" |
||||
chg_substr: "{{ item.chg_substr | default(omit) }}" |
||||
loop: |
||||
- pct_command: apt-get --assume-yes update |
||||
- pct_command: apt-get --assume-yes install python3 |
||||
chg_substr: The following NEW packages |
||||
- pct_command: apt-get --assume-yes install openssh-server |
||||
chg_substr: The following NEW packages |
||||
- pct_command: systemctl enable ssh.service |
||||
chg_substr: Synchronizing state |
||||
|
||||
- name: edit sshd config |
||||
include_tasks: tasks/pct_command.yml |
||||
vars: |
||||
pct_command: "sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/g' /etc/ssh/sshd_config" |
||||
|
||||
|
||||
- name: start sshd |
||||
include_tasks: tasks/pct_command.yml |
||||
vars: |
||||
pct_command: systemctl start ssh.service |
||||
|
||||
when: (container_distro | lower) in ['debian', 'ubuntu'] |
@ -0,0 +1,9 @@ |
||||
coredns_user: coredns |
||||
coredns_group: coredns |
||||
coredns_conf_dir: /etc/coredns |
||||
|
||||
coredns_conf_file: "{{ coredns_conf_dir }}/coredns.conf" |
||||
coredns_tls_file: "{{ coredns_conf_dir }}/tls.conf" |
||||
|
||||
coredns_cert_file: "{{ coredns_conf_dir }}/ecc384.crt" |
||||
coredns_key_file: "{{ coredns_conf_dir }}/ecc384.key" |
@ -0,0 +1,4 @@ |
||||
- name: restart coredns |
||||
service: |
||||
name: coredns |
||||
state: restarted |
@ -0,0 +1,119 @@ |
||||
- name: check if record is an object |
||||
fail: |
||||
msg: record must be an object |
||||
when: record is not mapping |
||||
|
||||
|
||||
- name: check if record zone is a string |
||||
fail: |
||||
msg: record zone must be a string |
||||
when: record.zone is defined and record.zone is not string |
||||
|
||||
|
||||
- name: check if record zone exists |
||||
fail: |
||||
msg: '"{{ record.zone }}" does not seem to be a valid zone' |
||||
when: (record.zone is defined) and |
||||
(record.zone != 'root') and |
||||
((int_zones is not defined) or (record.zone not in int_zones)) |
||||
|
||||
|
||||
- name: construct record parameters |
||||
set_fact: |
||||
ns_zone: "{%- if (record.zone is defined) and (record.zone != 'root') -%}{{ record.zone }}\ |
||||
{%- else -%}{{ ns_tld | d(int_tld) }}\ |
||||
{%- endif -%}" |
||||
ns_name: "{%- if record.name is defined -%}{{ record.name }}\ |
||||
{%- else -%}{{ inventory_hostname }}\ |
||||
{%- endif -%}" |
||||
ns_type: "{%- if record.type is defined -%}{{ record.type | upper }}\ |
||||
{%- else -%}A\ |
||||
{%- endif -%}" |
||||
ns_value: "{%- if record.value is defined -%}{{ record.value }}\ |
||||
{%- else -%}{{ ansible_host }}\ |
||||
{%- endif -%}" |
||||
|
||||
- name: set ns_quote |
||||
set_fact: |
||||
ns_quote: "{{ '\"' if ns_type == 'TXT' else '' }}" |
||||
|
||||
|
||||
- name: construct full name |
||||
set_fact: |
||||
ns_full_name: '{%- if ns_name != "@" -%}{{ ns_name }}.{%- endif -%}{{ ns_zone }}' |
||||
|
||||
|
||||
- name: construct regex part |
||||
set_fact: |
||||
ns_regex_part: '{%- if record.allow_multiple is defined -%}{{ (ns_quote ~ ns_value ~ ns_quote) | regex_escape() }}\.?{%- else -%}{{ "" | string }}{%- endif -%}' |
||||
|
||||
|
||||
- name: construct regex |
||||
set_fact: |
||||
ns_regex: '^{{ ns_full_name | regex_escape() }}\s+\d+\s+IN\s+{{ ns_type | regex_escape() }}\s+{{ ns_regex_part }}' |
||||
|
||||
|
||||
- name: show debug info |
||||
debug: |
||||
msg: "{{ ns_zone }} {{ ns_name }} {{ ns_type }} {{ ns_quote ~ ns_value ~ ns_quote }} --> {{ ns_regex }}" |
||||
|
||||
|
||||
- name: slurp zone file |
||||
slurp: |
||||
src: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}" |
||||
register: zf |
||||
changed_when: false |
||||
|
||||
|
||||
- name: enumerate stdout lines to check if an entry already exists |
||||
set_fact: |
||||
ns_exists: "{{ (zf.content | b64decode).split('\n') | select('search', ns_regex) | list | length > 0 }}" |
||||
|
||||
|
||||
- block: |
||||
- name: fail if there are multiple records |
||||
fail: |
||||
msg: single record mode is selected, but multiple records found |
||||
when: (zf.content | b64decode).split('\n') | select('search', ns_regex) | list | length > 1 |
||||
|
||||
|
||||
- name: grab the value |
||||
set_fact: |
||||
ns_old_value: "{{ (zf.content | b64decode).split('\n') | select('search', ns_regex) | map('regex_search', '\\s+?(\\S+?)\\.?$', '\\1') | first | join('') }}" |
||||
|
||||
|
||||
- name: replace the record |
||||
lineinfile: |
||||
path: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}" |
||||
regexp: '^\s*{{ ns_name | regex_escape() }}\s+IN\s+{{ ns_type | regex_escape() }}\s+' |
||||
line: "{{ ns_name }}\tIN\t{{ ns_type }}\t{{ ns_quote ~ ns_value ~ ns_quote }}" |
||||
backrefs: yes |
||||
when: ns_old_value != (ns_quote ~ ns_value ~ ns_quote) |
||||
register: rr1 |
||||
|
||||
when: ns_exists and rrset.allow_multiple is not defined |
||||
|
||||
|
||||
- name: add the record if it is missing |
||||
lineinfile: |
||||
path: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}" |
||||
line: "{{ ns_name }}\tIN\t{{ ns_type }}\t{{ ns_quote ~ ns_value ~ ns_quote }}" |
||||
when: not ns_exists |
||||
register: rr2 |
||||
|
||||
|
||||
- name: determine if records were changed |
||||
set_fact: |
||||
ns_records_changed: "{{ ((rr1 is defined) and rr1.changed) or ((rr2 is defined) and rr2.changed) }}" |
||||
|
||||
|
||||
- name: change serial |
||||
include_tasks: increase_serial.yml |
||||
when: ns_records_changed | d(false) == true |
||||
|
||||
|
||||
- name: restart coredns |
||||
service: |
||||
name: coredns |
||||
state: restarted |
||||
when: (ns_instant | d(false) == true) and (ns_records_changed or ns_serial_changed) |
@ -0,0 +1,21 @@ |
||||
- name: add default record |
||||
include_tasks: add_record.yml |
||||
vars: |
||||
record: {} |
||||
when: (ns_records | d([]) | length) == 0 |
||||
|
||||
|
||||
- name: process other items |
||||
include_tasks: add_record.yml |
||||
loop: "{{ ns_records | d([]) }}" |
||||
loop_control: |
||||
loop_var: record |
||||
|
||||
|
||||
- name: restart coredns |
||||
service: |
||||
name: coredns |
||||
state: restarted |
||||
when: (ns_instant | d(false) == false) and |
||||
((ns_records_changed | d(false) == true) or |
||||
(ns_serial_changed | d(false) == true)) |
@ -0,0 +1,47 @@ |
||||
- name: slurp zone file |
||||
slurp: |
||||
src: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}" |
||||
register: zf |
||||
changed_when: false |
||||
|
||||
|
||||
- name: get SOA serial value |
||||
set_fact: |
||||
ns_old_serial: '{{ zf.content | b64decode | regex_search(''@\s+IN\s+SOA\s+\S+\s+\S+\s*\(\s*(\d+)'', ''\1'') | first }}' |
||||
|
||||
|
||||
- name: get current date |
||||
include_tasks: tasks/get_datetime.yml |
||||
vars: |
||||
format: YYMMDD |
||||
|
||||
|
||||
- name: replace outdated serial with current date |
||||
set_fact: |
||||
ns_new_serial: "{{ (current_date_time | string) ~ '01'}}" |
||||
when: ns_old_serial[:8] != (current_date_time | string) |
||||
|
||||
|
||||
- name: increase current serial |
||||
set_fact: |
||||
ns_new_serial: "{{ (ns_old_serial | int) + 1 }}" |
||||
when: (ns_old_serial[:8] == (current_date_time | string)) and ((ns_old_serial[8:10] | int) < 99) |
||||
|
||||
|
||||
- name: do not change current serial if it had more than 99 iterations |
||||
set_fact: |
||||
ns_new_serial: "{{ ns_old_serial }}" |
||||
when: (ns_old_serial[:8] == (current_date_time | string)) and ((ns_old_serial[8:10] | int) >= 99) |
||||
|
||||
|
||||
- name: insert new serial |
||||
replace: |
||||
path: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}" |
||||
regexp: '(@\s+IN\s+SOA\s+\S+\s+\S+\s*\(\s*){{ ns_old_serial }}' |
||||
replace: '\g<1>{{ ns_new_serial }}' |
||||
register: result |
||||
|
||||
|
||||
- name: set fact if serial was changed |
||||
set_fact: |
||||
ns_serial_changed: "{{ result.changed }}" |
@ -0,0 +1,93 @@ |
||||
- name: install coredns and dependencies |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- coredns |
||||
- alpine: coredns-openrc |
||||
|
||||
|
||||
- name: create user and group |
||||
include_tasks: tasks/create_user.yml |
||||
vars: |
||||
user: |
||||
name: "{{ coredns_user }}" |
||||
group: "{{ coredns_group }}" |
||||
|
||||
|
||||
- name: create config directory |
||||
file: |
||||
path: "{{ coredns_conf_dir }}" |
||||
state: directory |
||||
owner: "{{ coredns_user }}" |
||||
group: "{{ coredns_group }}" |
||||
notify: restart coredns |
||||
|
||||
|
||||
- name: template corefile |
||||
template: |
||||
src: corefile.j2 |
||||
dest: "{{ coredns_conf_file }}" |
||||
force: yes |
||||
owner: "{{ coredns_user }}" |
||||
group: "{{ coredns_group }}" |
||||
mode: 0400 |
||||
notify: restart coredns |
||||
|
||||
|
||||
- name: template empty tls file if missing |
||||
copy: |
||||
content: '' |
||||
dest: "{{ coredns_tls_file }}" |
||||
force: no |
||||
owner: "{{ coredns_user }}" |
||||
group: "{{ coredns_group }}" |
||||
mode: 0400 |
||||
notify: restart coredns |
||||
|
||||
|
||||
- name: template root zone if missing |
||||
template: |
||||
src: zone.j2 |
||||
dest: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}" |
||||
force: no |
||||
mode: 0400 |
||||
owner: "{{ coredns_user }}" |
||||
group: "{{ coredns_group }}" |
||||
notify: restart coredns |
||||
|
||||
|
||||
- name: edit service config |
||||
lineinfile: |
||||
path: /etc/conf.d/coredns |
||||
regexp: "^COREDNS_CONFIG=" |
||||
line: "COREDNS_CONFIG={{ coredns_conf_file | quote }}" |
||||
notify: restart coredns |
||||
|
||||
|
||||
- name: template init script |
||||
template: |
||||
src: init.j2 |
||||
dest: /etc/init.d/coredns |
||||
force: yes |
||||
mode: 0755 |
||||
notify: restart coredns |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ coredns_conf_dir }}" |
||||
|
||||
|
||||
- name: enable and start coredns |
||||
service: |
||||
name: coredns |
||||
enabled: yes |
||||
state: started |
@ -0,0 +1,28 @@ |
||||
- name: deploy ecc384 cert |
||||
include_role: |
||||
name: ca |
||||
vars: |
||||
function: certs |
||||
ca_options: |
||||
mode: '0400' |
||||
owner: "{{ coredns_user }}" |
||||
group: "{{ coredns_group }}" |
||||
concat_inter: true |
||||
preset: web |
||||
ocsp_must_staple: false |
||||
notify: restart coredns |
||||
ca_certs: |
||||
- type: ecc384 |
||||
key: "{{ coredns_key_file }}" |
||||
cert: "{{ coredns_cert_file }}" |
||||
|
||||
|
||||
- name: template tls snippet file |
||||
template: |
||||
src: tls.j2 |
||||
dest: "{{ coredns_tls_file }}" |
||||
force: yes |
||||
owner: "{{ coredns_user }}" |
||||
group: "{{ coredns_group }}" |
||||
mode: 0400 |
||||
notify: restart coredns |
@ -0,0 +1,13 @@ |
||||
- name: install coredns |
||||
include_tasks: install.yml |
||||
when: function == 'install' |
||||
|
||||
|
||||
- name: install coredns tls enhancements |
||||
include_tasks: install_tls.yml |
||||
when: function == 'install_tls' |
||||
|
||||
|
||||
- name: add records |
||||
include_tasks: add_records.yml |
||||
when: function == 'add_records' |
@ -0,0 +1,15 @@ |
||||
(common) { |
||||
root {{ (coredns_conf_dir ~ '/') | quote }} |
||||
file {{ ((ns_tld | d(int_tld)) ~ '.zone') | quote }} |
||||
|
||||
any |
||||
bufsize {{ ns_edns0_bufsize | d(1232) }} |
||||
errors |
||||
loadbalance |
||||
} |
||||
|
||||
{{ ns_tld | d(int_tld) }} { |
||||
import common |
||||
} |
||||
|
||||
import {{ coredns_tls_file | quote }} |
@ -0,0 +1,14 @@ |
||||
#!/sbin/openrc-run |
||||
|
||||
name="$SVCNAME" |
||||
directory="{{ coredns_conf_dir }}" |
||||
command="/usr/bin/coredns" |
||||
command_args="-conf ${COREDNS_CONFIG} ${COREDNS_EXTRA_ARGS}" |
||||
command_user="{{ coredns_user }}:{{ coredns_group }}" |
||||
pidfile="/var/run/$SVCNAME.pid" |
||||
command_background=true |
||||
start_stop_daemon_args="--stdout-logger logger --stderr-logger logger" |
||||
|
||||
depend() { |
||||
after net |
||||
} |
@ -0,0 +1,9 @@ |
||||
tls://{{ ns_tld | d(int_tld) }}:853 { |
||||
import common |
||||
tls {{ coredns_cert_file | quote }} {{ coredns_key_file | quote }} |
||||
} |
||||
|
||||
https://{{ ns_tld | d(int_tld) }} { |
||||
import common |
||||
tls {{ coredns_cert_file | quote }} {{ coredns_key_file | quote }} |
||||
} |
@ -0,0 +1,32 @@ |
||||
{%- set primary_ns = inventory_hostname -%} |
||||
|
||||
{%- if ns_server_group is defined -%} |
||||
{%- set primary_ns = hostvars[groups[ns_server_group][0]]['inventory_hostname'] -%} |
||||
{%- endif -%} |
||||
|
||||
{%- set this_name = (ns_name | d(inventory_hostname)) -%} |
||||
{%- set this_primary_name = (hostvars[primary_ns]['ns_name'] | d(hostvars[primary_ns]['inventory_hostname'])) -%} |
||||
{%- set this_tld = (hostvars[primary_ns]['ns_tld'] | d(ns_tld) | d(int_tld)) -%} |
||||
|
||||
|
||||
|
||||
$ORIGIN {{ this_tld }}. |
||||
$TTL {{ ns_ttl | d(300) }} |
||||
|
||||
@ IN SOA {{ this_name ~ '.' ~ this_tld }}. {{ (ns_admin | replace('@', '.')) if ns_admin is defined else ('admin' ~ '.' ~ this_tld) }}. ( |
||||
2021010101 |
||||
{{ ns_refresh | d(1200) }} |
||||
{{ ns_retry | d(300) }} |
||||
{{ ns_expire | d(1209600) }} |
||||
{{ ns_neg_ttl | d(300) }} |
||||
) |
||||
|
||||
{% if ns_server_group is defined -%} |
||||
{% for host in groups[ns_server_group] -%} |
||||
@ IN NS {{ (hostvars[host]['ns_name'] | d(hostvars[host]['inventory_hostname'])) ~ '.' ~ this_tld }}. |
||||
{{ hostvars[host]['ns_name'] | d(hostvars[host]['inventory_hostname']) }} IN A {{ hostvars[host]['ansible_host'] }} |
||||
{% endfor -%} |
||||
{% else -%} |
||||
@ IN NS {{ this_primary_name ~ '.' ~ this_tld }}. |
||||
{{ this_primary_name }} IN A {{ ansible_host }} |
||||
{% endif -%} |
@ -0,0 +1 @@ |
||||
crl_dir: /opt/crl |
@ -0,0 +1,41 @@ |
||||
- name: install and configure nginx |
||||
include_role: |
||||
name: nginx |
||||
vars: |
||||
nginx: |
||||
servers: |
||||
- conf: nginx_crl |
||||
http: true |
||||
- conf: nginx_crl |
||||
certs: true |
||||
|
||||
|
||||
- name: create crl directory |
||||
file: |
||||
path: "{{ crl_dir }}" |
||||
state: directory |
||||
mode: 0500 |
||||
owner: nginx |
||||
group: nginx |
||||
|
||||
|
||||
- name: generate crls |
||||
include_role: |
||||
name: ca |
||||
vars: |
||||
function: crl |
||||
ca_options: |
||||
path: "{{ crl_dir }}" |
||||
mode: '0400' |
||||
owner: nginx |
||||
group: nginx |
||||
ca_crls: |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ crl_dir }}" |
@ -0,0 +1,4 @@ |
||||
location / { |
||||
root {{ crl_dir }}; |
||||
try_files $uri =404; |
||||
} |
@ -0,0 +1,290 @@ |
||||
dovecot_user: dovecot |
||||
dovecot_group: dovecot |
||||
dovecot_mail_user: dovemail |
||||
dovecot_mail_group: dovemail |
||||
dovecot_null_user: dovenull |
||||
|
||||
dovecot_conf_dir: /etc/dovecot |
||||
dovecot_tls_dir: "{{ dovecot_conf_dir }}/tls" |
||||
dovecot_sieve_dir: "{{ dovecot_conf_dir }}/sieve" |
||||
dovecot_mail_dir: /opt/mail |
||||
dovecot_script_dir: "{{ dovecot_conf_dir }}/scripts" |
||||
|
||||
dovecot_tls_dh2048: "{{ dovecot_tls_dir }}/dh2048.pem" |
||||
dovecot_tls_int_ecc384_key: "{{ dovecot_tls_dir }}/ecc384.key" |
||||
dovecot_tls_int_ecc384_cert: "{{ dovecot_tls_dir }}/ecc384.crt" |
||||
dovecot_tls_int_rsa2048_key: "{{ dovecot_tls_dir }}/rsa2048.key" |
||||
dovecot_tls_int_rsa2048_cert: "{{ dovecot_tls_dir }}/rsa2048.crt" |
||||
|
||||
|
||||
dovecot_drafts_name: Drafts |
||||
dovecot_junk_name: Junk |
||||
dovecot_sent_name: Sent |
||||
dovecot_trash_name: Trash |
||||
dovecot_expunged_name: .EXPUNGED |
||||
|
||||
dovecot_max_quota_mb: 5000 |
||||
|
||||
dovecot_default_config: |
||||
protocols: imap lmtp sieve |
||||
hostname: "{{ (mail_server.mua_actual_hostname | d(host_name)) ~ '@' ~ mail_server.tld }}" |
||||
login_greeting: "IMAPS {{ org }} (Dovecot) ready" |
||||
|
||||
auth_cache_ttl: 20m |
||||
auth_cache_size: 2M |
||||
auth_cache_negative_ttl: 5m |
||||
auth_mechanisms: |
||||
- plain |
||||
- login |
||||
- digest-md5 |
||||
- cram-md5 |
||||
- scram-sha-1 |
||||
- scram-sha-256 |
||||
auth_default_realm: "{{ mail_server.tld }}" |
||||
auth_realms: "{{ mail_server.tld }}" |
||||
auth_worker_max_count: 5 |
||||
|
||||
default_internal_user: "{{ dovecot_user }}" |
||||
default_internal_group: "{{ dovecot_group }}" |
||||
default_login_user: "{{ dovecot_null_user }}" |
||||
default_process_limit: 50 |
||||
default_vsz_limit: 64M |
||||
|
||||
disable_plaintext_auth: yes |
||||
|
||||
imap_capability: "+SPECIAL-USE" |
||||
imap_id_send: '"name" * "version" * support-email postmaster@{{ mail_server.tld }}' |
||||
|
||||
mail_attachment_detection_options: add-flags |
||||
mail_attribute_dict: "file:%h/mail_attrib" |
||||
mail_gid: "{{ dovecot_mail_group }}" |
||||
mail_home: "{{ dovecot_mail_dir }}/%Ld/%Ln" |
||||
mail_location: "mdbox:%h/mail:UTF-8" |
||||
mail_max_keyword_length: 100 |
||||
mail_server_admin: "mailto:{{ maintainer_email }}" |
||||
mail_server_comment: "Dovecot IMAPS server - {{ org }}" |
||||
mail_temp_scan_interval: 24h |
||||
mail_uid: "{{ dovecot_mail_user }}" |
||||
|
||||
postmaster_address: "postmaster@{{ mail_server.tld }}" |
||||
quota_full_tempfail: yes |
||||
recipient_delimiter: '+' |
||||
submission_client_workarounds: whitespace-before-path mailbox-for-path |
||||
|
||||
ssl: required |
||||
ssl_cert: "<{{ dovecot_tls_int_ecc384_cert }}" |
||||
ssl_key: "<{{ dovecot_tls_int_ecc384_key }}" |
||||
ssl_alt_cert: "<{{ dovecot_tls_int_rsa2048_cert }}" |
||||
ssl_alt_key: "<{{ dovecot_tls_int_rsa2048_key }}" |
||||
ssl_cipher_suites: "TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256" |
||||
ssl_dh: "<{{ dovecot_tls_dh2048 }}" |
||||
ssl_min_protocol: TLSv1.2 |
||||
ssl_prefer_server_ciphers: yes |
||||
|
||||
mail_plugins: "$mail_plugins mailbox_alias lazy_expunge listescape trash quota acl" |
||||
|
||||
|
||||
dovecot_protocols: |
||||
imap: |
||||
imap_metadata: yes |
||||
mail_plugins: "$mail_plugins imap_zlib imap_quota imap_acl imap_sieve" |
||||
lmtp: |
||||
mail_plugins: "$mail_plugins sieve" |
||||
lmtp_client_workarounds: whitespace-before-path mailbox-for-path |
||||
lmtp_user_concurrency_limit: 25 |
||||
lda: |
||||
mail_plugins: "$mail_plugins sieve" |
||||
lda_mailbox_autocreate: yes |
||||
lda_mailbox_autosubscribe: yes |
||||
sieve: |
||||
mail_max_userip_connections: 50 |
||||
|
||||
|
||||
dovecot_namespaces: |
||||
- name: inbox |
||||
opts: |
||||
inbox: yes |
||||
separator: '/' |
||||
|
||||
mailboxes: |
||||
- name: INBOX |
||||
opts: |
||||
auto: subscribe |
||||
|
||||
- name: "{{ dovecot_drafts_name }}" |
||||
opts: |
||||
auto: subscribe |
||||
special_use: '\Drafts' |
||||
|
||||
- name: "{{ dovecot_junk_name }}" |
||||
opts: |
||||
auto: subscribe |
||||
special_use: '\Junk' |
||||
autoexpunge: 90d |
||||
|
||||
- name: "{{ dovecot_sent_name }}" |
||||
opts: |
||||
auto: subscribe |
||||
special_use: '\Sent' |
||||
|
||||
- name: "{{ dovecot_trash_name }}" |
||||
opts: |
||||
auto: subscribe |
||||
special_use: '\Trash' |
||||
autoexpunge: 90d |
||||
|
||||
- name: "{{ dovecot_expunged_name }}" |
||||
opts: |
||||
auto: create |
||||
autoexpunge: 180d |
||||
|
||||
- name: shared |
||||
opts: |
||||
type: shared |
||||
separator: '/' |
||||
prefix: 'Общие/%%u/' |
||||
location: 'mdbox:%%h/mail:INDEXPVT=%h/shared_idx/%%u' |
||||
subscriptions: no |
||||
list: children |
||||
|
||||
|
||||
dovecot_dicts: |
||||
acl: "pgsql:{{ dovecot_conf_dir }}/dovecot-dict-sql.conf.ext" |
||||
|
||||
|
||||
dovecot_plugin_config: |
||||
trash: "{{ dovecot_conf_dir }}/dovecot-trash.conf.ext" |
||||
|
||||
lazy_expunge: "{{ dovecot_expunged_name }}" |
||||
lazy_expunge_only_last_instance: yes |
||||
|
||||
acl: "vfile:{{ dovecot_conf_dir }}/dovecot.acl" |
||||
acl_shared_dict: "proxy::acl" |
||||
|
||||
quota: "count:Account quota" |
||||
quota_exceeded_message: Mailbox quota exceeded |
||||
quota_grace: "5%%" |
||||
quota_max_mail_size: "{{ mail_server.max_mail_size_bytes ~ 'B' }}" |
||||
quota_rule: "*:storage={{ dovecot_max_quota_mb }}M" |
||||
quota_rule2: "{{ dovecot_trash_name }}:storage=+200M" |
||||
quota_rule3: "{{ dovecot_expunged_name }}:ignore" |
||||
quota_status_success: DUNNO |
||||
quota_status_nouser: DUNNO |
||||
quota_status_overquota: "452 4.2.2 User mailbox is full" |
||||
quota_vsizes: yes |
||||
|
||||
sieve_extensions: "-enotify -editheader" |
||||
sieve_global_extensions: "+vnd.dovecot.pipe +vnd.dovecot.filter +vnd.dovecot.execute" |
||||
sieve_max_actions: 64 |
||||
sieve_plugins: sieve_imapsieve sieve_extprograms |
||||
|
||||
sieve_pipe_bin_dir: "{{ dovecot_script_dir }}" |
||||
sieve_execute_bin_dir: "{{ dovecot_script_dir }}" |
||||
sieve_filter_bin_dir: "{{ dovecot_script_dir }}" |
||||
|
||||
sieve_spamtest_status_type: text |
||||
sieve_spamtest_status_header: X-Spam |
||||
sieve_spamtest_text_value0: No |
||||
sieve_spamtest_text_value10: Yes |
||||
|
||||
sieve_before: "{{ dovecot_sieve_dir }}/spam-to-folder.sieve" |
||||
|
||||
|
||||
dovecot_user_pass_db: |
||||
- type: passdb |
||||
opts: |
||||
driver: sql |
||||
args: "{{ dovecot_conf_dir }}/dovecot-sql.conf.ext" |
||||
- type: userdb |
||||
opts: |
||||
driver: prefetch |
||||
- type: userdb |
||||
opts: |
||||
driver: sql |
||||
args: "{{ dovecot_conf_dir }}/dovecot-sql.conf.ext" |
||||
|
||||
|
||||
dovecot_services: |
||||
imap: |
||||
opts: |
||||
service_count: 16 |
||||
process_limit: 256 |
||||
|
||||
imap-login: |
||||
opts: |
||||
service_count: 0 |
||||
process_min_avail: 1 |
||||
client_limit: 16 |
||||
service_count: 32 |
||||
|
||||
listeners: |
||||
- type: inet_listener |
||||
name: imap |
||||
opts: |
||||
port: 143 |
||||
|
||||
- type: inet_listener |
||||
name: imaps |
||||
opts: |
||||
port: 993 |
||||
ssl: yes |
||||
|
||||
lmtp: |
||||
opts: |
||||
client_limit: 1 |
||||
vsz_limit: 192M |
||||
|
||||
listeners: |
||||
- type: inet_listener |
||||
opts: |
||||
port: "{{ mail_server.mua_lmtp_port }}" |
||||
|
||||
auth: |
||||
listeners: |
||||
- type: inet_listener |
||||
opts: |
||||
port: "{{ mail_server.mua_auth_port }}" |
||||
- type: unix_listener auth-userdb |
||||
opts: |
||||
mode: 0666 |
||||
user: "{{ dovecot_user }}" |
||||
group: "{{ dovecot_group }}" |
||||
|
||||
quota-status: |
||||
opts: |
||||
executable: "/usr/libexec/dovecot/quota-status -p postfix" |
||||
|
||||
listeners: |
||||
- type: inet_listener |
||||
opts: |
||||
port: "{{ mail_server.mua_quota_port }}" |
||||
|
||||
auth-worker: |
||||
opts: |
||||
user: "{{ dovecot_user }}" |
||||
group: "{{ dovecot_group }}" |
||||
|
||||
dict: |
||||
opts: |
||||
user: "{{ dovecot_user }}" |
||||
group: "{{ dovecot_group }}" |
||||
listeners: |
||||
- type: unix_listener dict |
||||
opts: |
||||
mode: 0666 |
||||
user: "{{ dovecot_user }}" |
||||
group: "{{ dovecot_group }}" |
||||
|
||||
managesieve-login: |
||||
opts: |
||||
service_count: 0 |
||||
process_min_avail: 1 |
||||
|
||||
managesieve: |
||||
opts: |
||||
process_limit: 512 |
||||
|
||||
|
||||
dovecot_sieve_scripts: |
||||
- src: sieve-spam |
||||
dest: spam-to-folder |
@ -0,0 +1,4 @@ |
||||
- name: restart dovecot |
||||
service: |
||||
name: dovecot |
||||
state: restarted |
@ -0,0 +1,241 @@ |
||||
- name: set dovecot_cfg |
||||
set_fact: |
||||
dovecot_cfg: "{{ dovecot_default_config | d({}) | combine(dovecot_config | d({}), recursive=true) }}" |
||||
|
||||
|
||||
- name: install dovecot |
||||
include_tasks: tasks/install_packages.yml |
||||
vars: |
||||
package: |
||||
- dovecot |
||||
- dovecot-lmtpd |
||||
- dovecot-openrc |
||||
- dovecot-pgsql |
||||
- dovecot-pigeonhole-plugin |
||||
|
||||
|
||||
- name: create user and group |
||||
include_tasks: tasks/create_user.yml |
||||
vars: |
||||
user: |
||||
name: "{{ dovecot_user }}" |
||||
group: "{{ dovecot_group }}" |
||||
|
||||
|
||||
- name: create dovemail user and group |
||||
include_tasks: tasks/create_user.yml |
||||
vars: |
||||
user: |
||||
name: "{{ dovecot_mail_user }}" |
||||
group: "{{ dovecot_mail_group }}" |
||||
|
||||
|
||||
- name: create dovenull user and group |
||||
include_tasks: tasks/create_user.yml |
||||
vars: |
||||
user: |
||||
name: "{{ dovecot_null_user }}" |
||||
|
||||
|
||||
- name: create dovecot conf dir |
||||
file: |
||||
path: "{{ dovecot_conf_dir }}" |
||||
state: directory |
||||
mode: 0755 |
||||
owner: "{{ dovecot_user }}" |
||||
group: "{{ dovecot_group }}" |
||||
|
||||
|
||||
- name: create dovecot tls dir |
||||
file: |
||||
path: "{{ dovecot_tls_dir }}" |
||||
state: directory |
||||
mode: 0700 |
||||
|
||||
|
||||
- name: create dovecot mail dir |
||||
file: |
||||
path: "{{ dovecot_mail_dir }}" |
||||
state: directory |
||||
mode: "g+s,o-rwx" |
||||
owner: "{{ dovecot_mail_user }}" |
||||
group: "{{ dovecot_mail_group }}" |
||||
|
||||
|
||||
- name: create dovecot sieve dir |
||||
file: |
||||
path: "{{ dovecot_sieve_dir }}" |
||||
state: directory |
||||
mode: 0755 |
||||
owner: "{{ dovecot_mail_user }}" |
||||
group: "{{ dovecot_mail_group }}" |
||||
|
||||
|
||||
- name: generate dh params |
||||
include_role: |
||||
name: ca |
||||
vars: |
||||
function: dhparams |
||||
dh_params: |
||||
path: "{{ dovecot_tls_dh2048 }}" |
||||
mode: '0400' |
||||
remote_gen: yes |
||||
notify: restart dovecot |
||||
|
||||
|
||||
- name: remove unneeded dovecot files |
||||
file: |
||||
path: "{{ dovecot_conf_dir ~ '/' ~ item }}" |
||||
state: absent |
||||
loop: |
||||
- conf.d |
||||
- dovecot-dict-auth.conf.ext |
||||
- dovecot-oauth2.conf.ext |
||||
- dovecot-openssl.cnf |
||||
- users |
||||
notify: restart dovecot |
||||
|
||||
|
||||
- name: get dovemail user info |
||||
getent: |
||||
database: passwd |
||||
key: "{{ dovecot_mail_user }}" |
||||
changed_when: no |
||||
|
||||
|
||||
- name: set dovemail uid |
||||
set_fact: |
||||
dovecot_dovemail_uid: "{{ getent_passwd[dovecot_mail_user][1] }}" |
||||
|
||||
|
||||
- name: template dovecot configuration |
||||
template: |
||||
src: "{{ item if item is string else item.src }}.j2" |
||||
dest: "{{ dovecot_conf_dir ~ '/' ~ ((item ~ '.conf.ext') if item is string else item.dest) }}" |
||||
force: yes |
||||
mode: "{{ '0400' if (item is string) else (item.mode | d('0400')) }}" |
||||
lstrip_blocks: yes |
||||
loop: |
||||
- { src: dovecot-dict-sql, dest: dovecot-dict-sql.conf.ext, mode: '0444' } |
||||
- dovecot-sql |
||||
- dovecot-trash |
||||
- { src: dovecot-acl, dest: dovecot.acl } |
||||
- { src: dovecot, dest: dovecot.conf } |
||||
notify: restart dovecot |
||||
|
||||
|
||||
- name: edit permissions of dovecot plugin files |
||||
file: |
||||
path: "{{ dovecot_conf_dir ~ '/' ~ item }}" |
||||
state: file |
||||
owner: "{{ dovecot_mail_user }}" |
||||
group: "{{ dovecot_mail_group }}" |
||||
loop: |
||||
- dovecot.acl |
||||
- dovecot-sql.conf.ext |
||||
- dovecot-trash.conf.ext |
||||
- dovecot-dict-sql.conf.ext |
||||
notify: restart dovecot |
||||
|
||||
|
||||
- name: template sieve scripts |
||||
template: |
||||
src: "{{ item.src }}.j2" |
||||
dest: "{{ dovecot_sieve_dir ~ '/' ~ item.dest ~ '.sieve' }}" |
||||
force: yes |
||||
mode: 0400 |
||||
owner: "{{ dovecot_mail_user }}" |
||||
group: "{{ dovecot_mail_group }}" |
||||
loop: "{{ dovecot_sieve_scripts | d([]) }}" |
||||
register: result |
||||
|
||||
|
||||
- name: compile scripts |
||||
shell: |
||||
cmd: "sievec {{ (dovecot_sieve_dir ~ '/') | quote }}" |
||||
when: result.changed |
||||
notify: restart dovecot |
||||
|
||||
|
||||
- name: collect svbin files |
||||
find: |
||||
paths: "{{ dovecot_sieve_dir }}/" |
||||
patterns: "*.svbin" |
||||
recurse: yes |
||||
depth: 3 |
||||
register: svbin_files |
||||
|
||||
|
||||
- name: change svbin permissions |
||||
file: |
||||
path: "{{ item.path }}" |
||||
mode: 0400 |
||||
owner: "{{ dovecot_mail_user }}" |
||||
group: "{{ dovecot_mail_group }}" |
||||
loop: "{{ svbin_files.files | d([]) | flatten(levels=1) }}" |
||||
notify: restart dovecot |
||||
|
||||
|
||||
- name: add extra cname record |
||||
include_role: |
||||
name: ns |
||||
vars: |
||||
function: add_records |
||||
ns_add_default_record: no |
||||
ns_records: |
||||
- name: "{{ mail_server.mua_actual_hostname }}" |
||||
type: CNAME |
||||
value: "{{ host_fqdn }}" |
||||
when: mail_server.mua_actual_hostname is defined |
||||
|
||||
|
||||
- name: deploy certs |
||||
include_role: |
||||
name: certs |
||||
vars: |
||||
common: |
||||
owner: root |
||||
group: root |
||||
post_hook: service dovecot restart |
||||
notify: restart dovecot |
||||
hostname: "{{ mail_server.mua_actual_hostname }}" |
||||
certs: |
||||
- cert: "{{ dovecot_tls_int_ecc384_cert }}" |
||||
key: "{{ dovecot_tls_int_ecc384_key }}" |
||||
ecc: yes |
||||
- cert: "{{ dovecot_tls_int_rsa2048_cert }}" |
||||
key: "{{ dovecot_tls_int_rsa2048_key }}" |
||||
ecc: no |
||||
|
||||
|
||||
- name: flush handlers |
||||
meta: flush_handlers |
||||
|
||||
|
||||
- name: add directories to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ dovecot_conf_dir }}" |
||||
- "{{ dovecot_tls_dir }}" |
||||
- "{{ dovecot_sieve_dir }}" |
||||
- "{{ dovecot_script_dir }}" |
||||
|
||||
|
||||
- name: add mail dir to backup plan |
||||
include_role: |
||||
name: backup |
||||
vars: |
||||
function: add |
||||
backup_items: |
||||
- "{{ dovecot_mail_dir }}" |
||||
when: dovecot_backup_mail_dir | d(false) == true |
||||
|
||||
|
||||
- name: enable and start dovecot |
||||
service: |
||||
name: dovecot |
||||
enabled: yes |
||||
state: started |
@ -0,0 +1,8 @@ |
||||
* user={{ mail_server.admin_email }} lrwstipekxa |
||||
INBOX owner lrwstipek |
||||
{{ dovecot_sent_name }} owner lrwstipek |
||||
{{ dovecot_drafts_name }} owner lrwstipek |
||||
{{ dovecot_junk_name }} owner lrwstipek |
||||
{{ dovecot_trash_name }} owner lrwstipek |
||||
{{ dovecot_expunged_name }} owner |
||||
{{ dovecot_expunged_name }} anyone |
@ -0,0 +1,22 @@ |
||||
connect = host={{ hostvars[mail_server.db_server_hostname]['ansible_host'] }} user={{ mail_server.db_user }} password={{ mail_server.db_pass }} dbname={{ mail_server.db_name }} |
||||
|
||||
map { |
||||
pattern = shared/shared-boxes/user/$to/$from |
||||
table = mail_user_shares |
||||
value_field = dummy |
||||
|
||||
fields { |
||||
from_user = $from |
||||
to_user = $to |
||||
} |
||||
} |
||||
|
||||
map { |
||||
pattern = shared/shared-boxes/anyone/$from |
||||
table = mail_anyone_shares |
||||
value_field = dummy |
||||
|
||||
fields { |
||||
from_user = $from |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
driver = pgsql |
||||
connect = host={{ hostvars[mail_server.db_server_hostname]['ansible_host'] }} user={{ mail_server.db_user }} password={{ mail_server.db_pass }} dbname={{ mail_server.db_name }} |
||||
default_pass_scheme = PLAIN |
||||
|
||||
password_query = \ |
||||
SELECT username AS user, \ |
||||
( \ |
||||
SELECT domain FROM mail_domains WHERE id = domain_id \ |
||||
) AS domain, \ |
||||
password_plaintext AS password, \ |
||||
'{{ dovecot_mail_dir }}/%Ld/%Ln' AS userdb_home, \ |
||||
concat('*:bytes=', coalesce(nullif(quota_mb, 0), {{ dovecot_max_quota_mb }}), 'M') AS userdb_quota_rule, \ |
||||
{{ dovecot_dovemail_uid }} AS userdb_uid \ |
||||
FROM mail_users \ |
||||
WHERE \ |
||||
LOWER(username) = '%Ln' AND \ |
||||
domain_id = ( \ |
||||
SELECT id FROM mail_domains WHERE LOWER(domain) = '%Ld' \ |
||||
) AND \ |
||||
enabled = true; |
||||
|
||||
|
||||
|
||||
user_query = \ |
||||
SELECT username AS user, \ |
||||
( \ |
||||
SELECT domain FROM mail_domains WHERE id = domain_id \ |
||||
) AS domain, \ |
||||
'{{ dovecot_mail_dir }}/%Ld/%Ln' AS home, \ |
||||
concat('*:bytes=', coalesce(nullif(quota_mb, 0), {{ dovecot_max_quota_mb }}), 'M') AS quota_rule, \ |
||||
{{ dovecot_dovemail_uid }} AS uid \ |
||||
FROM mail_users \ |
||||
WHERE \ |
||||
LOWER(username) = '%Ln' AND \ |
||||
domain_id = ( \ |
||||
SELECT id FROM mail_domains WHERE LOWER(domain) = '%Ld' \ |
||||
) AND \ |
||||
enabled = true; |
||||
|
||||
|
||||
iterate_query = \ |
||||
SELECT username AS user, \ |
||||
( \ |
||||
SELECT domain FROM mail_domains WHERE id = domain_id \ |
||||
) AS domain \ |
||||
FROM mail_users \ |
||||
WHERE enabled = true; |
@ -0,0 +1,3 @@ |
||||
1 {{ dovecot_trash_name }} |
||||
2 {{ dovecot_junk_name }} |
||||
3 {{ dovecot_sent_name }} |
@ -0,0 +1,94 @@ |
||||
{% macro dovecot_option(option, padding = 0) -%} |
||||
{{- '' if (padding == 0) else (' ' * 4 * padding) -}} |
||||
{% if option.value is boolean -%} |
||||
{{ option.key }} = {{ 'yes' if option.value else 'no' }} |
||||
{% elif option.value | type_debug == 'list' -%} |
||||
{{ option.key }} = {{ option.value | join(' ') }} |
||||
{% elif option.value is mapping -%} |
||||
{{ option.key }} { |
||||
{% for suboption in (option.value | d({}) | dict2items) -%} |
||||
{{- dovecot_option(suboption, padding + 1) }} |
||||
{% endfor -%} |
||||
} |
||||
{% else -%} |
||||
{{ option.key }} = {{ option.value if option.value != None else '' }} |
||||
{% endif -%} |
||||
{% endmacro -%} |
||||
|
||||
|
||||
{% for option in (dovecot_cfg | d({}) | dict2items) -%} |
||||
{{ dovecot_option(option) }} |
||||
{%- endfor %} |
||||
|
||||
first_valid_uid = {{ dovecot_dovemail_uid }} |
||||
last_valid_uid = {{ dovecot_dovemail_uid }} |
||||
|
||||
|
||||
{% for proto in (dovecot_protocols | d({}) | dict2items) -%} |
||||
protocol {{ proto.key }} { |
||||
{% for option in (proto.value | d({}) | dict2items) -%} |
||||
{{ dovecot_option(option, 1) }} |
||||
{%- endfor -%} |
||||
} |
||||
{% endfor %} |
||||
|
||||
|
||||
{% for namespace in (dovecot_namespaces | d([])) -%} |
||||
namespace {{ namespace.name }} { |
||||
{% for option in (namespace.opts | d({}) | dict2items) -%} |
||||
{{ dovecot_option(option, 1) }} |
||||
{%- endfor -%} |
||||
|
||||
{% for mailbox in (namespace.mailboxes | d([])) -%} |
||||
{{- ' ' -}}mailbox {{ mailbox.name }} { |
||||
{% for mailbox_option in (mailbox.opts | d({}) | dict2items) -%} |
||||
{{ dovecot_option(mailbox_option, 2) }} |
||||
{%- endfor -%} |
||||
{{- ' ' -}}} |
||||
{% endfor -%} |
||||
} |
||||
{% endfor %} |
||||
|
||||
|
||||
{% if dovecot_dicts is mapping -%} |
||||
dict { |
||||
{% for option in (dovecot_dicts | d({}) | dict2items) -%} |
||||
{{ dovecot_option(option, 1) }} |
||||
{%- endfor -%} |
||||
} |
||||
{% endif %} |
||||
|
||||
|
||||
{% if dovecot_plugin_config is mapping -%} |
||||
plugin { |
||||
{% for option in (dovecot_plugin_config | d({}) | dict2items) -%} |
||||
{{ dovecot_option(option, 1) }} |
||||
{%- endfor -%} |
||||
} |
||||
{% endif %} |
||||
|
||||
|
||||
{% for db in (dovecot_user_pass_db | d([])) -%} |
||||
{{ db.type }} { |
||||
{% for option in (db.opts | d({}) | dict2items) -%} |
||||
{{ dovecot_option(option, 1) }} |
||||
{%- endfor -%} |
||||
} |
||||
{% endfor %} |
||||
|
||||
|
||||
{% for service in (dovecot_services | d({}) | dict2items) -%} |
||||
service {{ service.key }} { |
||||
{% for option in (service.value.opts | d({}) | dict2items) -%} |
||||
{{ dovecot_option(option, 1) }} |
||||
{%- endfor %} |
||||
|
||||
{% for listener in (service.value.listeners | d([])) -%} |
||||
{{- ' ' -}}{{ listener.type }} {{ listener.name | d('') }} { |
||||
{% for listener_option in (listener.opts | d({}) | dict2items) -%} |
||||
{{ dovecot_option(listener_option, 2) }} |
||||
{%- endfor -%} |
||||
{{- ' ' -}}} |
||||
{% endfor -%} |
||||
} |
||||
{% endfor %} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue