# Copyright 2017 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 argparse
import ctypes
import fcntl
import json
from select import select
import socket
import struct
import os
import subprocess
import sys
import time

if os.path.exists('/opt/confluent/bin/apiclient'):
    apiclient = '/opt/confluent/bin/apiclient'
elif os.path.exists('/etc/confluent/apiclient'):
    apiclient = '/etc/confluent/apiclient'

class IpmiMsg(ctypes.Structure):
    _fields_ = [('netfn', ctypes.c_ubyte),
                ('cmd', ctypes.c_ubyte),
                ('data_len', ctypes.c_short),
                ('data', ctypes.POINTER(ctypes.c_ubyte))]


class IpmiSystemInterfaceAddr(ctypes.Structure):
    _fields_ = [('addr_type', ctypes.c_int),
                ('channel', ctypes.c_short),
                ('lun', ctypes.c_ubyte)]


class IpmiRecv(ctypes.Structure):
    _fields_ = [('recv_type', ctypes.c_int),
                ('addr', ctypes.POINTER(IpmiSystemInterfaceAddr)),
                ('addr_len', ctypes.c_uint),
                ('msgid', ctypes.c_long),
                ('msg', IpmiMsg)]


class IpmiReq(ctypes.Structure):
    _fields_ = [('addr', ctypes.POINTER(IpmiSystemInterfaceAddr)),
                ('addr_len', ctypes.c_uint),
                ('msgid', ctypes.c_long),
                ('msg', IpmiMsg)]


_IONONE = 0
_IOWRITE = 1
_IOREAD = 2
IPMICTL_SET_MY_ADDRESS_CMD = (
    _IOREAD << 30 | ctypes.sizeof(ctypes.c_uint) << 16
    | ord('i') << 8 | 17)  # from ipmi.h
IPMICTL_SEND_COMMAND = (
    _IOREAD << 30 | ctypes.sizeof(IpmiReq) << 16
    | ord('i') << 8 | 13)  # from ipmi.h
# next is really IPMICTL_RECEIVE_MSG_TRUNC, but will only use that
IPMICTL_RECV = (
    (_IOWRITE | _IOREAD) << 30 | ctypes.sizeof(IpmiRecv) << 16
    | ord('i') << 8 | 11)  # from ipmi.h
BMC_SLAVE_ADDR = ctypes.c_uint(0x20)
CURRCHAN = 0xf
ADDRTYPE = 0xc


class Session(object):
    def __init__(self, devnode='/dev/ipmi0'):
        """Create a local session inband

        :param: devnode: The path to the ipmi device
        """
        self.ipmidev = open(devnode, 'r+')
        fcntl.ioctl(self.ipmidev, IPMICTL_SET_MY_ADDRESS_CMD, BMC_SLAVE_ADDR)
        # the interface is initted, create some reusable memory for our session
        self.databuffer = ctypes.create_string_buffer(4096)
        self.req = IpmiReq()
        self.rsp = IpmiRecv()
        self.addr = IpmiSystemInterfaceAddr()
        self.req.msg.data = ctypes.cast(
            ctypes.addressof(self.databuffer),
            ctypes.POINTER(ctypes.c_ubyte))
        self.rsp.msg.data = self.req.msg.data
        self.userid = None
        self.password = None

    def await_reply(self):
        rd, _, _ = select((self.ipmidev,), (), (), 1)
        while not rd:
            rd, _, _ = select((self.ipmidev,), (), (), 1)

    def pause(self, seconds):
        time.sleep(seconds)

    @property
    def parsed_rsp(self):
        response = {'netfn': self.rsp.msg.netfn, 'command': self.rsp.msg.cmd,
                    'code': bytearray(self.databuffer.raw)[0],
                    'data': bytearray(
                        self.databuffer.raw[1:self.rsp.msg.data_len])}
        return response

    def await_config(s, bmccfg, channel):
        vlan = bmccfg.get('bmcvlan', None)
        ipv4 = bmccfg.get('bmcipv4', None)
        prefix = bmccfg.get('prefixv4', None)
        gw = bmccfg.get('bmcgw', None)



    def raw_command(self,
                    netfn,
                    command,
                    data=(),
                    bridge_request=None,
                    retry=True,
                    delay_xmit=None,
                    timeout=None,
                    waitall=False, rslun=0):
        self.addr.channel = CURRCHAN
        self.addr.addr_type = ADDRTYPE
        self.addr.lun = rslun
        self.req.addr_len = ctypes.sizeof(IpmiSystemInterfaceAddr)
        self.req.addr = ctypes.pointer(self.addr)
        self.req.msg.netfn = netfn
        self.req.msg.cmd = command
        if data:
            data = memoryview(bytearray(data))
            try:
                self.databuffer[:len(data)] = data[:len(data)]
            except ValueError:
                self.databuffer[:len(data)] = data[:len(data)].tobytes()
        self.req.msg.data_len = len(data)
        fcntl.ioctl(self.ipmidev, IPMICTL_SEND_COMMAND, self.req)
        self.await_reply()
        self.rsp.msg.data_len = 4096
        self.rsp.addr = ctypes.pointer(self.addr)
        self.rsp.addr_len = ctypes.sizeof(IpmiSystemInterfaceAddr)
        fcntl.ioctl(self.ipmidev, IPMICTL_RECV, self.rsp)
        return self.parsed_rsp


