Python3 скрипт для конструктора конфиг-файлов с Jinja в Unix/Linux

Недавно, на работе появилась нужда в том, чтобы иметь один конфигурационный YAML-файл со всеми типами ENV и переменными для них. При этом, чтобы скрипт умел парсить его и подставлять во все вхождения во всех входящих файлах. Это очень удобно, когда у вас много типов машин с разными настройками. Я решил проблему, написал скрипт, все очень даже довольны.

Python3 скрипт для конструктора конфиг-файлов с Jinja в Unix/Linux

Начнем с того, что у становим питон и другие полезности:

Установка python в Unix/Linux — в процессе написания (в черновиках)

Установка pip/setuptools/wheel в Unix/Linux

Другие полезные темы:

Установка/Использование pexpect и python в Unix/Linux

Установка virtualenv + virtualenvwrapper в Unix/Linux

Pip3 имееться, теперь поставим пакеты:

$ pip3 install -U pyyaml argparse

Открываем файл, например, я его вот так назвал:

$ vim templater_py3.py

Содержание будет следующим:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import time
import os
import pathlib
import glob
import yaml

from jinja2 import Environment, StrictUndefined
from jinja2.loaders import FileSystemLoader
from jinja2.exceptions import UndefinedError, TemplateRuntimeError, TemplateNotFound


class Bgcolors:
    def __init__(self):
        self.colors = {
            'PURPURE': '\033[95m',
            'BLUE': '\033[94m',
            'GREEN': '\033[92m',
            'YELLOW': '\033[93m',
            'RED': '\033[91m',
            'ENDC': '\033[0m',
            'BOLD': '\033[1m',
            'UNDERLINE': '\033[4m'
        }


def direcroty(in_put, output):

    array_files = []

    if in_put is not None:
        if os.path.exists(os.path.dirname(in_put)):
            if os.path.isdir(os.path.dirname(in_put)):
                print(Bgcolors().colors['GREEN'] + 'Input is directory: {}'.format(pathlib.Path(in_put.strip())),
                      Bgcolors().colors['ENDC'])
                for file in glob.glob(os.path.join(in_put, '*')):
                    if os.path.isfile(file):
                        array_files.append(pathlib.Path(file.strip()))
            else:
                print('Input is file: {}'.format(pathlib.Path(in_put.strip())))
                if os.path.isfile(pathlib.Path(in_put.strip())):
                    array_files.append(pathlib.Path(in_put.strip()))
                else:
                    print(Bgcolors().colors['RED'] +
                          'Use correct PATH for configs or check your file: {}'.format(in_put), '\nIt doesnt exist!',
                          Bgcolors().colors['ENDC'])
                    exit(0)
        else:
            print(Bgcolors().colors['RED'] +
                  'Use correct PATH for configs or check your file: {}'.format(in_put), '\nIt doesnt exist!',
                  Bgcolors().colors['ENDC'])
            exit(0)
    else:
        print(Bgcolors().colors['RED'], 'Please set [--in_put-file] or [--in_put-dir] or use [--help] to get a help',
              Bgcolors().colors['ENDC'])
        exit(0)

    if output is not None:
        if os.path.exists(os.path.dirname(output)):
            print(Bgcolors().colors['GREEN'] + 'Output folder: ', pathlib.Path(output.strip()), '\n',
                  Bgcolors().colors['ENDC'])
        else:
            try:
                os.stat(pathlib.Path(os.path.dirname(output)))
                print(Bgcolors().colors['GREEN'] + 'Output folder: ', pathlib.Path(output.strip()), '\n',
                      Bgcolors().colors['ENDC'])
            except Exception:
                os.mkdir(output)
                print(Bgcolors().colors['GREEN'] + 'Output folder: ', pathlib.Path(output.strip()), '\n',
                      Bgcolors().colors['ENDC'])
    else:
        print(Bgcolors().colors['RED'], 'Please set [--output-dir] or use [--help] to get a help',
              Bgcolors().colors['ENDC'])
        exit(0)

    # print(array_files)

    return array_files


