#!/usr/bin/python3
'''
convert-to-json

This file is part of RTSLib-fb.
Copyright (c) 2013-2016 by Red Hat, Inc.

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.
'''

#
# A script to convert .lio format save files to json format.
#

import json
import re
from pathlib import Path


def human_to_bytes(hsize, kilo=1024):
    '''
    This function converts human-readable amounts of bytes to bytes.
    It understands the following units :
        - I{B} or no unit present for Bytes
        - I{k}, I{K}, I{kB}, I{KB} for kB (kilobytes)
        - I{m}, I{M}, I{mB}, I{MB} for MB (megabytes)
        - I{g}, I{G}, I{gB}, I{GB} for GB (gigabytes)
        - I{t}, I{T}, I{tB}, I{TB} for TB (terabytes)

    Note: The definition of I{kilo} defaults to 1kB = 1024Bytes.
    Strictly speaking, those should not be called I{kB} but I{kiB}.
    You can override that with the optional kilo parameter.

    @param hsize: The human-readable version of the Bytes amount to convert
    @type hsize: string or int
    @param kilo: Optional base for the kilo prefix
    @type kilo: int
    @return: An int representing the human-readable string converted to bytes
    '''
    size = hsize.replace('i', '')
    size = size.lower()
    if not re.match(r"^[0-9\.]+[k|m|g|t]?[b]?$", size):
        raise Exception(f"Cannot interpret size, wrong format: {hsize}")

    size = size.rstrip('ib')

    units = ['k', 'm', 'g', 't']
    try:
        power = units.index(size[-1]) + 1
    except ValueError:
        power = 0
        size = int(size)
    else:
        try:
            size = int(size[:-1])
        except ValueError:
            size = int(float(size[:-1]))

    return size * (int(kilo) ** power)

def parse_yesno(val):
    if val == "yes":
        return 1
    elif val == "no":
        return 0
    else:
        try:
            return int(val)
        except:
            return val

def parse_attributes(txt, cur):
    attribs = {}
    while txt[cur] != "}":
        name = txt[cur]
        val = txt[cur+1]
        attribs[name] = parse_yesno(val)
        cur += 2
    return (cur+1, attribs)

def parse_fileio(txt, cur):
    so = {'plugin': "fileio"}
    while txt[cur] != "}":
        if txt[cur] == "path":
            so["dev"] = txt[cur+1]
            cur += 2
            continue
        if txt[cur] == "size":
            so["size"] = human_to_bytes(txt[cur+1])
            cur += 2
            continue
        if txt[cur] == "buffered":
            # skip, recent LIO doesn't use for fileio
            cur += 2
            continue
        if txt[cur] == "attribute":
            cur, so["attributes"] = parse_attributes(txt, cur+2)
            continue
    return (cur+1, so)

def parse_block(txt, cur):
    so = {'plugin': "block"}
    while txt[cur] != "}":
        if txt[cur] == "path":
            so["dev"] = txt[cur+1]
            cur += 2
            continue
        if txt[cur] == "attribute":
            cur, so["attributes"] = parse_attributes(txt, cur+2)
            continue
    return (cur+1, so)

def parse_ramdisk(txt, cur):
    so = {'plugin': "ramdisk"}
    while txt[cur] != "}":
        if txt[cur] == "nullio":
            so["nullio"] = parse_yesno(txt[cur+1])
            cur += 2
            continue
        if txt[cur] == "size":
            so["size"] = human_to_bytes(txt[cur+1])
            cur += 2
            continue
        if txt[cur] == "attribute":
            cur, so["attributes"] = parse_attributes(txt, cur+2)
            continue
    return (cur+1, so)

so_types = {
    "fileio": parse_fileio,
    "rd_mcp": parse_ramdisk,
    "iblock": parse_block,
}

def parse_storage(txt, cur):
    name = txt[cur+3]
    ty = txt[cur+1]
    cur += 5
    (cur, d) = so_types[ty](txt, cur)
    d["name"] = name
    return (cur, d)

def parse_lun(txt, cur):
    index = int(txt[cur+1])
    plugin, name = txt[cur+3].split(":")
    return cur+4, {'index': index, 'plugin': plugin, 'name': name}

def parse_mapped_lun(txt, cur):
    mlun = {'index': txt[cur+1]}
    cur += 3
    while txt[cur] != "}":
        if txt[cur] == "target_lun":
            mlun["tpg_lun"] = parse_yesno(txt[cur+1])
            cur += 2
            continue
        if txt[cur] == "write_protect":
            mlun["write_protect"] = bool(parse_yesno(txt[cur+1]))
            cur += 2
            continue
    return cur+1, mlun