def _is_tsm(model):
    return model in ('7y00', '7z01', '7y98', '7y99')

def set_port(s, port, vendor, model):
    oport = port
    if vendor not in ('IBM', 'Lenovo'):
        raise Exception('{0} not implemented'.format(vendor))
    if _is_tsm(model):
        return set_port_tsm(s, port, model)
    else:
        set_port_xcc(s, port, model)
        return 1


def set_port_tsm(s, port, model):
    oport = port
    sys.stdout.write('Setting TSM port to "{}"...'.format(oport))
    sys.stdout.flush()
    if port == 'ocp':
        s.raw_command(0x32, 0x71, b'\x00\x01\x00')
    elif port == 'dedicated':
        s.raw_command(0x32, 0x71, b'\x00\x00\x00')
    else:
        raise Exception("Unsupported port for TSM")
    timer = 15
    while timer:
        timer = timer - 1
        time.sleep(1.0)
        sys.stdout.write('.')
        sys.stdout.flush()
    if port == 'ocp':
        iface = 0
        s.raw_command(0x32, 0x71, b'\x00\x00\x03')
    elif port == 'dedicated':
        iface = 1
        s.raw_command(0x32, 0x71, b'\x00\x01\x03')
    rsp = s.raw_command(0x32, 0x72, bytearray([4, iface, 0]))
    print('Complete')
    return int(rsp['data'][0])


def set_port_xcc(s, port, model):
    oport = port
    if port.lower() == 'dedicated':
        port = b'\x01'
    elif port.lower() in ('ml2', 'ocp'):
        port = b'\x02\x00'
    elif port.lower() == 'lom':
        if model == '7x58':
            port = b'\x00\x02'
        else:
            port = b'\x00\x00'
    else:
        port = port.split(' ')
        port = bytes(bytearray([int(x) for x in port]))
    currport = bytes(s.raw_command(0xc, 2, b'\x01\xc0\x00\x00')['data'][1:])
    if port == currport:
        sys.stdout.write('XCC port already set to "{}"\n'.format(oport))
        return
    sys.stdout.write('Setting XCC port to "{}"...'.format(oport))
    sys.stdout.flush()
    s.raw_command(0xc, 1, b'\x01\xc0' + port)
    tries = 60
    while currport != port and tries:
        tries -= 1
        time.sleep(0.5)
        sys.stdout.write('.')
        sys.stdout.flush()
        currport = bytes(s.raw_command(0xc, 2, b'\x01\xc0\x00\x00')['data'][1:])
    if not tries:
        raise Exception('Timeout attempting to set port')
    sys.stdout.write('Complete\n')


