#!/opt/slapos/parts/python3/bin/python3

import os
import sys
import subprocess
import time
import getopt
import sqlite3
import ssl
import urllib.request, urllib.error, urllib.parse
from xml.dom import minidom
import json
import hashlib
import string

conn = sqlite3.connect("/opt/slapos/slapproxy.db")
proxy_table_name_dict = {t.rstrip(string.digits): t for t, in conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table'")}

def fmt_date():
  return time.strftime("%Y%m%d")

# get all of the installed software types by checking the SR urls
# return a list, and run routine on all of them
def discover_software():
  qry = conn.execute("SELECT DISTINCT url FROM %(software)s" % proxy_table_name_dict)
  return [row[0] for row in qry if row[0]]

def get_connection_information(software_release):
  if 'software/apache-frontend' not in software_release:
    partition_reference = 'instance-of-%'
  elif 'software/apache-frontend' in software_release:
    partition_reference = 'apache-frontend-1'
  else:
    raise ValueError

  qry = conn.execute("SELECT connection_xml FROM %(partition)s WHERE connection_xml IS NOT NULL AND software_release=? AND partition_reference LIKE ?" % proxy_table_name_dict, (software_release, partition_reference) )
  xml = None
  for row in qry:
    xml = str(row[0])
    break

  if xml is None:
    return (None, None)

  instance = minidom.parseString(xml)
  if partition_reference.startswith('instance-of-'):
    try:
      el = instance.getElementsByTagName('parameter')[0]
      value = el.childNodes[0].nodeValue
    except:
      return "error"

    # remove leading and trailing '"' character
    # from serialized partition information inside SQlite DB
    if value.startswith('"') and value.endswith('"'):
      value = value[1:-1].replace('\\"', '"')

    if not value.startswith("{"):
      # Not a JSON blob - extract parameters by id attribute (e.g. Theia)
      param_dict = {}
      for param_el in instance.getElementsByTagName('parameter'):
        param_id = param_el.getAttribute('id')
        if param_id and len(param_el.childNodes) > 0:
          param_dict[param_id] = str(param_el.childNodes[0].nodeValue)
      if 'url' in param_dict:
        return (param_dict['url'], param_dict)
      return (None, None)

    json_text = json.loads(value)

    if 'family-admin' in json_text:
      return (json_text['family-admin'], json_text['inituser-password'])
    elif 'insecure' in json_text:
      return (json_text, None)
    elif 'url' in json_text:
      return (json_text['url'], None)
    else:
      return (None, None)
  elif partition_reference == 'apache-frontend-1':
    ip = None
    for el in instance.getElementsByTagName('parameter'):
      if el.getAttribute('id') == 'private-ipv4':
        if len(el.childNodes) > 0:
          ip = str(el.childNodes[0].nodeValue) or None
          break
    return (ip, None)
  else:
    raise ValueError

def check_tables():
  if 'partition' not in proxy_table_name_dict:
    print("tables aren't ready yet, your build may have failed, check logs in /opt/slapos/log/")
    sys.exit(0)

def get_build_status(software_release):
  sr_hash = hashlib.md5(software_release.encode()).hexdigest()
  try:
    os.stat(os.path.join('/opt/slapgrid', sr_hash, '.completed'))
  except OSError:
    try:
      f = open("/opt/slapos/log/slapos-node-software-" + fmt_date() + ".log")
    except Exception:
      try:
        f = open("/opt/slapos/log/slapos-node-software.log")
      except Exception:
        return False
    lines = f.readlines()
    if "ERROR" in lines[-3]:
      return "error"
    return False
  return True

# Check if the last two lines show the software finished building.
# If an error came just before this, we'll report failure.
# Otherwise it passed and we can move on.
# We want to open today's log, as it is most up to date

def status():
  software_release_list = discover_software()
  software_release_list_status = [get_build_status(software_release)
          for software_release in software_release_list]
  if len(software_release_list) > 0 and \
      software_release_list_status == [True for i in software_release_list]:
    build = True
  else:
    build = False

  if build:
    is_theia = any('software/theia' in sr for sr in software_release_list)
    has_frontend = any('apache-frontend' in sr for sr in software_release_list)

    if is_theia and not has_frontend:
      theia_sr = [q for q in software_release_list if 'software/theia' in q][0]
      theia_url, theia_info = get_connection_information(theia_sr)

      if theia_url is None:
        print("Build successful, please wait for instantiation")
        sys.exit(2)

      ctx = ssl.create_default_context()
      ctx.check_hostname = False
      ctx.verify_mode = ssl.CERT_NONE

      try:
        r1 = urllib.request.urlopen(theia_url, context=ctx)
        code = r1.getcode()
        if code in (200, 401):
          print(("Build successful, connect to:\n"
                 "  " + theia_url))
          if theia_info is not None:
            if 'username' in theia_info and 'password' in theia_info:
              print(("  username: %s  password: %s" % (theia_info['username'], theia_info['password'])))
        else:
          print("Build successful, please wait for instantiation")
          sys.exit(2)
      except urllib.error.HTTPError as e:
        if e.code in (200, 401):
          print(("Build successful, connect to:\n"
                 "  " + theia_url))
          if theia_info is not None:
            if 'username' in theia_info and 'password' in theia_info:
              print(("  username: %s  password: %s" % (theia_info['username'], theia_info['password'])))
        else:
          print("Theia not available yet: %s" % e)
          print("Build successful, please wait for instantiation")
          sys.exit(2)
      except urllib.error.URLError as e:
        print("Theia not available yet: %s" % e)
        print("Build successful, please wait for instantiation")
        sys.exit(2)
      return

    erp5_sr = [q for q in software_release_list if 'software/apache-frontend' not in q][0]
    frn_sr_list = [q for q in software_release_list
        if 'software/apache-frontend' in q]
    frn_sr = None
    if len(frn_sr_list):
      frn_sr = frn_sr_list[0]
    zope_ip, pw = get_connection_information(erp5_sr)

    try:
      # by default try new apporach (domain based)...
      hostname = open('/tmp/playbook-frontend-custom-domain').read().strip()
    except Exception:
      try:
        # ...and fall back to old one (IP based)
        hostname = open('/tmp/playbook-public-ipv4').read().strip()
      except Exception:
        frontend = None
    else:
      if len(hostname) == 0:
        frontend = zope_ip
      else:
        frontend = 'https://' + hostname
    if frn_sr is None:
      frontend_ip = -1
    else:
      frontend_ip, _ = get_connection_information(frn_sr)

    connected = False
    if frontend is not None:
      # as frontend is already up, check if all is ready to serve requests
      # before providing the access url to the user
      ctx = ssl.create_default_context()
      ctx.check_hostname = False
      ctx.verify_mode = ssl.CERT_NONE

      try:
        r1 = urllib.request.urlopen(frontend, context=ctx)
        r1_content = r1.read()
        if b'Zope Management Interface' in r1_content or \
           (b'Zope' in r1_content and b'Auto-generated default page' in r1_content):
          connected = True
        else:
          print('URL %s ready, but does not reply with Zope' % frontend)
      except urllib.error.URLError as e:
        print("Zope not available yet: %s" % e)

    if zope_ip is None or frontend is None or frontend_ip is None or not connected:
      print("Build successful, please wait for instantiation")
      sys.exit(2)

    print(("Build successful, connect to:\n"
           "  " + frontend))
    if pw is not None:
      print((" with\n"
             "  username: zope  password: " + pw))
  elif not build:
    if 'error' in [software_release_list_status]:
      print("An error occurred while building, check /opt/slapos/log/slapos-node-software-" + \
            fmt_date() + ".log for details")
      sys.exit(2)
    else:
      print("Your software is still building, be patient it can take a while")
      sys.exit(2)

  ipv6 = None
  # check if the services are actually running (run slapos node and parse output)
  if pw is None:
    zope_ip = "https://" + zope_ip[zope_ip.index("@")+1:]

  original_zope_ip = zope_ip
  if "[" in zope_ip and "]" in zope_ip:
    ipv6 = zope_ip[zope_ip.index("[")+1:zope_ip.index("]")]
    with open("/etc/hosts", "ra+") as f:
      if " erp5-instance" not in f.read():
        f.write("\n%s   erp5-instance\n" % ipv6)
    zope_ip = zope_ip.replace("[" + ipv6 + "]", "erp5-instance")

  ctx = ssl.create_default_context()
  ctx.check_hostname = False
  ctx.verify_mode = ssl.CERT_NONE

  try:
    r1 = urllib.request.urlopen(zope_ip, context=ctx)
  except urllib.error.URLError as e:
    print("At least one of your services isn't running! Check with slapos node")
    print("restart a service with slapos node restart slappart:service")
    print("")
    print("DEBUG information: %s" % e)
    sys.exit(2)

  if r1.getcode() != 200:
    print("At least one of your services isn't running! Check with slapos node")
    print("restart a service with slapos node restart slappart:service")
    sys.exit(2)

  if ipv6:
    print("")
    print("The URL above may require extra configuration if you want to access it")
    print("from another machine. You can install an apache locally and include the")
    print("the follow rewrite rule (http version):")
    print("""
  RewriteRule ^/(.*) %s/VirtualHostBase/http/%%{HTTP_HOST}/VirtualHostRoot/$1 [L,P]

or (https version):

  RewriteRule ^/(.*) %s/VirtualHostBase/https/%%{HTTP_HOST}/VirtualHostRoot/$1 [L,P]

""" % (original_zope_ip, original_zope_ip))


def info(software_release):
  if get_build_status(software_release):
    print(get_connection_information(software_release))
  else:
    print("Information unavailable at this time, run " + sys.argv[0] + " -s for details")

def usage():
  print ("Get the status and information of your ERP5 build\n"
         "Usage:")
  print ("  --help    (-h):  Print this message and exit\n"
         "  --status  (-s):  Print the status of your ERP5 build\n"
         "  --info    (-i):  Print the technical information about partition tables\n"
         "  --dump    (-d):  Dump the entire database (alias for slapos proxy show)\n")

def dump():
  subprocess.call(["slapos", "proxy", "show", "-u", "/opt/slapos/slapproxy.db"])

def main(argv):
  # parse command line options
  try:
    opts, args = getopt.getopt(argv, "sihd", ["status", "info", "help", "dump"])
  except getopt.error as msg:
    usage()
    sys.exit(2)
  if len(opts) == 0:
    usage()
    sys.exit(2)
  # process arguments
  for opt, arg in opts:
    if opt in ("-h", "--help"):
       usage()
       sys.exit()
    elif opt in ("-s", "--status"):
      check_tables()
      status()
    elif opt in ("-i", "--info"):
      check_tables()
      for sr in discover_software():
        info(sr)
    elif opt in ("-d", "--dump"):
      dump()

if __name__ == "__main__":
  main(sys.argv[1:])
