<reponame>rcmelendez/devo-lapua-docker
# -*- coding: utf-8 -*-
import socket
import json
import re
import cmd
import os
import math
import time
import readline
import pytz
from .utils.constants import PERIOD_REGEX, NUMBER_REGEX, STATUS, \
                             MESSAGES, OK_MESSAGE, KO_MESSAGE, \
                             END_OF_MESSAGE, LAPUA_VERSION
from .utils.config import get_config
from .utils.data import MODELS

__author__ = 'Devo (<NAME>)'


def complete_timezones(text, state):
    options = [x for x in pytz.all_timezones if x.startswith(text)]
    try:
        return options[state]
    except IndexError:
        return None


def default_completer(text, state):
    return []


class CmdTest(cmd.Cmd):

    CUSTOM = 'custom'

    MANDATORY_PARAMS = ["instance", "auth", "domain", "table", "period"]

    LOGGING_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR']

    DEFAULT_NAME_TEMPLATE = '{domain}__{table}__{period}__{time}'

    ALLOWED_AGG_FUNCTIONS = ['count', 'avg', 'min', 'max', 'hllppcount',
                             'median', 'nnavg', 'nnstddev', 'nnustddev',
                             'nnvar', 'nnuvar', 'percentile25', 'percentile5',
                             'percentile75', 'percentile95', 'stddev',
                             'ustddev', 'sum', 'sum2', 'var', 'uvar']

    DOMAIN_REGEX = re.compile('where.*client\s*=\s*[\'\"](\w+)[\'\"]')
    TABLE_REGEX = re.compile('from\s*([\w\.]+)')
    EVENTDATE_REGEX = re.compile('where.*eventdate')
    EVERY_REGEX = re.compile('group.*(every\s*(\d+[smhd]))')
    BY_REGEX = re.compile('group.*by\s*(.*)\s*select')
    COUNT_REGEX = re.compile('(group[\s\S]+select (?:{})\((?:\w*)\)\s+'
                             'as\s+\w+\s*(?:where\s*\w+\s*>\s*(\d+))?$)'.format(
                             '|'.join(ALLOWED_AGG_FUNCTIONS)))

    def __init__(self, conn):
        cmd.Cmd.__init__(self)
        self.conn = conn

    # OVERRIDING
    prompt = 'lapua> '

    def emptyline(self):
         pass

    def do_version(self, line):
        """version
        Returns deployed Lapua version."""
        print('v{}'.format(LAPUA_VERSION))

    def do_exit(self, line):
        """exit
        Close the client."""
        self.conn.send(self._build_msg('quit'))
        self.conn.close()
        print('Connection closed')
        return True

    def do_quit(self, line):
        """quit
        Close the client."""
        return self.do_exit(line)

    def do_get(self, job_name):
        """get <job_name>
        Get the configuration for <job_name> job.
        <job_name> must exist."""
        self.conn.send(self._build_msg('get', job_name))
        resp = self._receive()
        if resp.endswith('.json'):
            resp = resp[:-5]
        if resp == KO_MESSAGE:
            print('Error getting job config. Please, try again.')
        elif resp.startswith(OK_MESSAGE):
            if job_name.endswith('.json'):
                job_name = job_name[:-5]
            config = resp.replace(OK_MESSAGE, '')
            config = json.loads(config)
            print(json.dumps(config, indent=4))
        else:
            print('Error: {}'.format(resp))

    def do_test(self, job_name):
        """test <job_name>
        Test the configuration for <job_name> job.
        <job_name> must exist."""
        self.conn.send(self._build_msg('test', job_name))
        resp = self._receive()
        if resp == KO_MESSAGE:
            print('Error getting job config. Please, try again.')
        elif resp.startswith(OK_MESSAGE):
            result = json.loads(resp.replace(OK_MESSAGE, ''))
            print('Results for job {}:'.format(job_name))
            print(json.dumps(result, indent=4))
        else:
            print('Error: {}'.format(resp))

    def do_load(self, job_path):
        """load <job_path>
        Load the configuration from <job_path> and create a new job.
        <job_path> must exist."""
        if not os.path.isfile(job_path):
            print("Error: File {} not found".format(job_path))
            return

        try:
            with open(job_path, 'r') as file_desc:
                config = json.load(file_desc)
        except ValueError:
            print("Error: File {} is not a JSON file".format(job_path))
            return

        config['name'] = os.path.splitext(os.path.basename(job_path))[0]

        job = self._handle_creation(config)
        if job:
            self.conn.send(job)
            resp = self._receive()
            if not resp == KO_MESSAGE:
                print('Job {} successfully created. '
                      'Would you like to start running it?'.format(resp))
                while True:
                    response = self._prompt("(y/n): ", is_input=True)\
                        .lower().strip()
                    if response in ['y', 'n', 'yes', 'no']:
                        if response[0] == 'y':
                            self.do_run(resp)
                        break
                    print('Error: Enter "(y)es" or "(n)o"')
            else:
                print('Error creating job. Please, try again.')

    def do_list(self, line):
        """list
        Display all running and stopped jobs."""
        self.conn.send(self._build_msg('list', line or '*'))
        resp = self._receive()
        if resp == KO_MESSAGE:
            print('Error listing jobs. Please, try again.')
        else:
            print('Current jobs status:')
            status = json.loads(resp)
            running_jobs = []
            stopped_jobs = []
            for k, v in status.items():
                k = k.replace('.json', '')
                if v == STATUS.RUNNING:
                    running_jobs.append(k)
                else:
                    stopped_jobs.append(k)
            print('\n\tRUNNING[{}]'.format(len(running_jobs)))
            for k in running_jobs:
                print('\t - {}'.format(k))
            print('\n\tSTOPPED[{}]'.format(len(stopped_jobs)))
            for k in stopped_jobs:
                print('\t - {}'.format(k))

    def do_manage(self, line):
        """manage <domain/job> <name> <limit>
        Sets <domain/job> limit."""
        args = line.split(' ')
        if not len(args)==3:
            print('Error: missing argument')
            return

        args[0] = args[0].lower()
        if not args[0] in ['domain', 'job']:
            print('Error: type must be one of [domain, limit]')
            return

        if not NUMBER_REGEX.match(args[2]):
            print('Error: limit must be a number')
            return

        data = {'type': args[0], 'name': args[1], 'limit': int(args[2])}
        self.conn.send(self._build_msg('manage', data))
        resp = self._receive()
        if resp == OK_MESSAGE:
            print('Changes saved for {} {}.'.format(args[0], args[1]))
        else:
            print('Error managing {} {}.'.format(args[0], args[1]))

    def do_run(self, job_name):
        """run <job_name>
        Start running the <job_name> job.
        <job_name> must exist and not being currently running."""
        self.conn.send(self._build_msg('run', job_name))
        resp = self._receive()
        if resp.endswith('.json'):
            resp = resp[:-5]
        while resp != END_OF_MESSAGE:
            if resp.startswith(OK_MESSAGE):
                print('Job {} successfully started.'.format(
                    resp.replace(OK_MESSAGE, '')))
            elif resp.startswith(KO_MESSAGE):
                print('Error running job {}.'.format(
                    resp.replace(KO_MESSAGE, '')))
            else:
                print('Error: {}'.format(resp))
            resp = self._receive()

    def do_stop(self, job_name):
        """stop <job_name>
        Stop running the <job_name> job.
        <job_name> must exist and not being currently stopped."""
        self.conn.send(self._build_msg('stop', job_name))
        resp = self._receive()
        if resp.endswith('.json'):
            resp = resp[:-5]
        while resp != END_OF_MESSAGE:
            if resp.startswith(OK_MESSAGE):
                print('Job {} successfully stopped.'.format(
                    resp.replace(OK_MESSAGE, '')))
            elif resp.startswith(KO_MESSAGE):
                print('Error stopping job {}.'.format(
                    resp.replace(KO_MESSAGE, '')))
            else:
                print('Error: {}'.format(resp))
            resp = self._receive()

    def do_delete(self, job_name):
        """delete <job_name>
        Delete the <job_name> job.
        <job_name> must exist and not being currently running."""
        self.conn.send(self._build_msg('delete', job_name))
        resp = self._receive()
        if resp.endswith('.json'):
            resp = resp[:-5]
        while resp != END_OF_MESSAGE:
            if resp.startswith(OK_MESSAGE):
                print('Job {} successfully deleted.'.format(
                    resp.replace(OK_MESSAGE, '')))
            elif resp.startswith(KO_MESSAGE):
                print('Error deleting job {}.'.format(
                    resp.replace(KO_MESSAGE, '')))
            else:
                print('Error: {}'.format(resp))
            resp = self._receive()

    def do_create(self, custom):
        """create [custom]
        Create a new job. A form will ask for all the needed configuration.
        The optional [custom] argument lets you introduce a custom query."""
        job = self._create_job(custom=custom == CmdTest.CUSTOM)
        if job:
            self.conn.send(job)
            resp = self._receive()
            if resp.endswith('.json'):
                resp = resp[:-5]
            if not resp.startswith(KO_MESSAGE):
                print('Job {} successfully created. '
                      'Would you like to start running it?'.format(resp))
                while True:
                    response = self._prompt("(y/n): ", is_input=True)\
                        .lower().strip()
                    if response in ['y', 'n', 'yes', 'no']:
                        if response[0] == 'y':
                            self.do_run(resp)
                        break
                    print('Error: Enter "(y)es" or "(n)o"')
            else:
                resp = resp.replace(KO_MESSAGE, '')
                if resp:
                    print(resp)
                else:
                    print('Error creating job. Please, try again.')

    def _format_with_default(self, str, default):
        return str.format(' ({})'.format(default) if default else '')

    def _handle_creation(self, obj, edit=False):
        for param in CmdTest.MANDATORY_PARAMS:
            if param not in obj:
                print("Error: Missing mandatory parameter {}".format(param))
                return

        while True:
            self._prompt("{} job with this configuration?".format(
                'Edit' if edit else 'Create'))
            print(json.dumps(obj, indent=4))
            response = self._prompt("(y/n): ", is_input=True).lower().strip()
            if response in ['y', 'n', 'yes', 'no']:
                break
            print('Error: Enter "(y)es" or "(n)o"')

        if response in ['n', 'no']:
            return

        while True:
            default_name = CmdTest.DEFAULT_NAME_TEMPLATE.format(
                **obj, time=math.floor(time.time())).replace('.', '_')
            default_name_value = obj.get('name', default_name)
            name = self._prompt(
                self._format_with_default("job name{}: ",
                                          default_name_value),
                is_input=True).lower().strip() or default_name_value
            self.conn.send(self._build_msg('get', '{}.json'.format(name)))
            resp = self._receive()
            if not resp.startswith(OK_MESSAGE):
                obj['name'] = name
                break
            print('Error: job "{}" already exists.'.format(name))

        return self._build_msg("create", obj)

    def _create_job(self, default_config={}, custom=False):
        while True:
            available_instances = list(CONF['api'].keys())
            default_value = default_config.get('instance',
                                               ', '.join(available_instances))
            instance = self._prompt(
                self._format_with_default("API instance{}: ", default_value),
                is_input=True).lower().strip() or default_value
            if instance in available_instances:
                break
            print('Error: Instance must be one of {}'.format(
                available_instances))
        while True:
            available_auth_types = ['ks', 't']
            auth_type = self._prompt(
                self._format_with_default("API auth type{}: ",
                                          '[key + secret(ks), token(t)]'),
                is_input=True).lower().strip()
            if auth_type in available_auth_types:
                break
            print('Error: API auth type must be one of {}'.format(
                available_auth_types))
        if auth_type == 't':
            while True:
                token = self._prompt(
                    self._format_with_default("Token: ", None),
                    is_input=True).strip()
                if token:
                    break
                print('Error: Token cannot be undefined')
        else:
            while True:
                key = self._prompt(
                    self._format_with_default("API Key: ", None),
                    is_input=True).strip()
                if key:
                    break
                print('Error: API Key cannot be undefined')
            while True:
                secret = self._prompt(
                    self._format_with_default("API Secret: ", None),
                    is_input=True).strip()
                if secret:
                    break
                print('Error: API Secret cannot be undefined')
        if custom:
            while True:
                default_value = default_config.get('query', None)
                query = self._prompt(
                    self._format_with_default("Query{}: ", default_value),
                    is_input=True).lower().strip() or default_value
                if query:
                    # check domain
                    domain = CmdTest.DOMAIN_REGEX.findall(query)
                    if not domain:
                        print('Error: Domain not found in query')
                        continue
                    domain = domain[0]
                    # check table
                    table = CmdTest.TABLE_REGEX.findall(query)
                    if not table:
                        print('Error: Table not found in query')
                        continue
                    table = table[0]
                    # where eventdate filters
                    eventdate_filters = CmdTest.EVENTDATE_REGEX.findall(query)
                    if eventdate_filters:
                        print('Error: Query can\'t contain eventdate filters. '
                              'They will be filled in after the job '
                              'is created')
                        continue
                    # check period
                    every = CmdTest.EVERY_REGEX.findall(query)
                    if not every:
                        print('Error: Period not found in query')
                        continue
                    period = every[0][1]
                    if not PERIOD_REGEX.match(period):
                        print('Error: Period must match format \\d+[dhms]')
                        continue
                    # check host
                    group_by = CmdTest.BY_REGEX.findall(query)
                    if group_by:
                        group_by = group_by[0].split(',')
                        if len(group_by) > 1:
                            print('Error: Query must contain only '
                                  'one grouping key')
                            continue
                        else:
                            group_by = group_by[0].strip()
                    else:
                        group_by = None
                    # check count
                    match = CmdTest.COUNT_REGEX.findall(query)
                    if not match:
                        print('Error: Query must contain only '
                              'one of these aggregation functions: {}'.format(
                                  CmdTest.ALLOWED_AGG_FUNCTIONS))
                        continue
                    # check threshold
                    count, threshold = match[0]
                    if threshold:
                        if not NUMBER_REGEX.match(threshold):
                            print('Error: Threshold must be an integer number')
                            continue
                        threshold = int(threshold)
                    else:
                        threshold = 0
                    query = query.replace(every[0][0], '')
                    break
                else:
                    print('Error: Query cannot be undefined')
        else:
            while True:
                default_value = default_config.get('domain', None)
                domain = self._prompt(
                    self._format_with_default("Domain{}: ", default_value),
                    is_input=True).lower().strip() or default_value
                if domain:
                    break
                print('Error: Domain cannot be undefined')
            while True:
                default_value = default_config.get('table', None)
                table = self._prompt(
                    self._format_with_default("Table{}: ", default_value),
                    is_input=True).lower().strip() or default_value
                if table:
                    break
                print('Error: Table cannot be undefined')
            while True:
                default_value = default_config.get('period', None)
                period = self._prompt(
                    self._format_with_default("Period{}: ", default_value),
                    is_input=True).lower().strip() or default_value
                if period and PERIOD_REGEX.match(period):
                    break
                print('Error: Period must match format \\d+[dhms]')
            while True:
                default_value = str(default_config.get('threshold', '0'))
                threshold = self._prompt(
                    self._format_with_default("Threshold{}: ",
                                              default_value or '0'),
                    is_input=True).lower().strip() or default_value
                if NUMBER_REGEX.match(threshold):
                    threshold = int(threshold)
                    break
                print('Error: Threshold must be a number')
            default_value = default_config.get('group_by', None)
            group_by = self._prompt(
                self._format_with_default("Group by{}: ",
                                          default_value or 'None'),
                is_input=True).strip() or default_value

        if group_by:
            default_required_keys = default_config.get('required_keys', None)
            required_keys =  self._prompt(
                self._format_with_default(
                    "List of grouping keys that must exist in every period{}: ",
                    default_required_keys or 'None'),
                is_input=True) or default_required_keys

            if required_keys:
                required_keys = list(map(lambda x: x.strip(),
                                         required_keys.split(',')))

            default_alarms = default_config.get('max_alarms', 0)
            while True:
                max_alarms =  self._prompt(
                    self._format_with_default(
                        "Number of consecutive alarms per group value{}: ",
                        default_alarms or '0'),
                    is_input=True).lower().strip() or default_alarms

                if NUMBER_REGEX.match(str(max_alarms)):
                    max_alarms = int(max_alarms)
                    break
                print('Error: Max alarms must be a number')

        while True:
            default_value = default_config.get('related_periods', {})
            default_period = default_value.get('period', None)
            default_stdevs = default_value.get('stdevs', '1')
            default_amount = default_value.get('amount', '5')
            default_prepopulate = \
                default_value.get('prepopulate', 'true').lower()
            default_raw = default_value.get('send_raw_to_table', None)
            default_regression = default_value.get('regression', [])
            related_periods = self._prompt(
                self._format_with_default("Related Periods{}: ",
                                          default_period or 'None'),
                is_input=True).lower().strip() or default_period
            if related_periods and not PERIOD_REGEX.match(related_periods):
                print('Error: Related Periods must match format \\d+[dhms]')
                continue
            if not related_periods:
                break
            while True:
                stdevs = self._prompt(
                    self._format_with_default("Max. stdevs{}: ",
                                              default_stdevs or '1'),
                    is_input=True).lower().strip() or default_stdevs
                if stdevs and not NUMBER_REGEX.match(stdevs):
                    print('Error: Max. stdevs must be a positive number.')
                    continue
                stdevs = int(stdevs)
                break
            while True:
                amount = self._prompt(
                    self._format_with_default("Max. number of periods{}: ",
                                              default_amount or '5'),
                    is_input=True).lower().strip() or default_amount
                if amount:
                    if not NUMBER_REGEX.match(amount):
                        print('Error: Max. number of periods must be '
                              'a positive number.')
                        continue
                    amount = int(amount)
                    if amount < 2:
                        print('Error: Max. number of periods must be '
                              'greater or equal than 2.')
                        continue
                break
            while True:
                prepopulate = self._prompt(
                    self._format_with_default(
                        "Prepopulate previous periods{}: ",
                        default_prepopulate or 'true'),
                    is_input=True).lower().strip() or default_prepopulate
                if prepopulate in ['true', 'false']:
                    prepopulate = prepopulate == "true"
                    break
                print('Error: Prepopulate must be either "true" or "false"')

            send_raw_to_table = self._prompt(
                self._format_with_default("Send raw stats data to table{}: ",
                                          default_raw or 'None'),
                is_input=True).lower().strip() or default_raw

            if send_raw_to_table:
                while True:
                    regression = self._prompt(
                        self._format_with_default(
                            "Regression models to apply (none or several from: "
                            "[{}] max. 4): ", ','.join(MODELS.MAPPER.keys())),
                        is_input=True).lower().split(',') or default_regression
                    if not regression[0]:
                        regression = None
                        break

                    if len(regression) > 4:
                        print('Error: Max. 4 regression models')
                    else:
                        fail = False
                        for i in range(len(regression)):
                            regression[i] = regression[i].strip()
                            if not regression[i] in MODELS.MAPPER:
                                print('Error: Unrecognized regression '
                                      'model {}'.format(regression[i]))
                                fail = True
                                break
                        if not fail:
                            break
            else:
                regression = None

            break

        timezone = self._prompt(
            self._format_with_default("Timezone{}: ", 'UTC'),
            is_input=True, completer=complete_timezones) or None

        while True:
            null_as_zero =  self._prompt(
                self._format_with_default(
                    "Null response as zero{}?: ", 'false'),
                is_input=True).lower().strip() or None

            if null_as_zero:
                if not null_as_zero in ['true', 'false']:
                    print('Error: Must be true or false')
                    continue
                else:
                    null_as_zero = null_as_zero == 'true'
            break

        send_to = list(map(lambda x: x.strip(), self._prompt(
            self._format_with_default(
                "Send alerts to table/s{}: ",
                'one or several tags, blank for default table {}'.format(
                    CONF['targets']['monitor']
                )),
            is_input=True).lower().split(',')))

        if not send_to[0]:
            send_to = None

        if send_to:
            for idx, target in enumerate(send_to):
                while True:
                    custom_sender = self._prompt(
                        self._format_with_default(
                            "Set custom sender for {} (y/n)?: ".format(
                                target), None),
                        is_input=True).lower().strip()
                    if custom_sender in ['y', 'n']:
                        custom_sender = custom_sender == 'y'
                        break
                    print('Error: Enter "(y)es" or "(n)o"')

                if not custom_sender:
                    continue
                sender = {}
                while True:
                    address = self._prompt(
                        self._format_with_default(
                            "Devo address: ", None),
                        is_input=True).lower().strip()
                    if address:
                        sender['address'] = address
                        break
                    print('Error: Address cannot be undefined')
                while True:
                    port = self._prompt(
                        self._format_with_default(
                            "Devo port: ", None),
                        is_input=True).strip()
                    if port:
                        if NUMBER_REGEX.match(str(port)):
                            sender['port'] = int(port)
                            break
                        else:
                            print('Error: Port must be a number')
                            continue
                    print('Error: Port cannot be undefined')
                while True:
                    key = self._prompt(
                        self._format_with_default(
                            "Devo key path: ", None),
                        is_input=True).strip()
                    if key:
                        sender['key'] = key
                        break
                    print('Error: Key path cannot be undefined')
                while True:
                    cert = self._prompt(
                        self._format_with_default(
                            "Devo cert path: ", None),
                        is_input=True).strip()
                    if cert:
                        sender['cert'] = cert
                        break
                    print('Error: Cert path cannot be undefined')
                while True:
                    chain = self._prompt(
                        self._format_with_default(
                            "Devo chain path: ", None),
                        is_input=True).strip()
                    if chain:
                        sender['chain'] = chain
                        break
                    print('Error: Chain path cannot be undefined')
                sender['tag'] = target
                send_to[idx] = sender

        while True:
            default_retries = default_config.get('max_retries', 3)
            max_retries =  self._prompt(
                self._format_with_default(
                    "Max API calls retries{}: ", default_retries or '3'),
                is_input=True).lower().strip() or default_retries

            if NUMBER_REGEX.match(str(max_retries)):
                max_retries = int(max_retries)
                break
            print('Error: Max retries must be a number')

        while True:
            logging_options = ['file', 'console', 'devo']
            logging = list(map(lambda x: x.strip(), self._prompt(
                self._format_with_default(
                    "Job logging{}: ",
                    'one or several of {}, blank for default'.format(
                        logging_options
                    )),
                is_input=True).lower().split(',')))

            if not logging[0]:
                logging = None
                break
            error = ''
            for loggin_option in logging:
                if not loggin_option in logging_options:
                    error = (
                        'Error: Unrecognized option {}. Must be one or '
                        'several of [file, console, devo] or default.'.format(
                            loggin_option))
                    break
            if not error:
                break
            print(error)

        if logging:
            logging = {logging_option: {} for logging_option in logging}
            if 'console' in logging:
                while True:
                    level = self._prompt(
                        self._format_with_default(
                            "Console logging level{}: ", 'ERROR'),
                        is_input=True).upper().strip() or 'ERROR'
                    if level in CmdTest.LOGGING_LEVELS:
                        logging['console']['level'] = level
                        break
                    print('Error: Logging level must be one of {}'.format(
                        CmdTest.LOGGING_LEVELS))
            if 'file' in logging:
                while True:
                    level = self._prompt(
                        self._format_with_default(
                            "File logging level{}: ", 'DEBUG'),
                        is_input=True).upper().strip() or 'DEBUG'
                    if level in CmdTest.LOGGING_LEVELS:
                        logging['file']['level'] = level
                        break
                    print('Error: Logging level must be one of {}'.format(
                        CmdTest.LOGGING_LEVELS))
                path = self._prompt(
                    self._format_with_default(
                        "File logging path{}: ", 'default from conf.json'),
                    is_input=True).strip() or None
                if path:
                    logging['file']['path'] = path
            if 'devo' in logging:
                while True:
                    level = self._prompt(
                        self._format_with_default(
                            "Devo logging level{}: ", 'INFO'),
                        is_input=True).upper().strip() or 'INFO'
                    if level in CmdTest.LOGGING_LEVELS:
                        logging['devo']['level'] = level
                        break
                    print('Error: Logging level must be one of {}'.format(
                        CmdTest.LOGGING_LEVELS))
                while True:
                    sender = self._prompt(
                        self._format_with_default(
                            "Devo custom sender{}: ", 'false'),
                        is_input=True).lower().strip() or None
                    if sender and not sender in ['true', 'false']:
                        print('Error: Must be true or false')
                        continue
                    break
                if sender:
                    logging['devo']['sender'] = {}
                    while True:
                        address = self._prompt(
                            self._format_with_default(
                                "Devo logging address: ", None),
                            is_input=True).lower().strip()
                        if address:
                            logging['devo']['sender']['address'] = address
                            break
                        print('Error: Address cannot be undefined')
                    while True:
                        port = self._prompt(
                            self._format_with_default(
                                "Devo logging port: ", None),
                            is_input=True).strip()
                        if port:
                            if NUMBER_REGEX.match(str(port)):
                                logging['devo']['sender']['port'] = int(port)
                                break
                            else:
                                print('Error: Port must be a number')
                                continue
                        print('Error: Port cannot be undefined')
                    while True:
                        key = self._prompt(
                            self._format_with_default(
                                "Devo logging key path: ", None),
                            is_input=True).strip()
                        if key:
                            logging['devo']['sender']['key'] = key
                            break
                        print('Error: Key path cannot be undefined')
                    while True:
                        cert = self._prompt(
                            self._format_with_default(
                                "Devo logging cert path: ", None),
                            is_input=True).strip()
                        if cert:
                            logging['devo']['sender']['cert'] = cert
                            break
                        print('Error: Cert path cannot be undefined')
                    while True:
                        chain = self._prompt(
                            self._format_with_default(
                                "Devo logging chain path: ", None),
                            is_input=True).strip()
                        if chain:
                            logging['devo']['sender']['chain'] = chain
                            break
                        print('Error: Chain path cannot be undefined')
                    while True:
                        tag = self._prompt(
                            self._format_with_default(
                                "Devo logging table: ", None),
                            is_input=True).strip()
                        if tag:
                            logging['devo']['sender']['tag'] = tag
                            break
                        print('Error: Table cannot be undefined')

        obj = {
            "instance": instance,
            "domain": domain,
            "table": table,
            "period": period,
            "max_retries": max_retries,
            "threshold": threshold,
            "group_by": group_by,
            "related_periods": related_periods,
        }

        if auth_type == 't':
            obj['auth'] = {"token": token}
        else:
            obj['auth'] = {"key": key, "secret": secret}

        if send_to:
            obj['send_to'] = send_to

        if timezone:
            obj['timezone'] = timezone

        if null_as_zero:
            obj['null_as_zero'] = null_as_zero

        if logging:
            obj['logging'] = logging

        if group_by and required_keys:
            obj['required_keys'] = required_keys

        if group_by and max_alarms:
            obj['max_alarms'] = max_alarms

        if related_periods:
            obj['related_periods'] = {
                "periods": related_periods,
                "stdevs": stdevs,
                "amount": amount,
                "prepopulate": prepopulate,
                "send_raw_to_table": send_raw_to_table,
                "regression": regression
            }

        if custom:
            obj['query'] = query

        if 'name' in default_config:
            obj['name'] = default_config['name']

        return self._handle_creation(obj, edit=default_config)

    def _build_msg(self, command, data=None):
        return json.dumps({"command": command, "data": data}).encode('utf-8')

    def _prompt(self, raw_text, is_input=False, completer=default_completer):
        input_command = None
        if is_input:
            old_completer = readline.get_completer()
            readline.set_completer(completer)
            readline.set_completer_delims(' \t\n;')
            readline.parse_and_bind("tab: complete")
            input_command = input('lapua> {}'.format(raw_text))
            readline.set_completer(old_completer)
        else:
            print('lapua> {}'.format(raw_text))

        return input_command

    def _receive(self):
        size = self.conn.recv(8).decode('utf-8')
        return self.conn.recv(int(size)).decode('utf-8')