def check_vlan(s, vlan, channel):
    if vlan == 'off':
        vlan = b'\x00\x00'
    else:
        vlan = int(vlan)
        if vlan:
            vlan = vlan | 32768
        vlan = struct.pack('<H', vlan)
    currvlan = bytes(s.raw_command(0xc, 2, bytearray([channel, 0x14 ,0, 0]))['data'][1:])
    if bytearray(currvlan)[1] & 0b10000000 == 0:
        currvlan = b'\x00\x00'
    return currvlan == vlan


def set_vlan(s, vlan, channel):
    ovlan = vlan
    if vlan == 'off':
        vlan = b'\x00\x00'
    else:
        vlan = int(vlan)
        if vlan:
            vlan = vlan | 32768
        vlan = struct.pack('<H', vlan)
    if check_vlan(s, ovlan, channel):
        sys.stdout.write('VLAN already configured to "{0}"\n'.format(ovlan))
        return False
    rsp = s.raw_command(0xc, 1, bytearray([channel, 0x14]) + vlan)
    if rsp.get('code', 1) == 0:
        print('VLAN configured to "{}"'.format(ovlan))
    else:
        print('Error setting vlan: ' + repr(rsp))
    return True


def get_lan_channel(s):
    for chan in range(1, 16):
        rsp = s.raw_command(6, 0x42, bytearray([chan]))['data']
        if not rsp:
            continue
        medtype = int(rsp[1]) & 0b1111111
        if medtype not in (4, 6):
            continue
        rsp = s.raw_command(0xc, 2, bytearray([chan, 5, 0, 0]))
        if rsp.get('code', 1) == 0:
            return chan
    return 1


def check_ipv4(s, ipaddr, channel):
    ipaddr = bytearray(socket.inet_aton(ipaddr))
    rsp = s.raw_command(0xc, 2, bytearray([channel, 3, 0, 0]))['data'][-4:]
    return rsp == ipaddr

def set_ipv4(s, ipaddr, channel):
    oipaddr = ipaddr
    ipaddr = bytearray(socket.inet_aton(ipaddr))
    if check_ipv4(s, oipaddr, channel):
        print('IP Address already set to {}'.format(oipaddr))
        return False
    rsp = int(s.raw_command(0xc, 2, bytearray([channel, 4, 0, 0]))['data'][1]) & 0b1111
    if rsp != 1:
        sys.stdout.write("Changing configuration to static...")
        sys.stdout.flush()
        resp = s.raw_command(0xc, 1, bytearray([channel, 4, 1]))
        tries = 0
        while rsp != 1 and tries < 30:
            sys.stdout.write('.')
            sys.stdout.flush()
            tries += 1
            time.sleep(0.5)
            rsp = int(s.raw_command(0xc, 2, bytearray([channel, 4, 0, 0]))['data'][1]) & 0b1111
        sys.stdout.write('Complete\n')
        sys.stdout.flush()
    print('Setting IP to {}'.format(oipaddr))
    s.raw_command(0xc, 1, bytearray([channel, 3]) + ipaddr)
    return True


def check_subnet(s, prefix, channel):
    prefix = int(prefix)
    mask = bytearray(struct.pack('!I', (2**32 - 1) ^ (2**(32 - prefix) - 1)))
    rsp = s.raw_command(0xc, 2, bytearray([channel, 6, 0, 0]))['data'][-4:]
    return rsp == mask

def set_subnet(s, prefix, channel):
    oprefix = prefix
    prefix = int(prefix)
    mask = bytearray(struct.pack('!I', (2**32 - 1) ^ (2**(32 - prefix) - 1)))
    if check_subnet(s, prefix, channel):
        print('Subnet Mask already set to /{}'.format(oprefix))
        return False
    print('Setting subnet mask to /{}'.format(oprefix))
    s.raw_command(0xc, 1, bytearray([channel, 6]) + mask)
    return True


