risotto/ansible/library/machinectl.py
2022-12-21 16:35:58 +01:00

215 lines
7.9 KiB
Python

#!/usr/bin/python3
from time import sleep
from os import fdopen
from dbus import SystemBus, Array
from dbus.exceptions import DBusException
from subprocess import run
from ansible.module_utils.basic import AnsibleModule
def stop(bus, machines):
changed = False
remote_object = bus.get_object('org.freedesktop.machine1',
'/org/freedesktop/machine1',
False,
)
res = remote_object.ListMachines(dbus_interface='org.freedesktop.machine1.Manager')
started_machines = [str(r[0]) for r in res if str(r[0]) != '.host']
for host in machines:
if host not in started_machines:
continue
changed = True
remote_object.TerminateMachine(host, dbus_interface='org.freedesktop.machine1.Manager')
idx = 0
errors = []
while True:
res = remote_object.ListMachines(dbus_interface='org.freedesktop.machine1.Manager')
started_machines = [str(r[0]) for r in res if str(r[0]) != '.host']
for host in machines:
if host in started_machines:
break
else:
break
sleep(1)
idx += 1
if idx == 120:
errors.append('Cannot not stopped: ' + ','.join(started_machines))
break
return changed, errors
def start(bus, machines):
changed = False
remote_object = bus.get_object('org.freedesktop.machine1',
'/org/freedesktop/machine1',
False,
)
res = remote_object.ListMachines(dbus_interface='org.freedesktop.machine1.Manager')
started_machines = [str(r[0]) for r in res if str(r[0]) != '.host']
remote_object_system = bus.get_object('org.freedesktop.systemd1',
'/org/freedesktop/systemd1',
False,
)
for host in machines:
if host in started_machines:
continue
changed = True
service = f'systemd-nspawn@{host}.service'
remote_object_system.StartUnit(service, 'fail', dbus_interface='org.freedesktop.systemd1.Manager')
errors = []
idx = 0
while True:
res = remote_object.ListMachines(dbus_interface='org.freedesktop.machine1.Manager')
started_machines = [str(r[0]) for r in res if str(r[0]) != '.host']
for host in machines:
if host not in started_machines:
break
else:
break
sleep(1)
idx += 1
if idx == 120:
hosts = set(machines) - set(started_machines)
errors.append('Cannot not start: ' + ','.join(hosts))
break
if not errors:
idx = 0
for host in machines:
cmd = ['/usr/bin/systemctl', 'is-system-running']
error = False
while True:
try:
ret = []
res = remote_object.OpenMachineShell(host,
'',
cmd[0],
Array(cmd, signature='s'),
Array(['TERM=dumb'], signature='s'),
dbus_interface='org.freedesktop.machine1.Manager',
)
fd = res[0].take()
fh = fdopen(fd)
while True:
try:
ret.append(fh.readline().strip())
except OSError as err:
if err.errno != 5:
raise err from err
break
if not ret:
errors.append(f'Cannot check {host} status')
error = True
break
if ret[0] in ['running', 'degraded']:
break
except DBusException:
pass
idx += 1
sleep(1)
if idx == 120:
errors.append(f'Cannot not start {host} ({ret})')
break
if error:
continue
if ret and ret[0] == 'running':
continue
cmd = ['/usr/bin/systemctl', '--state=failed', '--no-legend', '--no-page']
res = remote_object.OpenMachineShell(host,
'',
cmd[0],
Array(cmd, signature='s'),
Array(['TERM=dumb'], signature='s'),
dbus_interface='org.freedesktop.machine1.Manager',
)
fd = res[0].take()
fh = fdopen(fd)
ret = []
idx2 = 0
while True:
try:
ret.append(fh.readline().strip())
except OSError as err:
if err.errno != 5:
raise err from err
break
idx2 += 1
if idx2 == 120:
errors.append(f'Cannot not get status to {host}')
break
errors.append(f'{host}: ' + '\n'.join(ret))
return changed, errors
def enable(machines):
cmd = ['/usr/bin/machinectl', 'enable'] + machines
run(cmd)
return True
def run_module():
# define available arguments/parameters a user can pass to the module
module_args = dict(
state=dict(type='str', required=True),
machines=dict(type='list', required=True),
)
# seed the result dict in the object
# we primarily care about changed and state
# changed is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
message=''
)
# the AnsibleModule object will be our abstraction working with Ansible
# this includes instantiation, a couple of common attr would be the
# args/params passed to the execution, as well as if the module
# supports check mode
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# if the user is working with this module in only check mode we do not
# want to make any changes to the environment, just return the current
# state with no modifications
if module.check_mode:
module.exit_json(**result)
# manipulate or modify the state as needed (this is going to be the
# part where your module will do what it needs to do)
machines = module.params['machines']
if module.params['state'] == 'stopped':
bus = SystemBus()
result['changed'], errors = stop(bus, machines)
if errors:
errors = '\n\n'.join(errors)
module.fail_json(msg=f'Some machines are not stopping correctly {errors}', **result)
elif module.params['state'] == 'started':
bus = SystemBus()
result['changed'], errors = start(bus, machines)
if errors:
errors = '\n\n'.join(errors)
module.fail_json(msg=f'Some machines are not running correctly {errors}', **result)
elif module.params['state'] == 'enabled':
result['changed'] = enable(machines)
else:
module.fail_json(msg=f"Unknown state: {module.params['state']}")
# in the event of a successful module execution, you will want to
# simple AnsibleModule.exit_json(), passing the key/value results
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()