#!/usr/bin/env python
import json
import os
from subprocess import PIPE, run, check_output
import sys


FUNC = sys.argv[1]  # add-crd|del-crd|test-last|recover-last|test-crd|recover-crd|test-migrate|migrate
NS = len(sys.argv) > 2 and sys.argv[2] or "default"


def checkKubeconfig():
    KUBECONFIG = "KUBECONFIG" in os.environ and os.environ['KUBECONFIG'] or None
    if KUBECONFIG is None or KUBECONFIG == "":
        print("ENV KUBECONFIG EMPTY")
        exit(1)


def recoverLast():
    print("GET BareMetalHostProfile list")
    cmd = "kubectl -n {} get bmhp -o json".format(NS).split(" ")
    result = run(cmd, stdout=PIPE, stderr=PIPE)
    if result.returncode != 0:
        print(result.stderr.decode())
        exit(1)
    bmhp_list = json.loads(result.stdout)

    for bmhp in bmhp_list['items']:
        name = bmhp['metadata']['name']
        if 'annotations' in bmhp['metadata'] and 'kubectl.kubernetes.io/last-applied-configuration' in bmhp['metadata']['annotations']:
            bmhp_old = json.loads(bmhp['metadata']['annotations']['kubectl.kubernetes.io/last-applied-configuration'])
        else:
            print("FAIL get last-applied-configuration for {}".format(name))
            continue

        patch = []

        devices = bmhp['spec']['devices']
        for i in range(0, len(devices)):
            device = bmhp['spec']['devices'][i]
            device_old = bmhp_old['spec']['devices'][i]

            if 'maxSizeGiB' in device_old['device'] and 'maxSize' not in device['device']:
                patch.append({
                    "op": "add",
                    "path": "/spec/devices/{}/device/maxSize".format(i),
                    "value": "{}Gi".format(device_old['device']['maxSizeGiB'])
                })
            if 'minSizeGiB' in device_old['device'] and 'minSize' not in device['device']:
                patch.append({
                    "op": "add",
                    "path": "/spec/devices/{}/device/minSize".format(i),
                    "value": "{}Gi".format(device_old['device']['minSizeGiB'])
                })

            if 'partitions' in device:
                partitions = device['partitions']
                for p in range(0, len(partitions)):
                    partition = device['partitions'][p]

                    partition_old = bmhp_old['spec']['devices'][i]['partitions'][p]
                    if 'sizeGiB' in partition_old and 'size' not in partition:
                        patch.append({
                            "op": "add",
                            "path": "/spec/devices/{}/partitions/{}/size".format(i, p),
                            "value": "{}Gi".format(partition_old['sizeGiB'])
                        })

        if 'logicalVolumes' in bmhp['spec'] and 'logicalVolumes' in bmhp_old['spec']:
            logicalVolumes = bmhp['spec']['logicalVolumes']
            for i in range(0, len(logicalVolumes)):
                logicalVolume = bmhp['spec']['logicalVolumes'][i]
                logicalVolume_old = bmhp_old['spec']['logicalVolumes'][i]

                if 'sizeGiB' in logicalVolume_old and 'size' not in logicalVolume:
                    patch.append({
                        "op": "add",
                        "path": "/spec/logicalVolumes/{}/size".format(i),
                        "value": "{}Gi".format(logicalVolume_old['sizeGiB'])
                    })

        if 'softRaidDevices' in bmhp['spec'] and 'softRaidDevices' in bmhp_old['spec']:
            softRaidDevices = bmhp['spec']['softRaidDevices']
            for i in range(0, len(softRaidDevices)):
                softRaidDevice = bmhp['spec']['softRaidDevices'][i]
                softRaidDevice_old = bmhp_old['spec']['softRaidDevices'][i]

                if 'devices' in softRaidDevice:
                    devices = bmhp['spec']['softRaidDevices'][i]['devices']
                    for p in range(0, len(devices)):
                        device = bmhp['spec']['softRaidDevices'][i]['devices'][p]
                        device_old = bmhp_old['spec']['softRaidDevices'][i]['devices'][p]

                        if 'maxSizeGiB' in device_old and 'maxSize' not in device:
                            patch.append({
                                "op": "add",
                                "path": "/spec/softRaidDevices/{}/device/{}/maxSize".format(i, p),
                                "value": "{}Gi".format(device_old['maxSizeGiB'])
                            })

                        if 'minSizeGiB' in device_old and 'minSize' not in device:
                            patch.append({
                                "op": "add",
                                "path": "/spec/softRaidDevices/{}/device/{}/minSize".format(i, p),
                                "value": "{}Gi".format(device_old['minSizeGiB'])
                            })

        if 'volumeGroups' in bmhp['spec'] and 'volumeGroups' in bmhp_old['spec']:
            volumeGroups = bmhp['spec']['volumeGroups']
            for i in range(0, len(volumeGroups)):
                volumeGroup = bmhp['spec']['volumeGroups'][i]
                volumeGroup_old = bmhp_old['spec']['volumeGroups'][i]

                if 'minSizeGiB' in volumeGroup_old and 'minSize' not in volumeGroup:
                    patch.append({
                        "op": "add",
                        "path": "/spec/volumeGroups/{}/minSize".format(i),
                        "value": "{}Gi".format(volumeGroup_old['minSizeGiB'])
                    })

                if 'devices' in volumeGroup:
                    devices = volumeGroup['devices']
                    for p in range(0, len(devices)):
                        device = volumeGroup['devices'][p]
                        device_old = volumeGroup_old['devices'][p]

                        if 'maxSizeGiB' in device_old and 'maxSize' not in device:
                            patch.append({
                                "op": "add",
                                "path": "/spec/volumeGroups/{}/device/{}/maxSize".format(i, p),
                                "value": "{}Gi".format(device_old['maxSizeGiB'])
                            })

                        if 'minSizeGiB' in device_old and 'minSize' not in device:
                            patch.append({
                                "op": "add",
                                "path": "/spec/volumeGroups/{}/device/{}/minSize".format(i, p),
                                "value": "{}Gi".format(device_old['minSizeGiB'])
                            })

        if len(patch) > 0:
            p = json.dumps(patch)
            print("PATCH BareMetalHostProfile {}".format(name))
            cmd = "kubectl -n {} patch bmhp {} --type json --patch '{}'".format(NS, name, p)
            print(cmd)
            if FUNC == "recover-last":
                output = check_output(cmd, shell=True)
                print(output.decode())
        else:
            print("BareMetalHostProfile {}: no updates".format(name))

    print("DONE")
    print()


