#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2025 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio
import os
import signal
import optparse
import shlex
import sys

try:
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
    pass
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.asynclient as client

class NullOpt(object):
    blame = None
    clear = None


def bailout(msg, code=1):
    sys.stderr.write(msg + '\n')
    sys.exit(code)


argparser = optparse.OptionParser(usage="Usage: %prog [options] <noderange> [setting|setting=value]")
argparser.add_option('-c', '--comparedefault', dest='comparedefault',
                     action='store_true', default=False,
                     help='Compare given settings to default or list settings '
                          'that are non default')
argparser.add_option('-b', '--batch', dest='batch', metavar='settings.batch',
                     default=False, help='Provide settings in a batch file')
argparser.add_option('-d', '--detail', dest='detail',
                     action='store_true', default=False,
                     help='Provide verbose information as available, such as '
                          'help text and possible valid values')
argparser.add_option('-e', '--extra', dest='extra',
                     action='store_true', default=False,
                     help='Access extra configuration.  Extra configuration is generally '
                           'reserved for unpopular or redundant options that may be slow to '
                           'read.  Notably the IMM category on Lenovo settings is considered '
                           'to be extra configuration')
argparser.add_option('-x', '--exclude', dest='exclude',
                     action='store_true', default=False,
                     help='Treat named settings as items to not '
                          'examine, compare, or restore default')
argparser.add_option('-a', '--advanced', dest='advanced',
                     action='store_true', default=False,
                     help='Include advanced settings, which are normally not '
                          'intended to be used without direction from the '
                          'relevant server vendor.')
argparser.add_option('-r', '--restoredefault', default=False,
                     dest='restoredefault', metavar="COMPONENT",
                     help='Restore the configuration of the node '
                          'to factory default for given component. '
                          'Currently "uefi" and "bmc" are supported components')
argparser.add_option('-m', '--maxnodes', type='int',
                     help='Specify a maximum number of '
                          'nodes to configure, '
                          'prompting if over the threshold')
(options, args) = argparser.parse_args()

cfgpaths = {
    'bmc.ipv4_address': (
        'configuration/management_controller/net_interfaces/management',
        'ipv4_address'),
    'bmc.ipv4_method': (
        'configuration/management_controller/net_interfaces/management',
        'ipv4_configuration'),
    'bmc.ipv4_gateway': (
        'configuration/management_controller/net_interfaces/management',
        'ipv4_gateway'),
    'bmc.static_ipv6_addresses': (
        'configuration/management_controller/net_interfaces/management',
        'static_v6_addresses'),
    'bmc.static_ipv6_gateway': (
        'configuration/management_controller/net_interfaces/management',
        'static_v6_gateway'),
    'bmc.vlan_id': (
        'configuration/management_controller/net_interfaces/management',
        'vlan_id'),
    'bmc.mac_address': (
        'configuration/management_controller/net_interfaces/management',
        'hw_addr'),
    'bmc.hostname': (
        'configuration/management_controller/hostname', 'hostname'),
}

autodeps = {
    'bmc.ipv4_address': (('bmc.ipv4_method', 'static'),)
}

try:
    noderange = args[0]
except IndexError:
    argparser.print_help()
    sys.exit(1)
client.check_globbing(noderange)
setmode = None
assignment = {}
queryparms = {}
printsys = []
printbmc = []
printextbmc = []
printallbmc = False
setsys = {}
forceset = False
needval = None

if len(args) == 1 or options.exclude:
    if not options.exclude:
        printsys = 'all'
    for candidate in cfgpaths:
        path, attrib = cfgpaths[candidate]
        path = '/noderange/{0}/{1}'.format(noderange, path)
        if path not in queryparms:
            queryparms[path] = {}
        queryparms[path][attrib] = candidate


def _assign_value():
    if key not in cfgpaths:
        setsys[key] = value
    for depkey, depval in autodeps.get(key, []):
        assignment[depkey] = depval
    assignment[key] = value


