feature: bash completes the vpn-* commands, their options and the names they take.
Release / release (push) Successful in 12s

This commit is contained in:
slawek committed 2026-10-26 11:35:46 +01:00
1 parent 8cac21845a
commit 81b7f4cac6
12 files changed
+459

No files matched your search

+22
View File
@@ -1043,3 +1043,25 @@ HTTPS, a public name and the web service. Clients are managed like service accou
it renames it in the clients. The signing key does not rotate; replacing it invalidates every ID it renames it in the clients. The signing key does not rotate; replacing it invalidates every ID
token issued. Wrong client secrets lock the client like any account. The implicit and hybrid token issued. Wrong client secrets lock the client like any account. The implicit and hybrid
flows, dynamic client registration and public clients without a secret are not offered. flows, dynamic client registration and public clients without a secret are not offered.
## ADR-38: Bash completions are files per command in bash-completion's completions directory
Status: Accepted
Context: The `vpn-*` commands have many subcommands, options and names (users, groups, issuing
CAs, templates, settings) that are hard to remember. bash-completion's README recommends its
`completionsdir`, `/usr/share/bash-completion/completions` on Ubuntu, whose files it loads on the
first Tab of their command, and calls the eagerly loaded `/etc/bash_completion.d` not
recommended; it prefers the name `<command>.bash`, which noble's 2.11 and resolute's 2.16 both
look up.
Decision: The package installs `<command>.bash` for each `vpn-*` command into
`/usr/share/bash-completion/completions` and recommends `bash-completion`. The completions read
the names they offer from the commands themselves (`--json list`) or the package's Python
modules (the schema, the configuration, the PKI's templates and reasons), not from copies, and
send errors to `/dev/null`, so a user who may not read them gets the fixed words only.
Consequences: Completions follow the code without being edited for each new setting or name. A
Tab that offers names runs a command or Python, which takes a fraction of a second. The files
are not conffiles; an administrator overrides one in `/etc/bash_completion.d` or the user's own
completions directory.
+17
View File
@@ -355,6 +355,23 @@ indented, with mode 0600, and reminds that `vpn-router-setup` applies it. The st
writes the file the same way, and a lock keeps the two from overwriting each other's change. A writes the file the same way, and a lock keeps the two from overwriting each other's change. A
refused request exits with status 2, any other failure with 1. refused request exits with status 2, any other failure with 1.
## Bash completion
Bash completes the commands, options and arguments of `vpn-router-user`, `vpn-router-group`,
`vpn-router-token`, `vpn-config-edit`, `vpn-router-backup`, `vpn-router-ca` and
`vpn-router-caddy` with the Tab key. The package installs a file per command in
`/usr/share/bash-completion/completions`, which the `bash-completion` package, recommended by
`vpn-router`, loads on the first Tab after the command. For root, who can read them, the
completions also offer the users and groups of the directory, the settings and site-to-site
connections of `vpn-router.json` with `true` and `false` for a boolean setting, and the issuing CAs and
certificate templates; for other users only the fixed words.
```sh
sudo -i
vpn-config-edit set oidc.<Tab>
vpn-router-user passwd <Tab>
```
## Applying changes ## Applying changes
```sh ```sh
+13
View File
@@ -1,3 +1,16 @@
vpn-router (0.28.0-1) unstable; urgency=medium
feature: bash completes the vpn-* commands, their options and the names they take.
* Bash completes the commands, options and arguments of vpn-router-user,
vpn-router-group, vpn-router-token, vpn-config-edit, vpn-router-backup,
vpn-router-ca and vpn-router-caddy, from
/usr/share/bash-completion/completions; for root also the users,
groups, issuing CAs, templates, settings and site-to-site connections.
* The package recommends bash-completion.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 26 Oct 2026 16:00:00 +0100
vpn-router (0.27.0-1) unstable; urgency=medium vpn-router (0.27.0-1) unstable; urgency=medium
feature: the web service is an OpenID Provider, which signs the directory's users in to other systems, the clients being service accounts with redirect URIs. feature: the web service is an OpenID Provider, which signs the directory's users in to other systems, the clients being service accounts with redirect URIs.
+1
View File
@@ -39,6 +39,7 @@ Depends: ${misc:Depends},
iproute2, iproute2,
systemd-resolved, systemd-resolved,
zstd zstd
Recommends: bash-completion
Suggests: caddy, Suggests: caddy,
rclone, rclone,
proxmox-backup-client-static proxmox-backup-client-static
+1
View File
@@ -31,5 +31,6 @@ src/usr/lib/vpn-router/generate-config usr/lib/vpn-router/
src/usr/lib/vpn-router/vpnrouter_platforms/*.py usr/lib/vpn-router/vpnrouter_platforms/ src/usr/lib/vpn-router/vpnrouter_platforms/*.py usr/lib/vpn-router/vpnrouter_platforms/
src/usr/share/vpn-router/templates/* usr/share/vpn-router/templates/ src/usr/share/vpn-router/templates/* usr/share/vpn-router/templates/
src/usr/share/vpn-router/sql/* usr/share/vpn-router/sql/ src/usr/share/vpn-router/sql/* usr/share/vpn-router/sql/
src/usr/share/bash-completion/completions/*.bash usr/share/bash-completion/completions/
src/usr/lib/systemd/system/caddy.service.d/vpn-router.conf usr/lib/systemd/system/caddy.service.d/ src/usr/lib/systemd/system/caddy.service.d/vpn-router.conf usr/lib/systemd/system/caddy.service.d/
src/usr/share/doc/vpn-router/vpn-router.json.example usr/share/doc/vpn-router/ src/usr/share/doc/vpn-router/vpn-router.json.example usr/share/doc/vpn-router/
@@ -0,0 +1,59 @@
# bash completion for vpn-config-edit -*- shell-script -*-
# Python of the package that answers from its schema (vpnrouter.SCHEMA, vpnrouter.CONNECTION) and, read as root, the
# site-to-site connections of the configuration: settings prints SECTION.OPTION of every setting and
# site_to_site.NAME.OPTION of every connection, connections site_to_site.NAME, and values the values of a boolean
# setting, $1.
_vpn_config_edit_python()
{
python3 -c '
import sys
sys.path.insert(0, "/usr/lib/vpn-router")
import vpnrouter
try:
connections = vpnrouter.read_settings().get("site_to_site", {})
except (OSError, vpnrouter.ConfigError):
connections = {}
if sys.argv[1] == "settings":
print("\n".join(f"{section}.{option}" for section, options in vpnrouter.SCHEMA.items() for option in options))
print("\n".join(f"site_to_site.{name}.{option}" for name in connections for option in vpnrouter.CONNECTION))
elif sys.argv[1] == "connections":
print("\n".join(f"site_to_site.{name}" for name in connections))
else:
section, _, option = sys.argv[2].rpartition(".")
defaults = vpnrouter.CONNECTION if section.startswith("site_to_site.") else vpnrouter.SCHEMA.get(section, {})
if isinstance(defaults.get(option), bool):
print("true\nfalse")
' "$@" 2>/dev/null
}
_vpn_config_edit()
{
local cur prev words cword split
_init_completion -s || return
# The command is the first word that is not an option; the arguments are the words after it.
local i command='' args=()
for ((i = 1; i < cword; i++)); do
if [[ -z $command ]]; then
[[ ${words[i]} != -* ]] && command=${words[i]}
elif [[ ${words[i]} != -* ]]; then
args+=("${words[i]}")
fi
done
if [[ $cur == -* ]]; then
COMPREPLY=($(compgen -W '--help' -- "$cur"))
return
fi
case $command,${#args[@]} in
,*) COMPREPLY=($(compgen -W 'show set remove' -- "$cur")) ;;
set,0) COMPREPLY=($(compgen -W "$(_vpn_config_edit_python settings)" -- "$cur")) ;;
set,1) COMPREPLY=($(compgen -W "$(_vpn_config_edit_python values "${args[0]}")" -- "$cur")) ;;
remove,0) COMPREPLY=($(compgen -W "$(_vpn_config_edit_python connections)" -- "$cur")) ;;
esac
} &&
complete -F _vpn_config_edit vpn-config-edit
# ex: filetype=sh
@@ -0,0 +1,41 @@
# bash completion for vpn-router-backup -*- shell-script -*-
_vpn_router_backup()
{
local cur prev words cword split
_init_completion -s || return
# The command is the first word that is not an option; the arguments are the words after it.
local i command='' args=0 pbs=false
for ((i = 1; i < cword; i++)); do
if [[ -z $command ]]; then
[[ ${words[i]} != -* ]] && command=${words[i]}
elif [[ ${words[i]} == --pbs ]]; then
pbs=true
elif [[ ${words[i]} != -* ]]; then
((args++))
fi
done
if [[ -z $command ]]; then
if [[ $cur == -* ]]; then
COMPREPLY=($(compgen -W '--help' -- "$cur"))
else
COMPREPLY=($(compgen -W 'run list restore' -- "$cur"))
fi
return
fi
if [[ $cur == -* ]]; then
COMPREPLY=($(compgen -W "--help$([[ $command == restore ]] && echo ' --pbs --yes')" -- "$cur"))
return
fi
# restore takes a local archive, or with --pbs a snapshot name, which vpn-router-backup list shows.
if [[ $command == restore ]] && ((args == 0)) && ! $pbs; then
_filedir
fi
} &&
complete -F _vpn_router_backup vpn-router-backup
# ex: filetype=sh
@@ -0,0 +1,98 @@
# bash completion for vpn-router-ca -*- shell-script -*-
# Python of the package that prints the revocation reasons (reasons) or, from the configuration read as root, the
# issuing CAs (cas) or the certificate templates (templates), which other users do not get.
_vpn_router_ca_python()
{
python3 -c '
import sys
sys.path.insert(0, "/usr/lib/vpn-router")
import vpnrouter
import vpnrouter_pki
if sys.argv[1] == "reasons":
print("\n".join(vpnrouter_pki.REASONS))
else:
cfg = vpnrouter.load_config()
print("\n".join(vpnrouter.issuing_cas(cfg) if sys.argv[1] == "cas" else vpnrouter.pki_templates(cfg)))
' "$@" 2>/dev/null
}
_vpn_router_ca()
{
local cur prev words cword split
_init_completion -s || return
# The command and, for ca and root, the action are the first words that are not options; the arguments are the
# words after them but option values.
local i command='' action='' args=0
for ((i = 1; i < cword; i++)); do
case ${words[i]} in
--ca | --template | --csr | --subject | --san | --pfx | --reason | --out) ((i++)) ;;
-*) ;;
*)
if [[ -z $command ]]; then
command=${words[i]}
elif [[ $command == @(ca|root) && -z $action ]]; then
action=${words[i]}
else
((args++))
fi
;;
esac
done
case $prev in
--ca)
COMPREPLY=($(compgen -W "$(_vpn_router_ca_python cas)" -- "$cur"))
return
;;
--template)
COMPREPLY=($(compgen -W "$(_vpn_router_ca_python templates)" -- "$cur"))
return
;;
--reason)
COMPREPLY=($(compgen -W "$(_vpn_router_ca_python reasons)" -- "$cur"))
return
;;
--csr | --pfx | --out)
_filedir
return
;;
--subject | --san)
return
;;
esac
$split && return
if [[ -z $command ]]; then
if [[ $cur == -* ]]; then
COMPREPLY=($(compgen -W '--json --help' -- "$cur"))
else
COMPREPLY=($(compgen -W 'status list issue revoke maintain ca root' -- "$cur"))
fi
return
fi
if [[ $cur == -* ]]; then
local options='--help'
case $command$action in
list) options+=' --ca' ;;
issue) options+=' --ca --template --csr --subject --san --pfx' ;;
revoke) options+=' --reason' ;;
rootexport-key) options+=' --out' ;;
rootremove-key) options+=' --yes' ;;
esac
COMPREPLY=($(compgen -W "$options" -- "$cur"))
return
fi
case $command,$action,$args in
ca,,*) COMPREPLY=($(compgen -W 'csr import' -- "$cur")) ;;
root,,*) COMPREPLY=($(compgen -W 'export-key remove-key import-key import-crl' -- "$cur")) ;;
ca,*,0) COMPREPLY=($(compgen -W "$(_vpn_router_ca_python cas)" -- "$cur")) ;;
ca,import,1 | root,import-key,0 | root,import-crl,0) _filedir ;;
esac
} &&
complete -F _vpn_router_ca vpn-router-ca
# ex: filetype=sh
@@ -0,0 +1,24 @@
# bash completion for vpn-router-caddy -*- shell-script -*-
_vpn_router_caddy()
{
local cur prev words cword
_init_completion || return
# After the command only --help is left.
local word
for word in "${words[@]:1:cword-1}"; do
if [[ $word != -* ]]; then
[[ $cur == -* ]] && COMPREPLY=($(compgen -W '--help' -- "$cur"))
return
fi
done
if [[ $cur == -* ]]; then
COMPREPLY=($(compgen -W '--json --help' -- "$cur"))
else
COMPREPLY=($(compgen -W 'status apply use-upstream' -- "$cur"))
fi
} &&
complete -F _vpn_router_caddy vpn-router-caddy
# ex: filetype=sh
@@ -0,0 +1,74 @@
# bash completion for vpn-router-group -*- shell-script -*-
# The names of the users and of the groups of the router's directory, read as root; nothing for other users.
_vpn_router_group_users()
{
vpn-router-user --json list 2>/dev/null |
python3 -c 'import json, sys; print("\n".join(user["name"] for user in json.load(sys.stdin)["users"]))' 2>/dev/null
}
_vpn_router_group_names()
{
vpn-router-group --json list 2>/dev/null |
python3 -c 'import json, sys; print("\n".join(group["name"] for group in json.load(sys.stdin)["groups"]))' 2>/dev/null
}
_vpn_router_group()
{
local cur prev words cword split
_init_completion -s || return
# The command is the first word that is not an option; the arguments are the words after it but option values.
local i command='' args=0
for ((i = 1; i < cword; i++)); do
if [[ -z $command ]]; then
[[ ${words[i]} != -* ]] && command=${words[i]}
continue
fi
case ${words[i]} in
--member | --description | --name) ((i++)) ;;
-*) ;;
*) ((args++)) ;;
esac
done
if [[ -z $command ]]; then
if [[ $cur == -* ]]; then
COMPREPLY=($(compgen -W '--json --help' -- "$cur"))
else
COMPREPLY=($(compgen -W 'list add update remove member-add member-remove' -- "$cur"))
fi
return
fi
case $prev in
--member)
COMPREPLY=($(compgen -W "$(_vpn_router_group_users)" -- "$cur"))
return
;;
--description | --name)
return
;;
esac
$split && return
if [[ $cur == -* ]]; then
local options='--help'
case $command in
add) options+=' --member --description' ;;
update) options+=' --name --description' ;;
esac
COMPREPLY=($(compgen -W "$options" -- "$cur"))
return
fi
# The group, which add takes new, then the user of member-add and member-remove.
if ((args == 0)) && [[ $command != @(list|add) ]]; then
COMPREPLY=($(compgen -W "$(_vpn_router_group_names)" -- "$cur"))
elif ((args == 1)) && [[ $command == member-@(add|remove) ]]; then
COMPREPLY=($(compgen -W "$(_vpn_router_group_users)" -- "$cur"))
fi
} &&
complete -F _vpn_router_group vpn-router-group
# ex: filetype=sh
@@ -0,0 +1,25 @@
# bash completion for vpn-router-token -*- shell-script -*-
_vpn_router_token()
{
local cur prev words cword split
_init_completion -s || return
case $prev in
--minutes | --hours | --days)
return
;;
esac
$split && return
# The lifetime is one of --minutes, --hours and --days.
local options='--json --help' word
for word in "${words[@]:1:cword-1}"; do
[[ $word == --@(minutes|hours|days)* ]] && break
done
[[ $word == --@(minutes|hours|days)* ]] || options+=' --minutes --hours --days'
COMPREPLY=($(compgen -W "$options" -- "$cur"))
} &&
complete -F _vpn_router_token vpn-router-token
# ex: filetype=sh
@@ -0,0 +1,84 @@
# bash completion for vpn-router-user -*- shell-script -*-
# The names of the users and of the groups of the router's directory, read as root; nothing for other users.
_vpn_router_user_names()
{
vpn-router-user --json list 2>/dev/null |
python3 -c 'import json, sys; print("\n".join(user["name"] for user in json.load(sys.stdin)["users"]))' 2>/dev/null
}
_vpn_router_user_groups()
{
vpn-router-group --json list 2>/dev/null |
python3 -c 'import json, sys; print("\n".join(group["name"] for group in json.load(sys.stdin)["groups"]))' 2>/dev/null
}
_vpn_router_user()
{
local cur prev words cword split
_init_completion -s || return
local commands='list add update passwd disable enable unlock remove profile otp-new otp-enrol otp-remove console
passkeys-remove'
# The command is the first word that is not an option; the arguments are the words after it but option values.
local i command='' args=0
for ((i = 1; i < cword; i++)); do
if [[ -z $command ]]; then
[[ ${words[i]} != -* ]] && command=${words[i]}
continue
fi
case ${words[i]} in
--first-name | --last-name | --email | --display-name | --description | --redirect-uri | \
--post-logout-redirect-uri | --allowed-group | --account) ((i++)) ;;
-*) ;;
*) ((args++)) ;;
esac
done
if [[ -z $command ]]; then
if [[ $cur == -* ]]; then
COMPREPLY=($(compgen -W '--json --help' -- "$cur"))
else
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
fi
return
fi
case $prev in
--allowed-group)
COMPREPLY=($(compgen -W "$(_vpn_router_user_groups)" -- "$cur"))
return
;;
--account)
# The local accounts console --account takes: a uid from 1000 to 19999, not nobody.
COMPREPLY=($(compgen -W "$(getent passwd | awk -F: '$3 >= 1000 && $3 < 20000 && $1 != "nobody" { print $1 }')" -- "$cur"))
return
;;
--first-name | --last-name | --email | --display-name | --description | --redirect-uri | --post-logout-redirect-uri)
return
;;
esac
$split && return
if [[ $cur == -* ]]; then
local options='--help'
case $command in
add) options+=' --first-name --last-name --email --service --display-name --description --redirect-uri
--post-logout-redirect-uri --allowed-group' ;;
update) options+=' --first-name --last-name --email --display-name --description --redirect-uri
--post-logout-redirect-uri --allowed-group' ;;
console) options+=' --posix --account --off' ;;
esac
COMPREPLY=($(compgen -W "$options" -- "$cur"))
return
fi
# The user name, which add takes new; otp-enrol then takes a code.
if ((args == 0)) && [[ $command != @(list|add) ]]; then
COMPREPLY=($(compgen -W "$(_vpn_router_user_names)" -- "$cur"))
fi
} &&
complete -F _vpn_router_user vpn-router-user
# ex: filetype=sh