def addCRD():
    patch = [
        {
            "op": "add",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/devices/items/properties/device/properties/minSizeGiB",
            "value": {"description": "deprecated", "format": "float", "type": "number"}
        },
        {
            "op": "add",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/devices/items/properties/device/properties/maxSizeGiB",
            "value": {"description": "deprecated", "format": "float", "type": "number"}
        },
        {
            "op": "add",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/devices/items/properties/partitions/items/properties/sizeGiB",
            "value": {"description": "deprecated", "format": "float", "type": "number"}
        },
        {
            "op": "add",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/logicalVolumes/items/properties/sizeGiB",
            "value": {"description": "deprecated", "format": "float", "type": "number"}
        },
        {
            "op": "add",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/softRaidDevices/items/properties/devices/items/properties/minSizeGiB",
            "value": {"description": "deprecated", "format": "float", "type": "number"}
        },
        {
            "op": "add",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/softRaidDevices/items/properties/devices/items/properties/maxSizeGiB",
            "value": {"description": "deprecated", "format": "float", "type": "number"}
        },
        {
            "op": "add",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/volumeGroups/items/properties/minSizeGiB",
            "value": {"description": "deprecated", "format": "float", "type": "number"}
        },
        {
            "op": "add",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/volumeGroups/items/properties/devices/items/properties/minSizeGiB",
            "value": {"description": "deprecated", "format": "float", "type": "number"}
        },
        {
            "op": "add",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/volumeGroups/items/properties/devices/items/properties/maxSizeGiB",
            "value": {"description": "deprecated", "format": "float", "type": "number"}
        }
    ]

    p = json.dumps(patch)
    print("PATCH CRD baremetalhostprofiles.metal3.io ADD deprecated fields")
    cmd = "kubectl patch crd baremetalhostprofiles.metal3.io --type json --patch '{}'".format(p)
    print(cmd)
    output = check_output(cmd, shell=True)
    print(output.decode())