def parse_config_line(arguments, single=False):
    global setmode, printallbmc, forceset, key, value, needval, candidate, path, attrib
    for pidx in range(0, len(arguments)):
        param = arguments[pidx]
        if param == 'show':
            continue  # forgive muscle memory of pasu users
        if param == 'set':
            setmode = True
            forceset = True
            continue
        if needval:
            key = needval
            needval = None
            if single:
                value = ' '.join(arguments[pidx:])
                _assign_value()
                break
            else:
                value = param
            _assign_value()
            continue
        if '=' in param or param[-1] == ':' or forceset:
            if setmode is None:
                setmode = True
            if setmode != True:
                bailout('Cannot do set and query in same command: Query detected but "{0}" appears to be set'.format(param))
            if '=' in param:
                key, _, value = param.partition('=')
                _assign_value()
            elif param[-1] == ':':
                needval = param[:-1]
            else:
                needval = param
        else:
            if setmode is None:
                setmode = False
            if setmode != False:
                bailout('Cannot do set and query in same command: Set mode detected but "{0}" appears to be a query'.format(param))
            if '.' not in param:
                if param == 'bmc':
                    printallbmc = True
                matchedparms = False
                for candidate in cfgpaths:
                    if candidate.startswith('{0}.'.format(param)):
                        matchedparms = True
                        if not options.exclude:
                            path, attrib = cfgpaths[candidate]
                            path = '/noderange/{0}/{1}'.format(noderange, path)
                            if path not in queryparms:
                                queryparms[path] = {}
                            queryparms[path][attrib] = candidate
                        else:
                            try:
                                del queryparms[path]
                            except KeyError:
                                pass
                if param.lower() == 'imm':
                    printextbmc.append(param)
                    options.extra = True
                elif not matchedparms:
                    printsys.append(param)
            elif param not in cfgpaths:
                if param.startswith('bmc.'):
                    printbmc.append(param.replace('bmc.', ''))
                elif param.lower().startswith('imm'):
                    options.extra = True
                    printextbmc.append(param)
                else:
                    printsys.append(param)
            else:
                path, attrib = cfgpaths[param]
                path = '/noderange/{0}/{1}'.format(noderange, path)
                if path not in queryparms:
                    queryparms[path] = {}
                queryparms[path][attrib] = param

async def main():
    global printsys
    if options.batch:
        printsys = []
        argfile = open(options.batch, 'r')
        argset = argfile.readline()
        while argset:
            try:
                argset = argset[:argset.index('#')]
            except ValueError:
                pass
            argset = argset.strip()
            if argset:
                parse_config_line(shlex.split(argset), single=True)
            argset = argfile.readline()
    else:
        parse_config_line(args[1:])
    session = client.Command()
    rcode = 0
    if options.restoredefault:
        await session.stop_if_noderange_over(noderange, options.maxnodes)
        if options.restoredefault.lower() in (
            'sys', 'system', 'uefi', 'bios'):
            async for fr in session.update(
                    '/noderange/{0}/configuration/system/clear'.format(noderange),
                    {'clear': True}):
                rcode |= client.printerror(fr)
            sys.exit(rcode)
        elif options.restoredefault.lower() in (
            'bmc', 'imm', 'xcc'):
            async for fr in session.update(
                    '/noderange/{0}/configuration/management_controller/clear'.format(noderange),
                    {'clear': True}):
                rcode |= client.printerror(fr)
            sys.exit(rcode)
        else:
            sys.stderr.write(
                'Unrecognized component to restore defaults: {0}\n'.format(
                    options.restoredefault))
            sys.exit(1)
    if setmode:
        await session.stop_if_noderange_over(noderange, options.maxnodes)
        if options.exclude:
            sys.stderr.write('Cannot use exclude and assign at the same time\n')
            sys.exit(1)
        updatebypath = {}
        attrnamebypath = {}
        for key in assignment:
            if key not in cfgpaths:
                if key.startswith('bmc.'):
                    path = 'configuration/management_controller/extended/all'
                    attrib = key.replace('bmc.', '')
                else:
                    path = 'configuration/system/all'
                    attrib = key
            else:
                path, attrib = cfgpaths[key]
            if path not in updatebypath:
                updatebypath[path] = {}
                attrnamebypath[path] = {}
            updatebypath[path][attrib] = assignment[key]
            attrnamebypath[path][attrib] = key
        # well, we want to expand things..
        # check ipv4, if requested change method to static
        for path in updatebypath:
            async for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
                                    updatebypath[path]):
                rcode |= client.printerror(fr)
                for node in fr.get('databynode', []):
                    r = fr['databynode'][node]
                    if 'value' not in r:
                        continue
                    keyval = r['value']
                    key, val = keyval.split('=')
                    if key in attrnamebypath[path]:
                        key = attrnamebypath[path][key]
                    print('{0}: {1}: {2}'.format(node, key, val))
    else:
        for path in queryparms:
            if options.comparedefault:
                continue
            rcode |= await client.print_attrib_path(path, session, list(queryparms[path]),
                                        NullOpt(), queryparms[path])
        if printsys == 'all' or printextbmc or printbmc or printallbmc:
            if printbmc or not printextbmc:
                rcode |= await client.print_attrib_path(
                            '/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
                            session, printbmc, options, attrprefix='bmc.')
            if options.extra:
                if options.advanced:
                        rcode |= await client.print_attrib_path(
                            '/noderange/{0}/configuration/management_controller/extended/extra_advanced'.format(noderange),
                            session, printextbmc, options)
                else:
                    rcode |= await client.print_attrib_path(
                            '/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange),
                            session, printextbmc, options)
        if printsys or options.exclude:
            if printsys == 'all':
                printsys = []
            if (options.comparedefault or printsys == []) and not options.advanced:
                path = '/noderange/{0}/configuration/system/all'.format(noderange)
            else:
                path = '/noderange/{0}/configuration/system/advanced'.format(
                    noderange)
            rcode = await client.print_attrib_path(path, session, printsys,
                                            options)
    sys.exit(rcode)

if __name__ == '__main__':
    asyncio.run(main())
