#!/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 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.client 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

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:
    session.stop_if_noderange_over(noderange, options.maxnodes)
    if options.restoredefault.lower() in (
        'sys', 'system', 'uefi', 'bios'):
        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'):
        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:
    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:
        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 |= 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 |= 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 |= client.print_attrib_path(
                        '/noderange/{0}/configuration/management_controller/extended/extra_advanced'.format(noderange),
                        session, printextbmc, options)
            else:
                rcode |= 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 = client.print_attrib_path(path, session, printsys,
                                         options)
sys.exit(rcode)