if __name__ == '__main__':
    CONF = get_config()
    SOCKET = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        SOCKET.connect((CONF['server']['address'], CONF['server']['port']))
        readline.set_completer_delims(' \t\n')
        CmdTest(SOCKET).cmdloop()
    except Exception as excpt:
        print("Error: Couldn't connect to server at {}:{}"\
            .format(CONF['server']['address'], CONF['server']['port']))
        print(excpt)

# -*- coding: utf-8 -*-
import socket
import json
import re
import cmd
import os
import math
import time
import readline
import pytz
from .utils.constants import PERIOD_REGEX, NUMBER_REGEX, STATUS, \
                             MESSAGES, OK_MESSAGE, KO_MESSAGE, \
                             END_OF_MESSAGE, LAPUA_VERSION
from .utils.config import get_config
from .utils.data import MODELS

__author__ = 'Devo (<NAME>)'


def complete_timezones(text, state):
    options = [x for x in pytz.all_timezones if x.startswith(text)]
    try:
        return options[state]
    except IndexError:
        return None


def default_completer(text, state):
    return []


class CmdTest(cmd.Cmd):

    CUSTOM = 'custom'

    MANDATORY_PARAMS = ["instance", "auth", "domain", "table", "period"]

    LOGGING_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR']

    DEFAULT_NAME_TEMPLATE = '{domain}__{table}__{period}__{time}'

    ALLOWED_AGG_FUNCTIONS = ['count', 'avg', 'min', 'max', 'hllppcount',
                             'median', 'nnavg', 'nnstddev', 'nnustddev',
                             'nnvar', 'nnuvar', 'percentile25', 'percentile5',
                             'percentile75', 'percentile95', 'stddev',
                             'ustddev', 'sum', 'sum2', 'var', 'uvar']

    DOMAIN_REGEX = re.compile('where.*client\s*=\s*[\'\"](\w+)[\'\"]')
    TABLE_REGEX = re.compile('from\s*([\w\.]+)')
    EVENTDATE_REGEX = re.compile('where.*eventdate')
    EVERY_REGEX = re.compile('group.*(every\s*(\d+[smhd]))')
    BY_REGEX = re.compile('group.*by\s*(.*)\s*select')
    COUNT_REGEX = re.compile('(group[\s\S]+select (?:{})\((?:\w*)\)\s+'
                             'as\s+\w+\s*(?:where\s*\w+\s*>\s*(\d+))?$)'.format(
                             '|'.join(ALLOWED_AGG_FUNCTIONS)))

    def __init__(self, conn):
        cmd.Cmd.__init__(self)
        self.conn = conn

    # OVERRIDING
    prompt = 'lapua> '

    def emptyline(self):
         pass

    def do_version(self, line):
        """version
        Returns deployed Lapua version."""
        print('v{}'.format(LAPUA_VERSION))

    def do_exit(self, line):
        """exit
        Close the client."""
        self.conn.send(self._build_msg('quit'))
        self.conn.close()
        print('Connection closed')
        return True

    def do_quit(self, line):
        """quit
        Close the client."""
        return self.do_exit(line)

    def do_get(self, job_name):
        """get <job_name>
        Get the configuration for <job_name> job.
        <job_name> must exist."""
        self.conn.send(self._build_msg('get', job_name))
        resp = self._receive()
        if resp.endswith('.json'):
            resp = resp[:-5]
        if resp == KO_MESSAGE:
            print('Error getting job config. Please, try again.')
        elif resp.startswith(OK_MESSAGE):
            if job_name.endswith('.json'):
                job_name = job_name[:-5]
            config = resp.replace(OK_MESSAGE, '')
            config = json.loads(config)
            print(json.dumps(config, indent=4))
        else:
            print('Error: {}'.format(resp))

    def do_test(self, job_name):
        """test <job_name>
        Test the configuration for <job_name> job.
        <job_name> must exist."""
        self.conn.send(self._build_msg('test', job_name))
        resp = self._receive()
        if resp == KO_MESSAGE:
            print('Error getting job config. Please, try again.')
        elif resp.startswith(OK_MESSAGE):
            result = json.loads(resp.replace(OK_MESSAGE, ''))
            print('Results for job {}:'.format(job_name))
            print(json.dumps(result, indent=4))
        else:
            print('Error: {}'.format(resp))

    def do_load(self, job_path):
        """load <job_path>
        Load the configuration from <job_path> and create a new job.
        <job_path> must exist."""
        if not os.path.isfile(job_path):
            print("Error: File {} not found".format(job_path))
            return

        try:
            with open(job_path, 'r') as file_desc:
                config = json.load(file_desc)
        except ValueError:
            print("Error: File {} is not a JSON file".format(job_path))
            return

        config['name'] = os.path.splitext(os.path.basename(job_path))[0]

        job = self._handle_creation(config)
        if job:
            self.conn.send(job)
            resp = self._receive()
            if not resp == KO_MESSAGE:
                print('Job {} successfully created. '
                      'Would you like to start running it?'.format(resp))
                while True:
                    response = self._prompt("(y/n): ", is_input=True)\
                        .lower().strip()
                    if response in ['y', 'n', 'yes', 'no']:
                        if response[0] == 'y':
                            self.do_run(resp)
                        break
                    print('Error: Enter "(y)es" or "(n)o"')
            else:
                print('Error creating job. Please, try again.')

    def do_list(self, line):
        """list
        Display all running and stopped jobs."""
        self.conn.send(self._build_msg('list', line or '*'))
        resp = self._receive()
        if resp == KO_MESSAGE:
            print('Error listing jobs. Please, try again.')
        else:
            print('Current jobs status:')
            status = json.loads(resp)
            running_jobs = []
            stopped_jobs = []
            for k, v in status.items():
                k = k.replace('.json', '')
                if v == STATUS.RUNNING:
                    running_jobs.append(k)
                else:
                    stopped_jobs.append(k)
            print('\n\tRUNNING[{}]'.format(len(running_jobs)))
            for k in running_jobs:
                print('\t - {}'.format(k))
            print('\n\tSTOPPED[{}]'.format(len(stopped_jobs)))
            for k in stopped_jobs:
                print('\t - {}'.format(k))

    def do_manage(self, line):
        """manage <domain/job> <name> <limit>
        Sets <domain/job> limit."""
        args = line.split(' ')
        if not len(args)==3:
            print('Error: missing argument')
            return

        args[0] = args[0].lower()
        if not args[0] in ['domain', 'job']:
            print('Error: type must be one of [domain, limit]')
            return

        if not NUMBER_REGEX.match(args[2]):
            print('Error: limit must be a number')
            return

        data = {'type': args[0], 'name': args[1], 'limit': int(args[2])}
        self.conn.send(self._build_msg('manage', data))
        resp = self._receive()
        if resp == OK_MESSAGE:
            print('Changes saved for {} {}.'.format(args[0], args[1]))
        else:
            print('Error managing {} {}.'.format(args[0], args[1]))

    def do_run(self, job_name):
        """run <job_name>
        Start running the <job_name> job.
        <job_name> must exist and not being currently running."""
        self.conn.send(self._build_msg('run', job_name))
        resp = self._receive()
        if resp.endswith('.json'):
            resp = resp[:-5]
        while resp != END_OF_MESSAGE:
            if resp.startswith(OK_MESSAGE):
                print('Job {} successfully started.'.format(
                    resp.replace(OK_MESSAGE, '')))
            elif resp.startswith(KO_MESSAGE):
                print('Error running job {}.'.format(
                    resp.replace(KO_MESSAGE, '')))
            else:
                print('Error: {}'.format(resp))
            resp = self._receive()

    def do_stop(self, job_name):
        """stop <job_name>
        Stop running the <job_name> job.
        <job_name> must exist and not being currently stopped."""
        self.conn.send(self._build_msg('stop', job_name))
        resp = self._receive()
        if resp.endswith('.json'):
            resp = resp[:-5]
        while resp != END_OF_MESSAGE:
            if resp.startswith(OK_MESSAGE):
                print('Job {} successfully stopped.'.format(
                    resp.replace(OK_MESSAGE, '')))
            elif resp.startswith(KO_MESSAGE):
                print('Error stopping job {}.'.format(
                    resp.replace(KO_MESSAGE, '')))
            else:
                print('Error: {}'.format(resp))
            resp = self._receive()

    def do_delete(self, job_name):
        """delete <job_name>
        Delete the <job_name> job.
        <job_name> must exist and not being currently running."""
        self.conn.send(self._build_msg('delete', job_name))
        resp = self._receive()
        if resp.endswith('.json'):
            resp = resp[:-5]
        while resp != END_OF_MESSAGE:
            if resp.startswith(OK_MESSAGE):
                print('Job {} successfully deleted.'.format(
                    resp.replace(OK_MESSAGE, '')))
            elif resp.startswith(KO_MESSAGE):
                print('Error deleting job {}.'.format(
                    resp.replace(KO_MESSAGE, '')))
            else:
                print('Error: {}'.format(resp))
            resp = self._receive()

    def do_create(self, custom):
        """create [custom]
        Create a new job. A form will ask for all the needed configuration.
        The optional [custom] argument lets you introduce a custom query."""
        job = self._create_job(custom=custom == CmdTest.CUSTOM)
        if job:
            self.conn.send(job)
            resp = self._receive()
            if resp.endswith('.json'):
                resp = resp[:-5]
            if not resp.startswith(KO_MESSAGE):
                print('Job {} successfully created. '
                      'Would you like to start running it?'.format(resp))
                while True:
                    response = self._prompt("(y/n): ", is_input=True)\
                        .lower().strip()
                    if response in ['y', 'n', 'yes', 'no']:
                        if response[0] == 'y':
                            self.do_run(resp)
                        break
                    print('Error: Enter "(y)es" or "(n)o"')
            else:
                resp = resp.replace(KO_MESSAGE, '')
                if resp:
                    print(resp)
                else:
                    print('Error creating job. Please, try again.')

    def _format_with_default(self, str, default):
        return str.format(' ({})'.format(default) if default else '')

    def _handle_creation(self, obj, edit=False):
        for param in CmdTest.MANDATORY_PARAMS:
            if param not in obj:
                print("Error: Missing mandatory parameter {}".format(param))
                return

        while True:
            self._prompt("{} job with this configuration?".format(
                'Edit' if edit else 'Create'))
            print(json.dumps(obj, indent=4))
            response = self._prompt("(y/n): ", is_input=True).lower().strip()
            if response in ['y', 'n', 'yes', 'no']:
                break
            print('Error: Enter "(y)es" or "(n)o"')

        if response in ['n', 'no']:
            return

        while True:
            default_name = CmdTest.DEFAULT_NAME_TEMPLATE.format(
                **obj, time=math.floor(time.time())).replace('.', '_')
            default_name_value = obj.get('name', default_name)
            name = self._prompt(
                self._format_with_default("job name{}: ",
                                          default_name_value),
                is_input=True).lower().strip() or default_name_value
            self.conn.send(self._build_msg('get', '{}.json'.format(name)))
            resp = self._receive()
            if not resp.startswith(OK_MESSAGE):
                obj['name'] = name
                break
            print('Error: job "{}" already exists.'.format(name))

        return self._build_msg("create", obj)

    def _create_job(self, default_config={}, custom=False):
        while True:
            available_instances = list(CONF['api'].keys())
            default_value = default_config.get('instance',
                                               ', '.join(available_instances))
            instance = self._prompt(
                self._format_with_default("API instance{}: ", default_value),
                is_input=True).lower().strip() or default_value
            if instance in available_instances:
                break
            print('Error: Instance must be one of {}'.format(
                available_instances))
        while True:
            available_auth_types = ['ks', 't']
            auth_type = self._prompt(
                self._format_with_default("API auth type{}: ",
                                          '[key + secret(ks), token(t)]'),
                is_input=True).lower().strip()
            if auth_type in available_auth_types:
                break
            print('Error: API auth type must be one of {}'.format(
                available_auth_types))
        if auth_type == 't':
            while True:
                token = self._prompt(
                    self._format_with_default("Token: ", None),
                    is_input=True).strip()
                if token:
                    break
                print('Error: Token cannot be undefined')
        else:
            while True:
                key = self._prompt(
                    self._format_with_default("API Key: ", None),
                    is_input=True).strip()
                if key:
                    break
                print('Error: API Key cannot be undefined')
            while True:
                secret = self._prompt(
                    self._format_with_default("API Secret: ", None),
                    is_input=True).strip()
                if secret:
                    break
                print('Error: API Secret cannot be undefined')
        if custom:
            while True:
                default_value = default_config.get('query', None)
                query = self._prompt(
                    self._format_with_default("Query{}: ", default_value),
                    is_input=True).lower().strip() or default_value
                if query:
                    # check domain
                    domain = CmdTest.DOMAIN_REGEX.findall(query)
                    if not domain:
                        print('Error: Domain not found in query')
                        continue
                    domain = domain[0]
                    # check table
                    table = CmdTest.TABLE_REGEX.findall(query)
                    if not table:
                        print('Error: Table not found in query')
                        continue
                    table = table[0]
                    # where eventdate filters
                    eventdate_filters = CmdTest.EVENTDATE_REGEX.findall(query)
                    if eventdate_filters:
                        print('Error: Query can\'t contain eventdate filters. '
                              'They will be filled in after the job '
                              'is created')
                        continue
                    # check period
                    every = CmdTest.EVERY_REGEX.findall(query)
                    if not every:
                        print('Error: Period not found in query')
                        continue
                    period = every[0][1]
                    if not PERIOD_REGEX.match(period):
                        print('Error: Period must match format \\d+[dhms]')
                        continue
                    # check host
                    group_by = CmdTest.BY_REGEX.findall(query)
                    if group_by:
                        group_by = group_by[0].split(',')
                        if len(group_by) > 1:
                            print('Error: Query must contain only '
                                  'one grouping key')
                            continue
                        else:
                            group_by = group_by[0].strip()
                    else:
                        group_by = None
                    # check count
                    match = CmdTest.COUNT_REGEX.findall(query)
                    if not match:
                        print('Error: Query must contain only '
                              'one of these aggregation functions: {}'.format(
                                  CmdTest.ALLOWED_AGG_FUNCTIONS))
                        continue
                    # check threshold
                    count, threshold = match[0]
                    if threshold:
                        if not NUMBER_REGEX.match(threshold):
                            print('Error: Threshold must be an integer number')
                            continue
                        threshold = int(threshold)
                    else:
                        threshold = 0
                    query = query.replace(every[0][0], '')
                    break
                else:
                    print('Error: Query cannot be undefined')
        else:
            while True:
                default_value = default_config.get('domain', None)
                domain = self._prompt(
                    self._format_with_default("Domain{}: ", default_value),
                    is_input=True).lower().strip() or default_value
                if domain:
                    break
                print('Error: Domain cannot be undefined')
            while True:
                default_value = default_config.get('table', None)
                table = self._prompt(
                    self._format_with_default("Table{}: ", default_value),
                    is_input=True).lower().strip() or default_value
                if table:
                    break
                print('Error: Table cannot be undefined')
            while True:
                default_value = default_config.get('period', None)
                period = self._prompt(
                    self._format_with_default("Period{}: ", default_value),
                    is_input=True).lower().strip() or default_value
                if period and PERIOD_REGEX.match(period):
                    break
                print('Error: Period must match format \\d+[dhms]')
            while True:
                default_value = str(default_config.get('threshold', '0'))
                threshold = self._prompt(
                    self._format_with_default("Threshold{}: ",
                                              default_value or '0'),
                    is_input=True).lower().strip() or default_value
                if NUMBER_REGEX.match(threshold):
                    threshold = int(threshold)
                    break
                print('Error: Threshold must be a number')
            default_value = default_config.get('group_by', None)
            group_by = self._prompt(
                self._format_with_default("Group by{}: ",
                                          default_value or 'None'),
                is_input=True).strip() or default_value

        if group_by:
            default_required_keys = default_config.get('required_keys', None)
            required_keys =  self._prompt(
                self._format_with_default(
                    "List of grouping keys that must exist in every period{}: ",
                    default_required_keys or 'None'),
                is_input=True) or default_required_keys

            if required_keys:
                required_keys = list(map(lambda x: x.strip(),
                                         required_keys.split(',')))

            default_alarms = default_config.get('max_alarms', 0)
            while True:
                max_alarms =  self._prompt(
                    self._format_with_default(
                        "Number of consecutive alarms per group value{}: ",
                        default_alarms or '0'),
                    is_input=True).lower().strip() or default_alarms

                if NUMBER_REGEX.match(str(max_alarms)):
                    max_alarms = int(max_alarms)
                    break
                print('Error: Max alarms must be a number')

        while True:
            default_value = default_config.get('related_periods', {})
            default_period = default_value.get('period', None)
            default_stdevs = default_value.get('stdevs', '1')
            default_amount = default_value.get('amount', '5')
            default_prepopulate = \
                default_value.get('prepopulate', 'true').lower()
            default_raw = default_value.get('send_raw_to_table', None)
            default_regression = default_value.get('regression', [])
            related_periods = self._prompt(
                self._format_with_default("Related Periods{}: ",
                                          default_period or 'None'),
                is_input=True).lower().strip() or default_period
            if related_periods and not PERIOD_REGEX.match(related_periods):
                print('Error: Related Periods must match format \\d+[dhms]')
                continue
            if not related_periods:
                break
            while True:
                stdevs = self._prompt(
                    self._format_with_default("Max. stdevs{}: ",
                                              default_stdevs or '1'),
                    is_input=True).lower().strip() or default_stdevs
                if stdevs and not NUMBER_REGEX.match(stdevs):
                    print('Error: Max. stdevs must be a positive number.')
                    continue
                stdevs = int(stdevs)
                break
            while True:
                amount = self._prompt(
                    self._format_with_default("Max. number of periods{}: ",
                                              default_amount or '5'),
                    is_input=True).lower().strip() or default_amount
                if amount:
                    if not NUMBER_REGEX.match(amount):
                        print('Error: Max. number of periods must be '
                              'a positive number.')
                        continue
                    amount = int(amount)
                    if amount < 2:
                        print('Error: Max. number of periods must be '
                              'greater or equal than 2.')
                        continue
                break
            while True:
                prepopulate = self._prompt(
                    self._format_with_default(
                        "Prepopulate previous periods{}: ",
                        default_prepopulate or 'true'),
                    is_input=True).lower().strip() or default_prepopulate
                if prepopulate in ['true', 'false']:
                    prepopulate = prepopulate == "true"
                    break
                print('Error: Prepopulate must be either "true" or "false"')

            send_raw_to_table = self._prompt(
                self._format_with_default("Send raw stats data to table{}: ",
                                          default_raw or 'None'),
                is_input=True).lower().strip() or default_raw

            if send_raw_to_table:
                while True:
                    regression = self._prompt(
                        self._format_with_default(
                            "Regression models to apply (none or several from: "
                            "[{}] max. 4): ", ','.join(MODELS.MAPPER.keys())),
                        is_input=True).lower().split(',') or default_regression
                    if not regression[0]:
                        regression = None
                        break

                    if len(regression) > 4:
                        print('Error: Max. 4 regression models')
                    else:
                        fail = False
                        for i in range(len(regression)):
                            regression[i] = regression[i].strip()
                            if not regression[i] in MODELS.MAPPER:
                                print('Error: Unrecognized regression '
                                      'model {}'.format(regression[i]))
                                fail = True
                                break
                        if not fail:
                            break
            else:
                regression = None

            break

        timezone = self._prompt(
            self._format_with_default("Timezone{}: ", 'UTC'),
            is_input=True, completer=complete_timezones) or None

        while True:
            null_as_zero =  self._prompt(
                self._format_with_default(
                    "Null response as zero{}?: ", 'false'),
                is_input=True).lower().strip() or None

            if null_as_zero:
                if not null_as_zero in ['true', 'false']:
                    print('Error: Must be true or false')
                    continue
                else:
                    null_as_zero = null_as_zero == 'true'
            break

        send_to = list(map(lambda x: x.strip(), self._prompt(
            self._format_with_default(
                "Send alerts to table/s{}: ",
                'one or several tags, blank for default table {}'.format(
                    CONF['targets']['monitor']
                )),
            is_input=True).lower().split(',')))

        if not send_to[0]:
            send_to = None

        if send_to:
            for idx, target in enumerate(send_to):
                while True:
                    custom_sender = self._prompt(
                        self._format_with_default(
                            "Set custom sender for {} (y/n)?: ".format(
                                target), None),
                        is_input=True).lower().strip()
                    if custom_sender in ['y', 'n']:
                        custom_sender = custom_sender == 'y'
                        break
                    print('Error: Enter "(y)es" or "(n)o"')

                if not custom_sender:
                    continue
                sender = {}
                while True:
                    address = self._prompt(
                        self._format_with_default(
                            "Devo address: ", None),
                        is_input=True).lower().strip()
                    if address:
                        sender['address'] = address
                        break
                    print('Error: Address cannot be undefined')
                while True:
                    port = self._prompt(
                        self._format_with_default(
                            "Devo port: ", None),
                        is_input=True).strip()
                    if port:
                        if NUMBER_REGEX.match(str(port)):
                            sender['port'] = int(port)
                            break
                        else:
                            print('Error: Port must be a number')
                            continue
                    print('Error: Port cannot be undefined')
                while True:
                    key = self._prompt(
                        self._format_with_default(
                            "Devo key path: ", None),
                        is_input=True).strip()
                    if key:
                        sender['key'] = key
                        break
                    print('Error: Key path cannot be undefined')
                while True:
                    cert = self._prompt(
                        self._format_with_default(
                            "Devo cert path: ", None),
                        is_input=True).strip()
                    if cert:
                        sender['cert'] = cert
                        break
                    print('Error: Cert path cannot be undefined')
                while True:
                    chain = self._prompt(
                        self._format_with_default(
                            "Devo chain path: ", None),
                        is_input=True).strip()
                    if chain:
                        sender['chain'] = chain
                        break
                    print('Error: Chain path cannot be undefined')
                sender['tag'] = target
                send_to[idx] = sender

        while True:
            default_retries = default_config.get('max_retries', 3)
            max_retries =  self._prompt(
                self._format_with_default(
                    "Max API calls retries{}: ", default_retries or '3'),
                is_input=True).lower().strip() or default_retries

            if NUMBER_REGEX.match(str(max_retries)):
                max_retries = int(max_retries)
                break
            print('Error: Max retries must be a number')

        while True:
            logging_options = ['file', 'console', 'devo']
            logging = list(map(lambda x: x.strip(), self._prompt(
                self._format_with_default(
                    "Job logging{}: ",
                    'one or several of {}, blank for default'.format(
                        logging_options
                    )),
                is_input=True).lower().split(',')))

            if not logging[0]:
                logging = None
                break
            error = ''
            for loggin_option in logging:
                if not loggin_option in logging_options:
                    error = (
                        'Error: Unrecognized option {}. Must be one or '
                        'several of [file, console, devo] or default.'.format(
                            loggin_option))
                    break
            if not error:
                break
            print(error)

        if logging:
            logging = {logging_option: {} for logging_option in logging}
            if 'console' in logging:
                while True:
                    level = self._prompt(
                        self._format_with_default(
                            "Console logging level{}: ", 'ERROR'),
                        is_input=True).upper().strip() or 'ERROR'
                    if level in CmdTest.LOGGING_LEVELS:
                        logging['console']['level'] = level
                        break
                    print('Error: Logging level must be one of {}'.format(
                        CmdTest.LOGGING_LEVELS))
            if 'file' in logging:
                while True:
                    level = self._prompt(
                        self._format_with_default(
                            "File logging level{}: ", 'DEBUG'),
                        is_input=True).upper().strip() or 'DEBUG'
                    if level in CmdTest.LOGGING_LEVELS:
                        logging['file']['level'] = level
                        break
                    print('Error: Logging level must be one of {}'.format(
                        CmdTest.LOGGING_LEVELS))
                path = self._prompt(
                    self._format_with_default(
                        "File logging path{}: ", 'default from conf.json'),
                    is_input=True).strip() or None
                if path:
                    logging['file']['path'] = path
            if 'devo' in logging:
                while True:
                    level = self._prompt(
                        self._format_with_default(
                            "Devo logging level{}: ", 'INFO'),
                        is_input=True).upper().strip() or 'INFO'
                    if level in CmdTest.LOGGING_LEVELS:
                        logging['devo']['level'] = level
                        break
                    print('Error: Logging level must be one of {}'.format(
                        CmdTest.LOGGING_LEVELS))
                while True:
                    sender = self._prompt(
                        self._format_with_default(
                            "Devo custom sender{}: ", 'false'),
                        is_input=True).lower().strip() or None
                    if sender and not sender in ['true', 'false']:
                        print('Error: Must be true or false')
                        continue
                    break
                if sender:
                    logging['devo']['sender'] = {}
                    while True:
                        address = self._prompt(
                            self._format_with_default(
                                "Devo logging address: ", None),
                            is_input=True).lower().strip()
                        if address:
                            logging['devo']['sender']['address'] = address
                            break
                        print('Error: Address cannot be undefined')
                    while True:
                        port = self._prompt(
                            self._format_with_default(
                                "Devo logging port: ", None),
                            is_input=True).strip()
                        if port:
                            if NUMBER_REGEX.match(str(port)):
                                logging['devo']['sender']['port'] = int(port)
                                break
                            else:
                                print('Error: Port must be a number')
                                continue
                        print('Error: Port cannot be undefined')
                    while True:
                        key = self._prompt(
                            self._format_with_default(
                                "Devo logging key path: ", None),
                            is_input=True).strip()
                        if key:
                            logging['devo']['sender']['key'] = key
                            break
                        print('Error: Key path cannot be undefined')
                    while True:
                        cert = self._prompt(
                            self._format_with_default(
                                "Devo logging cert path: ", None),
                            is_input=True).strip()
                        if cert:
                            logging['devo']['sender']['cert'] = cert
                            break
                        print('Error: Cert path cannot be undefined')
                    while True:
                        chain = self._prompt(
                            self._format_with_default(
                                "Devo logging chain path: ", None),
                            is_input=True).strip()
                        if chain:
                            logging['devo']['sender']['chain'] = chain
                            break
                        print('Error: Chain path cannot be undefined')
                    while True:
                        tag = self._prompt(
                            self._format_with_default(
                                "Devo logging table: ", None),
                            is_input=True).strip()
                        if tag:
                            logging['devo']['sender']['tag'] = tag
                            break
                        print('Error: Table cannot be undefined')

        obj = {
            "instance": instance,
            "domain": domain,
            "table": table,
            "period": period,
            "max_retries": max_retries,
            "threshold": threshold,
            "group_by": group_by,
            "related_periods": related_periods,
        }

        if auth_type == 't':
            obj['auth'] = {"token": token}
        else:
            obj['auth'] = {"key": key, "secret": secret}

        if send_to:
            obj['send_to'] = send_to

        if timezone:
            obj['timezone'] = timezone

        if null_as_zero:
            obj['null_as_zero'] = null_as_zero

        if logging:
            obj['logging'] = logging

        if group_by and required_keys:
            obj['required_keys'] = required_keys

        if group_by and max_alarms:
            obj['max_alarms'] = max_alarms

        if related_periods:
            obj['related_periods'] = {
                "periods": related_periods,
                "stdevs": stdevs,
                "amount": amount,
                "prepopulate": prepopulate,
                "send_raw_to_table": send_raw_to_table,
                "regression": regression
            }

        if custom:
            obj['query'] = query

        if 'name' in default_config:
            obj['name'] = default_config['name']

        return self._handle_creation(obj, edit=default_config)

    def _build_msg(self, command, data=None):
        return json.dumps({"command": command, "data": data}).encode('utf-8')

    def _prompt(self, raw_text, is_input=False, completer=default_completer):
        input_command = None
        if is_input:
            old_completer = readline.get_completer()
            readline.set_completer(completer)
            readline.set_completer_delims(' \t\n;')
            readline.parse_and_bind("tab: complete")
            input_command = input('lapua> {}'.format(raw_text))
            readline.set_completer(old_completer)
        else:
            print('lapua> {}'.format(raw_text))

        return input_command

    def _receive(self):
        size = self.conn.recv(8).decode('utf-8')
        return self.conn.recv(int(size)).decode('utf-8')


