#!/usr/bin/env python
#
# March 2017, Budhaditya Banerjee
# Copyright (c) 2017, 2019 by Cisco Systems, Inc.
# All rights reserved.
"""Script to validate Docker configuration and sync it to the host if valid.

For security reasons, only a subset of configuration options are explicitly
permitted:

- --insecure-registry option
- http_proxy, https_proxy and no_proxy environment variables
- DOCKER_VRF option
"""

import os
import re
import time
import logging
import shutil
import subprocess

from shutil import copyfile
from subprocess import call
from logging.handlers import SysLogHandler

"""
Set logging parameters
"""
log = logging.getLogger()
log.setLevel(logging.INFO)

handler = SysLogHandler(address = '/dev/log')
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

log.addHandler(handler)

"""
Files to be copied and used for logging.
"""
os.environ["TP_STORAGE_MOUNT"] = "/misc/app_host"

config_file_misc = os.environ["TP_STORAGE_MOUNT"] + "/etc/sysconfig/docker"
config_file_failed = os.environ["TP_STORAGE_MOUNT"] + "/etc/sysconfig/docker.failed"
config_file_etc = "/etc/sysconfig/docker"
host_log = "/var/log/docker.log"
misc_log = os.environ["TP_STORAGE_MOUNT"] + "/var/log/docker.log"
error_line = os.environ["TP_STORAGE_MOUNT"] + "/etc/sysconfig/error_line"

"""
Regex list covering all valid docker config options
Following are examples of valid config options:

DOCKER_OPTS=""
DOCKER_OPTS="<optional whitespace>--insecure-registry foo"
DOCKER_OPTS+="<whitespace>--insecure-registry bar"
export HTTP_PROXY="http://user@server.com:abcd"
export HTTPS_PROXY="https://user@server.com:abcd"
export http_proxy="http://user@server.com:abcd"
export https_proxy="https://user@server.com:abcd"
export NO_PROXY=""
DOCKER_VRF="vrf_name"
"""

DOCKER_OPTS=[]
DOCKER_OPTS.append(r'^DOCKER_OPTS=""$')
DOCKER_OPTS.append(r'^DOCKER_OPTS="(\s*--insecure-registry\s+\S+\s*)+"\s*$')
DOCKER_OPTS.append(r'^DOCKER_OPTS\+?="(\s+--insecure-registry\s+\S+)+\s*"\s*$')

DOCKER_EXPORT=[]
DOCKER_EXPORT.append(r'^export\s+HTTPS_PROXY=\s*"(\s*\S*)*\s*"\s*$')
DOCKER_EXPORT.append(r'^export\s+https_proxy=\s*"(\s*\S*)*\s*"\s*$')
DOCKER_EXPORT.append(r'^export\s+HTTP_PROXY=\s*"(\s*\S*)*\s*"\s*$')
DOCKER_EXPORT.append(r'^export\s+http_proxy=\s*"(\s*\S*)*\s*"\s*$')
DOCKER_EXPORT.append(r'^export\s+NO_PROXY=\s*"(\s*\S*)*\s*"\s*$')
DOCKER_EXPORT.append(r'^export\s+no_proxy=\s*"(\s*\S*)*\s*"\s*$')
  
DOCKER_VRF=r'^DOCKER_VRF="(\s*\S*)*\s*"\s*$'
SPC_COM=['^\s*#[\s|\S]*$',
         '^\s*$']

def xr_error(error):
    """Sync error for XR syslog
    """
    with open(error_line, "a") as file:
        file.write(error)
 
def revert_config(line):
    log.error("Incorrect syntax in %s ", line)
    xr_error("ERROR: Incorrect syntax in: " + line + "\n")
    log.error("Incorrect config file saved as %s ", config_file_failed)
    copyfile(config_file_misc, config_file_failed)
    copyfile(config_file_etc, config_file_misc)

def does_vrf_exist(vrf):
    if not vrf:
        vrf="global-vrf"
        return True
    elif not os.path.exists("/var/run/netns/{}".format(vrf)):
        log.error("VRF %s not created yet.", vrf)
        xr_error("ERROR: VRF " + vrf +  " not created yet\n")
        return False
    else:
	return True
        
def restart_docker():
    copyfile(config_file_misc, config_file_etc)
    call(["service", "docker", "restart"])

def sync_logs():
    while True:
        time.sleep(10)
        output = subprocess.check_output("service docker status", shell=True)
	pattern = "start/running"
        if pattern in output:
            time.sleep(10)
            copyfile(host_log, misc_log)
            log.info("Docker has restarted with new config. Logs can be seen in /var/log/docker.log")
            return True

def is_check_needed():
    """Check whether there is actually a change needing syncing.

    When we revert config, inotify is called after revert,
    which leads to another validation of config and an unnecessary restart
    of docker unless we are smart enough to detect that no change has occurred.
    """
    with open(config_file_misc, "r") as file1:
        with open(config_file_etc, "r") as file2:
            if file1.read() != file2.read():
                return True


def validate_config():
    """Read config file and validate
    """

    config_line = ""
    valid_config = True
    with open(config_file_misc, "r") as f:
        for line in f:
            """We want to keep processing all valid configs till we reach an invalid VRF
               or an incorrect config so as to start the daemon with all the options once
               and for all.
            """
            if (re.match("|".join(DOCKER_OPTS), line) or 
                re.match("|".join(DOCKER_EXPORT), line) or
                (re.match(DOCKER_VRF, line) and does_vrf_exist(line[12:-2])) or 
                re.match("|".join(SPC_COM), line)):
	        continue
	    else:
	        valid_config = False
                config_line = line
                break

    if valid_config:
        restart_docker()
        sync_logs()
    else:
        revert_config(config_line)

if __name__=="__main__":
    if is_check_needed():
        validate_config()

