This is part of a new weekly (or biweekly) installment of happenings on Discord, a community chat staffed by the developers and curated by its users.

cPanel migration recommendations

A feature was brought up (and implemented) to disallow username changes in WHMCS. WHMCS tracks accounts by username rather than an internal ID as with apnscp. Such protection is implemented in the 3.1 branch and will not be backported to 3.0, which sunsets in September.

In case you weren't following, customers can change their username and domain name in apnscp which will make WHMCS not know about it anymore. cpcmd config:set apnscp.config auth allow_username_change false will disable that now.

Injecting custom SPF records

apnscp ships with a very permissive SPF record (v=spf1 a ~all). Overriding it is a cinche with a surrogate. Create a file named email.php in /usr/local/apnscp/lib/modules/surrogates with the following lines, which overwrites the get_records() method used to fetch DNS records to the domain.

<?php declare(strict_types=1);

    use Opcenter\Dns\Record;

    class Email_Module_Surrogate extends Email_Module
    {
        public function provisioning_records(string $domain, string $subdomain = ''): array
        {
            $base = parent::provisioning_records($domain, $subdomain);
            // record to replace
            $spfRecord = new Record($domain, [
                'name' => $subdomain,
                'rr' => 'TXT',
                'parameter' => 'v=spf1 a:1.2.3.4'
            ]);

            foreach ($base as $record) {
                if (!$record->matches('rr', 'TXT')) {
                    continue;
                }

                if (0 !== strpos(ltrim($record['parameter'], '"'), 'v=spf1')) {
                    continue;
                }
                $record->merge($spfRecord);
            }

            return $base;
        }
    }
Drop it in surrogates/email.php, then cpcmd -d domain email:provisioning-records domain to confirm the TXT record is replaced. You could also add a new record,  $base[] = new Record($domain, [ ... ]) as well. A check + merge is used because we're replacing the stock SPF record

Note on surrogate vs provider

What's the difference between a surrogate and provider? A surrogate may alter a module's API changing method signatures or even add new methods. A provider may not add new methods but allows the exposed methods to behave differently.

This behavior loops back to the SOAP API (oddly enough, a frequently contentious topic!) that builds the WSDL on startup, which is the panel's API definitions. If a surrogate adds a method to a module, then that method is now part of the panel's API. Providers do not have this ability to augment the panel API.

DNS helper rewrite

scripts/change_dns.php is a bit more flexible now to assist in cPanel migrations. If apnscp is configured to use the same PowerDNS cluster as cPanel, then migration can occur in two parts, a dry run that migrates files over and preps DNS followed by an official move that changes the IP.

Usage: change_dns.php -d domain [--ttl=TTL] [--old=IP,IP2,...] [--new=IP]
-------------------------------------------------------------------------------
Reduce TTL for all IPs that match OLDIP: -d domain --old=OLDIP 
Reduce TTL on all IPs that match service definition: -d domain
Change service definition IP to new IP: -d domain --new=IP
Change all IPs that match OLDIP to NEWIP: -d domain --old=OLDIP --new=NEWIP
--ttl=TTL overrides recommended TTL (prep: 60/IP change: module default)

A sample migration would look like, using the cPanel migration helper:

ImportSite --format=cpanel --no-activate /mydomain.tar.gz
/usr/local/apnscp/bin/scripts/change_dns.php -d mydomain.com --old=CPANELIP
# wait 24 hours and pull an updated backup
ImportSite --format=cpanel --no-create /mydomain-updated.tar.gz
/usr/local/apnscp/bin/scripts/change_dns.php -d mydomain.com --old=CPANELIP --new="$(cpcmd -d mydomain.com dns:get-public-ip)"

The site is first created in apnscp. apnscp is conservative in provisioning - if a domain already exists in DNS it will not overwrite nor add a single record. change_dns.php thus performs the default TTL reduction on all A/AAAA records that match CPANELIP. A second pass is done this time skipping account creation and replacing files/users/data that has changed since the initial creation was made. Finally all records that match CPANELIP is updated with NEWIP. DNS TTL is reset to the module default (usually 12 hours). --ttl=TIME allows you to adjust the TTL applied.