It allows you to add any* script to any VM (including DispVMs) and have it executed at every start. The script can be plaintext or base64-encoded, like in AWS EC2 user-data.
On my dom0, the bash command-line length limit is:
[user@dom0 ~]$ getconf ARG_MAX
2097152
… which is exactly 2 Mib. You have to deduct another 20-30 bytes for the rest of the command-line, but all in all, that is AMPLE space for your startup script!
Edit: there may be other, more stringent, restrictions for the qvm-features command
Yeah, well … I was thinking about the command-line used by qvm-features to store the script (see the git repo) : qvm-features QUBENAME vm-config.user-data ONELINER/BASE64ENCODEDSCRIPT
And the base64 encoded script is not compressible while staying command-line friendly.
I changed the ExecStart line to run the following python script instead. The initial idea was to be able to provide a python script. So my solution is the following:
if it’s a clear text, run it directly (I have not properly tested this with complex oneliner!)
if it’s encoded, save it in a file and run the file, any supported script with a correct shebang should work
For this to happen, one need:
to set the feature vm-config.scripts with a list of “scripts” names, separated by spaces
to set a oneliner or a base64 encoded script for each provided name as a feature named vm-config.scripts.<NAME> where <NAME> is equal to the name in vm-config.scripts.
#!/usr/bin/env python3
import base64
import binascii
import subprocess
import shlex
from pathlib import Path
import qubesdb
FEATURE_NAME = '/vm-config/scripts'
SCRIPTS_ROOT = Path('/tmp/vm-config-script')
db = qubesdb.QubesDB()
def run_file(name: str, content: bytes):
path = SCRIPTS_ROOT / name
SCRIPTS_ROOT.mkdir(exist_ok=True)
with open(path, 'wb') as script_file:
script_file.write(content)
path.chmod(0o700)
subprocess.call(path)
def run_oneliner(content: bytes):
subprocess.run(shlex.split(content.decode()))
def run_script(name: str):
feature_name = f'{FEATURE_NAME}.{name}'
content = db.read(feature_name)
if content is None:
print(f"Impossible to read {feature_name}")
return
# The script content might be encoded in base64
decoded = base64.b64decode(content)
if base64.b64encode(decoded) == content:
run_file(name, base64.decodebytes(content))
else:
run_oneliner(content)
def main():
try:
raw_scripts = db.read(FEATURE_NAME).decode()
except AttributeError:
return
for script_name in raw_scripts.split():
print(f"Run script {script_name}")
run_script(script_name)
if __name__ == '__main__':
main()