mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-04 15:54:48 +01:00
Merge branch 'jammyjellyfish2204' of https://github.com/mail-in-a-box/mailinabox into jammyjellyfish2204
# Conflicts: # CHANGELOG.md # README.md # conf/nginx-top.conf # management/backup.py # setup/bootstrap.sh # setup/management.sh # setup/nextcloud.sh # setup/system.sh # setup/web.sh # setup/webmail.sh # setup/zpush.sh # tests/test_mail.py
This commit is contained in:
@@ -13,12 +13,7 @@ import dateutil.parser, dateutil.relativedelta, dateutil.tz
|
||||
import rtyaml
|
||||
from exclusiveprocess import Lock
|
||||
|
||||
from utils import load_environment, shell, wait_for_service, fix_boto
|
||||
|
||||
rsync_ssh_options = [
|
||||
"--ssh-options= -i /root/.ssh/id_rsa_miab",
|
||||
"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p 22 -i /root/.ssh/id_rsa_miab\"",
|
||||
]
|
||||
from utils import load_environment, shell, wait_for_service
|
||||
|
||||
def backup_status(env):
|
||||
# If backups are dissbled, return no status.
|
||||
@@ -65,9 +60,9 @@ def backup_status(env):
|
||||
"--archive-dir", backup_cache_dir,
|
||||
"--gpg-options", "--cipher-algo=AES256",
|
||||
"--log-fd", "1",
|
||||
config["target"],
|
||||
] + rsync_ssh_options,
|
||||
get_env(env),
|
||||
get_duplicity_target_url(config),
|
||||
] + get_duplicity_additional_args(env),
|
||||
get_duplicity_env_vars(env),
|
||||
trap=True)
|
||||
if code != 0:
|
||||
# Command failed. This is likely due to an improperly configured remote
|
||||
@@ -196,7 +191,43 @@ def get_passphrase(env):
|
||||
|
||||
return passphrase
|
||||
|
||||
def get_env(env):
|
||||
def get_duplicity_target_url(config):
|
||||
target = config["target"]
|
||||
|
||||
if get_target_type(config) == "s3":
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
target = list(urlsplit(target))
|
||||
|
||||
# Although we store the S3 hostname in the target URL,
|
||||
# duplicity no longer accepts it in the target URL. The hostname in
|
||||
# the target URL must be the bucket name. The hostname is passed
|
||||
# via get_duplicity_additional_args. Move the first part of the
|
||||
# path (the bucket name) into the hostname URL component, and leave
|
||||
# the rest for the path.
|
||||
target[1], target[2] = target[2].lstrip('/').split('/', 1)
|
||||
|
||||
target = urlunsplit(target)
|
||||
|
||||
return target
|
||||
|
||||
def get_duplicity_additional_args(env):
|
||||
config = get_backup_config(env)
|
||||
|
||||
if get_target_type(config) == 'rsync':
|
||||
return [
|
||||
"--ssh-options= -i /root/.ssh/id_rsa_miab",
|
||||
"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p 22 -i /root/.ssh/id_rsa_miab\"",
|
||||
]
|
||||
elif get_target_type(config) == 's3':
|
||||
# See note about hostname in get_duplicity_target_url.
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
target = urlsplit(config["target"])
|
||||
endpoint_url = urlunsplit(("https", target.netloc, '', '', ''))
|
||||
return ["--s3-endpoint-url", endpoint_url]
|
||||
|
||||
return []
|
||||
|
||||
def get_duplicity_env_vars(env):
|
||||
config = get_backup_config(env)
|
||||
|
||||
env = { "PASSPHRASE" : get_passphrase(env) }
|
||||
@@ -248,7 +279,7 @@ def perform_backup(full_backup):
|
||||
if quit:
|
||||
sys.exit(code)
|
||||
|
||||
service_command("php8.1-fpm", "stop", quit=True)
|
||||
service_command("php8.0-fpm", "stop", quit=True)
|
||||
service_command("postfix", "stop", quit=True)
|
||||
service_command("dovecot", "stop", quit=True)
|
||||
service_command("slapd", "stop", quit=True)
|
||||
@@ -276,17 +307,17 @@ def perform_backup(full_backup):
|
||||
"--volsize", "250",
|
||||
"--gpg-options", "--cipher-algo=AES256",
|
||||
env["STORAGE_ROOT"],
|
||||
config["target"],
|
||||
get_duplicity_target_url(config),
|
||||
"--allow-source-mismatch"
|
||||
] + rsync_ssh_options,
|
||||
get_env(env))
|
||||
] + get_duplicity_additional_args(env),
|
||||
get_duplicity_env_vars(env))
|
||||
finally:
|
||||
# Start services again.
|
||||
service_command("miabldap-capture", "start", quit=False)
|
||||
service_command("slapd", "start", quit=False)
|
||||
service_command("dovecot", "start", quit=False)
|
||||
service_command("postfix", "start", quit=False)
|
||||
service_command("php8.1-fpm", "start", quit=False)
|
||||
service_command("php8.0-fpm", "start", quit=False)
|
||||
|
||||
# Remove old backups. This deletes all backup data no longer needed
|
||||
# from more than 3 days ago.
|
||||
@@ -297,9 +328,9 @@ def perform_backup(full_backup):
|
||||
"--verbosity", "error",
|
||||
"--archive-dir", backup_cache_dir,
|
||||
"--force",
|
||||
config["target"]
|
||||
] + rsync_ssh_options,
|
||||
get_env(env))
|
||||
get_duplicity_target_url(config)
|
||||
] + get_duplicity_additional_args(env),
|
||||
get_duplicity_env_vars(env))
|
||||
|
||||
# From duplicity's manual:
|
||||
# "This should only be necessary after a duplicity session fails or is
|
||||
@@ -312,9 +343,9 @@ def perform_backup(full_backup):
|
||||
"--verbosity", "error",
|
||||
"--archive-dir", backup_cache_dir,
|
||||
"--force",
|
||||
config["target"]
|
||||
] + rsync_ssh_options,
|
||||
get_env(env))
|
||||
get_duplicity_target_url(config)
|
||||
] + get_duplicity_additional_args(env),
|
||||
get_duplicity_env_vars(env))
|
||||
|
||||
# Change ownership of backups to the user-data user, so that the after-bcakup
|
||||
# script can access them.
|
||||
@@ -350,9 +381,9 @@ def run_duplicity_verification():
|
||||
"--compare-data",
|
||||
"--archive-dir", backup_cache_dir,
|
||||
"--exclude", backup_root,
|
||||
config["target"],
|
||||
get_duplicity_target_url(config),
|
||||
env["STORAGE_ROOT"],
|
||||
] + rsync_ssh_options, get_env(env))
|
||||
] + get_duplicity_additional_args(env), get_duplicity_env_vars(env))
|
||||
|
||||
def run_duplicity_restore(args):
|
||||
env = load_environment()
|
||||
@@ -362,9 +393,9 @@ def run_duplicity_restore(args):
|
||||
"/usr/bin/duplicity",
|
||||
"restore",
|
||||
"--archive-dir", backup_cache_dir,
|
||||
config["target"],
|
||||
] + rsync_ssh_options + args,
|
||||
get_env(env))
|
||||
get_duplicity_target_url(config),
|
||||
] + get_duplicity_additional_args(env) + args,
|
||||
get_duplicity_env_vars(env))
|
||||
|
||||
def list_target_files(config):
|
||||
import urllib.parse
|
||||
@@ -421,7 +452,6 @@ def list_target_files(config):
|
||||
|
||||
elif target.scheme == "s3":
|
||||
# match to a Region
|
||||
fix_boto() # must call prior to importing boto
|
||||
import boto.s3
|
||||
from boto.exception import BotoServerError
|
||||
custom_region = False
|
||||
|
||||
@@ -122,7 +122,6 @@ def index():
|
||||
no_users_exist = (len(get_mail_users(env)) == 0)
|
||||
no_admins_exist = (len(get_admins(env)) == 0)
|
||||
|
||||
utils.fix_boto() # must call prior to importing boto
|
||||
import boto.s3
|
||||
backup_s3_hosts = [(r.name, r.endpoint) for r in boto.s3.regions()]
|
||||
|
||||
|
||||
@@ -58,36 +58,33 @@ def get_ssl_certificates(env):
|
||||
# Not a valid PEM format for a PEM type we care about.
|
||||
continue
|
||||
|
||||
# Remember where we got this object.
|
||||
pem._filename = fn
|
||||
|
||||
# Is it a private key?
|
||||
if isinstance(pem, RSAPrivateKey):
|
||||
private_keys[pem.public_key().public_numbers()] = pem
|
||||
private_keys[pem.public_key().public_numbers()] = { "filename": fn, "key": pem }
|
||||
|
||||
# Is it a certificate?
|
||||
if isinstance(pem, Certificate):
|
||||
certificates.append(pem)
|
||||
certificates.append({ "filename": fn, "cert": pem })
|
||||
|
||||
# Process the certificates.
|
||||
domains = { }
|
||||
for cert in certificates:
|
||||
# What domains is this certificate good for?
|
||||
cert_domains, primary_domain = get_certificate_domains(cert)
|
||||
cert._primary_domain = primary_domain
|
||||
cert_domains, primary_domain = get_certificate_domains(cert["cert"])
|
||||
cert["primary_domain"] = primary_domain
|
||||
|
||||
# Is there a private key file for this certificate?
|
||||
private_key = private_keys.get(cert.public_key().public_numbers())
|
||||
private_key = private_keys.get(cert["cert"].public_key().public_numbers())
|
||||
if not private_key:
|
||||
continue
|
||||
cert._private_key = private_key
|
||||
cert["private_key"] = private_key
|
||||
|
||||
# Add this cert to the list of certs usable for the domains.
|
||||
for domain in cert_domains:
|
||||
# The primary hostname can only use a certificate mapped
|
||||
# to the system private key.
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
if cert._private_key._filename != os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem'):
|
||||
if cert["private_key"]["filename"] != os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem'):
|
||||
continue
|
||||
|
||||
domains.setdefault(domain, []).append(cert)
|
||||
@@ -97,16 +94,16 @@ def get_ssl_certificates(env):
|
||||
now = datetime.datetime.utcnow()
|
||||
ret = { }
|
||||
for domain, cert_list in domains.items():
|
||||
#for c in cert_list: print(domain, c.not_valid_before, c.not_valid_after, "("+str(now)+")", c.issuer, c.subject, c._filename)
|
||||
#for c in cert_list: print(domain, c["cert"].not_valid_before, c["cert"].not_valid_after, "("+str(now)+")", c["cert"].issuer, c["cert"].subject, c._filename if hasattr(c,"_filename") else "")
|
||||
cert_list.sort(key = lambda cert : (
|
||||
# must be valid NOW
|
||||
cert.not_valid_before <= now <= cert.not_valid_after,
|
||||
cert["cert"].not_valid_before <= now <= cert["cert"].not_valid_after,
|
||||
|
||||
# prefer one that is not self-signed
|
||||
cert.issuer != cert.subject,
|
||||
cert["cert"].issuer != cert["cert"].subject,
|
||||
|
||||
# prefer one that is not our temporary ca
|
||||
"Temporary-Mail-In-A-Box-CA" not in "%s" % cert.issuer.rdns,
|
||||
"Temporary-Mail-In-A-Box-CA" not in "%s" % cert["cert"].issuer.rdns,
|
||||
|
||||
###########################################################
|
||||
# The above lines ensure that valid certificates are chosen
|
||||
@@ -116,7 +113,7 @@ def get_ssl_certificates(env):
|
||||
|
||||
# prefer one with the expiration furthest into the future so
|
||||
# that we can easily rotate to new certs as we get them
|
||||
cert.not_valid_after,
|
||||
cert["cert"].not_valid_after,
|
||||
|
||||
###########################################################
|
||||
# We always choose the certificate that is good for the
|
||||
@@ -131,15 +128,15 @@ def get_ssl_certificates(env):
|
||||
|
||||
# in case a certificate is installed in multiple paths,
|
||||
# prefer the... lexicographically last one?
|
||||
cert._filename,
|
||||
cert["filename"],
|
||||
|
||||
), reverse=True)
|
||||
cert = cert_list.pop(0)
|
||||
ret[domain] = {
|
||||
"private-key": cert._private_key._filename,
|
||||
"certificate": cert._filename,
|
||||
"primary-domain": cert._primary_domain,
|
||||
"certificate_object": cert,
|
||||
"private-key": cert["private_key"]["filename"],
|
||||
"certificate": cert["filename"],
|
||||
"primary-domain": cert["primary_domain"],
|
||||
"certificate_object": cert["cert"],
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
@@ -135,7 +135,7 @@ def check_service(i, service, env):
|
||||
|
||||
# IPv4 ok but IPv6 failed. Try the PRIVATE_IPV6 address to see if the service is bound to the interface.
|
||||
elif service["port"] != 53 and try_connect(env["PRIVATE_IPV6"]):
|
||||
output.print_error("%s is running (and available over IPv4 and the local IPv6 address), but it is not publicly accessible at %s:%d." % (service['name'], env['PUBLIC_IP'], service['port']))
|
||||
output.print_error("%s is running (and available over IPv4 and the local IPv6 address), but it is not publicly accessible at %s:%d." % (service['name'], env['PUBLIC_IPV6'], service['port']))
|
||||
else:
|
||||
output.print_error("%s is running and available over IPv4 but is not accessible over IPv6 at %s port %d." % (service['name'], env['PUBLIC_IPV6'], service['port']))
|
||||
|
||||
@@ -253,6 +253,18 @@ def check_free_disk_space(rounded_values, env, output):
|
||||
if rounded_values: disk_msg = "The disk has less than 15% free space."
|
||||
output.print_error(disk_msg)
|
||||
|
||||
# Check that there's only one duplicity cache. If there's more than one,
|
||||
# it's probably no longer in use, and we can recommend clearing the cache
|
||||
# to save space. The cache directory may not exist yet, which is OK.
|
||||
backup_cache_path = os.path.join(env['STORAGE_ROOT'], 'backup/cache')
|
||||
try:
|
||||
backup_cache_count = len(os.listdir(backup_cache_path))
|
||||
except:
|
||||
backup_cache_count = 0
|
||||
if backup_cache_count > 1:
|
||||
output.print_warning("The backup cache directory {} has more than one backup target cache. Consider clearing this directory to save disk space."
|
||||
.format(backup_cache_path))
|
||||
|
||||
def check_free_memory(rounded_values, env, output):
|
||||
# Check free memory.
|
||||
percent_free = 100 - psutil.virtual_memory().percent
|
||||
@@ -658,7 +670,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
|
||||
if len(ds) > 0:
|
||||
output.print_line("")
|
||||
output.print_line("The DS record is currently set to:")
|
||||
for rr in ds:
|
||||
for rr in sorted(ds):
|
||||
output.print_line("Key Tag: {0}, Algorithm: {1}, Digest Type: {2}, Digest: {3}".format(*rr))
|
||||
|
||||
def check_mail_domain(domain, env, output):
|
||||
|
||||
@@ -193,14 +193,6 @@ def wait_for_service(port, public, env, timeout):
|
||||
return False
|
||||
time.sleep(min(timeout/4, 1))
|
||||
|
||||
def fix_boto():
|
||||
# Google Compute Engine instances install some Python-2-only boto plugins that
|
||||
# conflict with boto running under Python 3. Disable boto's default configuration
|
||||
# file prior to importing boto so that GCE's plugin is not loaded:
|
||||
import os
|
||||
os.environ["BOTO_CONFIG"] = "/etc/boto3.cfg"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from web_update import get_web_domains
|
||||
env = load_environment()
|
||||
|
||||
Reference in New Issue
Block a user