def parse_acl(txt, cur):
    acl = {'node_wwn': txt[cur+1]}
    mapped_luns = []
    cur += 3
    while txt[cur] != "}":
        if txt[cur] == "attribute":
            cur, acl["attributes"] = parse_attributes(txt, cur+2)
            continue
        if txt[cur] == "auth":
            cur, auth = parse_attributes(txt, cur+2)
            if len(auth):
                acl["auth"] = auth
            continue
        if txt[cur] == "mapped_lun":
            cur, mlun = parse_mapped_lun(txt, cur)
            mapped_luns.append(mlun)
    acl["mapped_luns"] = mapped_luns
    return cur+1, acl

def parse_tpg(tag, txt, cur):
    if tag is None:
        tag = int(txt[cur+1])
        cur += 2
    tpg = {'tag': tag}
    luns = []
    acls = []
    portals = []
    cur += 3
    while txt[cur] != "}":
        if txt[cur] == "enable":
            tpg["enable"] = parse_yesno(txt[cur+1])
            cur += 2
            continue
        if txt[cur] == "attribute":
            cur, tpg["attributes"] = parse_attributes(txt, cur+2)
            continue
        if txt[cur] == "parameter":
            cur, tpg["parameters"] = parse_attributes(txt, cur+2)
            continue
        if txt[cur] == "auth":
            cur, auth = parse_attributes(txt, cur+2)
            if len(auth):
                tpg["auth"] = auth
            continue
        if txt[cur] == "lun":
            cur, lun = parse_lun(txt, cur)
            luns.append(lun)
            continue
        if txt[cur] == "acl":
            cur, acl = parse_acl(txt, cur)
            acls.append(acl)
            continue
        if txt[cur] == "portal":
            ip, port = txt[cur+1].split(":")
            portal = {'ip_address': ip, 'port': port}
            portals.append(portal)
            cur += 2
            continue
    if len(luns):
        tpg["luns"] = luns
    if len(acls):
        tpg["node_acls"] = acls
    if len(portals):
        tpg["portals"] = portals
    return cur+1, tpg


def parse_target(fabric, txt, cur):
    target = {'wwn': txt[cur+1], 'fabric': fabric}
    tpgs = []
    tpgt = None
    # handle multiple tpgts
    if txt[cur+2] == "{":
        extra = 1
    else:
        extra = 0
        tpgt = int(txt[cur+3])
    cur += 2 + extra
    while txt[cur] != "}":
        cur, tpg = parse_tpg(tpgt, txt, cur)
        tpgs.append(tpg)
    target["tpgs"] = tpgs
    return cur+extra, target

def parse_fabric(txt, cur):
    fabric = txt[cur+1]
    cur += 3
    while txt[cur] != "}":
        if txt[cur] == "discovery_auth":
            cur, disco = parse_attributes(txt, cur+2)
            new_disco = {}
            if disco.get("enable"):
                new_disco["discovery_enable_auth"] = disco.get("enable")
            if disco.get("userid"):
                new_disco["discovery_userid"] = disco.get("userid")
            if disco.get("password"):
                new_disco["discovery_password"] = disco.get("password")
            if disco.get("mutual_userid"):
                new_disco["discovery_mutual_userid"] = disco.get("mutual_userid")
            if disco.get("mutual_password"):
                new_disco["discovery_mutual_password"] = disco.get("mutual_password")
            new_disco["name"] = "iscsi"
            fabs.append(new_disco)
            continue
        if txt[cur] == "target":
            cur, t = parse_target(fabric, txt, cur)
            targs.append(t)
            continue
    return cur

sos = []
fabs = []
targs = []

# a basic tokenizer that splits on whitespace and handles double quotes
def split(s):
    new_lst = []
    in_quotes = False
    new_str = []
    for c in s:
        if c not in ' \n\t"':
            new_str.append(c)
        elif c == '"' and not in_quotes:
            in_quotes = True
        elif c == '"' and in_quotes:
            in_quotes = False
            if len(new_str) == 0:
                # don't include things that are set to '""'
                del new_lst[-1]
        elif in_quotes:  # append ws if in quotes
            new_str.append(c)
        elif len(new_str):  # not in quotes, break on ws if anything in new_str
            new_lst.append("".join(new_str))
            new_str = []
        else:
            pass # drop ws

    return new_lst

def parse(txt, cur):
    cur = 0
    end = len(txt) - 1
    while cur != end:
        if txt[cur] == "storage":
            cur, d = parse_storage(txt, cur)
            sos.append(d)
        elif txt[cur] == "fabric":
            cur = parse_fabric(txt, cur)

with Path("/etc/target/scsi_target.lio").open() as f:
    txt = f.read()
    txt = split(txt)
    cur = parse(txt, 0)

output = {'storage_objects': sos, 'fabric_modules': fabs, 'targets': targs}

print(json.dumps(output, indent=2, sort_keys=True))
