#!/usr/bin/python3

from time import sleep
from os import fdopen
from dbus import SystemBus, Array
from dbus.exceptions import DBusException

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:
                    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 = []
                    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[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 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)

    bus = SystemBus()

    # 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':
        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':
        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)
    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()