def delCRD():
    patch = [
        {
            "op": "remove",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/devices/items/properties/device/properties/minSizeGiB",
        },
        {
            "op": "remove",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/devices/items/properties/device/properties/maxSizeGiB",
        },
        {
            "op": "remove",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/devices/items/properties/partitions/items/properties/sizeGiB",
        },
        {
            "op": "remove",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/logicalVolumes/items/properties/sizeGiB",
        },
        {
            "op": "remove",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/softRaidDevices/items/properties/devices/items/properties/minSizeGiB",
        },
        {
            "op": "remove",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/softRaidDevices/items/properties/devices/items/properties/maxSizeGiB",
        },
        {
            "op": "remove",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/volumeGroups/items/properties/minSizeGiB",
        },
        {
            "op": "remove",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/volumeGroups/items/properties/devices/items/properties/minSizeGiB",
        },
        {
            "op": "remove",
            "path": "/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/volumeGroups/items/properties/devices/items/properties/maxSizeGiB",
        }
    ]

    p = json.dumps(patch)
    print("PATCH CRD baremetalhostprofiles.metal3.io REMOVE deprecated fields")
    cmd = "kubectl patch crd baremetalhostprofiles.metal3.io --type json --patch '{}'".format(p)
    print(cmd)
    output = check_output(cmd, shell=True)
    print(output.decode())


def migrate():
    print("GET BareMetalHostProfile list")
    cmd = "kubectl -n {} get bmhp -o json".format(NS).split(" ")
    result = run(cmd, stdout=PIPE, stderr=PIPE)
    bmhp_list = json.loads(result.stdout)

    for bmhp in bmhp_list['items']:
        name = bmhp['metadata']['name']

        patch = []

        devices = bmhp['spec']['devices']
        for i in range(0, len(devices)):
            device = devices[i]

            if 'maxSizeGiB' in device['device'] and 'maxSize' not in device['device']:
                patch.append({
                    "op": "add",
                    "path": "/spec/devices/{}/device/maxSize".format(i),
                    "value": "{}Gi".format(device['device']['maxSizeGiB'])
                })

            if 'minSizeGiB' in device['device'] and 'minSize' not in device['device']:
                patch.append({
                    "op": "add",
                    "path": "/spec/devices/{}/device/minSize".format(i),
                    "value": "{}Gi".format(device['device']['minSizeGiB'])
                })

            if 'partitions' in device:
                partitions = device['partitions']
                for p in range(0, len(partitions)):
                    partition = partitions[p]

                    if 'sizeGiB' in partition and 'size' not in partition:
                        patch.append({
                            "op": "add",
                            "path": "/spec/devices/{}/partitions/{}/size".format(i, p),
                            "value": "{}Gi".format(partition['sizeGiB'])
                        })

        if 'logicalVolumes' in bmhp['spec']:
            logicalVolumes = bmhp['spec']['logicalVolumes']
            for i in range(0, len(logicalVolumes)):
                logicalVolume = logicalVolumes[i]

                if 'sizeGiB' in logicalVolume and 'size' not in logicalVolume:
                    patch.append({
                        "op": "add",
                        "path": "/spec/logicalVolumes/{}/size".format(i),
                        "value": "{}Gi".format(logicalVolume['sizeGiB'])
                    })

        if 'softRaidDevices' in bmhp['spec']:
            softRaidDevices = bmhp['spec']['softRaidDevices']
            for i in range(0, len(softRaidDevices)):
                softRaidDevice = softRaidDevices[i]

                if 'devices' in softRaidDevice:
                    devices = softRaidDevice['devices']
                    for p in range(0, len(devices)):
                        device = devices[p]

                        if 'maxSizeGiB' in device and 'maxSize' not in device:
                            patch.append({
                                "op": "add",
                                "path": "/spec/softRaidDevices/{}/device/{}/maxSize".format(i, p),
                                "value": "{}Gi".format(device['maxSizeGiB'])
                            })

                        if 'minSizeGiB' in device and 'minSize' not in device:
                            patch.append({
                                "op": "add",
                                "path": "/spec/softRaidDevices/{}/device/{}/minSize".format(i, p),
                                "value": "{}Gi".format(device['minSizeGiB'])
                            })

        if 'volumeGroups' in bmhp['spec']:
            volumeGroups = bmhp['spec']['volumeGroups']
            for i in range(0, len(volumeGroups)):
                volumeGroup = volumeGroups[i]

                if 'minSizeGiB' in volumeGroup and 'minSize' not in volumeGroup:
                    patch.append({
                        "op": "add",
                        "path": "/spec/volumeGroups/{}/minSize".format(i),
                        "value": "{}Gi".format(volumeGroup['minSizeGiB'])
                    })

                if 'devices' in volumeGroup:
                    devices = volumeGroup['devices']
                    for p in range(0, len(devices)):
                        device = devices[p]

                        if 'maxSizeGiB' in device and 'maxSize' not in device:
                            patch.append({
                                "op": "add",
                                "path": "/spec/volumeGroups/{}/device/{}/maxSize".format(i, p),
                                "value": "{}Gi".format(device['maxSizeGiB'])
                            })

                        if 'minSizeGiB' in device and 'minSize' not in device:
                            patch.append({
                                "op": "add",
                                "path": "/spec/volumeGroups/{}/device/{}/minSize".format(i, p),
                                "value": "{}Gi".format(device['minSizeGiB'])
                            })

        if len(patch) > 0:
            p = json.dumps(patch)
            print("PATCH BareMetalHostProfile {}".format(name))
            cmd = "kubectl -n {} patch bmhp {} --type json --patch '{}'".format(NS, name, p)
            print(cmd)
            if FUNC == "recover-crd" or FUNC == "migrate":
                output = check_output(cmd, shell=True)
                print(output.decode())
        else:
            print("BareMetalHostProfile {}: no updates".format(name))

    print("DONE")
    print()


