fix: an upgrade keeps the configuration and asks nothing.
Release / release (push) Successful in 18s

This commit is contained in:
slawek committed 2026-10-24 02:08:23 +02:00
1 parent 08693f7e68
commit cda33fc4c7
4 files changed
+81 -37

No files matched your search

+13
View File
@@ -1,3 +1,16 @@
vpn-router (0.5.8-1) unstable; urgency=medium
fix: an upgrade keeps the configuration and asks nothing.
* postinst passes on only the debconf answers that were given. The
defaults of questions never asked, such as platform none, no longer
replace settings of vpn-router.json.
* Without an answer that changes it, the configuration ucf is given is
vpn-router.json byte for byte, so an upgrade has no conflict to ask
about; settings the file leaves out keep their defaults when it is read.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sat, 24 Oct 2026 20:00:00 +0200
vpn-router (0.5.7-1) unstable; urgency=medium
feature: users have a first name, a last name and an e-mail address.
+30 -19
View File
@@ -2,28 +2,39 @@
set -e
. /usr/share/debconf/confmodule
# The answer to a question in ANSWER, or "" when the question has not been seen; the
# default debconf holds for an unseen question is not an answer.
answer() {
ANSWER=""
db_fget "$1" seen
if [ "$RET" = "true" ]; then
db_get "$1"
ANSWER="$RET"
fi
}
case "$1" in
configure)
# --- Read the installer's answers ---
db_get vpn-router/platform; VPN_ROUTER_PLATFORM="$RET"
db_get vpn-router/mode; VPN_ROUTER_MODE="$RET"
db_get vpn-router/int_addr; VPN_ROUTER_INT_ADDR="$RET"
db_get vpn-router/external_interface; VPN_ROUTER_EXTERNAL_INTERFACE="$RET"
db_get vpn-router/internal_interface; VPN_ROUTER_INTERNAL_INTERFACE="$RET"
db_get vpn-router/local_fqdn; VPN_ROUTER_LOCAL_FQDN="$RET"
db_get vpn-router/local_id_mode; VPN_ROUTER_LOCAL_ID_MODE="$RET"
db_get vpn-router/local_cidrs; VPN_ROUTER_LOCAL_CIDRS="$RET"
db_get vpn-router/int_gateway_ip; VPN_ROUTER_INT_GATEWAY_IP="$RET"
db_get vpn-router/remote_addrs; VPN_ROUTER_REMOTE_ADDRS="$RET"
db_get vpn-router/remote_id; VPN_ROUTER_REMOTE_ID="$RET"
db_get vpn-router/remote_cidrs; VPN_ROUTER_REMOTE_CIDRS="$RET"
db_get vpn-router/psk; VPN_ROUTER_PSK="$RET"
db_get vpn-router/p2s_enabled; VPN_ROUTER_P2S_ENABLED="$RET"
db_get vpn-router/p2s_address_pool; VPN_ROUTER_P2S_ADDRESS_POOL="$RET"
db_get vpn-router/p2s_ca_name; VPN_ROUTER_P2S_CA_NAME="$RET"
db_get vpn-router/wg_enabled; VPN_ROUTER_WG_ENABLED="$RET"
db_get vpn-router/wg_address; VPN_ROUTER_WG_ADDRESS="$RET"
db_get vpn-router/wg_listen_port; VPN_ROUTER_WG_LISTEN_PORT="$RET"
answer vpn-router/platform; VPN_ROUTER_PLATFORM="$ANSWER"
answer vpn-router/mode; VPN_ROUTER_MODE="$ANSWER"
answer vpn-router/int_addr; VPN_ROUTER_INT_ADDR="$ANSWER"
answer vpn-router/external_interface; VPN_ROUTER_EXTERNAL_INTERFACE="$ANSWER"
answer vpn-router/internal_interface; VPN_ROUTER_INTERNAL_INTERFACE="$ANSWER"
answer vpn-router/local_fqdn; VPN_ROUTER_LOCAL_FQDN="$ANSWER"
answer vpn-router/local_id_mode; VPN_ROUTER_LOCAL_ID_MODE="$ANSWER"
answer vpn-router/local_cidrs; VPN_ROUTER_LOCAL_CIDRS="$ANSWER"
answer vpn-router/int_gateway_ip; VPN_ROUTER_INT_GATEWAY_IP="$ANSWER"
answer vpn-router/remote_addrs; VPN_ROUTER_REMOTE_ADDRS="$ANSWER"
answer vpn-router/remote_id; VPN_ROUTER_REMOTE_ID="$ANSWER"
answer vpn-router/remote_cidrs; VPN_ROUTER_REMOTE_CIDRS="$ANSWER"
answer vpn-router/psk; VPN_ROUTER_PSK="$ANSWER"
answer vpn-router/p2s_enabled; VPN_ROUTER_P2S_ENABLED="$ANSWER"
answer vpn-router/p2s_address_pool; VPN_ROUTER_P2S_ADDRESS_POOL="$ANSWER"
answer vpn-router/p2s_ca_name; VPN_ROUTER_P2S_CA_NAME="$ANSWER"
answer vpn-router/wg_enabled; VPN_ROUTER_WG_ENABLED="$ANSWER"
answer vpn-router/wg_address; VPN_ROUTER_WG_ADDRESS="$ANSWER"
answer vpn-router/wg_listen_port; VPN_ROUTER_WG_LISTEN_PORT="$ANSWER"
export VPN_ROUTER_PLATFORM VPN_ROUTER_MODE VPN_ROUTER_INT_ADDR \
VPN_ROUTER_EXTERNAL_INTERFACE \
@@ -6,18 +6,15 @@ Called from postinst, which hands the result to ucf(1). ucf compares it with
this script never writes to /etc and never has to worry about destroying an
administrator's edits.
The candidate is built in three layers, each overriding the one before:
1. the shipped defaults
2. the current contents of /etc/vpn-router/vpn-router.json, if it exists
3. any debconf answer that was actually given
Layer 2 is what makes reconfiguration safe: a setting the administrator edited
by hand, and that debconf has nothing to say about, is carried through
unchanged, so ucf only sees a diff where something really changed. An empty
answer means "leave alone", which is why clearing the pre-shared key from the
debconf database after installation does not blank it on the next
dpkg-reconfigure.
The candidate is the current /etc/vpn-router/vpn-router.json as it is, or the
shipped defaults when there is none, with every debconf answer that was given
written over it. Without an answer that changes it, the candidate is the current
file byte for byte, so
an upgrade or a reconfiguration that changes nothing gives ucf nothing to ask;
defaults the file leaves out are applied by vpnrouter.load_config() when the
file is read. An empty answer means "leave alone", which is why clearing the
pre-shared key from the debconf database after installation does not blank it
on the next dpkg-reconfigure.
Answers arrive as VPN_ROUTER_* environment variables and are converted to the
type of the setting they fill: comma-separated lists become arrays, "true" and
@@ -62,8 +59,21 @@ def main():
return 2
output = sys.argv[1]
cfg = vpnrouter.load_config()
try:
current = vpnrouter.CONFIG_FILE.read_text(encoding='utf-8')
except FileNotFoundError:
current = None
if current is None:
cfg = vpnrouter.load_config()
else:
try:
cfg = json.loads(current)
except ValueError as exc:
vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: {exc}')
if not isinstance(cfg, dict):
vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: the top level is not an object')
answered = False
for variable, (section, option) in ANSWERS.items():
value = os.environ.get(variable)
if not value:
@@ -76,11 +86,19 @@ def main():
value = int(value) if value.isdigit() else value
elif isinstance(default, list):
value = [item.strip() for item in value.split(',') if item.strip()]
cfg[section][option] = value
if not isinstance(cfg.get(section, {}), dict):
vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: {section} is not an object')
if cfg.setdefault(section, {}).get(option) != value:
cfg[section][option] = value
answered = True
# Without a change the candidate is the current file byte for byte, as ucf compares checksums.
with open(output, 'w', encoding='utf-8') as handle:
json.dump(cfg, handle, indent=2)
handle.write('\n')
if current is not None and not answered:
handle.write(current)
else:
json.dump(cfg, handle, indent=2)
handle.write('\n')
os.chmod(output, 0o600)
return 0