def jinja_render(in_put, output, env, env_type, yml_files):

    array_files = direcroty(in_put, output)
    sections = ['default', env, env_type]
    # add properties that we know about
    config = {'ENVIRONMENT_TYPE': env_type, 'ENVIRONMENT': env}

    # Create the jinja2 environment.
    # Notice the use of trim_blocks, which greatly helps control whitespace.
    j2_env = Environment(
        loader=FileSystemLoader(''),
        trim_blocks=True,
        autoescape=False,
        undefined=StrictUndefined)

    if yml_files is not None:
        for yml_file in yml_files:
            with open(yml_file) as stream:
                try:
                    # print(yaml.safe_load(stream))
                    cfg_yaml = yaml.safe_load(stream.read())
                    for key in sections:
                        props = cfg_yaml.get(key)
                        # print('{0} : {1}'.format(key,props), len(props))
                        if props is not None:
                            # print(yaml.dump(props, default_flow_style=False))
                            config.update(props)
                except yaml.YAMLError as exc:
                    print(exc)
        for envriron in os.environ:
            config.update({envriron: os.environ.get(envriron)})
            # print(envriron, os.environ.get(envriron))
    else:
        print(Bgcolors().colors['RED'], 'Please set [--yml] or use [--help] to get a help',
              Bgcolors().colors['ENDC'])
        exit(0)

    print(yaml.dump(config, default_flow_style=False))
    errs = []

    for file in array_files:
        # file_name = os.path.basename(file)
        head, file_name = os.path.split(file.as_posix())
        template = j2_env.get_template(file.as_posix())
        try:
            out_content = template.render(env=os.environ, **config)
            out_file = output + '/' + file_name
            try:
                f = open(out_file, 'wb')
                f.write(out_content.encode('utf-8'))
                f.close()
                print(Bgcolors().colors['GREEN'] + 'The [{}] file has been written.'.format(out_file)+
                      Bgcolors().colors['ENDC'])
            except ValueError:
                print('I cant write to file: ', ValueError)
            # print('file:', file,'\n', out_content)
        except UndefinedError as e:
            if e not in errs:
                errs.append(str(e))
                # errs.append(str(e).split("'")[1])
    if len(errs) > 0:
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
        print('Total errors: [{}]'.format(len(errs)))
        print(errs)
        print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')

    return jinja_render


def main():
    start__time = time.time()
    parser = argparse.ArgumentParser(prog='python3 script_name.py -h',
                                     usage='python3 script_name.py {ARGS}',
                                     add_help=True,
                                     prefix_chars='--/',
                                     epilog='''created by Vitalii Natarov''')
    parser.add_argument('--version', action='version', version='v0.2.0')
    parser.add_argument('--input-file', '--input-dir', dest='input', help='Set input file or folder to find configs',
                        default='./templates')
    parser.add_argument('--output-dir', dest='output', help='Set output a folder for outgoing files',
                        default='./output')
    parser.add_argument('--env', dest='env', help='Environment: tst, sit01, prd, etc', default=None)
    parser.add_argument('--env-type', dest='env_type', help='Environment Type: prod or nonprod', default=None)
    parser.add_argument('--yml', dest='yml_files', nargs="+", help='Config property yml files', default=None)

    results = parser.parse_args()

    in_put = os.path.expanduser(results.input)
    output = os.path.expanduser(results.output)
    env = results.env
    env_type = results.env_type
    yml_files = results.yml_files

    jinja_render(in_put, output, env, env_type, yml_files)

    end__time = round(time.time() - start__time, 2)
    print("--- %s seconds ---" % end__time)
    print(Bgcolors().colors['GREEN'], "============================================================",
          Bgcolors().colors['ENDC'])
    print(Bgcolors().colors['GREEN'], "==========================FINISHED==========================",
          Bgcolors().colors['ENDC'])
    print(Bgcolors().colors['GREEN'], "============================================================",
          Bgcolors().colors['ENDC'])


if __name__ == '__main__':
    main()

Чтобы получить помощь по скрипты, выполните:

$ python3 templater_py3.py -h
usage: python3 script_name.py {ARGS}

optional arguments:
  -h, --help            show this help message and exit
  --version             show program's version number and exit
  --input-file INPUT, --input-dir INPUT
                        Set input file or folder to find configs
  --output-dir OUTPUT   Set output a folder for outgoing files
  --env ENV             Environment: tst, sit01, prd, etc
  --env-type ENV_TYPE   Environment Type: prod or nonprod
  --yml YML_FILES [YML_FILES ...]
                        Config property yml files

created by Vitalii Natarov

Где:

  • —input-file или —input-dir — Входные данные (папка или файл) для обработки jinja-ей. Т.е чтобы джинжа заменила все вхождения в конфиге или других файлах.
  • —output-dir — Выходные данные будут складываться в данную папку.
  • —env — Имя окружающей среды (service1, service2).
  • —env-type — Тип окружающей среды (prod, nonprod, dev).
  • —yml — Ямл файл со всеми настройками для всех типов ENV и ENV в целом.

Я приведу пример использования данного скрипта:

$ python3 templater_py3.py --input-dir=templates --yml configProps.yml --env=test --env-type=nonprod --ouput-dir=output

При необходимости, скрипт понимает что нужно создать output папку, по этому, ее не обязательно создавать.

Данный скрипт тестировался на:

  • MacOS с python 3.7.
  • CentOS 7 с python 3.4, 3.5.

Немного позже, выложу данный скрипт на Github и обновлю статью.

Вот и все, статья «Python3 скрипт для конструктора конфиг-файлов с Jinja в Unix/Linux» завершена.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.