def recoverCRD():
    addCRD()
    migrate()
    delCRD()


BIN = os.path.basename(sys.argv[0])

usage_text = '''
use {BIN} add-crd|del-crd|test-last|recover-last|test-crd|recover-crd|test-migrate|migrate [namespace]
\t add-crd: add deprecated fields to crd
\t del-crd: delete deprecated fields from crd
\t test-last: test recover from last-applied-configuration with only print commands
\t recover-last: recover from last-applied-configuration
\t test-crd: test recover with add deprecated fields to crd with only print commands
\t recover-crd: recover with add deprecated fields to crd
\t test-migrate test migration fields with only print commands
\t migrate: migration fields
'''.format(BIN=BIN)

help_text = '''
main use:
---------
Export env variable KUBECONFIG for mgmgt cluster
$ export KUBECONFIG=~/.kube/kubeconfig-mgmt

Necessarily create objects backup
$ kubectl -n {NS} get bmhp -o yaml > bmhp-backup.yml


Try recover values from objects by re add deprecated fields to CRD

1. Recover deprecated fields in CRD
$ {BIN} add-crd

2. Recover possible if patches printed, review proposed patches (it read-only command)
$ {BIN} test-migrate {NS}

3. Create objects backup with deprecated fields
$ kubectl -n {NS} get bmhp -p yaml > bmhp-backup-1.yml

4. Migrate deprecated fields
$ {BIN} migrate {NS}

5. In any case do delete deprecated fields from CRD
$ {BIN} del-crd


If previous variant not worked do try recover values from last-applied-configuration annotation.

1. Recover possible if patches printed, review proposed patches (it read-only command)
$ {BIN} test-last {NS}

2. Recover values deprecated fields
$ {BIN} recover-last {NS}
---------
'''.format(BIN=BIN, NS=NS if NS != "" and NS != "default" else "namespace")


def print_help():
    print(usage_text)
    print(help_text)
    exit(1)


if FUNC == "help":
    print_help()

checkKubeconfig()

if FUNC == "test-last" or FUNC == "recover-last":
    recoverLast()
elif FUNC == "test-crd" or FUNC == "recover-crd":
    recoverCRD()
elif FUNC == "test-migrate" or FUNC == "migrate":
    migrate()
elif FUNC == "add-crd":
    addCRD()
elif FUNC == "del-crd":
    delCRD()
else:
    print_help()
