#!/usr/bin/env bash set +x set -Eeuo pipefail umask 077 HUB='https://hub.netmarga.com' CORE_RELEASE='0.5.53' APP='/opt/isp-core' STATE='/var/lib/isp-core' PHASE='preflight' trap 'code=$?; printf "\nCore installer failed during %s (line %s, exit %s). See /var/log/netmarga/core-installer.log and rerun the installer.\n" "$PHASE" "$LINENO" "$code" >&2; exit "$code"' ERR fail() { if [[ ${CORE_QUIET:-0} == 1 ]]; then printf '\nInstallation stopped: %s\n' "$*" >&4; else printf '\nInstallation stopped: %s\n' "$*" >&2; fi; exit 1; } [[ "$EUID" -eq 0 ]] || fail 'Run with sudo or as root.' printf 'Netmarga Core %s installer.\n' "$CORE_RELEASE" [[ ! -f /etc/isp-platform/hub.env ]] || fail 'Run this on a customer server, not on the Hub.' . /etc/os-release case "$ID:$VERSION_ID" in ubuntu:22.04|ubuntu:24.04|ubuntu:26.04|debian:12|debian:13) ;; *) fail 'Use Ubuntu 22.04/24.04/26.04 or Debian 12/13.';; esac [[ -d /run/systemd/system ]] || fail 'A running systemd server or VM is required.' if [[ -n "${ISP_CORE_INSTALL_FD:-}" ]]; then [[ "$ISP_CORE_INSTALL_FD" =~ ^[0-9]+$ && "/proc/$$/fd/$ISP_CORE_INSTALL_FD" -ef /run/lock/isp-core-install.lock ]] || fail 'Invalid inherited update lock.' exec 9>&"$ISP_CORE_INSTALL_FD" else exec 9>/run/lock/isp-core-install.lock fi flock -n 9 || fail 'Another installer is running.' exec 4>&2 for target in "$APP" "$STATE" "$APP/INSTALL-IN-PROGRESS" "$APP/INSTALLER-MANAGED" "$STATE/panel-config.json" "$STATE/panel-access.txt" "$STATE/panel.key" "$STATE/panel.crt" "$STATE/credentials.json" /etc/systemd/system/isp-core.service /etc/systemd/system/isp-core-panel.service; do [[ ! -L "$target" ]] || fail "Refusing a symbolic link at $target." done if [[ -e "$APP" && ! -f "$APP/INSTALLER-MANAGED" && ! -f "$APP/INSTALL-IN-PROGRESS" && -n "$(ls -A -- "$APP")" ]]; then fail 'An unmanaged Core directory already exists.'; fi if [[ -e /etc/systemd/system/isp-core-panel.service && ! -f "$APP/INSTALLER-MANAGED" && ! -f "$APP/INSTALL-IN-PROGRESS" ]]; then fail 'An unmanaged Core panel service already exists.'; fi FRESH_INSTALL=1 if [[ -f "$APP/INSTALLER-MANAGED" ]]; then FRESH_INSTALL=0 INSTALLED_RELEASE=$(cat "$APP/INSTALLER-MANAGED") [[ "$INSTALLED_RELEASE" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail 'Invalid existing Core release marker.' if [[ "$(printf '%s\n%s\n' "$INSTALLED_RELEASE" "$CORE_RELEASE" | sort -V | tail -n 1)" != "$CORE_RELEASE" ]]; then fail "Core $INSTALLED_RELEASE is newer than this installer ($CORE_RELEASE). Download the current installer from the Hub." fi printf 'Updating Core from %s to %s. Existing panel usernames and passwords will be preserved.\n' "$INSTALLED_RELEASE" "$CORE_RELEASE" else printf 'Installing Core %s for the first time.\n' "$CORE_RELEASE" fi if [[ ! -f "$APP/INSTALLER-MANAGED" && ! -f "$STATE/bootstrap-state.json" ]]; then export NETMARGA_COMPANY="${NETMARGA_COMPANY:-$(hostname)}" export NETMARGA_CONTACT="${NETMARGA_CONTACT:-Administrator}" export NETMARGA_EMAIL="${NETMARGA_EMAIL:-admin@setup.invalid}" fi install -d -m 0700 /var/log/netmarga exec 3>&1 4>&2 CORE_QUIET=1 exec >>/var/log/netmarga/core-installer.log 2>&1 trap 'code=$?; printf "\nCore installer failed during %s (line %s, exit %s). See /var/log/netmarga/core-installer.log.\n" "$PHASE" "$LINENO" "$code" >&4; exit "$code"' ERR printf 'Installation is in progress. Please wait...\n' >&3 PHASE='operating-system prerequisites' printf 'NOC applications use a separate module installer; no NOC runtime is installed by this script.\n' printf 'Installing only Core prerequisites. Optional applications are installed from the web panel.\n' # Country mirrors can be unreachable on otherwise healthy IPv4-only servers. if [[ "$ID" == ubuntu ]] && command -v curl >/dev/null; then for source in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do [[ -f "$source" ]] || continue mirror=$(sed -nE 's@.*https?://([a-z]{2}\.archive\.ubuntu\.com)/ubuntu.*@\1@p' "$source" | head -n 1) [[ -n "$mirror" ]] || continue if ! curl -4 -fsS --connect-timeout 5 --max-time 10 "http://$mirror/ubuntu/dists/$VERSION_CODENAME/InRelease" -o /dev/null; then curl -4 -fsS --connect-timeout 5 --max-time 15 "http://archive.ubuntu.com/ubuntu/dists/$VERSION_CODENAME/InRelease" -o /dev/null || fail 'Ubuntu package mirrors are unreachable.' printf 'Country package mirror unavailable; using archive.ubuntu.com (IPv4).\n' [[ -e "$source.netmarga.save" ]] || cp -p -- "$source" "$source.netmarga.save" sed -i "s/$mirror/archive.ubuntu.com/g" "$source" fi done fi if ! command -v python3 >/dev/null || ! command -v curl >/dev/null || ! command -v openssl >/dev/null || ! command -v wg >/dev/null || ! command -v sshd >/dev/null || ! command -v sudo >/dev/null || [[ ! -f /etc/ssl/certs/ca-certificates.crt ]]; then apt-get -o Acquire::ForceIPv4=true -o Acquire::Retries=2 -o Acquire::http::Timeout=20 update -qq DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::ForceIPv4=true -o Acquire::Retries=2 -o Acquire::http::Timeout=20 install -y -qq --no-install-recommends python3 curl openssl ca-certificates wireguard-tools openssh-server sudo fi if ! python3 -c 'import cryptography.fernet, yaml' 2>/dev/null; then apt-get -o Acquire::ForceIPv4=true -o Acquire::Retries=2 -o Acquire::http::Timeout=20 update -qq DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::ForceIPv4=true -o Acquire::Retries=2 -o Acquire::http::Timeout=20 install -y -qq --no-install-recommends python3-cryptography python3-yaml fi python3 -c 'import sys; assert sys.version_info >= (3,10)' WORK=$(mktemp -d /tmp/isp-core-setup.XXXXXXXX) trap 'rm -rf -- "$WORK"' EXIT PHASE='network and storage checks' curl --proto '=https' --tlsv1.2 --connect-timeout 10 --max-time 20 -fsS "$HUB/install-core.sh" -o /dev/null || fail 'Hub HTTPS endpoint is unreachable.' if [[ "${NETMARGA_EXPAND_ROOT:-1}" == 1 ]]; then python3 - <<'CORE_STORAGE_PY' #!/usr/bin/env python3 """Grow a dedicated, supported root disk only; --check never changes storage.""" import json import os from pathlib import Path import shutil import subprocess import sys import time CHECK = '--check' in sys.argv def run(*args): return subprocess.check_output(args, text=True).strip() def report(tool, field, columns): return json.loads(run(tool, '--reportformat', 'json', '-o', columns))['report'][0][field] def nodes(items): for item in items: yield item yield from nodes(item.get('children', [])) def ensure(tool, package): if shutil.which(tool): return subprocess.run(['apt-get', '-o', 'Acquire::ForceIPv4=true', '-o', 'Acquire::Retries=2', '-o', 'Acquire::http::Timeout=20', 'update', '-qq'], check=True) subprocess.run(['apt-get', '-o', 'Acquire::ForceIPv4=true', '-o', 'Acquire::Retries=2', '-o', 'Acquire::http::Timeout=20', 'install', '-y', '-qq', '--no-install-recommends', package], check=True, env={**os.environ, 'DEBIAN_FRONTEND': 'noninteractive'}) def main(): mount = json.loads(run('findmnt', '--json', '-o', 'SOURCE,FSTYPE', '/'))['filesystems'][0] root = os.path.realpath(mount['source']) fs = mount['fstype'] print('Root storage:', root, fs, flush=True) if fs not in ('ext4', 'xfs'): print('Automatic expansion skipped: root filesystem is not ext4 or XFS.'); return inventory = list(nodes(json.loads(run('lsblk', '--json', '--bytes', '-o', 'PATH,KNAME,TYPE,SIZE,PKNAME'))['blockdevices'])) devices = {os.path.realpath(n['path']): n for n in inventory} device = devices.get(root) vg = None if device and device['type'] == 'lvm': volumes = report('lvs', 'lv', 'lv_path,vg_name,segtype') match = [v for v in volumes if os.path.realpath(v['lv_path'].strip()) == root] if len(match) != 1 or match[0]['segtype'].strip() != 'linear': print('Automatic expansion skipped: complex LVM layout.'); return lv_path = match[0]['lv_path'].strip() vg = match[0]['vg_name'].strip() peers = [v for v in volumes if v['vg_name'].strip() == vg] pvs = [p for p in report('pvs', 'pv', 'pv_name,vg_name') if p['vg_name'].strip() == vg] if len(peers) != 1 or len(pvs) != 1: print('Automatic expansion skipped: shared or multi-disk volume group.'); return device = devices.get(os.path.realpath(pvs[0]['pv_name'].strip())) if not device or device['type'] not in ('part', 'disk'): print('Automatic expansion skipped: encrypted, RAID or unsupported storage.'); return disk = device number = None if device['type'] == 'part': parent = device.get('pkname') disk = next((n for n in inventory if n['kname'] == parent), None) if not disk or disk['type'] != 'disk': print('Automatic expansion skipped: partition is not on a plain disk.'); return base = Path('/sys/class/block') / device['kname'] number = (base / 'partition').read_text().strip() start = int((base / 'start').read_text()) * 512 siblings = [n for n in inventory if n['type'] == 'part' and n.get('pkname') == parent] if any(int((Path('/sys/class/block') / n['kname'] / 'start').read_text()) * 512 > start for n in siblings): print('Automatic expansion skipped: root partition is not last on disk.'); return room = int(disk['size']) - start - int(device['size']) else: room = 0 print('Disk size: %.1f GiB; trailing unused space: %.1f GiB.' % (int(disk['size']) / 2**30, max(room, 0) / 2**30), flush=True) if CHECK: if vg: print(run('vgs', '--units', 'g', '-o', 'vg_name,vg_size,vg_free', vg)) print('Supported root layout; check only, no changes.'); return ensure('resize2fs' if fs == 'ext4' else 'xfs_growfs', 'e2fsprogs' if fs == 'ext4' else 'xfsprogs') backup = Path('/var/backups/isp-core/storage') / str(time.time_ns()) backup.mkdir(parents=True, mode=0o700) if vg: subprocess.run(['vgcfgbackup', '-f', str(backup / 'lvm.conf'), vg], check=True) if number and room > 16 * 2**20: ensure('growpart', 'cloud-guest-utils') ensure('sfdisk', 'fdisk') (backup / 'partition-table.txt').write_text(run('sfdisk', '--dump', disk['path'])) subprocess.run(['growpart', '--dry-run', disk['path'], number], check=True) subprocess.run(['growpart', disk['path'], number], check=True) if vg: subprocess.run(['pvresize', device['path']], check=True) free = float(run('vgs', '--noheadings', '--units', 'b', '--nosuffix', '-o', 'vg_free', vg)) if free >= 4 * 2**20: subprocess.run(['lvextend', '-l', '+100%FREE', lv_path], check=True) if fs == 'ext4': ensure('resize2fs', 'e2fsprogs') subprocess.run(['resize2fs', root], check=True) else: ensure('xfs_growfs', 'xfsprogs') subprocess.run(['xfs_growfs', '/'], check=True) print(run('df', '-h', '/'), flush=True) if __name__ == '__main__': main() CORE_STORAGE_PY else printf 'Automatic root expansion disabled by NETMARGA_EXPAND_ROOT.\n' fi available=$(df -Pk / | awk 'NR==2 {print $4}') [[ "$available" -ge 524288 ]] || fail 'At least 512 MiB free root space is required for Core.' printf 'Server resources: '; nproc; free -h; df -h / if [[ "$(timedatectl show -p NTPSynchronized --value 2>/dev/null || true)" != yes ]]; then printf 'Notice: system clock is not yet synchronized. Check your existing NTP service.\n' fi PHASE='verified Core downloads' SOURCE_DIR='' if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]:-}" ]]; then SOURCE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) fi fetch() { if [[ -f "$SOURCE_DIR/$1" && "$SOURCE_DIR" != "$APP" ]]; then cp -- "$SOURCE_DIR/$1" "$WORK/$1" else curl --proto '=https' --tlsv1.2 --connect-timeout 15 --max-time 90 --retry 2 -fsS "$HUB/downloads/core/$CORE_RELEASE/$1" -o "$WORK/$1" fi } for name in core_health.py core_backup.py core_backup_worker.py core_setup.py core_agent.py core_panel.py core_vpn_bootstrap.py librenms_module.py librenms_install.py librenms_proxy.py module_install.py radius_install.py radius_sync.py install-librenms.sh install-traffic.sh crm.py core_accounts.py core_roles.py core_ui.py core_settings.py core_update.py business_modules.py noc_orchestrator.py oxidized_profiles.py subscriber_portal.py isp-core.service isp-core-panel.service isp-core-librenms-install.service isp-core-librenms-install.timer isp-core-module-install.service isp-core-module-install.timer; do fetch "$name"; done printf '%s %s\n' '0f4bde02ec3626ef4d2e37cc70a85e3d4455e49a99677d2826193cb433abc701' "$WORK/core_health.py" '18428bc74b31a85fbee6ef3c6ec2823482dde0be78f35edaeb854c6ada6861ed' "$WORK/core_backup.py" 'c6f4d93d986b54091089c53b4a926d5394100d817fa57c3612ad4f8ee44dd397' "$WORK/core_backup_worker.py" 'd433b8cdeea3d8cafe13b84b31778a82769cc3c0b4f84a1420b6c5e2dc05f606' "$WORK/core_setup.py" 'c6f28402e0441ff53b85c9e1721f7163fccbe60506489d1e8675b8eadd201c91' "$WORK/oxidized_profiles.py" '57e7f70b0860f56d8d6b24cff0f5c1b830657eb6cf21935b513df4a6d3a6d2f5' "$WORK/business_modules.py" '86a6b5a7eadb9726809a548d0b5ca3d2fc223d4d8d17c46fdb4ec485eb12bd17' "$WORK/noc_orchestrator.py" 'ebb96378e5f0ab001001268c0d6f60a5d45d5f07c515552618c419218452ee81' "$WORK/subscriber_portal.py" 'a669671ff9bdc2271711dc4dc1c5f627313446fd7b977f6b1be825958d4e7761' "$WORK/core_update.py" '948863072a831849568f93961f0e7a17f12802b0a01263e2a614a99a139a96fd' "$WORK/core_accounts.py" 'cef99e810ca828c7d7d87cd7f211af8f67797078374db0ea56f7a84ec0df3c49' "$WORK/core_ui.py" 'c4a44f2c562562bc84d4cd7cb365e3fab150440ddf5f34c9fa2e32aa48f325ac' "$WORK/core_settings.py" 'b1d140411c01d2597047f7b5335d0c5682c258c27803fc86cfcfac594203cbe1' "$WORK/crm.py" '05407aa0349e445f24364e06d393bac267cfe9cdd29f8f5bc24c37c77c2f154f' "$WORK/core_vpn_bootstrap.py" 'b8ff9ff11f4faaa41e7abb13ed17eb5186029049627b25dae8c67295e7a3ce20' "$WORK/librenms_module.py" 'f392c63a21381090adad8b23ad3f763120c3454fa64c184e82ddea13544e5a6e' "$WORK/librenms_install.py" 'b9a3a316a6a1c38ce1d181e07470f0ae74e671a1983c8a5f4c034090e5fd94fc' "$WORK/librenms_proxy.py" 'd8827dd30ff875654d7649351359e0045c95e95c9ab29a79e75334697fb7c0bb' "$WORK/module_install.py" 'f5548b76ee2da2596566ab3587237ffa1d7f73567ecb16896d161a7128e31952' "$WORK/radius_install.py" 'cca932498dddfa64a0343eaf673820fe5d78ecd50dde8874bb5c22cca3969c4b' "$WORK/radius_sync.py" '6e2e75fbff47dfca70e15a94b7cd1efde45ba11ce295836ab6a25394f674d6a5' "$WORK/install-librenms.sh" '152d9be8fbbdb526fdd6c2a31fdb822010c2c811426bce707faef51b5b8ea08e' "$WORK/install-traffic.sh" '74511e16222f679d937e883815cfc323f7b406db27fc0c0e7cc988eb30aa21f5' "$WORK/core_agent.py" 'd4b53c3369e63ebbd17aed18776691c08faedabef127ac427a40afcc14cf1d2f' "$WORK/core_panel.py" 'e82025eba257a447ec3ebd80f83b66fb9f80e8a390ccf5110d728e142a93151b' "$WORK/isp-core.service" 'b96c943cd7e66a1e49211281c15f16beff5beae5aaa22ea6e077f6c06fe03b07' "$WORK/isp-core-panel.service" '4f3cf8166d0ad6625285b23812b02290f4c9f71026af526fe8b590bc42706369' "$WORK/isp-core-librenms-install.service" '207cf28c423cf93aacb9aecedfc6267ff48f94c8a2a7eae02379deca1ac4b149' "$WORK/isp-core-librenms-install.timer" '1410331ee332744601c3d2d6bfd0d227e8229dd254ff66a44ef074dc14e1bda7' "$WORK/isp-core-module-install.service" '9f6c2f82aebabdee3606f7e6d69e991ac0ec99725882c5b98e1fd7597b2f6eff' "$WORK/isp-core-module-install.timer" | sha256sum --check --status || fail 'Package checksum verification failed. Download a fresh package.' if getent passwd isp-core >/dev/null; then [[ "$(getent passwd isp-core | cut -d: -f6)" == "$STATE" ]] || fail 'An unrelated isp-core account exists.' else useradd --system --user-group --home-dir "$STATE" --shell /usr/sbin/nologin isp-core fi install -d -m 0755 -o root -g root "$APP" install -d -m 0700 -o isp-core -g isp-core "$STATE" [[ -f "$APP/INSTALLER-MANAGED" ]] || touch "$APP/INSTALL-IN-PROGRESS" if [[ ! -f "$APP/INSTALLER-MANAGED" ]]; then PHASE='Hub VPN enrollment' python3 "$WORK/core_vpn_bootstrap.py" || fail 'Hub VPN enrollment did not complete. Rerun this installer after resolving the error.' else systemctl is-active --quiet wg-quick@wg-hub || fail 'Existing Hub VPN is not active; update stopped.' getent passwd hub-ops >/dev/null || fail 'Hub management account is missing; update stopped.' fi PHASE='panel accounts and services' EXISTING_PANEL=0 [[ ! -f "$APP/core_panel.py" ]] || EXISTING_PANEL=1 IDENTITY_RESULT=$(python3 "$WORK/core_accounts.py" "$STATE" "$EXISTING_PANEL") || fail 'Existing login state could not be preserved. See the error above.' if [[ ! -f "$STATE/panel.crt" || ! -f "$STATE/panel.key" ]]; then python3 - "$WORK/tls.conf" <<'PY' import sys,subprocess,ipaddress ips=['127.0.0.1'] for value in subprocess.check_output(['hostname','-I'],text=True).split(): try: ipaddress.ip_address(value) if value not in ips: ips.append(value) except ValueError: pass config='[req]\ndistinguished_name=dn\nx509_extensions=ext\nprompt=no\n[dn]\nCN=ISP Core\n[ext]\nsubjectAltName=@names\n[names]\nDNS.1=localhost\n' config+=''.join('IP.'+str(i+1)+'='+ip+'\n' for i,ip in enumerate(ips)) open(sys.argv[1],'w').write(config) PY openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 365 -config "$WORK/tls.conf" -keyout "$STATE/panel.key" -out "$STATE/panel.crt" 2>/dev/null fi for name in first-setup.json setup-link-token panel-config.json panel-access.txt panel.key panel.crt accounts.json accounts-initialized; do [[ ! -L "$STATE/$name" ]] || fail 'Refusing a linked panel credential file.' [[ -e "$STATE/$name" ]] || continue chown isp-core:isp-core "$STATE/$name" chmod 0600 "$STATE/$name" done for name in core_health.py core_backup.py core_backup_worker.py core_setup.py core_agent.py core_panel.py core_vpn_bootstrap.py librenms_module.py librenms_install.py librenms_proxy.py module_install.py radius_install.py radius_sync.py crm.py core_accounts.py core_roles.py core_ui.py core_settings.py core_update.py business_modules.py noc_orchestrator.py oxidized_profiles.py subscriber_portal.py; do if [[ -f "$APP/$name" ]]; then install -m 0644 "$APP/$name" "$APP/$name.previous"; fi install -m 0644 -o root -g root "$WORK/$name" "$APP/$name" done install -m 0755 -o root -g root "$WORK/install-librenms.sh" "$APP/install-librenms.sh" install -m 0755 -o root -g root "$WORK/install-traffic.sh" "$APP/install-traffic.sh" for name in isp-core.service isp-core-panel.service isp-core-librenms-install.service isp-core-librenms-install.timer isp-core-module-install.service isp-core-module-install.timer; do install -m 0644 "$WORK/$name" "/etc/systemd/system/$name"; done systemctl daemon-reload systemctl enable isp-core.service isp-core-panel.service systemctl enable --now isp-core-librenms-install.timer systemctl enable --now isp-core-module-install.timer systemctl restart isp-core.service isp-core-panel.service systemctl is-active --quiet isp-core.service || fail 'Core did not start.' systemctl is-active --quiet isp-core-panel.service || fail 'Core panel did not start.' for attempt in 1 2 3 4 5; do curl -k --connect-timeout 2 --max-time 5 -fsS https://127.0.0.1:8443/ -o /dev/null && break [[ "$attempt" -lt 5 ]] || fail 'Core HTTPS panel health check failed; existing shell accounts remain available.' sleep 1 done cat > /etc/systemd/system/isp-core-update.service <<'UNIT' [Unit] Description=Apply administrator-requested Core updates After=network-online.target [Service] Type=oneshot ExecStart=/usr/bin/python3 /opt/isp-core/core_update.py TimeoutStartSec=1200 UMask=0077 UNIT cat > /etc/systemd/system/isp-core-update.timer <<'UNIT' [Unit] Description=Check for approved Core update requests [Timer] OnBootSec=30 OnUnitInactiveSec=15 AccuracySec=1 [Install] WantedBy=timers.target UNIT systemctl daemon-reload systemctl enable --now isp-core-update.timer cat > /etc/systemd/system/isp-core-health.service <<'UNIT' [Unit] Description=Netmarga server resource monitoring After=network.target [Service] Type=oneshot ExecStart=/usr/bin/python3 -S /opt/isp-core/core_health.py TimeoutStartSec=30 UMask=0077 Nice=15 IOSchedulingClass=idle UNIT cat > /etc/systemd/system/isp-core-health.timer <<'UNIT' [Unit] Description=Sample Netmarga Core CPU, memory and storage [Timer] OnBootSec=15 OnUnitInactiveSec=30 AccuracySec=1 [Install] WantedBy=timers.target UNIT systemctl daemon-reload systemctl enable --now isp-core-health.timer systemctl start --no-block isp-core-health.service cat > /etc/systemd/system/isp-core-backup-estimate.service <<'UNIT' [Unit] Description=Estimate Netmarga backup source size and staging capacity [Service] Type=oneshot ExecStart=/usr/bin/python3 -S /opt/isp-core/core_health.py --estimate TimeoutStartSec=1200 UMask=0077 Nice=15 IOSchedulingClass=idle UNIT cat > /etc/systemd/system/isp-core-backup-estimate.timer <<'UNIT' [Unit] Description=Refresh Netmarga backup capacity estimates [Timer] OnBootSec=60 OnUnitInactiveSec=900 [Install] WantedBy=timers.target UNIT systemctl daemon-reload systemctl enable --now isp-core-backup-estimate.timer systemctl start --no-block isp-core-backup-estimate.service cat > /etc/systemd/system/isp-core-backup.service <<'UNIT' [Unit] Description=Netmarga encrypted full and incremental backups After=network-online.target docker.service [Service] Type=oneshot ExecStart=/usr/bin/python3 -S /opt/isp-core/core_backup_worker.py TimeoutStartSec=infinity UMask=0077 Nice=10 IOSchedulingClass=idle [Install] WantedBy=multi-user.target UNIT cat > /etc/systemd/system/isp-core-backup.timer <<'UNIT' [Unit] Description=Check Netmarga backup schedule and administrator requests [Timer] OnBootSec=120 OnUnitInactiveSec=60 AccuracySec=10 [Install] WantedBy=timers.target UNIT systemctl daemon-reload systemctl enable --now isp-core-backup.timer cat > /etc/systemd/system/isp-core-ssl.service <<'UNIT' [Unit] Description=Install customer-approved Core SSL certificate [Service] Type=oneshot ExecStart=/usr/bin/python3 /opt/isp-core/core_setup.py TimeoutStartSec=1500 UMask=0077 UNIT cat > /etc/systemd/system/isp-core-ssl.timer <<'UNIT' [Unit] Description=Check approved Core SSL requests [Timer] OnBootSec=30 OnUnitInactiveSec=20 [Install] WantedBy=timers.target UNIT systemctl daemon-reload systemctl enable --now isp-core-ssl.timer if [[ "$FRESH_INSTALL" == 1 ]]; then PHASE='verified management access and login lockdown' PYTHONPATH="$APP" python3 -c 'from core_vpn_bootstrap import lock_previous_logins; lock_previous_logins()' fi PYTHONPATH="$APP" python3 -c 'from pathlib import Path; from business_modules import ensure_noc; ensure_noc(Path("/var/lib/isp-core"))' chown isp-core:isp-core "$STATE/business.sqlite3" printf '%s\n' "$CORE_RELEASE" > "$APP/INSTALLER-MANAGED" chmod 0644 "$APP/INSTALLER-MANAGED" rm -f -- "$APP/INSTALL-IN-PROGRESS" exec 1>&3 2>&4 PANEL_IP=$(hostname -I | awk '{print $1}') printf '\nInstallation completed.\nUsername: admin\n' if [[ -f "$STATE/first-setup.json" && -f "$STATE/setup-link-token" ]]; then SETUP_TOKEN=$(cat "$STATE/setup-link-token") printf 'Open this one-time link to set your password and organisation details:\nhttps://%s:8443/setup#setup=%s\n' "$PANEL_IP" "$SETUP_TOKEN" else printf 'Open https://%s:8443/ and sign in with your existing password.\n' "$PANEL_IP" fi