def check_gateway(s, gw, channel):
    gw = bytearray(socket.inet_aton(gw))
    rsp = s.raw_command(0xc, 2, bytearray([channel, 12, 0, 0]))['data'][-4:]
    return rsp == gw

def set_gateway(s, gw, channel):
    ogw = gw
    gw = bytearray(socket.inet_aton(gw))
    rsp = s.raw_command(0xc, 2, bytearray([channel, 12, 0, 0]))['data'][-4:]
    if check_gateway(s, ogw, channel):
        print('Gateway already set to {}'.format(ogw))
        return False
    print('Setting gateway to {}'.format(ogw))
    s.raw_command(0xc, 1, bytearray([channel, 12]) + gw)
    return True

def dotwait():
    sys.stdout.write('.')
    sys.stdout.flush()
    time.sleep(0.5)

def main():
    a = argparse.ArgumentParser(description='Locally configure a BMC device')
    a.add_argument('-v', help='vlan id or off to disable vlan tagging')
    a.add_argument('-p', help='Which port to use (dedicated, lom, ocp, ml2)')
    a.add_argument('-i', help='JSON configuration file to read for '
                   'configuration')
    a.add_argument('-c', help='Use Confluent API to direct BMC configuration',
                   action='store_true')
    args = a.parse_args()
    if args.i:
        bmccfg = json.load(open(args.i))
    elif args.c:
        bmccfgsrc = subprocess.check_output(
            [sys.executable, apiclient, '/confluent-api/self/bmcconfig', '-j'])
        bmccfg = json.loads(bmccfgsrc)
    else:
        bmccfg = {}
    if args.p is not None:
        bmccfg['bmcport'] = args.p
    if args.v is not None:
        bmccfg['bmcvlan'] = args.v
    vendor = open('/sys/devices/virtual/dmi/id/sys_vendor').read()
    vendor = vendor.strip()
    try:
        model = open('/sys/devices/virtual/dmi/id/product_sku').read().strip()
        if model == 'none':
            raise Exception('No SKU')
    except Exception:
        model = open('/sys/devices/virtual/dmi/id/product_name').read()
    if vendor in ('Lenovo', 'IBM'):
        if '[' in model and ']' in model:
            model = model.split('[')[1].split(']')[0]
        model = model[:4].lower()
    s = Session('/dev/ipmi0')
    if not bmccfg:
        print("No BMC configuration specified, exiting.")
        return
    if bmccfg.get('bmcport', None):
        channel = set_port(s, bmccfg['bmcport'], vendor, model)
    else:
        channel = get_lan_channel(s)
    awaitvlan = False
    awaitip = False
    awaitprefix = False
    awaitgw = False
    if bmccfg.get('bmcvlan', None):
        awaitvlan = set_vlan(s, bmccfg['bmcvlan'], channel)
    if bmccfg.get('bmcipv4', None):
        awaitip = set_ipv4(s, bmccfg['bmcipv4'], channel)
    if bmccfg.get('prefixv4', None):
        awaitprefix = set_subnet(s, bmccfg['prefixv4'], channel)
    if bmccfg.get('bmcgw', None):
        awaitgw = set_gateway(s, bmccfg['bmcgw'], channel)
    sys.stdout.write('Waiting for changes to take effect...')
    sys.stdout.flush()
    while awaitvlan and not check_vlan(s, bmccfg['bmcvlan'], channel):
        dotwait()
    while awaitip and not check_ipv4(s, bmccfg['bmcipv4'], channel):
        dotwait()
    while awaitprefix and not check_subnet(s, bmccfg['prefixv4'], channel):
        dotwait()
    while awaitprefix and not check_gateway(s, bmccfg['bmcgw'], channel):
        dotwait()
    sys.stdout.write('done\n')
    sys.stdout.flush()
    #await_config(s, bmccfg, channel)


if __name__ == '__main__':
    main()
