#!/usr/bin/python3


import argparse
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
    sys.path.append(path)

import confluent.client as client
import confluent.sortutil as sortutil

def cleararm(nr, cli):
    nodes = set([])
    for rsp in cli.read('/noderange/{0}/attributes/current'.format(nr)):
        for node in rsp.get('databynode', {}):
            nodeinfo = rsp['databynode'][node]
            for attr in nodeinfo:
                if attr == 'deployment.apiarmed':
                    curr = nodeinfo[attr].get('value', '')
                    if curr == 'continuous':
                        nodes.add(node)
    noderange = nr
    if nodes:
        noderange += ',-({0})'.format(','.join(nodes))
    for rsp in cli.update('/noderange/{0}/attributes/current'.format(noderange),
                          {'deployment.apiarmed': ''}):
        pass


def armonce(nr, cli):
    nodes = set([])
    for rsp in cli.read('/noderange/{0}/attributes/current'.format(nr)):
        for node in rsp.get('databynode', {}):
            nodeinfo = rsp['databynode'][node]
            for attr in nodeinfo:
                if attr == 'deployment.apiarmed':
                    curr = nodeinfo[attr].get('value', '')
                    if curr == 'continuous':
                        nodes.add(node)
    noderange = nr
    if nodes:
        noderange += ',-({0})'.format(','.join(nodes))
    for rsp in cli.update('/noderange/{0}/attributes/current'.format(noderange),
                          {'deployment.apiarmed': 'once'}):
        pass


def setpending(nr, profile, profilebynodes, cli):
    if profilebynodes:
        for node in sortutil.natural_sort(profilebynodes):
            prof = profilebynodes[node]
            args = {'deployment.pendingprofile': prof, 'deployment.state': '', 'deployment.state_detail': ''}
            if not prof.startswith('genesis-'):
                args['deployment.stagedprofile'] = ''
                args['deployment.profile'] = ''
            for rsp in cli.update('/nodes/{0}/attributes/current'.format(node),
                                  args):
                pass
        return
    args = {'deployment.pendingprofile': profile, 'deployment.state': '', 'deployment.state_detail': ''}
    if not profile.startswith('genesis-'):
        args['deployment.stagedprofile'] = ''
        args['deployment.profile'] = ''
    for rsp in cli.update('/noderange/{0}/attributes/current'.format(nr),
                          args):
        pass


def clearpending(nr, cli):
    for rsp in cli.update('/noderange/{0}/attributes/current'.format(nr),
                          {'deployment.pendingprofile': ''}):
        pass