if __name__ == '__main__':
    CONF = get_config()
    SOCKET = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        SOCKET.connect((CONF['server']['address'], CONF['server']['port']))
        readline.set_completer_delims(' \t\n')
        CmdTest(SOCKET).cmdloop()
    except Exception as excpt:
        print("Error: Couldn't connect to server at {}:{}"\
            .format(CONF['server']['address'], CONF['server']['port']))
        print(excpt)
# -*- coding: utf-8 -*- # OVERRIDING version         Returns deployed Lapua version. exit         Close the client. quit         Close the client. get <job_name>         Get the configuration for <job_name> job.         <job_name> must exist. test <job_name>         Test the configuration for <job_name> job.         <job_name> must exist. load <job_path>         Load the configuration from <job_path> and create a new job.         <job_path> must exist. list         Display all running and stopped jobs. manage <domain/job> <name> <limit>         Sets <domain/job> limit. run <job_name>         Start running the <job_name> job.         <job_name> must exist and not being currently running. stop <job_name>         Stop running the <job_name> job.         <job_name> must exist and not being currently stopped. delete <job_name>         Delete the <job_name> job.         <job_name> must exist and not being currently running. create [custom]         Create a new job. A form will ask for all the needed configuration.         The optional [custom] argument lets you introduce a custom query. # check domain # check table # where eventdate filters # check period # check host # check count # check threshold