feat: create stalwart module (#418)
- add stalwart module - add psql database on the shared patroni instance - add ceph-rgw credentials to eyaml - ensure psql pass and s3 access key are converted to sensitive Reviewed-on: #418
This commit was merged in pull request #418.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# @summary Manages Stalwart Mail Server configuration
|
||||
#
|
||||
# @api private
|
||||
class stalwart::config {
|
||||
assert_private()
|
||||
|
||||
# Create base directories (package creates user/group and base dirs)
|
||||
file { [$stalwart::config_dir, $stalwart::data_dir, $stalwart::webadmin_unpack_path]:
|
||||
ensure => directory,
|
||||
owner => 'stalwart',
|
||||
group => 'stalwart',
|
||||
mode => '0750',
|
||||
}
|
||||
|
||||
# Ensure log directory exists
|
||||
file { '/var/log/stalwart':
|
||||
ensure => directory,
|
||||
owner => 'stalwart',
|
||||
group => 'stalwart',
|
||||
mode => '0755',
|
||||
}
|
||||
|
||||
# Main configuration file
|
||||
file { "${stalwart::config_dir}/config.toml":
|
||||
ensure => file,
|
||||
owner => 'stalwart',
|
||||
group => 'stalwart',
|
||||
mode => '0640',
|
||||
content => epp('stalwart/config.toml.epp', {
|
||||
'cluster_size' => $stalwart::cluster_size,
|
||||
'other_cluster_members' => $stalwart::other_cluster_members,
|
||||
'effective_node_id' => $stalwart::effective_node_id,
|
||||
'bind_address' => $stalwart::bind_address,
|
||||
'advertise_address' => $stalwart::advertise_address,
|
||||
'postgresql_host' => $stalwart::postgresql_host,
|
||||
'postgresql_port' => $stalwart::postgresql_port,
|
||||
'postgresql_database' => $stalwart::postgresql_database,
|
||||
'postgresql_user' => $stalwart::postgresql_user,
|
||||
'postgresql_password' => $stalwart::postgresql_password.unwrap,
|
||||
'postgresql_ssl' => $stalwart::postgresql_ssl,
|
||||
's3_endpoint' => $stalwart::s3_endpoint,
|
||||
's3_bucket' => $stalwart::s3_bucket,
|
||||
's3_region' => $stalwart::s3_region,
|
||||
's3_access_key' => $stalwart::s3_access_key,
|
||||
's3_secret_key' => $stalwart::s3_secret_key.unwrap,
|
||||
's3_key_prefix' => $stalwart::s3_key_prefix,
|
||||
'domains' => $stalwart::domains,
|
||||
'postfix_relay_host' => $stalwart::postfix_relay_host,
|
||||
'enable_imap' => $stalwart::enable_imap,
|
||||
'enable_imap_tls' => $stalwart::enable_imap_tls,
|
||||
'enable_http' => $stalwart::enable_http,
|
||||
'data_dir' => $stalwart::data_dir,
|
||||
'tls_cert' => $stalwart::tls_cert,
|
||||
'tls_key' => $stalwart::tls_key,
|
||||
'log_level' => $stalwart::log_level,
|
||||
'fallback_admin_user' => $stalwart::fallback_admin_user,
|
||||
'fallback_admin_password' => $stalwart::fallback_admin_password,
|
||||
'webadmin_unpack_path' => $stalwart::webadmin_unpack_path,
|
||||
'webadmin_resource_url' => $stalwart::webadmin_resource_url,
|
||||
'webadmin_auto_update' => $stalwart::webadmin_auto_update,
|
||||
'node_facts' => $facts,
|
||||
}),
|
||||
notify => Service['stalwart'],
|
||||
}
|
||||
|
||||
# Create directories for storage
|
||||
file { "${stalwart::data_dir}/queue":
|
||||
ensure => directory,
|
||||
owner => 'stalwart',
|
||||
group => 'stalwart',
|
||||
mode => '0750',
|
||||
}
|
||||
|
||||
file { "${stalwart::data_dir}/reports":
|
||||
ensure => directory,
|
||||
owner => 'stalwart',
|
||||
group => 'stalwart',
|
||||
mode => '0750',
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# @summary Manages DNS autodiscovery records for Stalwart
|
||||
#
|
||||
# @param target_host
|
||||
# FQDN to point DNS records to (defaults to current server)
|
||||
#
|
||||
# @api private
|
||||
class stalwart::dns (
|
||||
Stdlib::Fqdn $target_host = $facts['networking']['fqdn'],
|
||||
) {
|
||||
assert_private()
|
||||
|
||||
# Create autodiscovery DNS records for each domain
|
||||
$stalwart::domains.each |$domain| {
|
||||
|
||||
# Autoconfig record for Thunderbird/Mozilla clients
|
||||
profiles::dns::record { "autoconfig_${domain}":
|
||||
record => "autoconfig.${domain}",
|
||||
type => 'CNAME',
|
||||
value => "${target_host}.",
|
||||
zone => $domain,
|
||||
order => 100,
|
||||
}
|
||||
|
||||
# Autodiscover record for Outlook/Microsoft clients
|
||||
profiles::dns::record { "autodiscover_${domain}":
|
||||
record => "autodiscover.${domain}",
|
||||
type => 'CNAME',
|
||||
value => "${target_host}.",
|
||||
zone => $domain,
|
||||
order => 101,
|
||||
}
|
||||
|
||||
# IMAP SRV records
|
||||
profiles::dns::record { "imap_srv_${domain}":
|
||||
record => "_imap._tcp.${domain}",
|
||||
type => 'SRV',
|
||||
value => "10 1 143 ${target_host}.",
|
||||
zone => $domain,
|
||||
order => 102,
|
||||
}
|
||||
|
||||
profiles::dns::record { "imaps_srv_${domain}":
|
||||
record => "_imaps._tcp.${domain}",
|
||||
type => 'SRV',
|
||||
value => "10 1 993 ${target_host}.",
|
||||
zone => $domain,
|
||||
order => 103,
|
||||
}
|
||||
|
||||
# CalDAV and CardDAV SRV records
|
||||
profiles::dns::record { "caldav_srv_${domain}":
|
||||
record => "_caldav._tcp.${domain}",
|
||||
type => 'SRV',
|
||||
value => "10 1 443 ${target_host}.",
|
||||
zone => $domain,
|
||||
order => 104,
|
||||
}
|
||||
|
||||
profiles::dns::record { "carddav_srv_${domain}":
|
||||
record => "_carddav._tcp.${domain}",
|
||||
type => 'SRV',
|
||||
value => "10 1 443 ${target_host}.",
|
||||
zone => $domain,
|
||||
order => 105,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
# @summary Main class for managing Stalwart Mail Server
|
||||
#
|
||||
# This class provides a comprehensive setup of Stalwart Mail Server with
|
||||
# clustering, authentication, storage, and protocol support.
|
||||
#
|
||||
# @example Basic Stalwart setup
|
||||
# class { 'stalwart':
|
||||
# node_id => 1,
|
||||
# postgresql_host => 'pgsql.example.com',
|
||||
# postgresql_database => 'stalwart',
|
||||
# postgresql_user => 'stalwart',
|
||||
# postgresql_password => Sensitive('secretpassword'),
|
||||
# s3_endpoint => 'https://ceph-rgw.example.com',
|
||||
# s3_bucket => 'stalwart-blobs',
|
||||
# s3_access_key => 'accesskey',
|
||||
# s3_secret_key => Sensitive('secretkey'),
|
||||
# domains => ['example.com'],
|
||||
# postfix_relay_host => 'postfix.example.com',
|
||||
# }
|
||||
#
|
||||
# @param node_id
|
||||
# Unique identifier for this node in the cluster (1-N). If not specified,
|
||||
# automatically calculated based on sorted position in cluster member list.
|
||||
#
|
||||
# @param cluster_role
|
||||
# Role name for cluster member discovery via query_nodes()
|
||||
#
|
||||
#
|
||||
# @param postgresql_host
|
||||
# PostgreSQL server hostname/IP
|
||||
#
|
||||
# @param postgresql_port
|
||||
# PostgreSQL server port
|
||||
#
|
||||
# @param postgresql_database
|
||||
# PostgreSQL database name
|
||||
#
|
||||
# @param postgresql_user
|
||||
# PostgreSQL username
|
||||
#
|
||||
# @param postgresql_password
|
||||
# PostgreSQL password (Sensitive)
|
||||
#
|
||||
# @param postgresql_ssl
|
||||
# Enable SSL/TLS for PostgreSQL connections
|
||||
#
|
||||
# @param s3_endpoint
|
||||
# S3/Ceph-RGW endpoint URL
|
||||
#
|
||||
# @param s3_bucket
|
||||
# S3 bucket name for blob storage
|
||||
#
|
||||
# @param s3_region
|
||||
# S3 region
|
||||
#
|
||||
# @param s3_access_key
|
||||
# S3 access key
|
||||
#
|
||||
# @param s3_secret_key
|
||||
# S3 secret key (Sensitive)
|
||||
#
|
||||
# @param s3_key_prefix
|
||||
# S3 key prefix for stalwart objects
|
||||
#
|
||||
# @param domains
|
||||
# Array of domains this server handles
|
||||
#
|
||||
# @param postfix_relay_host
|
||||
# Postfix relay host for SMTP delivery
|
||||
#
|
||||
# @param bind_address
|
||||
# IP address to bind services to
|
||||
#
|
||||
# @param advertise_address
|
||||
# IP address to advertise to cluster members
|
||||
#
|
||||
# @param enable_imap
|
||||
# Enable IMAP protocol listener
|
||||
#
|
||||
# @param enable_imap_tls
|
||||
# Enable IMAP over TLS listener
|
||||
#
|
||||
# @param enable_http
|
||||
# Enable HTTP listener for JMAP/WebDAV/Autodiscovery
|
||||
#
|
||||
# @param enable_smtp_relay
|
||||
# Enable SMTP for postfix relay communication
|
||||
#
|
||||
# @param package_ensure
|
||||
# Package version to install
|
||||
#
|
||||
# @param config_dir
|
||||
# Stalwart configuration directory
|
||||
#
|
||||
# @param data_dir
|
||||
# Stalwart data directory
|
||||
#
|
||||
# @param log_level
|
||||
# Logging verbosity level
|
||||
#
|
||||
# @param manage_firewall
|
||||
# Whether to manage firewall rules
|
||||
#
|
||||
# @param tls_cert
|
||||
# Path to TLS certificate file
|
||||
#
|
||||
# @param tls_key
|
||||
# Path to TLS private key file
|
||||
#
|
||||
# @param manage_dns_records
|
||||
# Whether to create DNS autodiscovery records
|
||||
#
|
||||
class stalwart (
|
||||
String $cluster_role,
|
||||
Stdlib::Host $postgresql_host,
|
||||
String $postgresql_database,
|
||||
String $postgresql_user,
|
||||
Sensitive[String] $postgresql_password,
|
||||
Stdlib::HTTPUrl $s3_endpoint,
|
||||
String $s3_bucket,
|
||||
String $s3_access_key,
|
||||
Sensitive[String] $s3_secret_key,
|
||||
Array[Stdlib::Fqdn] $domains,
|
||||
Stdlib::Host $postfix_relay_host,
|
||||
Optional[Integer] $node_id = undef,
|
||||
Stdlib::Port $postgresql_port = 5432,
|
||||
Boolean $postgresql_ssl = true,
|
||||
String $s3_region = 'us-east-1',
|
||||
String $s3_key_prefix = 'stalwart/',
|
||||
Stdlib::IP::Address $bind_address = $facts['networking']['ip'],
|
||||
Stdlib::IP::Address $advertise_address = $facts['networking']['ip'],
|
||||
Boolean $enable_imap = true,
|
||||
Boolean $enable_imap_tls = true,
|
||||
Boolean $enable_http = true,
|
||||
Boolean $enable_smtp_relay = true,
|
||||
String $package_ensure = 'present',
|
||||
Stdlib::Absolutepath $config_dir = '/opt/stalwart/etc',
|
||||
Stdlib::Absolutepath $data_dir = '/var/lib/stalwart',
|
||||
Enum['error','warn','info','debug','trace'] $log_level = 'info',
|
||||
Boolean $manage_firewall = false,
|
||||
Stdlib::Absolutepath $tls_cert = '/etc/pki/tls/vault/certificate.crt',
|
||||
Stdlib::Absolutepath $tls_key = '/etc/pki/tls/vault/private.key',
|
||||
Boolean $manage_dns_records = true,
|
||||
Optional[Stdlib::Fqdn] $loadbalancer_host = undef,
|
||||
String $fallback_admin_user = 'admin',
|
||||
Sensitive[String] $fallback_admin_password = Sensitive('admin'),
|
||||
Stdlib::Absolutepath $webadmin_unpack_path = "${data_dir}/webadmin",
|
||||
Stdlib::HTTPUrl $webadmin_resource_url = 'https://github.com/stalwartlabs/webadmin/releases/latest/download/webadmin.zip',
|
||||
Boolean $webadmin_auto_update = true,
|
||||
) {
|
||||
|
||||
# Calculate node_id from last 4 digits of hostname if not provided
|
||||
$my_fqdn = $facts['networking']['fqdn']
|
||||
$hostname = $facts['networking']['hostname']
|
||||
|
||||
# Query cluster members for validation
|
||||
$cluster_query = "enc_role='${cluster_role}' and country='${facts['country']}' and region='${facts['region']}'"
|
||||
$cluster_members_raw = query_nodes($cluster_query, 'networking.fqdn')
|
||||
$cluster_members = $cluster_members_raw ? {
|
||||
undef => [],
|
||||
default => $cluster_members_raw,
|
||||
}
|
||||
$sorted_cluster_members = sort($cluster_members)
|
||||
|
||||
# Calculate cluster information for templates
|
||||
$other_cluster_members = $sorted_cluster_members.filter |$member| { $member != $my_fqdn }
|
||||
$cluster_size = length($sorted_cluster_members)
|
||||
|
||||
# Extract last 4 digits from hostname (e.g., ausyd1nxvm1234 -> 1234)
|
||||
if $hostname =~ /^.*(\d{4})$/ {
|
||||
$hostname_digits = $1
|
||||
$calculated_node_id = Integer($hostname_digits)
|
||||
} else {
|
||||
fail("Unable to extract 4-digit node ID from hostname '${hostname}'. Hostname must end with 4 digits or specify node_id manually.")
|
||||
}
|
||||
|
||||
# Use provided node_id or calculated one
|
||||
$effective_node_id = $node_id ? {
|
||||
undef => $calculated_node_id,
|
||||
default => $node_id,
|
||||
}
|
||||
|
||||
# Validate parameters
|
||||
if $effective_node_id < 1 {
|
||||
fail('node_id must be a positive integer')
|
||||
}
|
||||
|
||||
if empty($domains) {
|
||||
fail('At least one domain must be specified')
|
||||
}
|
||||
|
||||
if !($my_fqdn in $sorted_cluster_members) {
|
||||
fail("This node (${my_fqdn}) is not found in cluster members for role '${cluster_role}' in ${facts['country']}-${facts['region']}")
|
||||
}
|
||||
|
||||
|
||||
# Include sub-classes in dependency order
|
||||
include stalwart::install
|
||||
include stalwart::config
|
||||
include stalwart::service
|
||||
|
||||
# Handle DNS records if requested
|
||||
if $manage_dns_records {
|
||||
if $loadbalancer_host {
|
||||
# Only first node in cluster creates DNS records pointing to load balancer
|
||||
if $my_fqdn == $sorted_cluster_members[0] {
|
||||
class { 'stalwart::dns':
|
||||
target_host => $loadbalancer_host,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
# Current behavior: each server creates its own DNS records
|
||||
include stalwart::dns
|
||||
}
|
||||
}
|
||||
|
||||
# Class ordering
|
||||
Class['stalwart::install']
|
||||
-> Class['stalwart::config']
|
||||
-> Class['stalwart::service']
|
||||
|
||||
if $manage_dns_records {
|
||||
Class['stalwart::service'] -> Class['stalwart::dns']
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# @summary Manages Stalwart Mail Server package installation
|
||||
#
|
||||
# @api private
|
||||
class stalwart::install {
|
||||
assert_private()
|
||||
|
||||
# Install stalwart package (user/group created by package preinstall script)
|
||||
package { 'stalwart':
|
||||
ensure => $stalwart::package_ensure,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# @summary Manages Stalwart Mail Server service
|
||||
#
|
||||
# @api private
|
||||
class stalwart::service {
|
||||
assert_private()
|
||||
|
||||
# Service is installed by the RPM package
|
||||
service { 'stalwart':
|
||||
ensure => running,
|
||||
enable => true,
|
||||
subscribe => [
|
||||
File[$stalwart::tls_cert],
|
||||
File[$stalwart::tls_key],
|
||||
],
|
||||
}
|
||||
|
||||
# Add capability to bind to privileged ports (143, 443, 993)
|
||||
systemd::manage_dropin { 'bind-capabilities.conf':
|
||||
ensure => present,
|
||||
unit => 'stalwart.service',
|
||||
service_entry => {
|
||||
'AmbientCapabilities' => 'CAP_NET_BIND_SERVICE',
|
||||
},
|
||||
notify => Service['stalwart'],
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user