Merge branch 'master' of vcs.bingo-boom.ru:difrex/surok into test
This commit is contained in:
commit
49e264ddd1
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ surok/__pycache__
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.swp
|
||||
selfcheck
|
||||
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2106, Denis Zheleztsov <difrex.punk@gmail.com>
|
||||
Copyright (c) 2016, Denis Zheleztsov <difrex.punk@gmail.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
8
README.en.md
Normal file
8
README.en.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Surok
|
||||
|
||||
Service discovery for Apache Mesos.
|
||||
|
||||
* Jinja2 Templates
|
||||
* Discovery over mesos-dns
|
||||
* Applications config reload
|
||||
|
@ -1,7 +1,12 @@
|
||||
{
|
||||
"marathon": "TODO",
|
||||
"confd": "conf.d",
|
||||
"marathon": {
|
||||
"force": true,
|
||||
"host": "http://marathon.mesos:8080",
|
||||
"enabled": true
|
||||
},
|
||||
"confd": "/etc/surok/conf.d",
|
||||
"domain": "marathon.mesos",
|
||||
"wait_time": 20,
|
||||
"lock_dir": "/var/tmp"
|
||||
"lock_dir": "/var/tmp",
|
||||
"loglevel": "info"
|
||||
}
|
||||
|
40
debian/changelog
vendored
40
debian/changelog
vendored
@ -1,3 +1,43 @@
|
||||
surok (0.7-3) jessie; urgency=medium
|
||||
|
||||
* Small fix in marathon restart
|
||||
|
||||
-- Denis Zheleztsov <difrex@bingo-boom.ru> Fri, 14 Oct 2016 12:41:31 +0300
|
||||
|
||||
surok (0.7-2) testing; urgency=medium
|
||||
|
||||
* Fixed sys module
|
||||
|
||||
-- Denis Zheleztsov <difrex@bingo-boom.ru> Fri, 14 Oct 2016 11:53:45 +0300
|
||||
|
||||
surok (0.7-1) testing; urgency=medium
|
||||
|
||||
* New dependencies
|
||||
|
||||
-- Denis Zheleztsov <difrex@bingo-boom.ru> Fri, 14 Oct 2016 11:39:36 +0300
|
||||
|
||||
surok (0.7) testing; urgency=medium
|
||||
|
||||
* Marathon restart implementation
|
||||
* WARNING: BROKEN BACKWARD COMPATIBILY WITH OLD MAIN CONFIG
|
||||
!!! Please update your config first !!!
|
||||
|
||||
-- Denis Zheleztsov <difrex@bingo-boom.ru> Fri, 14 Oct 2016 11:28:07 +0300
|
||||
|
||||
surok (0.5.5) testing; urgency=medium
|
||||
|
||||
* #closes SD-10
|
||||
* Group switch
|
||||
* Version bump
|
||||
|
||||
-- Denis Zheleztsov <difrex@bingo-boom.ru> Tue, 11 Oct 2016 15:14:44 +0300
|
||||
|
||||
surok (0.3.2) testing; urgency=medium
|
||||
|
||||
* Remove ending '.' in hostname. (for those fucking libs that knows nothing about RFC)
|
||||
|
||||
-- Denis Ryabyy <vv1r0x@gmail.com> Fri, 12 Aug 2016 10:48:12 +0300
|
||||
|
||||
surok (0.1-1) testing; urgency=low
|
||||
|
||||
* Initial release (Closes: BBONL-1696)
|
||||
|
2
debian/control
vendored
2
debian/control
vendored
@ -8,5 +8,5 @@ Vcs-Git: http://vcs.bingo-boom.ru/difrex/surok.git
|
||||
|
||||
Package: surok
|
||||
Architecture: all
|
||||
Depends: python3-jinja2, python3-dnsq
|
||||
Depends: python3-jinja2, python3-dnsq, python3-requests
|
||||
Description: Service discovery for Apache Mesos clusters
|
||||
|
10
debian/install
vendored
10
debian/install
vendored
@ -1,6 +1,6 @@
|
||||
conf/surok.json etc/surok/conf
|
||||
surok/templates.py usr/lib/python3/dist-packages/surok
|
||||
surok/system.py usr/lib/python3/dist-packages/surok
|
||||
surok/__init__.py usr/lib/python3/dist-packages/surok
|
||||
surok/discovery.py usr/lib/python3/dist-packages/surok
|
||||
surok.py usr/bin
|
||||
surok/templates.py opt/surok/surok
|
||||
surok/system.py opt/surok/surok
|
||||
surok/__init__.py opt/surok/surok
|
||||
surok/discovery.py opt/surok/surok
|
||||
surok.py opt/surok
|
||||
|
36
debian/patches/path-change
vendored
36
debian/patches/path-change
vendored
@ -1,36 +0,0 @@
|
||||
Description: <short summary of the patch>
|
||||
TODO: Put a short summary on the line above and replace this paragraph
|
||||
with a longer explanation of this change. Complete the meta-information
|
||||
with other relevant fields (see below for details). To make it easier, the
|
||||
information below has been extracted from the changelog. Adjust it or drop
|
||||
it.
|
||||
.
|
||||
surok (0.1-1) unstable; urgency=low
|
||||
.
|
||||
* Initial release (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
||||
Author: Denis Zheleztsov <difrex@bingo-boom.ru>
|
||||
|
||||
---
|
||||
The information above should follow the Patch Tagging Guidelines, please
|
||||
checkout http://dep.debian.net/deps/dep3/ to learn about the format. Here
|
||||
are templates for supplementary fields that you might want to add:
|
||||
|
||||
Origin: <vendor|upstream|other>, <url of original patch>
|
||||
Bug: <url in upstream bugtracker>
|
||||
Bug-Debian: https://bugs.debian.org/<bugnumber>
|
||||
Bug-Ubuntu: https://launchpad.net/bugs/<bugnumber>
|
||||
Forwarded: <no|not-needed|url proving that it has been forwarded>
|
||||
Reviewed-By: <name and email of someone who approved the patch>
|
||||
Last-Update: <YYYY-MM-DD>
|
||||
|
||||
--- surok-0.1.orig/surok.py
|
||||
+++ surok-0.1/surok.py
|
||||
@@ -9,7 +9,7 @@ from surok.discovery import resolve
|
||||
from surok.system import reload_conf
|
||||
|
||||
# Load base configurations
|
||||
-f = open('conf/surok.json', 'r')
|
||||
+f = open('/etc/surok/conf/surok.json', 'r')
|
||||
conf = json.loads(f.read())
|
||||
print(conf)
|
||||
f.close()
|
1
debian/patches/series
vendored
1
debian/patches/series
vendored
@ -1 +0,0 @@
|
||||
path-change
|
1
debian/source/format
vendored
1
debian/source/format
vendored
@ -1 +0,0 @@
|
||||
3.0 (quilt)
|
33
doc/ru/app.configuration.md
Normal file
33
doc/ru/app.configuration.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Конфигурация приложения
|
||||
|
||||
/etc/surok/conf.d/app.json
|
||||
```
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"name": "kioskservice",
|
||||
"group": "production.romania",
|
||||
"ports": ["web", "socket"]
|
||||
}
|
||||
],
|
||||
"conf_name": "kiosk",
|
||||
"template": "/etc/surok/templates/kiosk.jj2",
|
||||
"dest": "/etc/nginx/sites-available/kioskservice.conf",
|
||||
"reload_cmd": "/bin/systemctl reload nginx",
|
||||
"run_cmd": ["/usr/bin/node", "-c", "config.json"]
|
||||
}
|
||||
```
|
||||
|
||||
Давайте разберем конфигурационный файл по опциям
|
||||
* services - array. Список хэшей с описанием сервисов
|
||||
name - string. Имя сервиса. Это имя приложения в marathon
|
||||
group - string. Группа в которой находится сервис. Группу можно узнать в marathon. Записывается в обратном порядке. Т.е. если у нас есть группа /webapps/php, то записывать её следует, как php.webapps
|
||||
Если группа не указана, то сурок ожидает группу в переменной окружения SUROK_DISCOVERY_GROUP, если и SUROK_DISCOVERY_GROUP нет, то берется группа marathon(0.5.5).
|
||||
ports - array. Список имен портов сервиса. Не обязательная опция.
|
||||
* conf_name - string. Название конфига. Должен быть уникальным значением. Слежит для создания и чтения lock конфигурации.
|
||||
* template - string. Абсолютный путь к файлу шаблона.
|
||||
* dest - string. Абсолютный путь к файлу в который запишется результат генерации шаблона.
|
||||
* reload_cmd - string. Команда, которая будет выполнена в случае обноления конфига.
|
||||
В reload_cmd можно использовать переменные окружения:
|
||||
```"reload_cmd": "/usr/bin/killall -9 calc || true && /usr/local/bin/calc -c /app/calc.conf ${CALC_NUM}"```
|
||||
* run_cmd(v0.6) - array. Список с командой на выполнение. Используется внутри контейнера вместо reload_cmd.
|
23
doc/ru/surok.configuration.md
Normal file
23
doc/ru/surok.configuration.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Конфигурация Surok
|
||||
|
||||
**/etc/surok/conf/surok.json**
|
||||
Разберем конфигурационный файл по опциям
|
||||
```
|
||||
{
|
||||
"marathon": "10.0.1.199:8080",
|
||||
"confd": "/etc/surok/conf.d",
|
||||
"domain": "marathon.mesos",
|
||||
"wait_time": 20,
|
||||
"lock_dir": "/var/tmp",
|
||||
"loglevel": "info|debug"
|
||||
"container": true|false
|
||||
}
|
||||
```
|
||||
|
||||
* marathon(v0.7) - string. Адрес Marathon Sheduler.
|
||||
* confd - strig. Абсолютный путь до директории с конфигурационными файлами приложений.
|
||||
* domain - string. Домен, который обслуживает mesos-dns.
|
||||
* wait_time - int. Время в секундах сколько Surok ждет до того, как начать заново делать запросы на обнаружение сервисов.
|
||||
* lock_dir - string. Абсолютный путь до директории с lock-конфигурациями.
|
||||
* loglevel - string. Уровень логирования.
|
||||
* container(v0.6) - boolean. Определяем внутри или нет контейнера запущен сурок. Меняется логика работы.
|
104
doc/ru/templates.md
Normal file
104
doc/ru/templates.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Шиблоны
|
||||
|
||||
Шаблоны для Surok пишутся на Jinja2. Возможно, стоит прочитать документацию.
|
||||
|
||||
## Словарь my в шаблоне
|
||||
|
||||
Surok заполняет словарь my и передает его в шаблон.
|
||||
```
|
||||
{
|
||||
"services": {
|
||||
"nginx": [
|
||||
{
|
||||
"name": "nginx.testing-kl92-s0.marathon.mesos.",
|
||||
"port": "31200"
|
||||
},
|
||||
{
|
||||
"name": "nginx.testing-kl123-s1.marathon.mesos.",
|
||||
"port": "32230"
|
||||
}
|
||||
],
|
||||
"emailsender": [
|
||||
{
|
||||
"name": "emailsender.testing-kl92-s0.marathon.mesos.",
|
||||
"port": "31201"
|
||||
},
|
||||
{
|
||||
"name": "emailsender.testing-kl123-s1.marathon.mesos.",
|
||||
"port": "32232"
|
||||
}
|
||||
],
|
||||
"service-with-defined-ports": {
|
||||
"web": [
|
||||
{
|
||||
"name": "f.q.d.n",
|
||||
"port": 12341
|
||||
}
|
||||
],
|
||||
"rpc": [
|
||||
{
|
||||
"name": "f.q.d.n",
|
||||
"port": 12342
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"HOME": "/var/lib/nginx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Пример реального шаблона
|
||||
|
||||
```
|
||||
upstream matrix-http {
|
||||
hash $remote_addr;
|
||||
{% for server in my['services']['matrix']['http'] %}
|
||||
server {{server['name']}}:{{server['port']}} max_fails=3;
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
upstream riot-http {
|
||||
hash $remote_addr;
|
||||
{% for server in my['services']['riot'] %}
|
||||
server {{server['name']}}:{{server['port']}} max_fails=3;
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 10.15.56.157:80;
|
||||
server_name matrix.example.com;
|
||||
|
||||
client_max_body_size 10m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://riot-http;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /_matrix/ {
|
||||
proxy_pass http://matrix-http;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
Так для upstream matrix-http используются именованные порты, а для riot-http – нет.
|
||||
|
||||
## Проверки в шаблоне
|
||||
|
||||
Переменная _my['env']_ является классом python _os.environ_, что позваоляет нам строить различные проверки, например:
|
||||
|
||||
```
|
||||
{% if my['env'].get('DB_HOST') %}
|
||||
host = '{{my['env']['DB_HOST']}}'
|
||||
{% else %}
|
||||
host = 'localhost'
|
||||
{% endif %}
|
||||
```
|
||||
|
23
surok.py
23
surok.py
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from time import sleep
|
||||
import os
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
import json
|
||||
@ -23,24 +24,27 @@ if args.config:
|
||||
# Read config file
|
||||
f = open(surok_conf, 'r')
|
||||
conf = json.loads(f.read())
|
||||
print(conf)
|
||||
f.close()
|
||||
|
||||
|
||||
# Get app configurations
|
||||
# Return list of patches to app discovery configuration
|
||||
def get_configs():
|
||||
confs = [f for f in listdir(conf['confd']) if isfile( join(conf['confd'], f) )]
|
||||
confs = [f for f in listdir(conf['confd']) if isfile(
|
||||
join(conf['confd'], f))]
|
||||
return confs
|
||||
|
||||
|
||||
# Get Surok App configuration
|
||||
# Read app conf from file and return dict
|
||||
def load_app_conf(app):
|
||||
# Load OS environment to app_conf
|
||||
f = open(conf['confd'] + '/' + app)
|
||||
c = json.loads(f.read())
|
||||
f.close()
|
||||
|
||||
c['env'] = os.environ
|
||||
|
||||
return c
|
||||
|
||||
|
||||
@ -56,20 +60,23 @@ while 1:
|
||||
for app in confs:
|
||||
app_conf = load_app_conf(app)
|
||||
|
||||
# Will be removed later
|
||||
# For old configs
|
||||
try:
|
||||
loglevel = conf['loglevel']
|
||||
except:
|
||||
conf['loglevel'] = 'info'
|
||||
|
||||
# Resolve services
|
||||
app_hosts = resolve(app_conf, conf)
|
||||
|
||||
# Populate my dictionary
|
||||
my = {"services": app_hosts,
|
||||
"conf_name": app_conf['conf_name']
|
||||
}
|
||||
"conf_name": app_conf['conf_name']}
|
||||
|
||||
# Generate config from template
|
||||
service_conf = gen(my, app_conf['template'])
|
||||
|
||||
stdout, first = reload_conf(service_conf, app_conf, first)
|
||||
print(stdout)
|
||||
|
||||
first = reload_conf(service_conf, app_conf, first, conf)
|
||||
|
||||
sleep(conf['wait_time'])
|
||||
|
||||
|
@ -1,18 +1,106 @@
|
||||
import dns.resolver
|
||||
import dns.query
|
||||
from dns.exception import DNSException
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
# Logger configuration
|
||||
# This need to be moved
|
||||
def get_logger():
|
||||
# Configure logging
|
||||
FORMAT = '%(asctime) %(message)s'
|
||||
logging.basicConfig(format=FORMAT)
|
||||
logger = logging.getLogger(__name__)
|
||||
return logger
|
||||
|
||||
|
||||
# Resolve service from mesos-dns SRV record
|
||||
# return dict {"servicename": [{"name": "service.f.q.d.n.", "port": 9999}]}
|
||||
def resolve(app, conf):
|
||||
hosts = {}
|
||||
services = app['services']
|
||||
domain = conf['domain']
|
||||
for service in services:
|
||||
hosts[service['name']] = []
|
||||
try:
|
||||
for rdata in dns.resolver.query('_' + service['name'] + '.' + service['group'] + '._tcp.' + domain, 'SRV'):
|
||||
info = str(rdata).split()
|
||||
server = { 'name': info[3], 'port': info[2] }
|
||||
logger = get_logger()
|
||||
|
||||
hosts[ service['name'] ].append(server)
|
||||
except Exception as e:
|
||||
print("Could not resolve " + service['name'] + '.' + service['group'] + '._tcp.' + domain)
|
||||
for service in services:
|
||||
hosts[service['name']] = {}
|
||||
|
||||
group = get_group(service, app)
|
||||
if group is False:
|
||||
logger.error('Group is not defined in config, SUROK_DISCOVERY_GROUP and MARATHON_APP_ID')
|
||||
logger.error('Not in Mesos launch?')
|
||||
sys.exit(2)
|
||||
|
||||
# Port name from app config
|
||||
ports = None
|
||||
try:
|
||||
ports = service['ports']
|
||||
except:
|
||||
pass
|
||||
|
||||
# This is fast fix for port naming
|
||||
# Will be rewrite later
|
||||
fqdn = ''
|
||||
if ports is not None:
|
||||
for port_name in ports:
|
||||
fqdn = '_' + port_name + '.' + '_' + service['name'] + '.' + group + '._tcp.' + domain
|
||||
hosts[service['name']][port_name] = do_query(fqdn, conf['loglevel'])
|
||||
else:
|
||||
fqdn = '_' + service['name'] + '.' + group + '._tcp.' + domain
|
||||
hosts[service['name']] = do_query(fqdn, conf['loglevel'])
|
||||
|
||||
return hosts
|
||||
|
||||
|
||||
# Do SRV queries
|
||||
# Return array: [{"name": "f.q.d.n", "port": 8876}]
|
||||
def do_query(fqdn, loglevel):
|
||||
logger = get_logger()
|
||||
servers = []
|
||||
try:
|
||||
query = dns.resolver.query(fqdn, 'SRV')
|
||||
query.lifetime = 1.0
|
||||
|
||||
for rdata in query:
|
||||
info = str(rdata).split()
|
||||
server = {'name': info[3][:-1], 'port': info[2]}
|
||||
servers.append(server)
|
||||
except DNSException as e:
|
||||
if loglevel != 'info':
|
||||
logger.error("Could not resolve " + fqdn + ': ' + str(e))
|
||||
|
||||
return servers
|
||||
|
||||
|
||||
# Groups switch
|
||||
# Priority: config, environment, marathon environment
|
||||
def get_group(service, app):
|
||||
# Check group in app conf
|
||||
if 'group' in service:
|
||||
return service['group']
|
||||
# Check environment variable
|
||||
elif app['env'].get('SUROK_DISCOVERY_GROUP'):
|
||||
return app['env']['SUROK_DISCOVERY_GROUP']
|
||||
# Check marathon environment variable
|
||||
elif app['env'].get('MARATHON_APP_ID'):
|
||||
group = parse_marathon_app_id(app['env']['MARATHON_APP_ID'])
|
||||
return group
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
# Parse MARATHON_APP_ID
|
||||
# Return marathon.group
|
||||
def parse_marathon_app_id(marathon_app_id):
|
||||
marathon_app_id = marathon_app_id.split('/')
|
||||
group = ''
|
||||
counter = len(marathon_app_id) - 2
|
||||
i = 0
|
||||
while counter > i:
|
||||
group = group + marathon_app_id[counter]
|
||||
if counter != i + 1:
|
||||
group += '.'
|
||||
counter -= 1
|
||||
|
||||
return group
|
||||
|
@ -1,6 +1,10 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import requests
|
||||
|
||||
|
||||
# Get old configuration
|
||||
def get_old(name, service_conf):
|
||||
|
||||
try:
|
||||
@ -26,7 +30,7 @@ def write_lock(name, service_conf):
|
||||
|
||||
|
||||
def do_reload(service_conf, app_conf):
|
||||
print( 'Write new configuration of ' + app_conf['conf_name'] )
|
||||
logging.warning('Write new configuration of ' + app_conf['conf_name'])
|
||||
|
||||
f = open(app_conf['dest'], 'w')
|
||||
f.write(service_conf)
|
||||
@ -39,16 +43,46 @@ def do_reload(service_conf, app_conf):
|
||||
return stdout
|
||||
|
||||
|
||||
def reload_conf(service_conf, app_conf, first):
|
||||
def reload_conf(service_conf, app_conf, first, conf):
|
||||
|
||||
# Check first loop
|
||||
if first == True:
|
||||
if first is True:
|
||||
stdout = do_reload(service_conf, app_conf)
|
||||
first = False
|
||||
return stdout, first
|
||||
logging.info(stdout)
|
||||
return first
|
||||
|
||||
# Check marathon enabled in configuration
|
||||
if conf['marathon']['enabled'] is True:
|
||||
restart_self_in_marathon(conf['marathon'])
|
||||
|
||||
if get_old(app_conf['conf_name'], service_conf) != 1:
|
||||
stdout = do_reload(service_conf, app_conf)
|
||||
return stdout, first
|
||||
logging.info(stdout)
|
||||
return first
|
||||
else:
|
||||
return 'Same config ' + app_conf['conf_name'] + ' Skip reload', first
|
||||
if conf['loglevel'] == 'debug':
|
||||
logging.debug('Same config ' +
|
||||
app_conf['conf_name'] +
|
||||
' Skip reload')
|
||||
return first
|
||||
|
||||
|
||||
# Do POST request to marathon API
|
||||
# /v2/apps//app/name/restart
|
||||
def restart_self_in_marathon(marathon):
|
||||
host = marathon['host']
|
||||
|
||||
# Check MARATHON_APP_ID environment varible
|
||||
if os.environ.get('MARATHON_APP_ID') is not True:
|
||||
logging.error('Cannot find MARATHON_APP_ID. Not in Mesos?')
|
||||
sys.exit(2)
|
||||
app_id = os.environ['MARATHON_APP_ID']
|
||||
uri = 'http://' + host + '/v2/apps/' + app_id + '/restart'
|
||||
|
||||
# Ok. In this step we made restart request to Marathon
|
||||
if marathon['force'] is True:
|
||||
r = requests.post(uri, data = {'force': 'true'})
|
||||
else:
|
||||
r = requests.post(uri, data = {'force': 'false'})
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from jinja2 import Environment, PackageLoader, Template
|
||||
from jinja2 import Template
|
||||
import os
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user