572 lines
20 KiB
Python
572 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""Install or validate the fixed Truf host-agent deployment."""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
PROJECT = Path('/opt/truf')
|
|
DEPLOY = PROJECT / 'deploy/host-agent'
|
|
INSTALL_ROOT = Path('/usr/lib/truf-host-agent')
|
|
SYSTEMD = Path('/etc/systemd/system')
|
|
TMPFILES = Path('/etc/tmpfiles.d/truf-host-agent.conf')
|
|
ACTIVE = Path('/etc/truf/runtime')
|
|
WORKER_PACKAGES = Path('/etc/truf/worker-packages')
|
|
DEPLOYMENT_PROFILE = Path('/etc/truf/deployment-profile')
|
|
AGENT_SOCKET = Path('/run/truf/host-agent.sock')
|
|
RUNTIME_UID = RUNTIME_GID = 10001
|
|
RUNTIME_GROUP = 'truf-runtime'
|
|
MAX_COPY_BYTES = 4 * 1024 * 1024
|
|
MAX_COMPOSE_OUTPUT_BYTES = 4 * 1024 * 1024
|
|
UNITS = ('truf-host-agent.socket', 'truf-host-agent.service')
|
|
SCRIPTS = ('truf_host_agent.py', 'truf_host_agent_install.py')
|
|
FIXED_ENV = {
|
|
'PATH': '/usr/sbin:/usr/bin:/sbin:/bin',
|
|
'LANG': 'C',
|
|
'LC_ALL': 'C',
|
|
}
|
|
STANDALONE_PROFILE = {
|
|
'name': 'standalone-edge-v1',
|
|
'compose_files': ('compose.yaml', 'compose.edge.yaml'),
|
|
'runtime_network': None,
|
|
'runtime_ports': [{
|
|
'mode': 'host', 'target': 443, 'published': '443', 'protocol': 'tcp',
|
|
}],
|
|
'runtime_cpus': 2.0,
|
|
'runtime_mem_limit': str(6 * 1024 ** 3),
|
|
'edge_cap_add': ['NET_BIND_SERVICE'],
|
|
'data_volume': {'name': 'truf-docker_data'},
|
|
}
|
|
SHARED_HOST_PROFILE = {
|
|
'name': 'shared-host-edge-v1',
|
|
'compose_files': ('compose.yaml', 'compose.shared-host.yaml'),
|
|
'runtime_network': 'host',
|
|
'runtime_ports': None,
|
|
'runtime_cpus': 0.9,
|
|
'runtime_mem_limit': str(720 * 1024 ** 2),
|
|
'edge_cap_add': None,
|
|
'data_volume': {'name': 'truf-remote-server-data', 'external': True},
|
|
}
|
|
|
|
|
|
def _compose_config_command(profile):
|
|
return (
|
|
'/usr/bin/docker', 'compose', '--ansi', 'never', '--project-name',
|
|
'truf-docker', '--env-file', '/etc/truf-edge/edge.env',
|
|
'--project-directory', '/opt/truf',
|
|
*(item for name in profile['compose_files'] for item in (
|
|
'--file', '/opt/truf/' + name,
|
|
)),
|
|
'config', '--format', 'json',
|
|
)
|
|
|
|
|
|
COMPOSE_CONFIG_COMMAND = _compose_config_command(STANDALONE_PROFILE)
|
|
EXPECTED_RUNTIME_MOUNTS = (
|
|
('volume', 'data', '/data', False, None),
|
|
('bind', '/etc/truf/runtime', '/data/config', True, False),
|
|
('bind', '/etc/truf/worker-packages', '/data/worker-packages', True, False),
|
|
(
|
|
'bind', '/var/lib/truf/runtime-document-candidates',
|
|
'/data/runtime-document-candidates', False, False,
|
|
),
|
|
('bind', '/run/truf/host-agent.sock', '/run/truf/host-agent.sock', True, False),
|
|
(
|
|
'bind', '/var/lib/truf/host-agent/results',
|
|
'/data/host-agent-results', True, False,
|
|
),
|
|
('bind', '/run/truf-postgres', '/run/truf-postgres', False, False),
|
|
)
|
|
|
|
|
|
class InstallError(RuntimeError):
|
|
def __init__(self, category):
|
|
self.category = str(category)
|
|
super().__init__('host-agent deployment validation failed')
|
|
|
|
|
|
def _deployment_profile():
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(
|
|
DEPLOYMENT_PROFILE,
|
|
os.O_RDONLY | getattr(os, 'O_CLOEXEC', 0)
|
|
| getattr(os, 'O_NOFOLLOW', 0),
|
|
)
|
|
before = os.fstat(descriptor)
|
|
if (
|
|
not stat.S_ISREG(before.st_mode) or before.st_uid != 0
|
|
or before.st_gid != 0 or stat.S_IMODE(before.st_mode) != 0o444
|
|
or before.st_nlink != 1 or before.st_size > 64
|
|
):
|
|
raise InstallError('profile')
|
|
payload = os.read(descriptor, 65)
|
|
after = os.fstat(descriptor)
|
|
if (
|
|
len(payload) > 64 or before.st_dev != after.st_dev
|
|
or before.st_ino != after.st_ino or before.st_mode != after.st_mode
|
|
or before.st_uid != after.st_uid or before.st_gid != after.st_gid
|
|
or before.st_nlink != after.st_nlink or before.st_size != after.st_size
|
|
or before.st_mtime_ns != after.st_mtime_ns
|
|
):
|
|
raise InstallError('profile')
|
|
except FileNotFoundError:
|
|
return STANDALONE_PROFILE
|
|
except InstallError:
|
|
raise
|
|
except OSError:
|
|
raise InstallError('profile') from None
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
try:
|
|
name = payload.decode('ascii').strip()
|
|
except UnicodeDecodeError:
|
|
raise InstallError('profile') from None
|
|
profiles = {
|
|
STANDALONE_PROFILE['name']: STANDALONE_PROFILE,
|
|
SHARED_HOST_PROFILE['name']: SHARED_HOST_PROFILE,
|
|
}
|
|
if name not in profiles:
|
|
raise InstallError('profile')
|
|
return profiles[name]
|
|
|
|
|
|
def _run(command, timeout=120):
|
|
try:
|
|
subprocess.run(
|
|
tuple(command), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL, cwd='/', env=FIXED_ENV, shell=False,
|
|
timeout=timeout, check=True,
|
|
)
|
|
except Exception:
|
|
raise InstallError('command') from None
|
|
|
|
|
|
def _capture(command, timeout=120):
|
|
try:
|
|
result = subprocess.run(
|
|
tuple(command), stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL, cwd='/', env=FIXED_ENV, shell=False,
|
|
timeout=timeout, check=True,
|
|
)
|
|
if len(result.stdout) > MAX_COMPOSE_OUTPUT_BYTES:
|
|
raise InstallError('command')
|
|
return result.stdout
|
|
except InstallError:
|
|
raise
|
|
except Exception:
|
|
raise InstallError('command') from None
|
|
|
|
|
|
def _unit_active(unit):
|
|
if unit not in UNITS:
|
|
raise InstallError('command')
|
|
try:
|
|
result = subprocess.run(
|
|
('/usr/bin/systemctl', 'is-active', '--quiet', unit),
|
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL, cwd='/', env=FIXED_ENV, shell=False,
|
|
timeout=120, check=False,
|
|
)
|
|
except Exception:
|
|
raise InstallError('command') from None
|
|
if result.returncode not in (0, 3, 4):
|
|
raise InstallError('command')
|
|
return result.returncode == 0
|
|
|
|
|
|
def _validate_compose_projection(payload, profile=None):
|
|
profile = profile or STANDALONE_PROFILE
|
|
try:
|
|
projection = json.loads(payload)
|
|
if type(projection) is not dict or projection.get('name') != 'truf-docker':
|
|
raise InstallError('compose')
|
|
volume_definitions = projection.get('volumes')
|
|
if (
|
|
type(volume_definitions) is not dict
|
|
or volume_definitions.get('data') != profile['data_volume']
|
|
):
|
|
raise InstallError('compose')
|
|
services = projection.get('services')
|
|
runtime = services.get('runtime') if type(services) is dict else None
|
|
edge = services.get('edge') if type(services) is dict else None
|
|
if (
|
|
type(runtime) is not dict or type(edge) is not dict
|
|
or runtime.get('network_mode') != profile['runtime_network']
|
|
or runtime.get('ports') != profile['runtime_ports']
|
|
or runtime.get('cpus') != profile['runtime_cpus']
|
|
or runtime.get('mem_limit') != profile['runtime_mem_limit']
|
|
or edge.get('network_mode') != 'service:runtime'
|
|
or edge.get('cap_add') != profile['edge_cap_add']
|
|
or edge.get('image') != 'truf-local:edge'
|
|
):
|
|
raise InstallError('compose')
|
|
mounts = runtime.get('volumes') if type(runtime) is dict else None
|
|
if type(mounts) is not list:
|
|
raise InstallError('compose')
|
|
observed = []
|
|
for mount in mounts:
|
|
if type(mount) is not dict or type(mount.get('read_only', False)) is not bool:
|
|
raise InstallError('compose')
|
|
kind = mount.get('type')
|
|
if kind == 'volume':
|
|
if set(mount) - {'type', 'source', 'target', 'read_only'}:
|
|
raise InstallError('compose')
|
|
create_host_path = None
|
|
elif kind == 'bind':
|
|
if set(mount) - {'type', 'source', 'target', 'read_only', 'bind'}:
|
|
raise InstallError('compose')
|
|
binding = mount.get('bind')
|
|
if binding not in ({}, {'create_host_path': False}):
|
|
raise InstallError('compose')
|
|
create_host_path = False
|
|
else:
|
|
raise InstallError('compose')
|
|
observed.append((
|
|
kind, mount.get('source'), mount.get('target'),
|
|
mount.get('read_only', False), create_host_path,
|
|
))
|
|
if tuple(observed) != EXPECTED_RUNTIME_MOUNTS:
|
|
raise InstallError('compose')
|
|
except InstallError:
|
|
raise
|
|
except Exception:
|
|
raise InstallError('compose') from None
|
|
|
|
|
|
def _details(path, *, directory, uid, gid, mode):
|
|
try:
|
|
value = os.stat(path, follow_symlinks=False)
|
|
except OSError:
|
|
raise InstallError('metadata') from None
|
|
expected = stat.S_ISDIR if directory else stat.S_ISREG
|
|
if (
|
|
not expected(value.st_mode) or value.st_uid != uid or value.st_gid != gid
|
|
or stat.S_IMODE(value.st_mode) != mode
|
|
or (not directory and value.st_nlink != 1)
|
|
):
|
|
raise InstallError('metadata')
|
|
return value
|
|
|
|
|
|
def _read_source(path, maximum=MAX_COPY_BYTES):
|
|
descriptor = None
|
|
try:
|
|
descriptor = os.open(
|
|
path, os.O_RDONLY | getattr(os, 'O_CLOEXEC', 0)
|
|
| getattr(os, 'O_NOFOLLOW', 0),
|
|
)
|
|
before = os.fstat(descriptor)
|
|
if (
|
|
not stat.S_ISREG(before.st_mode) or before.st_nlink != 1
|
|
or before.st_uid != 0 or stat.S_IMODE(before.st_mode) & 0o022
|
|
):
|
|
raise InstallError('source')
|
|
with os.fdopen(descriptor, 'rb') as handle:
|
|
descriptor = None
|
|
payload = handle.read(maximum + 1)
|
|
after = os.fstat(handle.fileno())
|
|
if len(payload) > maximum or (before.st_dev, before.st_ino, before.st_size) != (
|
|
after.st_dev, after.st_ino, after.st_size,
|
|
):
|
|
raise InstallError('source')
|
|
return payload
|
|
except InstallError:
|
|
raise
|
|
except Exception:
|
|
raise InstallError('source') from None
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _secure_tree(path):
|
|
try:
|
|
root = os.stat(path, follow_symlinks=False)
|
|
if (
|
|
not stat.S_ISDIR(root.st_mode)
|
|
or root.st_uid != 0
|
|
or stat.S_IMODE(root.st_mode) & 0o022
|
|
):
|
|
raise InstallError('project')
|
|
for current, directories, files in os.walk(path, topdown=True, followlinks=False):
|
|
current_path = Path(current)
|
|
entries = ((name, True) for name in directories)
|
|
entries = tuple(entries) + tuple((name, False) for name in files)
|
|
current_details = os.stat(current_path, follow_symlinks=False)
|
|
if (
|
|
not stat.S_ISDIR(current_details.st_mode)
|
|
or current_details.st_uid != 0
|
|
or stat.S_IMODE(current_details.st_mode) & 0o022
|
|
):
|
|
raise InstallError('project')
|
|
for name, directory in entries:
|
|
details = os.stat(current_path / name, follow_symlinks=False)
|
|
expected = stat.S_ISDIR if directory else stat.S_ISREG
|
|
if (
|
|
not expected(details.st_mode)
|
|
or details.st_uid != 0
|
|
or stat.S_IMODE(details.st_mode) & 0o022
|
|
or (not directory and details.st_nlink != 1)
|
|
):
|
|
raise InstallError('project')
|
|
except InstallError:
|
|
raise
|
|
except Exception:
|
|
raise InstallError('project') from None
|
|
|
|
|
|
def _same_file(installed, source):
|
|
if not _read_source(installed) == _read_source(source):
|
|
raise InstallError('installed_content')
|
|
|
|
|
|
def _ensure_install_root():
|
|
try:
|
|
INSTALL_ROOT.mkdir(mode=0o755)
|
|
except FileExistsError:
|
|
pass
|
|
except OSError:
|
|
raise InstallError('write') from None
|
|
_details(INSTALL_ROOT, directory=True, uid=0, gid=0, mode=0o755)
|
|
|
|
|
|
def _root_directory(path):
|
|
try:
|
|
details = os.stat(path, follow_symlinks=False)
|
|
except OSError:
|
|
raise InstallError('metadata') from None
|
|
if (
|
|
not stat.S_ISDIR(details.st_mode)
|
|
or details.st_uid != 0
|
|
or stat.S_IMODE(details.st_mode) & 0o022
|
|
):
|
|
raise InstallError('metadata')
|
|
|
|
|
|
def _secure_executable(path):
|
|
try:
|
|
link = os.lstat(path)
|
|
resolved = os.path.realpath(path)
|
|
target = os.stat(path)
|
|
parent = os.stat(Path(path).parent, follow_symlinks=False)
|
|
except OSError:
|
|
raise InstallError('executable') from None
|
|
if (
|
|
link.st_uid != 0
|
|
or not (stat.S_ISREG(link.st_mode) or stat.S_ISLNK(link.st_mode))
|
|
or not resolved.startswith(('/usr/bin/', '/usr/sbin/'))
|
|
or not stat.S_ISREG(target.st_mode) or target.st_uid != 0
|
|
or stat.S_IMODE(target.st_mode) & 0o022
|
|
or not stat.S_ISDIR(parent.st_mode) or parent.st_uid != 0
|
|
or stat.S_IMODE(parent.st_mode) & 0o022
|
|
):
|
|
raise InstallError('executable')
|
|
|
|
|
|
def _runtime_group_exists():
|
|
if sys.platform != 'linux':
|
|
raise InstallError('group')
|
|
try:
|
|
import grp
|
|
named = grp.getgrnam(RUNTIME_GROUP)
|
|
numbered = grp.getgrgid(RUNTIME_GID)
|
|
except KeyError:
|
|
return False
|
|
except Exception:
|
|
raise InstallError('group') from None
|
|
if named.gr_gid != RUNTIME_GID or numbered.gr_name != RUNTIME_GROUP:
|
|
raise InstallError('group')
|
|
return True
|
|
|
|
|
|
def _ensure_runtime_group():
|
|
if _runtime_group_exists():
|
|
return
|
|
try:
|
|
import grp
|
|
grp.getgrgid(RUNTIME_GID)
|
|
except KeyError:
|
|
pass
|
|
except Exception:
|
|
raise InstallError('group') from None
|
|
else:
|
|
raise InstallError('group')
|
|
_secure_executable('/usr/sbin/groupadd')
|
|
_run(('/usr/sbin/groupadd', '--system', '--gid', str(RUNTIME_GID), RUNTIME_GROUP))
|
|
if not _runtime_group_exists():
|
|
raise InstallError('group')
|
|
|
|
|
|
def _validate_agent_socket():
|
|
try:
|
|
details = os.stat(AGENT_SOCKET, follow_symlinks=False)
|
|
except OSError:
|
|
raise InstallError('socket') from None
|
|
if (
|
|
not stat.S_ISSOCK(details.st_mode)
|
|
or details.st_uid != 0
|
|
or details.st_gid != RUNTIME_GID
|
|
or stat.S_IMODE(details.st_mode) != 0o660
|
|
):
|
|
raise InstallError('socket')
|
|
|
|
|
|
def _write(path, payload, *, uid, gid, mode, replace):
|
|
temporary = path.parent / ('.' + path.name + '.truf-install')
|
|
descriptor = None
|
|
try:
|
|
try:
|
|
os.unlink(temporary)
|
|
except FileNotFoundError:
|
|
pass
|
|
descriptor = os.open(
|
|
temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
| getattr(os, 'O_NOFOLLOW', 0), mode,
|
|
)
|
|
os.fchmod(descriptor, mode)
|
|
os.fchown(descriptor, uid, gid)
|
|
view = memoryview(payload)
|
|
while view:
|
|
written = os.write(descriptor, view)
|
|
if written <= 0:
|
|
raise OSError('short write')
|
|
view = view[written:]
|
|
os.fsync(descriptor)
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
if not replace and path.exists():
|
|
os.unlink(temporary)
|
|
return
|
|
os.replace(temporary, path)
|
|
parent = os.open(path.parent, os.O_RDONLY | getattr(os, 'O_DIRECTORY', 0))
|
|
try:
|
|
os.fsync(parent)
|
|
finally:
|
|
os.close(parent)
|
|
except Exception:
|
|
try:
|
|
os.unlink(temporary)
|
|
except OSError:
|
|
pass
|
|
raise InstallError('write') from None
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
payload = None
|
|
|
|
|
|
def validate(*, require_socket=True):
|
|
if sys.platform != 'linux' or not hasattr(os, 'geteuid') or os.geteuid() != 0:
|
|
raise InstallError('root')
|
|
_root_directory(PROJECT.parent)
|
|
_root_directory(INSTALL_ROOT.parent)
|
|
if not _runtime_group_exists():
|
|
raise InstallError('group')
|
|
_details(PROJECT, directory=True, uid=0, gid=0, mode=0o755)
|
|
_details(DEPLOY, directory=True, uid=0, gid=0, mode=0o755)
|
|
_details(INSTALL_ROOT, directory=True, uid=0, gid=0, mode=0o755)
|
|
_secure_tree(PROJECT / 'app')
|
|
_details(ACTIVE, directory=True, uid=0, gid=0, mode=0o755)
|
|
_details(WORKER_PACKAGES, directory=True, uid=0, gid=0, mode=0o755)
|
|
_details(ACTIVE / 'config.yaml', directory=False, uid=RUNTIME_UID, gid=RUNTIME_GID, mode=0o600)
|
|
_details(ACTIVE / 'secrets.yaml', directory=False, uid=RUNTIME_UID, gid=RUNTIME_GID, mode=0o600)
|
|
layouts = (
|
|
('/var/lib/truf/runtime-document-candidates', RUNTIME_UID, RUNTIME_GID, 0o700),
|
|
('/var/lib/truf/host-agent', 0, 0, 0o700),
|
|
('/var/lib/truf/host-agent/backups', 0, 0, 0o700),
|
|
('/var/lib/truf/host-agent/operations', 0, 0, 0o700),
|
|
('/var/lib/truf/host-agent/results', 0, RUNTIME_GID, 0o750),
|
|
('/run/truf-postgres', RUNTIME_UID, RUNTIME_GID, 0o700),
|
|
)
|
|
for path, uid, gid, mode in layouts:
|
|
_details(Path(path), directory=True, uid=uid, gid=gid, mode=mode)
|
|
for executable in (
|
|
'/usr/bin/python3', '/usr/bin/docker', '/usr/bin/systemctl',
|
|
'/usr/bin/systemd-analyze', '/usr/bin/systemd-tmpfiles',
|
|
'/usr/sbin/groupadd',
|
|
):
|
|
_secure_executable(executable)
|
|
for unit in UNITS:
|
|
_details(SYSTEMD / unit, directory=False, uid=0, gid=0, mode=0o644)
|
|
_same_file(SYSTEMD / unit, DEPLOY / unit)
|
|
_details(TMPFILES, directory=False, uid=0, gid=0, mode=0o644)
|
|
_same_file(TMPFILES, DEPLOY / 'truf-host-agent.conf')
|
|
for script in SCRIPTS:
|
|
_details(INSTALL_ROOT / script, directory=False, uid=0, gid=0, mode=0o755)
|
|
_same_file(INSTALL_ROOT / script, DEPLOY / script)
|
|
profile = _deployment_profile()
|
|
for compose_file in profile['compose_files']:
|
|
_read_source(PROJECT / compose_file)
|
|
try:
|
|
docker_socket = os.stat('/run/docker.sock', follow_symlinks=False)
|
|
except OSError:
|
|
raise InstallError('socket') from None
|
|
if (
|
|
not stat.S_ISSOCK(docker_socket.st_mode)
|
|
or docker_socket.st_uid != 0
|
|
or stat.S_IMODE(docker_socket.st_mode) & 0o002
|
|
):
|
|
raise InstallError('socket')
|
|
if require_socket:
|
|
_validate_agent_socket()
|
|
_run(('/usr/bin/python3', '-I', '-B', '-c', 'import psycopg, yaml'))
|
|
_run(('/usr/bin/docker', 'compose', 'version'))
|
|
_run(('/usr/bin/systemd-analyze', 'verify', *(str(SYSTEMD / unit) for unit in UNITS)))
|
|
_validate_compose_projection(
|
|
_capture(_compose_config_command(profile)), profile,
|
|
)
|
|
|
|
|
|
def install():
|
|
if sys.platform != 'linux' or not hasattr(os, 'geteuid') or os.geteuid() != 0:
|
|
raise InstallError('root')
|
|
_ensure_runtime_group()
|
|
for unit in UNITS:
|
|
if _unit_active(unit):
|
|
_run(('/usr/bin/systemctl', 'stop', unit))
|
|
_ensure_install_root()
|
|
for script in SCRIPTS:
|
|
_write(INSTALL_ROOT / script, _read_source(DEPLOY / script), uid=0, gid=0, mode=0o755, replace=True)
|
|
for unit in UNITS:
|
|
_write(SYSTEMD / unit, _read_source(DEPLOY / unit), uid=0, gid=0, mode=0o644, replace=True)
|
|
_write(TMPFILES, _read_source(DEPLOY / 'truf-host-agent.conf'), uid=0, gid=0, mode=0o644, replace=True)
|
|
_run(('/usr/bin/systemd-tmpfiles', '--create', str(TMPFILES)))
|
|
_write(
|
|
ACTIVE / 'config.yaml', _read_source(PROJECT / 'app/config.linux.yaml'),
|
|
uid=RUNTIME_UID, gid=RUNTIME_GID, mode=0o600, replace=False,
|
|
)
|
|
_write(
|
|
ACTIVE / 'secrets.yaml', b'{}\n', uid=RUNTIME_UID, gid=RUNTIME_GID,
|
|
mode=0o600, replace=False,
|
|
)
|
|
validate(require_socket=False)
|
|
_run(('/usr/bin/systemctl', 'daemon-reload'))
|
|
_run(('/usr/bin/systemctl', 'enable', 'truf-host-agent.socket'))
|
|
_run(('/usr/bin/systemctl', 'restart', 'truf-host-agent.socket'))
|
|
_validate_agent_socket()
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 2 or sys.argv[1] not in ('install', 'validate'):
|
|
raise InstallError('arguments')
|
|
install() if sys.argv[1] == 'install' else validate()
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
result = main()
|
|
except Exception as exc:
|
|
print(
|
|
'host-agent deployment failed (' + type(exc).__name__ + '); details withheld',
|
|
file=sys.stderr,
|
|
)
|
|
result = 1
|
|
raise SystemExit(result)
|