# -*- coding: utf-8; Mode: Python; indent-tabs-mode: nil; tab-width: 4 -*-
#
# «dgx-bmc» - Configure the BMC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

from __future__ import print_function

import os
import re
import subprocess
import pwd
import sys
import shlex

import debconf

from ubiquity import misc, plugin, validation

NAME = 'nv-config-bmc'
AFTER = 'usersetup'
BEFORE = 'network'
WEIGHT = 20

def createUserIPMI(username, password):
    # 1. create new user, with admin priviledges
    # 2. assign random string for password of default account
    #      qct.admin for dgx-1
    #      admin for dgx-2 and dgx-a100 and dgx-a800
    #      admin and Administrator for dgxstation-a100
    #      root for DGX GB200
    #
    # Need to find ID for new user that doesn't conflict with any current users

    (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool -c user list 1")

    nextID = 4  # Guess what index to use, and make sure it doesn't conflict
    noMatches = False
    newUserExists = False
    ipmiPWLen = 16

    # If PW length is > 16, set ipmiPWLen to 20
    if len(password) > 16:
        ipmiPWLen = 20

    admin_regexes = [r'([0-9]+),(qct.admin),.*', r'([0-9]+),(admin),.*', r'([0-9]+),(Administrator),.*',r'([0-9]+),(root),.*']
    all_admin_ids = []

    while (noMatches == False):
        nextIDStart = nextID
        for line in output.split("\n"):
            match = re.compile(r'^([0-9]+),([a-z0-9]+),.*', re.IGNORECASE).match(line)
            if match:
                if match.group(1) == str(nextID):
                   nextID += 1
                if match.group(2) == username:
                   existingID = int(match.group(1))
                   if not newUserExists:
                       sys.stderr.write("BMC username %s already exists\n" % username)
                   newUserExists = True

            # Loop through possible BMC admin names.
            #   DGX1 = qct.admin
            #   DGX2, DGX A800, DGX A100 = admin
            #   DGXStation A100 = admin and Administrator
            for reg in admin_regexes:
                match = re.compile(reg, re.IGNORECASE).match(line)
                # Found a match, append the ID to all_admin_ids list
                if match and match.group(1) not in all_admin_ids:
                    all_admin_ids.append(match.group(1))

        if nextID == nextIDStart:
            # Made it through all the output without matches
            noMatches = True

    # Log a message if we weren't able to find any admin users
    if not all_admin_ids:
        sys.stderr.write("Cannot find default BMC admin user\n")

    # Disable all admin users
    for admid in all_admin_ids:
        # randomString() returns 16-character password, so no need to manually
        # specify password length parameter
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool user set password %d %s" % (int(admid), randomString()))
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool user disable %d" % int(admid))

    if newUserExists == True:   # If user exists, then just make the password match.  Assume rest is already setup
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool user set password %d %s %d" % (existingID, shlex.quote(password), ipmiPWLen))

        # Do not assume the user has been enabled
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool user enable %d" % existingID)

        # Enable SOL payload for this user
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool sol payload enable 1 %d" % existingID)

    else:
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool user set name %d %s" % (nextID, username))

        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool user set password %d %s %d" % (nextID, shlex.quote(password), ipmiPWLen))

        # set priviledge mode to Administrator
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool user priv %d 4 1" % nextID)

        # Need to enable access as well
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool channel setaccess 1 %d privilege=4 ipmi=on" % nextID)

        # Also need to enable!
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool user enable %d" % nextID)

        # Enable SOL payload for this user
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool sol payload enable 1 %d" % nextID)

        # This is how we enable the KVM flag for Remote Console, and vMedia (bit0, and bit1, respectively)
        (status, output) = subprocess.getstatusoutput("/usr/bin/ipmitool raw 0x32 0xA3 %d 0x03 0x00 0x00 0x00" % nextID)

def randomString():
    import string
    import random

    size = 16
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits

    return ''.join(random.choice(chars) for _ in range(size))

def FrontendIsUbiquity():
    return 'UBIQUITY_FRONTEND' in os.environ and os.environ['UBIQUITY_FRONTEND'] == 'debconf_ui'

def make_error_string(controller, errors):
    """Returns a newline-separated string of translated error reasons."""
    return "\n".join([controller.get_string(error) for error in errors])

def has_bmc():
    cmd = "ipmitool mc info"

    (status, _) = subprocess.getstatusoutput(cmd)
    if status != 0:
        sys.stderr.write("No BMC found\n")
        return False

    sys.stdout.write("System has a BMC\n")
    return True

def has_bypass_file():
    bypass_file = "/var/tmp/bypass-bmc-password"

    if os.path.exists(bypass_file):
        sys.stdout.write("Found bypass-file %s\n" % (bypass_file))
        return True

    sys.stdout.write("bypass-file not found\n")
    return False

def get_passwd_lens():
    pwmax = 20
    pwmin = 1
    platfuncs = "/usr/local/sbin/nv_scripts/plat_funcs.bash"
    mincmd = ". " + platfuncs + " && plat_get_bmc_passwd_min_len"
    maxcmd = ". " + platfuncs + " && plat_get_bmc_passwd_max_len"

    (status, pwmin) = subprocess.getstatusoutput(mincmd)
    (status, pwmax) = subprocess.getstatusoutput(maxcmd)

    return (int(pwmin), int(pwmax))

def do_passwd_complexity_checks():
    platfuncs = "/usr/local/sbin/nv_scripts/plat_funcs.bash"
    cmd = ". " + platfuncs + " && plat_get_bmc_passwd_has_complexity_reqs"

    (status, _) = subprocess.getstatusoutput(cmd)
    if status == 0:
        return True

    return False

def gen_bmc_passwd(basestr):
    (min_length, max_length) = get_passwd_lens()

    # Backfill to with zeroes for platforms that require a minimum length
    return basestr.ljust(min_length, '0')

def get_first_user():
    try:
        # XXX 'oem' user not getting deleted during installation
        sysuser = pwd.getpwuid(1001)
    except KeyError:
        sysuser = pwd.getpwuid(1000)

    return sysuser.pw_name

class PageBase(plugin.PluginUI):
    def __init__(self):
        self.suffix = misc.dmimodel()
        self.allow_password_empty = False

    def get_password(self):
        """Get the bmc user's password."""
        raise NotImplementedError('get_password')

    def get_verified_password(self):
        """Get the bmc user's password confirmation."""
        raise NotImplementedError('get_verified_password')

    def password_error(self, msg):
        """The selected password was bad."""
        raise NotImplementedError('password_error')

    def clear_errors(self):
        pass

    def info_loop(self, *args):
        """Verify user input."""
        pass


class PageGtk(PageBase):
    plugin_title = 'ubiquity/text/wireless_password_label'

    def __init__(self, controller, *args, **kwargs):
        # Do not render this page if there is no BMC, or if the bypass
        # file exists
        if not has_bmc():
            sys.stdout.write("%s: PageGtk early return\n" % (NAME))
            return

        if has_bypass_file():
            sys.stdout.write("%s: PageGtk early return\n" % (NAME))

            # Sill return early but set generated password
            defuser = get_first_user()
            defpass = gen_bmc_passwd(defuser)
            createUserIPMI(defuser, defpass)
            return

        from gi.repository import Gio, Gtk

        PageBase.__init__(self, *args, **kwargs)
        self.resolver = Gio.Resolver.get_default()
        self.controller = controller

        builder = Gtk.Builder()
        self.controller.add_builder(builder)
        builder.add_from_file(os.path.join(
            os.environ['UBIQUITY_GLADE'], 'stepNvBMCpasswd.ui'))
        builder.connect_signals(self)
        self.page = builder.get_object('stepNvBMCpasswd')
        self.password = builder.get_object('password')
        self.verified_password = builder.get_object('verified_password')
        self.password_error_label = builder.get_object('password_error_label')

        self.password_ok = builder.get_object('password_ok')
        self.password_strength = builder.get_object('password_strength')
        (self.password_min_length, self.password_max_length) = get_passwd_lens()

        # Dodgy hack to let us center the contents of the page without it
        # moving as elements appear and disappear, specifically the full name
        # okay check icon and the hostname error messages.
        paddingbox = builder.get_object('paddingbox')

        def func(box):
            box.get_parent().child_set_property(box, 'expand', False)
            box.set_size_request(box.get_allocation().width / 2, -1)

        paddingbox.connect('realize', func)

        # Some signals need to be connected by hand so that we have the
        # handler ids.

        # The UserSetup component takes care of preseeding passwd/user-uid.
        misc.execute_root('apt-install', 'oem-config-gtk', 'oem-config-slideshow-ubuntu')

        self.resolver_ok = True
        self.plugin_widgets = self.page

    # Functions called by the Page.

    def get_password(self):
        return self.password.get_text()

    def get_verified_password(self):
        return self.verified_password.get_text()

    def password_error(self, msg):
        self.password_strength.hide()
        m = '<small><span foreground="darkred"><b>%s</b></span></small>' % msg
        self.password_error_label.set_markup(m)
        self.password_error_label.show()

    def check_password_complexity(self, password):
        """ Return true iff the password contains an upper, a lower, a
        digit, and a special character """

        upper = lower = digit = symbol = 0
        for char in self.get_password():
            if char.isdigit():
                digit += 1
            elif char.islower():
                lower += 1
            elif char.isupper():
                upper += 1
            else:
                symbol += 1
        if (digit * lower * upper * symbol) == 0:
            return False
        else:
            return True

    def clear_errors(self):
        self.password_error_label.hide()

    # Callback functions.

    def info_loop(self, widget):
        """check if all entries from Identification screen are filled. Callback
        defined in ui file."""

        # Do some initial validation.  We have to process all the widgets so we
        # can know if we can really show the next button.  Otherwise we'd show
        # it on any field being valid.
        complete = True
        password_ok = True

        # Check password length first
        if len(self.get_password()) < self.password_min_length:
            self.password_ok.hide()
            self.clear_errors()
            self.password_error("Shorter than %s characters" % \
                str(self.password_min_length))
            password_ok = False
        elif len(self.get_password()) > self.password_max_length:
            self.password_ok.hide()
            self.clear_errors()
            self.password_error("Longer than %s characters" % \
                str(self.password_max_length))
            password_ok = False
        elif do_passwd_complexity_checks():
            password_ok = self.check_password_complexity(self.get_password())
            if not password_ok:
                self.password_ok.hide()
                self.clear_errors()
                self.password_error("Must contain upper, lower, digit, and special character")

        if password_ok:
            self.password_ok.hide()
            self.clear_errors()
            password_ok = validation.gtk_password_validate(
                self.controller,
                self.password,
                self.verified_password,
                self.password_ok,
                self.password_error_label,
                self.password_strength,
                self.allow_password_empty,
            )

        complete = complete and password_ok

        self.controller.allow_go_forward(complete)


class PageKde(PageBase):
    plugin_breadcrumb = 'ubiquity/text/breadcrumb_user'

    def __init__(self, controller, *args, **kwargs):
        PageBase.__init__(self, *args, **kwargs)
        self.controller = controller

        from PyQt5 import uic
        from PyQt5.QtGui import QPixmap

        self.plugin_widgets = uic.loadUi(
            '/usr/share/ubiquity/qt/stepNvBMCpasswd.ui')
        self.page = self.plugin_widgets

        if self.controller.oem_config:
            self.page.login_pass.hide()


        # The UserSetup component takes care of preseeding passwd/user-uid.
        misc.execute_root('apt-install', 'oem-config-kde')

        warningIcon = QPixmap(
            "/usr/share/icons/oxygen/48x48/status/dialog-warning.png")
        self.page.password_error_image.setPixmap(warningIcon)

        self.clear_errors()

        # self.page.password.textChanged[str].connect(self.on_password_changed)
        # self.page.verified_password.textChanged[str].connect(
        #    self.on_verified_password_changed)
        self.page.login_pass.clicked[bool].connect(self.on_login_pass_clicked)
        self.page.login_auto.clicked[bool].connect(self.on_login_auto_clicked)

        self.page.password_debug_warning_label.setVisible(
            'UBIQUITY_DEBUG' in os.environ)

    def on_password_changed(self):
        pass

    def on_verified_password_changed(self):
        pass

    def get_password(self):
        return str(self.page.password.text())

    def get_verified_password(self):
        return str(self.page.verified_password.text())

    def password_error(self, msg):
        self.page.password_error_reason.setText(msg)
        self.page.password_error_image.show()
        self.page.password_error_reason.show()

    def clear_errors(self):
        self.page.password_error_image.hide()
        self.page.password_error_reason.hide()


class PageDebconf(PageBase):
    plugin_title = 'ubiquity/text/wireless_password_label'

    def __init__(self, controller, *args, **kwargs):
        self.controller = controller


class PageNoninteractive(PageBase):
    def __init__(self, controller, *args, **kwargs):
        PageBase.__init__(self, *args, **kwargs)
        self.controller = controller
        self.password = ''
        self.verifiedpassword = ''
        self.console = self.controller._wizard.console


    def get_password(self):
        """Get the user's password."""
        return self.controller.dbfilter.db.get('bmcpasswd/bmc-password')

    def get_verified_password(self):
        """Get the user's password confirmation."""
        return self.controller.dbfilter.db.get('bmcpasswd/bmc-password-again')

    def password_error(self, msg):
        """The selected password was bad."""
        print('\nBad password: %s' % msg, file=self.console)
        import getpass
        self.password = getpass.getpass('Password: ')
        self.verifiedpassword = getpass.getpass('Password again: ')

    def clear_errors(self):
        pass


class Page(plugin.Plugin):
    def prepare(self, unfiltered=False):
        if FrontendIsUbiquity():
            return ['/usr/share/nvidia/nv-bmc-passwd.sh']

        # Load debconf templates
        (_, _) = subprocess.getstatusoutput(
            '/usr/share/nvidia/nv-bmc-passwd.sh load_only')

        # We need to call info_loop as we switch to the page so the next button
        # gets disabled.
        self.ui.info_loop(None)

        # End here, don't return a command to fall through to
        # Page[Gtk|Kde] cases
        return

    def ok_handler(self):
        self.ui.clear_errors()

        password = self.ui.get_password()
        password_confirm = self.ui.get_verified_password()

        self.preseed('bmcpasswd/bmc-password', password)
        self.preseed('bmcpasswd/bmc-password-again', password_confirm)
        if self.ui.controller.oem_config:
            self.preseed('passwd/user-uid', '29999')
        else:
            self.preseed('passwd/user-uid', '')

        plugin.Plugin.ok_handler(self)

    def error(self, priority, question):
        if question.startswith('bmcpasswd/bmc-password-'):
            self.ui.password_error(self.extended_description(question))
        else:
            self.ui.error_dialog(
                self.description(question),
                self.extended_description(question))
        return plugin.Plugin.error(self, priority, question)

class Install(plugin.InstallPlugin):
    def install(self, target, progress, *args, **kwargs):
        import syslog

        # By the time we get to the install phase, console output in
        # GTK mode no longer gets saved to oem-config.log.  So instead
        # we'll use the following log function
        def log_to_syslog(msg):
            syslog.syslog("oem-config: %s: %s" % (NAME, msg))

        bmcusername = get_first_user()
        bmcpassword = ""

        try:
            # Retrieve user specified password
            bmcpassword = self.db.get('bmcpasswd/bmc-password')

            # If debconf entries are there, but empty
            if not bmcpassword:
                bmcpassword = bmcusername
        except:
            log_to_syslog("Failed to retrieve BMC user info")
            bmcpassword = bmcusername

        log_to_syslog("Calling createUserIPMI")
        createUserIPMI(bmcusername, bmcpassword)
        return plugin.InstallPlugin.install(
            self, target, progress, *args, **kwargs)
