#!/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()