def main(args):
    ap = argparse.ArgumentParser(description='Deploy OS to nodes')
    ap.add_argument('-c', '--clear', help='Clear any pending deployment action', action='store_true')
    ap.add_argument('-n', '--network', help='Initiate deployment over PXE/HTTP', action='store_true')
    ap.add_argument('-b', '--bootmethod', help='Specify network boot method (e.g., network, http)', default='network')
    ap.add_argument('-p', '--prepareonly', help='Prepare only, skip any interaction with a BMC associated with this deployment action', action='store_true')
    ap.add_argument('-m', '--maxnodes', help='Specifiy a maximum nodes to be deployed')
    ap.add_argument('-r', '--redeploy', help='Redeploy nodes with the current or pending profile', action='store_true')
    ap.add_argument('noderange', help='Set of nodes to deploy')
    ap.add_argument('profile', nargs='?', help='Profile name to deploy')
    args, extra = ap.parse_known_args(args)
    if args.profile is None and len(extra) == 1:
        args.profile = extra[0]
        extra = extra[1:]
    if args.profile and not args.network:
        sys.stderr.write('-n is a required argument currently to perform an install, optionally with -p\n')
        return 1
    if not args.profile and args.network and not args.redeploy:
        sys.stderr.write('Both noderange and a profile name are required arguments to request a network deployment\n')
        return 1
    if args.clear and args.profile:
        sys.stderr.write(
            'The -c/--clear option should not be used with a profile, '
            'it is a request to not deploy any profile, and will clear '
            'whatever the current profile is without being specified\n')
        return 1
    if extra:
        sys.stderr.write('Unrecognized arguments: ' + repr(extra) + '\n')
    c = client.Command()
    c.stop_if_noderange_over(args.noderange, args.maxnodes)
    errnodes = set([])
    for rsp in c.read('/noderange/{0}/nodes/'.format(args.noderange)):
        if 'error' in rsp:
            sys.stderr.write(rsp['error'] + '\n')
            sys.exit(1)
    profilebynode = {}
    if args.clear:
        cleararm(args.noderange, c)
        clearpending(args.noderange, c)
    elif args.redeploy:
        hadpending = {}
        for rsp in c.read('/noderange/{0}/attributes/current'.format(args.noderange)):
            for node in rsp.get('databynode', {}):
                nodeinfo = rsp['databynode'][node]
                for attr in nodeinfo:
                    if attr == 'deployment.pendingprofile':
                        curr = nodeinfo[attr].get('value', '')
                        if curr:
                            hadpending[node] = True
                            profilebynode[node] = curr
                    if attr == 'deployment.stagedprofile':
                        curr = nodeinfo[attr].get('value', '')
                        if curr and node not in hadpending:
                            profilebynode[node] = curr
                    if attr == 'deployment.profile':
                        curr = nodeinfo[attr].get('value', '')
                        if curr and node not in profilebynode:
                            profilebynode[node] = curr
        for lockinfo in c.read('/noderange/{0}/deployment/lock'.format(args.noderange)):
            for node in lockinfo.get('databynode', {}):
                lockstate = lockinfo['databynode'][node]['lock']['value']
                if lockstate == 'locked':
                    lockednodes.append(node)     
    if args.profile and profilebynode:
        sys.stderr.write('The -r/--redeploy option cannot be used with a profile, it redeploys the current or pending profile\n')
        return 1
    if args.profile or profilebynode:
        lockednodes = []
        for lockinfo in c.read('/noderange/{0}/deployment/lock'.format(args.noderange)):
            for node in lockinfo.get('databynode', {}):
                lockstate = lockinfo['databynode'][node]['lock']['value']
                if lockstate == 'locked':
                    lockednodes.append(node)
        if lockednodes:
            sys.stderr.write('Requested noderange has nodes with locked deployment: ' + ','.join(lockednodes))
            sys.stderr.write('\n')
            sys.exit(1)
        if args.profile:
            profnames = []
            for prof in c.read('/deployment/profiles/'):
                profname = prof.get('item', {}).get('href', None)
                if profname:
                    profname = profname.replace('/', '')
                    profnames.append(profname)
                    if profname == args.profile:
                        break
            else:
                sys.stderr.write('The specified profile "{}" is not an available profile\n'.format(args.profile))
                if profnames:
                    sys.stderr.write('The following profiles are available:\n')
                    for profname in profnames:
                        sys.stderr.write('    ' + profname + '\n')
                else:
                    sys.stderr.write('No deployment profiles available, try osdeploy import or imgutil capture\n')
                sys.exit(1)
        armonce(args.noderange, c)
        setpending(args.noderange, args.profile, profilebynode, c)
    else:
        databynode = {}
        for r in c.read('/noderange/{0}/attributes/current'.format(args.noderange)):
            dbn = r.get('databynode', {})
            for node in dbn:
                if node not in databynode:
                    databynode[node] = {}
                for attr in dbn[node]:
                    if attr in ('deployment.pendingprofile', 'deployment.apiarmed', 'deployment.stagedprofile', 'deployment.profile', 'deployment.state', 'deployment.state_detail'):
                        databynode[node][attr] = dbn[node][attr].get('value', '')
        for node in sortutil.natural_sort(databynode):
            profile = databynode[node].get('deployment.pendingprofile', '')
            if profile:
                profile = 'pending: {}'.format(profile)
            else:
                profile = databynode[node].get('deployment.stagedprofile', '')
                if profile:
                    profile = 'staged: {}'.format(profile)
                else:
                    profile = databynode[node].get('deployment.profile', '')
                    if profile:
                        profile = 'completed: {}'.format(profile)
                    else:
                        profile= 'No profile pending or applied'
            armed = databynode[node].get('deployment.apiarmed', '')
            if armed in ('once', 'continuous'):
                armed = ' (node authentication armed)'
            else:
                armed = ''
            stateinfo = ''
            deploymentstate = databynode[node].get('deployment.state', '')
            if deploymentstate:
                statedetails = databynode[node].get('deployment.state_detail', '')
                if statedetails:
                    stateinfo = '{}: {}'.format(deploymentstate, statedetails)
                else:
                    stateinfo = deploymentstate
            if stateinfo:
                print('{0}: {1} ({2})'.format(node, profile, stateinfo))
            else:
                print('{0}: {1}{2}'.format(node, profile, armed))
        sys.exit(0)
    if not args.clear and args.network and not args.prepareonly:
        rc = c.simple_noderange_command(args.noderange, '/boot/nextdevice', args.bootmethod,
                                        bootmode='uefi',
                                        persistent=False,
                                        errnodes=errnodes)
        if errnodes:
            sys.stderr.write(
                'Unable to set boot device for following nodes: {0}\n'.format(
                    ','.join(errnodes)))
            return 1
        rc |= c.simple_noderange_command(args.noderange, '/power/state', 'boot')
    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))
