commit
44a10b7fbb
1
AUTHORS
1
AUTHORS
@ -1,2 +1,3 @@
|
|||||||
Denis Zheleztsov <difrex.punk@gmail.com>
|
Denis Zheleztsov <difrex.punk@gmail.com>
|
||||||
Denis Ryabyy <vv1r0x@gmail.com>
|
Denis Ryabyy <vv1r0x@gmail.com>
|
||||||
|
Evgeniy Vasilev <oren-ibc@yandex.ru>
|
||||||
|
19
README.ru.md
19
README.ru.md
@ -1,5 +1,5 @@
|
|||||||
# Surok
|
# Surok
|
||||||
[![Build Status](https://travis-ci.org/Difrex/surok.svg?branch=master)](https://travis-ci.org/Difrex/surok)
|
[![Build Status](https://travis-ci.org/Surkoveds/surok.svg?branch=master)](https://travis-ci.org/Surkoveds/surok)
|
||||||
|
|
||||||
Обнаружение сервисов для Apache Mesos.
|
Обнаружение сервисов для Apache Mesos.
|
||||||
|
|
||||||
@ -17,10 +17,27 @@ cd build
|
|||||||
deb-пакет будет лежать в build/out
|
deb-пакет будет лежать в build/out
|
||||||
|
|
||||||
Сборка базового docker-образа surok
|
Сборка базового docker-образа surok
|
||||||
|
Ubuntu Xenial
|
||||||
```
|
```
|
||||||
cd build
|
cd build
|
||||||
./build.sh surok_image
|
./build.sh surok_image
|
||||||
```
|
```
|
||||||
|
Alpine image
|
||||||
|
```
|
||||||
|
cd build
|
||||||
|
./build.sh alpine
|
||||||
|
```
|
||||||
|
CentOS image
|
||||||
|
```
|
||||||
|
cd build
|
||||||
|
./build.sh centos
|
||||||
|
```
|
||||||
|
|
||||||
|
ENTRYPOINT : ```cd /opt/surok && pytho3 surok.py -c /etc/surok/conf/surok.json```
|
||||||
|
|
||||||
|
## Документация
|
||||||
|
|
||||||
|
[Wiki](https://github.com/Surkoveds/surok/wiki)
|
||||||
|
|
||||||
## Известные проблемы
|
## Известные проблемы
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ set -e
|
|||||||
. functions.sh
|
. functions.sh
|
||||||
|
|
||||||
function run_tests() {
|
function run_tests() {
|
||||||
docker run -ti -v $(pwd)/tests.py:/opt/surok/tests.py \
|
docker run --rm -ti -v $(pwd)/tests.py:/opt/surok/tests.py \
|
||||||
-v $(pwd)/tests_entrypoint.sh:/tests_entrypoint.sh \
|
-v $(pwd)/tests_entrypoint.sh:/tests_entrypoint.sh \
|
||||||
--entrypoint /tests_entrypoint.sh \
|
--entrypoint /tests_entrypoint.sh \
|
||||||
surok_base:latest
|
surok_base:latest
|
||||||
|
423
build/tests.py
Normal file → Executable file
423
build/tests.py
Normal file → Executable file
@ -1,71 +1,372 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
import unittest
|
import unittest
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
import surok.config
|
||||||
|
import surok.logger
|
||||||
|
import surok.discovery
|
||||||
|
import hashlib
|
||||||
|
from surok.config import Config
|
||||||
|
|
||||||
|
class Logger(surok.logger.Logger):
|
||||||
|
_out=''
|
||||||
|
_err=''
|
||||||
|
def _log2err(self,out):
|
||||||
|
self._err+=out
|
||||||
|
|
||||||
|
def _log2out(self,out):
|
||||||
|
self._out+=out
|
||||||
|
|
||||||
|
def geterr(self):
|
||||||
|
return self._err
|
||||||
|
|
||||||
|
def getout(self):
|
||||||
|
return self._out
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._err=''
|
||||||
|
self._out=''
|
||||||
|
|
||||||
|
class DiscoveryTestingTemplate:
|
||||||
|
_testing={}
|
||||||
|
_testing_fqdn_a={
|
||||||
|
"test.zzz0.test":['10.0.0.1','10.1.0.1'],
|
||||||
|
"test.zzz1.test":['10.0.1.1','10.1.1.1'],
|
||||||
|
"test.zzz2.test":['10.0.2.1','10.1.2.1'],
|
||||||
|
"test.zzz3.test":['10.0.3.1','10.1.3.1']
|
||||||
|
}
|
||||||
|
_testing_fqdn_srv={}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._orig_logger=surok.logger.Logger()
|
||||||
|
|
||||||
|
def do_query_a(self,fqdn):
|
||||||
|
res=self._testing_fqdn_a.get(fqdn,[])
|
||||||
|
if res:
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
self._orig_logger.error('Testing FQDN '+fqdn+' not found in test A records')
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
def do_query_srv(self,fqdn):
|
||||||
|
res=self._testing_fqdn_srv.get(fqdn,[])
|
||||||
|
if res or fqdn.startswith('_tname_e.') or fqdn.find('._udp.'):
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
self._orig_logger.error('Testing FQDN '+fqdn+' not found in test SRV records')
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
def update_data(self):
|
||||||
|
class_name=self.__class__.__name__
|
||||||
|
tgen={
|
||||||
|
"name": ["zzz0","zzy0","zzy1","zzz1"],
|
||||||
|
"host": ["test.zzz0.test","test.zzz1.test","test.zzz2.test","test.zzz3.test"],
|
||||||
|
"serv": ["tname_aa","tname_ab","tname_ba","tname_bb"],
|
||||||
|
"ports": [12341,12342,12343,12344],
|
||||||
|
"servicePorts": [21221,21222,21223,21224]
|
||||||
|
}
|
||||||
|
if self._testing.get(class_name,True):
|
||||||
|
if class_name == 'DiscoveryMarathon':
|
||||||
|
_tasks=[]
|
||||||
|
_ports={}
|
||||||
|
for id in (0,1,2,3):
|
||||||
|
ports=[]+tgen['ports']
|
||||||
|
servicePorts=[]+tgen['servicePorts']
|
||||||
|
appId='/'.join(str(tgen['name'][id]+'.xxx.yyy.').split('.')[::-1])
|
||||||
|
_ports[appId]=[]
|
||||||
|
for pid in (0,1,2,3):
|
||||||
|
ports[pid]+=pid*10
|
||||||
|
servicePorts[pid]+=pid*100
|
||||||
|
for prot in ['tcp','udp']:
|
||||||
|
if pid<2 or prot == 'tcp':
|
||||||
|
_ports[appId].append({'containerPort': 0,
|
||||||
|
'hostPort': 0,
|
||||||
|
'labels': {},
|
||||||
|
'name': tgen['serv'][pid],
|
||||||
|
'protocol': prot,
|
||||||
|
'servicePort': servicePorts[pid]})
|
||||||
|
|
||||||
|
_tasks.append({'appId':appId,
|
||||||
|
'host':tgen['host'][id],
|
||||||
|
'ports':ports,
|
||||||
|
'servicePorts':servicePorts})
|
||||||
|
#_tname_a._zzy0.yyy.xxx._tcp.marathon.mesos
|
||||||
|
self._tasks=_tasks
|
||||||
|
self._ports=_ports
|
||||||
|
elif class_name == 'DiscoveryMesos':
|
||||||
|
for id in (0,1,2,3):
|
||||||
|
ports=[]+tgen['ports']
|
||||||
|
for pid in (0,1,2,3):
|
||||||
|
ports[pid]+=pid*10
|
||||||
|
for prot in ['tcp','udp']:
|
||||||
|
if pid<2 or prot == 'tcp':
|
||||||
|
for fqdn in ['_'+tgen['serv'][pid]+'._'+tgen['name'][id]+'.xxx.yyy._'+prot+'.'+self._config['mesos'].get('domain'),
|
||||||
|
'_'+tgen['name'][id]+'.xxx.yyy._'+prot+'.'+self._config['mesos'].get('domain')]:
|
||||||
|
if not self._testing_fqdn_srv.get(fqdn):
|
||||||
|
self._testing_fqdn_srv[fqdn]=[]
|
||||||
|
self._testing_fqdn_srv[fqdn].append({'name':tgen['host'][id],'port':ports[pid]})
|
||||||
|
|
||||||
|
self._testing[class_name]=False
|
||||||
|
|
||||||
|
class DiscoveryMesos(DiscoveryTestingTemplate,surok.discovery.DiscoveryMesos):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DiscoveryMarathon(DiscoveryTestingTemplate,surok.discovery.DiscoveryMarathon):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Discovery(surok.discovery.Discovery):
|
||||||
|
_discoveries={}
|
||||||
|
def __init__(self):
|
||||||
|
self._config=Config()
|
||||||
|
self._logger=Logger()
|
||||||
|
if not self._discoveries.get('mesos_dns'):
|
||||||
|
self._discoveries['mesos_dns']=DiscoveryMesos()
|
||||||
|
if not self._discoveries.get('marathon_api'):
|
||||||
|
self._discoveries['marathon_api']=DiscoveryMarathon()
|
||||||
|
|
||||||
|
class Test01_Logger(unittest.TestCase):
|
||||||
|
def test_01_logger_default_level(self):
|
||||||
|
logger = Logger()
|
||||||
|
self.assertEqual(logger.get_level(), 'info')
|
||||||
|
|
||||||
|
def test_02_logger_output_levels(self):
|
||||||
|
message='log message'
|
||||||
|
tests={
|
||||||
|
'debug':{
|
||||||
|
'assertIn':['ERROR: {}','WARNING: {}','INFO: {}','DEBUG: {}'],
|
||||||
|
'assertNotIn':[]
|
||||||
|
},
|
||||||
|
'info':{
|
||||||
|
'assertIn':['ERROR: {}','WARNING: {}','INFO: {}'],
|
||||||
|
'assertNotIn':['DEBUG: {}']
|
||||||
|
},
|
||||||
|
'warning':{
|
||||||
|
'assertIn':['ERROR: {}','WARNING: {}'],
|
||||||
|
'assertNotIn':['INFO: {}','DEBUG: {}']
|
||||||
|
},
|
||||||
|
'error':{
|
||||||
|
'assertIn':['ERROR: {}'],
|
||||||
|
'assertNotIn':['WARNING: {}','INFO: {}','DEBUG: {}']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger = Logger()
|
||||||
|
for value01 in tests.keys():
|
||||||
|
logger.reset()
|
||||||
|
logger.set_level(value01)
|
||||||
|
logger.error(message)
|
||||||
|
logger.warning(message)
|
||||||
|
logger.info(message)
|
||||||
|
logger.debug(message)
|
||||||
|
resmessage=logger.geterr()+logger.getout()
|
||||||
|
for test_name in tests[value01].keys():
|
||||||
|
for test_value in tests[value01][test_name]:
|
||||||
|
with self.subTest(msg='Testing Logger for ...', loglevel=value01):
|
||||||
|
test_message=test_value.format(message)
|
||||||
|
eval('self.{}(test_message,resmessage)'.format(test_name))
|
||||||
|
|
||||||
|
class Test02_LoadConfig(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_01_default_values(self):
|
||||||
|
config=Config()
|
||||||
|
with self.subTest(msg='Testing default values for Config.', dump=config.dump()):
|
||||||
|
self.assertEqual(config.get('confd'), '/etc/surok/conf.d')
|
||||||
|
self.assertEqual(config.get('default_discovery'), 'mesos_dns')
|
||||||
|
self.assertEqual(config.get('lock_dir'), '/var/tmp')
|
||||||
|
self.assertEqual(config.get('loglevel'), 'info')
|
||||||
|
self.assertEqual(dict(config.get('marathon',{})).get('enabled'), False)
|
||||||
|
self.assertEqual(dict(config.get('mesos',{})).get('enabled'), False)
|
||||||
|
self.assertEqual(dict(config.get('memcached',{})).get('enabled'), False)
|
||||||
|
self.assertEqual(config.get('version'), '0.7')
|
||||||
|
self.assertEqual(config.get('wait_time'), 20)
|
||||||
|
|
||||||
|
def test_02_main_conf(self):
|
||||||
|
config=Config('/etc/surok/conf/surok.json')
|
||||||
|
with self.subTest(msg='Testing load config for Config.', dump=config.dump()):
|
||||||
|
self.assertEqual(config.hash(), '545c20b322a6ba5fef9c7d2416d80178f26a924b')
|
||||||
|
|
||||||
|
def test_03_apps_conf(self):
|
||||||
|
tests=[
|
||||||
|
{
|
||||||
|
'env':{},
|
||||||
|
'self_check.json':'a4e109b9fec696776fd3df091b607e9c1489748c',
|
||||||
|
'marathon_check.json':'6be7f26d421d4a0a2e7b089184be0c0e3a50f986'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'env':{'SUROK_DISCOVERY_GROUP':'xxx.yyy'},
|
||||||
|
'self_check.json':'38ab770ff2ba69bf70673288425337ff3c18a807',
|
||||||
|
'marathon_check.json':'7b0cb4eab2d8e0f901cc567df28b17279af21baa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'env':{'MARATHON_APP_ID':'/xxx/yyy/zzz'},
|
||||||
|
'self_check.json':'cbd2a15179649d0e06f98bd64e024481a944d65c',
|
||||||
|
'marathon_check.json':'08a382d14285feb1f22b92ba597ecf73d654a2e0'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
config=Config()
|
||||||
|
for test in tests:
|
||||||
|
config.set('env',test['env'])
|
||||||
|
config.update_apps()
|
||||||
|
for app in config.apps:
|
||||||
|
with self.subTest(msg='Testing AppConfig for ...', env=test['env'], conf_name=app.get('conf_name'), dump=app.dump()):
|
||||||
|
self.assertEqual(test[app.get('conf_name')],app.hash())
|
||||||
|
|
||||||
|
def test_04_apps_conf(self):
|
||||||
|
tests={
|
||||||
|
'confd':{
|
||||||
|
'assertEqual': ['/var', '/var/tmp', '/etc/surok/conf.d'],
|
||||||
|
'assertNotEqual': [20, '/var/tmp1', '/etc/surok/conf/surok.json', 1, None, True]
|
||||||
|
},
|
||||||
|
'default_discovery':{
|
||||||
|
'assertEqual':['marathon_api', 'mesos_dns'],
|
||||||
|
'assertNotEqual':[20, 'test', None]
|
||||||
|
},
|
||||||
|
'lock_dir':{
|
||||||
|
'assertEqual':['/var', '/etc/surok/conf.d', '/var/tmp'],
|
||||||
|
'assertNotEqual':[20, '/var/tmp1', '/etc/surok/conf/surok.json', 1, None, True]
|
||||||
|
},
|
||||||
|
'loglevel':{
|
||||||
|
'assertEqual':['error', 'debug', 'info', 'warning'],
|
||||||
|
'assertNotEqual':['errrr', 'DEBUG','warn', 'test', 1, None, True]
|
||||||
|
},
|
||||||
|
'version':{
|
||||||
|
'assertEqual': ['0.7', '0.8'],
|
||||||
|
'assertNotEqual': ['0,7', '07', '0.9', 0.7, 0.8, None]
|
||||||
|
},
|
||||||
|
'wait_time':{
|
||||||
|
'assertEqual': [10, 15, 20],
|
||||||
|
'assertNotEqual': ['10', '15', None, True]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
config=Config()
|
||||||
|
for name01 in tests.keys():
|
||||||
|
oldvalue=config.get(name01)
|
||||||
|
for test_name in tests[name01].keys():
|
||||||
|
for value01 in tests[name01][test_name]:
|
||||||
|
config.set_config({name01:value01})
|
||||||
|
test_value=config.get(name01)
|
||||||
|
with self.subTest(msg='Testing Config Change for values...', name=name01, value=value01, test_value=test_value):
|
||||||
|
eval('self.{}(value01, test_value)'.format(test_name))
|
||||||
|
config.set(name01,oldvalue)
|
||||||
|
|
||||||
|
|
||||||
class TestLoadConfig(unittest.TestCase):
|
class Test03_Discovery(unittest.TestCase):
|
||||||
|
|
||||||
def test_main_conf(self):
|
|
||||||
# Load base configurations
|
|
||||||
surok_conf = '/etc/surok/conf/surok.json'
|
|
||||||
# Read config file
|
|
||||||
f = open(surok_conf, 'r')
|
|
||||||
conf = json.loads(f.read())
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
self.assertIn('confd', conf)
|
|
||||||
self.assertTrue(os.path.isdir(conf['confd']))
|
|
||||||
self.assertIn('domain', conf)
|
|
||||||
self.assertIn('wait_time', conf)
|
|
||||||
self.assertIn('lock_dir', conf)
|
|
||||||
self.assertTrue(os.path.isdir(conf['lock_dir']))
|
|
||||||
|
|
||||||
|
|
||||||
class TestLogger(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_info(self):
|
|
||||||
from surok.logger import make_message
|
|
||||||
m = make_message
|
|
||||||
self.assertIn('INFO', m({'level': 'INFO', 'raw': 'log message'}))
|
|
||||||
|
|
||||||
def test_warning(self):
|
|
||||||
from surok.logger import make_message
|
|
||||||
m = make_message
|
|
||||||
self.assertIn('WARNING', m({'level': 'WARNING', 'raw': 'log message'}))
|
|
||||||
|
|
||||||
def test_error(self):
|
|
||||||
from surok.logger import make_message
|
|
||||||
m = make_message
|
|
||||||
self.assertIn('ERROR', m({'level': 'ERROR', 'raw': 'log message'}))
|
|
||||||
|
|
||||||
def test_info(self):
|
|
||||||
from surok.logger import make_message
|
|
||||||
m = make_message
|
|
||||||
self.assertIn('DEBUG', m({'level': 'DEBUG', 'raw': 'log message'}))
|
|
||||||
|
|
||||||
|
|
||||||
class TestMemcachedDiscovery(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_discovery_memcache(self):
|
|
||||||
from surok.system import discovery_memcached
|
|
||||||
|
|
||||||
# Load base configurations
|
|
||||||
surok_conf = '/etc/surok/conf/surok.json'
|
|
||||||
# Read config file
|
|
||||||
f = open(surok_conf, 'r')
|
|
||||||
conf = json.loads(f.read())
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
self.assertEqual(discovery_memcached(conf), [])
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetGroup(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_get_group(self):
|
|
||||||
from surok.discovery import get_group
|
|
||||||
self.assertFalse(get_group({}, {'env': os.environ}))
|
|
||||||
|
|
||||||
|
def test_01_discovery(self):
|
||||||
|
tests={
|
||||||
|
'T':{ #mesos_enabled
|
||||||
|
'T':{ #marathon_enabled
|
||||||
|
'0.7':{ #version
|
||||||
|
'mesos_dns':{ #default_discovery
|
||||||
|
'marathon_check.json':'ef55fb10c20df700cb715f4836eadb2d0cfa9cc1', #app['conf_name']
|
||||||
|
'self_check.json':'53b8ddc27e357620f01ea75a7ab827cd90c77446'
|
||||||
|
},
|
||||||
|
'marathon_api':{
|
||||||
|
'marathon_check.json':'ef55fb10c20df700cb715f4836eadb2d0cfa9cc1',
|
||||||
|
'self_check.json':'53b8ddc27e357620f01ea75a7ab827cd90c77446'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'0.8':{
|
||||||
|
'mesos_dns':{
|
||||||
|
'marathon_check.json':'2016238426eb8ee7df9c4c016b6aecbfdf251a9b',
|
||||||
|
'self_check.json':'b6279a3e8e2fbbc78c6b302ef109bd6e2b456d9f'
|
||||||
|
},
|
||||||
|
'marathon_api':{
|
||||||
|
'marathon_check.json':'2016238426eb8ee7df9c4c016b6aecbfdf251a9b',
|
||||||
|
'self_check.json':'b6279a3e8e2fbbc78c6b302ef109bd6e2b456d9f'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'F':{
|
||||||
|
'0.7':{
|
||||||
|
'mesos_dns':{
|
||||||
|
'marathon_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f',
|
||||||
|
'self_check.json':'53b8ddc27e357620f01ea75a7ab827cd90c77446'
|
||||||
|
},
|
||||||
|
'marathon_api':{
|
||||||
|
'marathon_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f',
|
||||||
|
'self_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'0.8':{
|
||||||
|
'mesos_dns':{
|
||||||
|
'marathon_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f',
|
||||||
|
'self_check.json':'b6279a3e8e2fbbc78c6b302ef109bd6e2b456d9f'
|
||||||
|
},
|
||||||
|
'marathon_api':{
|
||||||
|
'marathon_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f',
|
||||||
|
'self_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'F':{
|
||||||
|
'T':{
|
||||||
|
'0.7':{
|
||||||
|
'mesos_dns':{
|
||||||
|
'marathon_check.json':'ef55fb10c20df700cb715f4836eadb2d0cfa9cc1',
|
||||||
|
'self_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f'
|
||||||
|
},
|
||||||
|
'marathon_api':{
|
||||||
|
'marathon_check.json':'ef55fb10c20df700cb715f4836eadb2d0cfa9cc1',
|
||||||
|
'self_check.json':'53b8ddc27e357620f01ea75a7ab827cd90c77446'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'0.8':{
|
||||||
|
'mesos_dns':{
|
||||||
|
'marathon_check.json':'2016238426eb8ee7df9c4c016b6aecbfdf251a9b',
|
||||||
|
'self_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f'
|
||||||
|
},
|
||||||
|
'marathon_api':{
|
||||||
|
'marathon_check.json':'2016238426eb8ee7df9c4c016b6aecbfdf251a9b',
|
||||||
|
'self_check.json':'b6279a3e8e2fbbc78c6b302ef109bd6e2b456d9f'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'F':{
|
||||||
|
'0.7':{
|
||||||
|
'mesos_dns':{
|
||||||
|
'marathon_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f',
|
||||||
|
'self_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f'
|
||||||
|
},
|
||||||
|
'marathon_api':{
|
||||||
|
'marathon_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f',
|
||||||
|
'self_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'0.8':{
|
||||||
|
'mesos_dns':{
|
||||||
|
'marathon_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f',
|
||||||
|
'self_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f'
|
||||||
|
},
|
||||||
|
'marathon_api':{
|
||||||
|
'marathon_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f',
|
||||||
|
'self_check.json':'bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config=Config('/etc/surok/conf/surok.json')
|
||||||
|
config.set('env',{'SUROK_DISCOVERY_GROUP':'xxx.yyy'})
|
||||||
|
discovery=Discovery()
|
||||||
|
for mesos_enabled in tests.keys():
|
||||||
|
for marathon_enabled in tests[mesos_enabled].keys():
|
||||||
|
for version in tests[mesos_enabled][marathon_enabled].keys():
|
||||||
|
for default_discovery in tests[mesos_enabled][marathon_enabled][version].keys():
|
||||||
|
config.set_config({'default_discovery':default_discovery,
|
||||||
|
'mesos':{'enabled':(mesos_enabled=='T')},
|
||||||
|
'marathon':{'enabled':(marathon_enabled=='T')},
|
||||||
|
'version':version})
|
||||||
|
discovery.update_data()
|
||||||
|
for app in config.apps:
|
||||||
|
conf_name=app.get('conf_name')
|
||||||
|
with self.subTest(msg='Testing Discovery for values...', config=config.dump(), conf_name=conf_name):
|
||||||
|
self.assertEqual(hashlib.sha1(json.dumps(discovery.resolve(app), sort_keys=True).encode()).hexdigest(),
|
||||||
|
tests[mesos_enabled][marathon_enabled][version][default_discovery][conf_name])
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
12
conf.d/marathon_check.json
Normal file
12
conf.d/marathon_check.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"name": "zzy*",
|
||||||
|
"ports": ["tname_a*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"template": "templates/selfcheck.jj2",
|
||||||
|
"dest": "selfcheck",
|
||||||
|
"reload_cmd": "/bin/echo selfcheck ok",
|
||||||
|
"discovery": "marathon_api"
|
||||||
|
}
|
22
conf.d/self_check.json
Normal file
22
conf.d/self_check.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"name": "zzy0",
|
||||||
|
"ports": ["tname_aa", "tname_ab", "tname_ba", "tname_bb", "tname_d"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zzy1",
|
||||||
|
"ports": ["tname_aa", "tname_ab", "tname_ba", "tname_bb"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zzz0",
|
||||||
|
"ports": ["tname_aa", "tname_bb"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zzz1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"template": "templates/selfcheck.jj2",
|
||||||
|
"dest": "selfcheck",
|
||||||
|
"reload_cmd": "/bin/echo selfcheck ok"
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"services": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"group": "mesos"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"conf_name": "selfcheck",
|
|
||||||
"template": "templates/selfcheck.jj2",
|
|
||||||
"dest": "selfcheck",
|
|
||||||
"reload_cmd": "/bin/echo selfcheck ok"
|
|
||||||
}
|
|
@ -1,15 +1,20 @@
|
|||||||
{
|
{
|
||||||
|
"version": "0.8",
|
||||||
"marathon": {
|
"marathon": {
|
||||||
|
"enabled": false,
|
||||||
|
"restart": false,
|
||||||
"force": true,
|
"force": true,
|
||||||
"host": "http://marathon.mesos:8080",
|
"host": "http://marathon.mesos:8080"
|
||||||
"enabled": false
|
|
||||||
},
|
},
|
||||||
|
"mesos": {
|
||||||
|
"enabled": false,
|
||||||
|
"domain": "marathon.mesos"
|
||||||
|
},
|
||||||
|
"default_discovery": "mesos_dns",
|
||||||
"confd": "/etc/surok/conf.d",
|
"confd": "/etc/surok/conf.d",
|
||||||
"domain": "marathon.mesos",
|
|
||||||
"wait_time": 20,
|
"wait_time": 20,
|
||||||
"lock_dir": "/var/tmp",
|
"lock_dir": "/var/tmp",
|
||||||
"loglevel": "info",
|
"loglevel": "info",
|
||||||
"container": false,
|
|
||||||
"memcached": {
|
"memcached": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"discovery": {
|
"discovery": {
|
||||||
|
9
debian/changelog
vendored
9
debian/changelog
vendored
@ -1,3 +1,12 @@
|
|||||||
|
surok (0.8) testing; urgency=medium
|
||||||
|
|
||||||
|
* Version bump
|
||||||
|
* new config structure
|
||||||
|
* Port type
|
||||||
|
* Discovery over Marathon API
|
||||||
|
|
||||||
|
-- Denis Zheleztsov <difrex.punk@gmail.com> Tue, 07 Feb 2017 12:41:52 +0300
|
||||||
|
|
||||||
surok (0.7.4.3) testing; urgency=medium
|
surok (0.7.4.3) testing; urgency=medium
|
||||||
|
|
||||||
* Fixed #9
|
* Fixed #9
|
||||||
|
4
debian/install
vendored
4
debian/install
vendored
@ -1,7 +1,11 @@
|
|||||||
conf/surok.json etc/surok/conf
|
conf/surok.json etc/surok/conf
|
||||||
|
conf.d/self_check.json etc/surok/conf.d
|
||||||
|
conf.d/marathon_check.json etc/surok/conf.d
|
||||||
|
templates/selfcheck.jj2 etc/surok/templates
|
||||||
surok/templates.py opt/surok/surok
|
surok/templates.py opt/surok/surok
|
||||||
surok/system.py opt/surok/surok
|
surok/system.py opt/surok/surok
|
||||||
surok/logger.py opt/surok/surok
|
surok/logger.py opt/surok/surok
|
||||||
surok/__init__.py opt/surok/surok
|
surok/__init__.py opt/surok/surok
|
||||||
surok/discovery.py opt/surok/surok
|
surok/discovery.py opt/surok/surok
|
||||||
|
surok/config.py opt/surok/surok
|
||||||
surok.py opt/surok
|
surok.py opt/surok
|
||||||
|
@ -34,9 +34,9 @@
|
|||||||
}
|
}
|
||||||
</code>
|
</code>
|
||||||
* <strong>services</strong>. List of hashes with required services for app.
|
* <strong>services</strong>. List of hashes with required services for app.
|
||||||
1. <em>name</em> - string. App name in Marathon.
|
1. <em>name</em> - string. App name in Marathon. If you use a Marathon discovery, you can use the "*" at the end of the string to indicate any character.
|
||||||
2. <em>group</em> - string. App group in Marathon. Optional. Discovery policy: 1) config 2) SUROK<em>DISCOVERY</em>GROUP environment variable 3) Marathon API
|
2. <em>group</em> - string. App group in Marathon. Optional. Discovery policy: 1) config 2) SUROK<em>DISCOVERY</em>GROUP environment variable 3) Marathon API
|
||||||
3. <em>ports</em> - list. Name of opened port. In marathon of course. Optional.
|
3. <em>ports</em> - list. Name of opened port. In marathon of course. If you use a Marathon discovery, you can use the "*" at the end of the string to indicate any character. Optional.
|
||||||
* <strong>conf<em>name</strong>. Unique app config name.
|
* <strong>conf<em>name</strong>. Unique app config name.
|
||||||
* <strong>template</strong>. Jinja2 template location.
|
* <strong>template</strong>. Jinja2 template location.
|
||||||
* <strong>dest</strong>. Destination config path.
|
* <strong>dest</strong>. Destination config path.
|
||||||
|
@ -5,26 +5,27 @@ conf.d/myapp.json
|
|||||||
{
|
{
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "myapp",
|
"name": "kioskservice",
|
||||||
"group": "backend.production",
|
"group": "production.romania",
|
||||||
"ports": ["proxy", "api"]
|
"ports": ["web", "socket"]
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "nginx",
|
|
||||||
"group": "frontend.production"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"conf_name": "myapp_backend_production",
|
"discovery": "mesos_dns",
|
||||||
"template": "/etc/surok/templates/myapp.jj2",
|
"group": "dev.web",
|
||||||
"dest": "/etc/myapp/myapp.cfg",
|
"conf_name": "kiosk",
|
||||||
"reload_cmd": "killall -9 myapp; /usr/local/bin/myapp -config /etc/myapp/myapp.cfg"
|
"template": "/etc/surok/templates/kiosk.jj2",
|
||||||
|
"dest": "/etc/nginx/sites-available/kioskservice.conf",
|
||||||
|
"reload_cmd": "/sbin/nginx -t && /bin/systemctl reload nginx",
|
||||||
|
"run_cmd": ["/usr/bin/node", "-c", "config.json"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
* **services**. List of hashes with required services for app.
|
* **services**. List of hashes with required services for app.
|
||||||
1. _name_ - string. App name in Marathon.
|
1. _name_ - string. App name in Marathon. If you use a Marathon discovery, you can use the "*" at the end of the string to indicate any character.
|
||||||
2. _group_ - string. App group in Marathon. Optional. Discovery policy: 1) config 2) SUROK_DISCOVERY_GROUP environment variable 3) Marathon API
|
2. _group_ - string. App group in Marathon. Optional. Discovery policy: 1) config 2) SUROK_DISCOVERY_GROUP environment variable 3) Marathon API
|
||||||
3. _ports_ - list. Name of opened port. In marathon of course. Optional.
|
3. _ports_ - list. Name of opened port. In marathon of course. If you use a Marathon discovery, you can use the "*" at the end of the string to indicate any character. Optional.
|
||||||
* **conf_name**. Unique app config name.
|
* **conf_name**. Unique app config name.
|
||||||
* **template**. Jinja2 template location.
|
* **template**. Jinja2 template location.
|
||||||
* **dest**. Destination config path.
|
* **dest**. Destination config path.
|
||||||
* **reload_cmd**. Command to execute if generated config is changed.
|
* **reload_cmd**. Command to execute if generated config is changed.
|
||||||
|
* **discovery**. Use custom discovery for app.
|
||||||
|
* **group**. Default group for all required services.
|
||||||
|
@ -19,12 +19,21 @@
|
|||||||
<code>
|
<code>
|
||||||
{
|
{
|
||||||
"marathon": {
|
"marathon": {
|
||||||
|
"enabled": false,
|
||||||
|
"restart": false,
|
||||||
"force": true,
|
"force": true,
|
||||||
"host": "http://marathon.mesos:8080",
|
"host": "http://marathon.mesos:8080"
|
||||||
"enabled": false
|
|
||||||
},
|
},
|
||||||
|
"consul": {
|
||||||
|
"enabled": false,
|
||||||
|
"domain": "service.dc1.consul"
|
||||||
|
},
|
||||||
|
"mesos":{
|
||||||
|
"enabled": true,
|
||||||
|
"domain": "marathon.mesos"
|
||||||
|
},
|
||||||
|
"default_discovery": "mesos_dns",
|
||||||
"confd": "/etc/surok/conf.d",
|
"confd": "/etc/surok/conf.d",
|
||||||
"domain": "marathon.mesos",
|
|
||||||
"wait_time": 20,
|
"wait_time": 20,
|
||||||
"lock_dir": "/var/tmp",
|
"lock_dir": "/var/tmp",
|
||||||
"loglevel": "info",
|
"loglevel": "info",
|
||||||
|
@ -1,21 +1,25 @@
|
|||||||
# Surok main config file
|
# Surok main config file (0.8.x)
|
||||||
|
|
||||||
Default location is /etc/surok/conf/surok.json
|
Default location is **/etc/surok/conf/surok.json**
|
||||||
|
|
||||||
conf/surok.json
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
|
"version": "0.8"
|
||||||
"marathon": {
|
"marathon": {
|
||||||
|
"enabled": false,
|
||||||
|
"restart": false,
|
||||||
"force": true,
|
"force": true,
|
||||||
"host": "http://marathon.mesos:8080",
|
"host": "http://marathon.mesos:8080"
|
||||||
"enabled": false
|
|
||||||
},
|
},
|
||||||
|
"mesos":{
|
||||||
|
"enabled": true,
|
||||||
|
"domain": "marathon.mesos"
|
||||||
|
},
|
||||||
|
"default_discovery": "mesos_dns",
|
||||||
"confd": "/etc/surok/conf.d",
|
"confd": "/etc/surok/conf.d",
|
||||||
"domain": "marathon.mesos",
|
|
||||||
"wait_time": 20,
|
"wait_time": 20,
|
||||||
"lock_dir": "/var/tmp",
|
"lock_dir": "/var/tmp",
|
||||||
"loglevel": "info",
|
"loglevel": "info",
|
||||||
"container": false,
|
|
||||||
"memcached": {
|
"memcached": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"discovery": {
|
"discovery": {
|
||||||
@ -27,10 +31,52 @@ conf/surok.json
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
* **marathon section**. Restarting app over marathon api if config changed. Disabled by default.
|
|
||||||
* **confd**. Directory where located configs apps.
|
## Config file options
|
||||||
* **domain**. Domain served by mesos-dns.
|
* **version** - *string. Optional. "0.7" by default.*
|
||||||
* **lock_dir**. Directory where surok writes temporary configs after resolving.
|
Config files and templates version. Accept "0.7" or "0.8".
|
||||||
* **wait_time**. Sleep time in main loop.
|
* "0.7" - config files <= 0.7.х version
|
||||||
* **container**. Not implemented.
|
* "0.8" - >= 0.8.x config files version
|
||||||
* **memcached section**. Memcached support. Disabled by default.
|
|
||||||
|
##### 0.8 version
|
||||||
|
* **marathon**, **mesos**, **consul**, **memcached** - *dict/hash. Optional. '{"enable":false}'. by default*
|
||||||
|
Surok working with folowing systems. If system is disabled parameters will be ignored.
|
||||||
|
* **enable** - *boolean. Optional. false by default*
|
||||||
|
Enable/disable system for usage.
|
||||||
|
|
||||||
|
Specific variables:
|
||||||
|
* For Marathon API "marathon"
|
||||||
|
* **force** - *boolean. Optional. true by default*
|
||||||
|
Force restart container over API.
|
||||||
|
* **restart** - *boolean. Optional. false by default*
|
||||||
|
Enable/disable restarting container
|
||||||
|
* **host** - *string. Optional. "http://marathon.mesos:8080" by default*
|
||||||
|
Marathon address.
|
||||||
|
* For mesos-dns "mesos"
|
||||||
|
* **domain** - *string. Optional. "marathon.mesos" by default*
|
||||||
|
mesos-dns private domain
|
||||||
|
* For Memcached "memcached"
|
||||||
|
* **hosts** - memcached hosts
|
||||||
|
* **discovery**
|
||||||
|
* **enabled** - boolean. Enable/disable disovery memcached service
|
||||||
|
* **service** - string. memcached app name
|
||||||
|
* **group** - string. memcached app group
|
||||||
|
* **default_discovery** - *string. Optional. "mesos_dns" by default*
|
||||||
|
Accept values:
|
||||||
|
* "mesos_dns" - mesos-dns
|
||||||
|
* "marathon_api"- Marathon API
|
||||||
|
* **confd** - *strig. Required.*
|
||||||
|
Path to directory with app config files.
|
||||||
|
* **wait_time** - *int. Required.*
|
||||||
|
Time in seconds how much Surok waits before starting to re-do the requests for service discovery
|
||||||
|
* **lock_dir** - *string. Required.*
|
||||||
|
Path to directory where Surok write lock-files.
|
||||||
|
* **loglevel** - *string. Optional. "info" by default*
|
||||||
|
Logleve. Accept values: "debug", "info", "warning", "error"
|
||||||
|
|
||||||
|
##### < 0.8 versions
|
||||||
|
|
||||||
|
* **marathon**
|
||||||
|
* **enabled** - boolean. Enable/disable container restart. Renamed to "restart" in 0.8 version.
|
||||||
|
* **domain** - string. mesos-dns private domain. Moved to "mesos" hashtable in 0.8 version.
|
||||||
|
Discovery over mesos-dns enabled all times.
|
||||||
|
@ -31,6 +31,7 @@ Service discovery for Apache Mesos.
|
|||||||
|
|
||||||
* Denis Zheleztsov <difrex.punk@gmail.com>
|
* Denis Zheleztsov <difrex.punk@gmail.com>
|
||||||
* Denis Ryabyy <vv1r0x@gmail.com>
|
* Denis Ryabyy <vv1r0x@gmail.com>
|
||||||
|
* Evgeniy Vasilev <oren-ibc@yandex.ru>
|
||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
|
@ -5,8 +5,11 @@
|
|||||||
|
|
||||||
- [Templates](#templates)
|
- [Templates](#templates)
|
||||||
- [my dictionary in templates](#my-dictionary-in-templates)
|
- [my dictionary in templates](#my-dictionary-in-templates)
|
||||||
|
- [0.8 version](#08-version)
|
||||||
- [Real template example](#real-template-example)
|
- [Real template example](#real-template-example)
|
||||||
- [Checks in template](#checks-in-template)
|
- [Checks in template](#checks-in-template)
|
||||||
|
- [0.7 version](#07-version)
|
||||||
|
- [Real template example](#real-template-example)
|
||||||
|
|
||||||
<!-- markdown-toc end -->
|
<!-- markdown-toc end -->
|
||||||
|
|
||||||
@ -15,55 +18,74 @@ Surok using Jinja2 for templates. [Jinja2 documentation](http://jinja.pocoo.org/
|
|||||||
|
|
||||||
## my dictionary in templates
|
## my dictionary in templates
|
||||||
|
|
||||||
|
### 0.8 version
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"nginx": [
|
"asterisk": [
|
||||||
{
|
{
|
||||||
"name": "nginx.testing-kl92-s0.marathon.mesos.",
|
"name": "nginx.testing-kl92-s0.marathon.mesos.",
|
||||||
"port": "31200",
|
"ip": [
|
||||||
"ip": ["10.10.10.1"]
|
"10.0.0.1",
|
||||||
|
"11.0.0.1"
|
||||||
|
],
|
||||||
|
"tcp": {
|
||||||
|
"rpc":31200,
|
||||||
|
"web":31201,
|
||||||
|
"sip":32000
|
||||||
|
},
|
||||||
|
"udp": {
|
||||||
|
"sip":31201
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nginx.testing-kl123-s1.marathon.mesos.",
|
"name": "nginx.testing-kl123-s1.marathon.mesos.",
|
||||||
"port": "32230",
|
"ip": [
|
||||||
"ip": ["10.10.10.2"]
|
"10.0.0.2",
|
||||||
}
|
"11.0.0.2"
|
||||||
],
|
],
|
||||||
"emailsender": [
|
"tcp": {
|
||||||
{
|
"rpc":31210,
|
||||||
"name": "emailsender.testing-kl92-s0.marathon.mesos.",
|
"web":31211,
|
||||||
"port": "31201",
|
"sip":32010
|
||||||
"ip": ["10.10.10.1"]
|
|
||||||
},
|
},
|
||||||
{
|
"udp": {
|
||||||
"name": "emailsender.testing-kl123-s1.marathon.mesos.",
|
"sip":31211
|
||||||
"port": "32232",
|
}
|
||||||
"ip": ["10.10.10.1"]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"service-with-defined-ports": {
|
"email": [
|
||||||
"web": [
|
|
||||||
{
|
{
|
||||||
"name": "f.q.d.n",
|
"name": "nginx.testing-kl92-s0.marathon.mesos.",
|
||||||
"port": 12341
|
"ip": [
|
||||||
|
"10.0.0.1"
|
||||||
|
],
|
||||||
|
"tcp": {
|
||||||
|
"smtp":31200,
|
||||||
|
"pop":31201
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rpc": [
|
"anyport": [
|
||||||
{
|
{
|
||||||
"name": "f.q.d.n",
|
"name": "nginx.testing-kl92-s0.marathon.mesos.",
|
||||||
"port": 12342
|
"ip": [
|
||||||
}
|
"10.0.0.1"
|
||||||
|
],
|
||||||
|
"tcp": [
|
||||||
|
31200,
|
||||||
|
31201
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
"env": {
|
"env": {
|
||||||
"HOME": "/var/lib/nginx"
|
"HOME": "/var/lib/nginx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Real template example
|
#### Real template example
|
||||||
|
|
||||||
nginx config
|
nginx config
|
||||||
```
|
```
|
||||||
@ -104,7 +126,7 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Checks in template
|
#### Checks in template
|
||||||
|
|
||||||
_my['env']_ is a python os.environ class. Look bellow:
|
_my['env']_ is a python os.environ class. Look bellow:
|
||||||
```
|
```
|
||||||
@ -114,3 +136,90 @@ host = '{{my['env']['DB_HOST']}}'
|
|||||||
host = 'localhost'
|
host = 'localhost'
|
||||||
{% endif %}
|
{% endif %}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 0.7 version
|
||||||
|
|
||||||
|
my dictionary in template
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"name-of-port0": [
|
||||||
|
{
|
||||||
|
"name": "f.q.d.n",
|
||||||
|
"port": 12341
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name-of-port2": [
|
||||||
|
{
|
||||||
|
"name": "f.q.d.n",
|
||||||
|
"port": 12342
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"HOME": "/var/lib/nginx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Real template example
|
||||||
|
|
||||||
|
```
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
"ports": ["web", "socket"]
|
"ports": ["web", "socket"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"discovery": "mesos_dns",
|
||||||
|
"group": "dev.web",
|
||||||
"conf_name": "kiosk",
|
"conf_name": "kiosk",
|
||||||
"template": "/etc/surok/templates/kiosk.jj2",
|
"template": "/etc/surok/templates/kiosk.jj2",
|
||||||
"dest": "/etc/nginx/sites-available/kioskservice.conf",
|
"dest": "/etc/nginx/sites-available/kioskservice.conf",
|
||||||
@ -31,3 +33,5 @@
|
|||||||
В reload_cmd можно использовать переменные окружения:
|
В reload_cmd можно использовать переменные окружения:
|
||||||
```"reload_cmd": "/usr/bin/killall -9 calc || true && /usr/local/bin/calc -c /app/calc.conf ${CALC_NUM}"```
|
```"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.
|
* run_cmd(v0.6) - array. Список с командой на выполнение. Используется внутри контейнера вместо reload_cmd.
|
||||||
|
* discovery - Опциональный. переопределяет метод обнаружения сервисов для приложения
|
||||||
|
* group - Опциональный. Группа по-умолчанию.
|
||||||
|
@ -1,30 +1,81 @@
|
|||||||
# Конфигурация Surok (0.7.x)
|
# Конфигурация Surok (0.8.x)
|
||||||
|
**/etc/surok/conf/surok.json** Разберем конфигурационный файл по опциям
|
||||||
**/etc/surok/conf/surok.json**
|
|
||||||
Разберем конфигурационный файл по опциям
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
|
"version": "0.8"
|
||||||
"marathon": {
|
"marathon": {
|
||||||
|
"enabled": false,
|
||||||
|
"restart": false,
|
||||||
"force": true,
|
"force": true,
|
||||||
"host": "marathon.mesos:8080",
|
"host": "http://marathon.mesos:8080"
|
||||||
"enabled": true
|
|
||||||
},
|
},
|
||||||
|
"mesos":{
|
||||||
|
"enabled": true,
|
||||||
|
"domain": "marathon.mesos"
|
||||||
|
},
|
||||||
|
"default_discovery": "mesos_dns",
|
||||||
"confd": "/etc/surok/conf.d",
|
"confd": "/etc/surok/conf.d",
|
||||||
"domain": "marathon.mesos",
|
|
||||||
"wait_time": 20,
|
"wait_time": 20,
|
||||||
"lock_dir": "/var/tmp",
|
"lock_dir": "/var/tmp",
|
||||||
"loglevel": "info|debug"
|
"loglevel": "info",
|
||||||
"container": true|false
|
"memcached": {
|
||||||
|
"enabled": false,
|
||||||
|
"discovery": {
|
||||||
|
"enabled": false,
|
||||||
|
"service": "memcached",
|
||||||
|
"group": "system"
|
||||||
|
},
|
||||||
|
"hosts": ["localhost:11211"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
## Опции файла конфигурации
|
||||||
|
* **version** - *string. Не обязательный. По умолчанию "0.7".*
|
||||||
|
Версия файлов конфигурации, шаблонов. На текущий момент может принимать значения "0.7" или "0.8".
|
||||||
|
* значение "0.7" - файлы конфигурации версии 0.7.х и более ранних
|
||||||
|
* значение "0.8" - файлы конфигурации версии 0.8
|
||||||
|
|
||||||
|
##### версия 0.8
|
||||||
|
* **marathon**, **mesos**, **consul**, **memcached** - *dict/hash. Не обязательный. По умолчанию '{"enable":false}'.*
|
||||||
|
Системы с которыми работает сурок. Если система выключена, то параметры системы и их наличие уже не важны.
|
||||||
|
* **enable** - *boolean. Не обязательный. По умолчанию false.*
|
||||||
|
Доступность системы для использования.
|
||||||
|
|
||||||
|
специфичные параметры:
|
||||||
|
* для Marathon API "marathon"
|
||||||
|
* **force** - *boolean. Не обязательный. По умолчанию true.*
|
||||||
|
Рестарт контейнера с force или нет.
|
||||||
|
* **restart** - *boolean. Не обязательный. По умолчанию false.*
|
||||||
|
Вкл/выкл. рестарта контейнера
|
||||||
|
* **host** - *string. Не обязательный. По умолчанию "http://marathon.mesos:8080".*
|
||||||
|
Адрес Marathon.
|
||||||
|
* для mesos DNS "mesos"
|
||||||
|
* **domain** - *string. Не обязательный. По умолчанию "marathon.mesos".*
|
||||||
|
Приватный домен Mesos DNS
|
||||||
|
* для Memcached "memcached"
|
||||||
|
* **hosts** -
|
||||||
|
* **discovery** -
|
||||||
|
* **enabled** -
|
||||||
|
* **service** -
|
||||||
|
* **group** -
|
||||||
|
* **default_discovery** - *string. Не обязательный. По умолчанию "mesos_dns".*
|
||||||
|
|
||||||
|
Может принимать значения:
|
||||||
|
* "mesos_dns" - Mesos DNS
|
||||||
|
* "marathon_api"- Marathon API
|
||||||
|
* **confd** - *strig. Обязательный.*
|
||||||
|
Абсолютный путь до директории с конфигурационными файлами приложений.
|
||||||
|
* **wait_time** - *int. Обязательный.*
|
||||||
|
Время в секундах сколько Surok ждет до того, как начать заново делать запросы на обнаружение сервисов.
|
||||||
|
* **lock_dir** - *string. Обязательный.*
|
||||||
|
Абсолютный путь до директории с lock-конфигурациями.
|
||||||
|
* **loglevel** - *string. Не обязательный. По умолчанию "info".*
|
||||||
|
Уровень логирования. Может принимать значения: "debug", "info", "warning", "error"
|
||||||
|
|
||||||
|
##### версия 0.7 и более ранние
|
||||||
|
Особенности для файла конфигурации
|
||||||
|
* **marathon**
|
||||||
|
* **enabled** - boolean. Вкл/выкл. рестарта контейнера. В версии 0.8 переименована в "restart".
|
||||||
|
* **domain** - string. Приватный домен Mesos DNS. В версии 0.8 перемещен в dict "mesos".
|
||||||
|
Обнаружение Mesos DNS включено всегда.
|
||||||
|
|
||||||
* marathon(v0.7) - hash. В текущей версии отвечает за перезапуск контейнера. Обнаружение сервисов через Marathon пока недоступно.
|
|
||||||
1. force - boolean. Рестарт контейнера с force или нет.
|
|
||||||
2. host - string. Адрес Marathon.
|
|
||||||
3. enabled - boolean. Вкл/выкл.
|
|
||||||
* confd - strig. Абсолютный путь до директории с конфигурационными файлами приложений.
|
|
||||||
* domain - string. Домен, который обслуживает mesos-dns.
|
|
||||||
* wait_time - int. Время в секундах сколько Surok ждет до того, как начать заново делать запросы на обнаружение сервисов.
|
|
||||||
* lock_dir - string. Абсолютный путь до директории с lock-конфигурациями.
|
|
||||||
* loglevel - string. Уровень логирования.
|
|
||||||
* container(v0.6) - boolean. Определяем внутри или нет контейнера запущен сурок. Меняется логика работы.
|
|
||||||
|
@ -4,6 +4,127 @@
|
|||||||
|
|
||||||
## Словарь my в шаблоне
|
## Словарь my в шаблоне
|
||||||
|
|
||||||
|
### Версия 0.8
|
||||||
|
Surok заполняет словарь my и передает его в шаблон.
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"services": {
|
||||||
|
"asterisk": [
|
||||||
|
{
|
||||||
|
"name": "nginx.testing-kl92-s0.marathon.mesos.",
|
||||||
|
"ip": [
|
||||||
|
"10.0.0.1",
|
||||||
|
"11.0.0.1"
|
||||||
|
],
|
||||||
|
"tcp": {
|
||||||
|
"rpc":31200,
|
||||||
|
"web":31201,
|
||||||
|
"sip":32000
|
||||||
|
},
|
||||||
|
"udp": {
|
||||||
|
"sip":31201
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nginx.testing-kl123-s1.marathon.mesos.",
|
||||||
|
"ip": [
|
||||||
|
"10.0.0.2",
|
||||||
|
"11.0.0.2"
|
||||||
|
],
|
||||||
|
"tcp": {
|
||||||
|
"rpc":31210,
|
||||||
|
"web":31211,
|
||||||
|
"sip":32010
|
||||||
|
},
|
||||||
|
"udp": {
|
||||||
|
"sip":31211
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"email": [
|
||||||
|
{
|
||||||
|
"name": "nginx.testing-kl92-s0.marathon.mesos.",
|
||||||
|
"ip": [
|
||||||
|
"10.0.0.1"
|
||||||
|
],
|
||||||
|
"tcp": {
|
||||||
|
"smtp":31200,
|
||||||
|
"pop":31201
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"anyport": [
|
||||||
|
{
|
||||||
|
"name": "nginx.testing-kl92-s0.marathon.mesos.",
|
||||||
|
"ip": [
|
||||||
|
"10.0.0.1"
|
||||||
|
],
|
||||||
|
"tcp": [
|
||||||
|
31200,
|
||||||
|
31201
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"env": {
|
||||||
|
"HOME": "/var/lib/nginx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Пример реального шаблона
|
||||||
|
|
||||||
|
```
|
||||||
|
upstream matrix-http {
|
||||||
|
hash $remote_addr;
|
||||||
|
{% for server in my['services']['matrix'] %}
|
||||||
|
server {{server['name']}}:{{server['tcp']['http']}} max_fails=3;
|
||||||
|
{% endfor %}
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream riot-http {
|
||||||
|
hash $remote_addr;
|
||||||
|
{% for server in my['services']['riot'] %}
|
||||||
|
server {{server['name']}}:{{server['tcp'][0]}} 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 -%}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Версия 0.7
|
||||||
Surok заполняет словарь my и передает его в шаблон.
|
Surok заполняет словарь my и передает его в шаблон.
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
@ -29,13 +150,13 @@ Surok заполняет словарь my и передает его в шаб
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"service-with-defined-ports": {
|
"service-with-defined-ports": {
|
||||||
"web": [
|
"name-of-port0": [
|
||||||
{
|
{
|
||||||
"name": "f.q.d.n",
|
"name": "f.q.d.n",
|
||||||
"port": 12341
|
"port": 12341
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rpc": [
|
"name-of-port2": [
|
||||||
{
|
{
|
||||||
"name": "f.q.d.n",
|
"name": "f.q.d.n",
|
||||||
"port": 12342
|
"port": 12342
|
||||||
|
67
surok.py
67
surok.py
@ -7,71 +7,42 @@ from os.path import isfile, join
|
|||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
from surok.templates import gen
|
from surok.templates import gen
|
||||||
from surok.discovery import resolve
|
from surok.discovery import Discovery
|
||||||
from surok.system import reload_conf
|
from surok.system import reload_conf
|
||||||
|
from surok.logger import Logger
|
||||||
|
from surok.config import Config
|
||||||
|
|
||||||
|
logger = Logger()
|
||||||
# Load base configurations
|
|
||||||
surok_conf = '/etc/surok/conf/surok.json'
|
|
||||||
|
|
||||||
# Command line arguments
|
# Command line arguments
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('-c', '--config', help='surok.json path')
|
parser.add_argument('-c', '--config', help='surok.json path')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.config:
|
|
||||||
surok_conf = args.config
|
|
||||||
|
|
||||||
# Read config file
|
|
||||||
f = open(surok_conf, 'r')
|
|
||||||
conf = json.loads(f.read())
|
|
||||||
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))]
|
|
||||||
return sorted(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
|
|
||||||
|
|
||||||
|
# Load base configurations
|
||||||
|
config = Config(args.config if args.config else '/etc/surok/conf/surok.json')
|
||||||
|
|
||||||
# Main loop
|
# Main loop
|
||||||
###########
|
#
|
||||||
|
|
||||||
|
discovery = Discovery()
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
confs = get_configs()
|
# Update config from discovery object
|
||||||
for app in confs:
|
discovery.update_data()
|
||||||
app_conf = load_app_conf(app)
|
for app in config.apps:
|
||||||
|
|
||||||
# Will be removed later
|
app_hosts = discovery.resolve(app)
|
||||||
# For old configs
|
|
||||||
loglevel = 'info'
|
|
||||||
if 'loglevel' in conf:
|
|
||||||
loglevel = conf['loglevel']
|
|
||||||
|
|
||||||
# Resolve services
|
|
||||||
app_hosts = resolve(app_conf, conf)
|
|
||||||
|
|
||||||
# Populate my dictionary
|
# Populate my dictionary
|
||||||
my = {"services": app_hosts,
|
my = {"services": app_hosts,
|
||||||
"conf_name": app_conf['conf_name']}
|
"conf_name": app['conf_name']}
|
||||||
|
|
||||||
|
logger.debug('my=', my)
|
||||||
|
|
||||||
# Generate config from template
|
# Generate config from template
|
||||||
service_conf = gen(my, app_conf['template'])
|
service_conf = gen(my, app['template'])
|
||||||
|
|
||||||
reload_conf(service_conf, app_conf, conf, app_hosts)
|
reload_conf(service_conf, app, config, app_hosts)
|
||||||
|
|
||||||
sleep(conf['wait_time'])
|
sleep(config['wait_time'])
|
||||||
|
15
surok.spec
15
surok.spec
@ -1,6 +1,6 @@
|
|||||||
Summary: Simple service discovery for Apache Mesos clusters
|
Summary: Simple service discovery for Apache Mesos clusters
|
||||||
Name: surok
|
Name: surok
|
||||||
Version: 0.7.4.3
|
Version: 0.8
|
||||||
Release: 1
|
Release: 1
|
||||||
License: BSD
|
License: BSD
|
||||||
Group: admin
|
Group: admin
|
||||||
@ -25,10 +25,12 @@ install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/logger.py %{buildr
|
|||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/system.py %{buildroot}/opt/surok/surok
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/system.py %{buildroot}/opt/surok/surok
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/discovery.py %{buildroot}/opt/surok/surok
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/discovery.py %{buildroot}/opt/surok/surok
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/templates.py %{buildroot}/opt/surok/surok
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/templates.py %{buildroot}/opt/surok/surok
|
||||||
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/config.py %{buildroot}/opt/surok/surok
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok.py %{buildroot}/opt/surok
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok.py %{buildroot}/opt/surok
|
||||||
mkdir -p %{buildroot}/etc/surok/{conf,conf.d,templates}
|
mkdir -p %{buildroot}/etc/surok/{conf,conf.d,templates}
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf/surok.json %{buildroot}/etc/surok/conf
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf/surok.json %{buildroot}/etc/surok/conf
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf.d/selfcheck.json %{buildroot}/etc/surok/conf.d
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf.d/self_check.json %{buildroot}/etc/surok/conf.d
|
||||||
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf.d/marathon_check.json %{buildroot}/etc/surok/conf.d
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/templates/selfcheck.jj2 %{buildroot}/etc/surok/templates
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/templates/selfcheck.jj2 %{buildroot}/etc/surok/templates
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/requriments.txt %{buildroot}/opt/surok
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/requriments.txt %{buildroot}/opt/surok
|
||||||
|
|
||||||
@ -59,7 +61,11 @@ cd /opt/surok && pip3 install -r requriments.txt
|
|||||||
/opt/surok/surok/templates.py
|
/opt/surok/surok/templates.py
|
||||||
/opt/surok/surok/templates.pyc
|
/opt/surok/surok/templates.pyc
|
||||||
/opt/surok/surok/templates.pyo
|
/opt/surok/surok/templates.pyo
|
||||||
/etc/surok/conf.d/selfcheck.json
|
/opt/surok/surok/config.py
|
||||||
|
/opt/surok/surok/config.pyc
|
||||||
|
/opt/surok/surok/config.pyo
|
||||||
|
/etc/surok/conf.d/self_check.json
|
||||||
|
/etc/surok/conf.d/marathon_check.json
|
||||||
/etc/surok/templates/selfcheck.jj2
|
/etc/surok/templates/selfcheck.jj2
|
||||||
/opt/surok/requriments.txt
|
/opt/surok/requriments.txt
|
||||||
%defattr(-,root,root,-)
|
%defattr(-,root,root,-)
|
||||||
@ -67,6 +73,9 @@ cd /opt/surok && pip3 install -r requriments.txt
|
|||||||
|
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Tue Feb 7 2017 Denis Zheleztsov <difrex.punk@gmail.com>
|
||||||
|
- New major release
|
||||||
|
- Discovery over marathon api
|
||||||
* Mon Nov 14 2016 Denis Zheleztsov <difrex.punk@gmail.com> -
|
* Mon Nov 14 2016 Denis Zheleztsov <difrex.punk@gmail.com> -
|
||||||
- Initial build.
|
- Initial build.
|
||||||
|
|
||||||
|
420
surok/config.py
Normal file
420
surok/config.py
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
# Public names
|
||||||
|
__all__ = ['Config', 'AppConfig']
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from .logger import *
|
||||||
|
|
||||||
|
# Logger link
|
||||||
|
|
||||||
|
# Config singleton link
|
||||||
|
_config_singleton = None
|
||||||
|
|
||||||
|
'''
|
||||||
|
Test values
|
||||||
|
==================================================
|
||||||
|
key - key
|
||||||
|
value - value of key
|
||||||
|
type_value - type of value
|
||||||
|
type_par - additional parameters for test
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
Public Config object
|
||||||
|
==================================================
|
||||||
|
.set_config(conf_data) - set config data
|
||||||
|
Use: conf_data(str type) - path of json config file
|
||||||
|
conf_data(dict type) - dict with config
|
||||||
|
.set(key,value) - set config key
|
||||||
|
.get(key) - get config key
|
||||||
|
.update_apps() - update apps config data
|
||||||
|
.apps - Apps object. List of AppConfig oblects
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class _ConfigTemplate(dict):
|
||||||
|
_conf = {}
|
||||||
|
|
||||||
|
def _init_conf(self, params):
|
||||||
|
conf = {}
|
||||||
|
for k in params.keys():
|
||||||
|
if params[k].get('params'):
|
||||||
|
conf[k] = self._init_conf(params[k].get('params'))
|
||||||
|
else:
|
||||||
|
if params[k].get('value') is not None:
|
||||||
|
conf[k] = params[k].get('value')
|
||||||
|
return conf
|
||||||
|
|
||||||
|
def __init__(self, *conf_data):
|
||||||
|
self._logger = Logger()
|
||||||
|
if not self._conf:
|
||||||
|
self._conf = self._init_conf(self._params)
|
||||||
|
for c in conf_data:
|
||||||
|
self.set_config(c)
|
||||||
|
|
||||||
|
def _set_conf_params(self, oldconf, testconf, params):
|
||||||
|
conf = oldconf if oldconf else {}
|
||||||
|
for key in testconf.keys():
|
||||||
|
resvalue = None
|
||||||
|
param = params.get(key)
|
||||||
|
oldvalue = conf.get(key)
|
||||||
|
testvalue = testconf.get(key)
|
||||||
|
if param is None:
|
||||||
|
self._logger.error('Parameter "', key, '" value "', testvalue,
|
||||||
|
'" type is "', type(testvalue).__name__, '" not found')
|
||||||
|
else:
|
||||||
|
type_param = param.get('type')
|
||||||
|
resvalue = []
|
||||||
|
if type(testvalue).__name__ != 'list':
|
||||||
|
testvalue = [testvalue]
|
||||||
|
for testitem in testvalue:
|
||||||
|
if self._test_value(key, testitem, param):
|
||||||
|
if 'dict' in type_param:
|
||||||
|
if param.get('params'):
|
||||||
|
res = self._set_conf_params(
|
||||||
|
oldvalue, testitem, param.get('params'))
|
||||||
|
if res is not None:
|
||||||
|
resvalue.append(res)
|
||||||
|
else:
|
||||||
|
resvalue.append(testitem)
|
||||||
|
if 'list' not in type_param:
|
||||||
|
resvalue = list([None] + resvalue).pop()
|
||||||
|
if resvalue is not None and 'do' in type_param:
|
||||||
|
if not self._do_type_set(key, resvalue, param):
|
||||||
|
self._logger.warning(
|
||||||
|
'Parameter "', key, '" current "', resvalue, '" type is "', type(resvalue).__name__, '" testing failed')
|
||||||
|
resvalue = None
|
||||||
|
if resvalue is not None:
|
||||||
|
conf[key] = resvalue
|
||||||
|
return conf
|
||||||
|
|
||||||
|
def _test_value(self, key, value, param):
|
||||||
|
type_param = param.get('type')
|
||||||
|
type_value = [
|
||||||
|
x for x in type_param if x in ['str', 'int', 'bool', 'dict']]
|
||||||
|
if type_value:
|
||||||
|
if type(value).__name__ not in type_value:
|
||||||
|
self._logger.error(
|
||||||
|
'Parameter "', key, '" must be ', type_value,
|
||||||
|
' types, current "', value, '" (', type(value).__name__, ')')
|
||||||
|
return False
|
||||||
|
if 'value' in type_param:
|
||||||
|
if value not in param.get('values', []):
|
||||||
|
self._logger.error(
|
||||||
|
'Value "', value, '" of key "', key, '" unknown')
|
||||||
|
return False
|
||||||
|
if 'dir' in type_param:
|
||||||
|
if not os.path.isdir(value):
|
||||||
|
self._logger.error('Path "{}" not present'.format(value))
|
||||||
|
return False
|
||||||
|
elif 'file' in type_param:
|
||||||
|
if not os.path.isfile(value):
|
||||||
|
self._logger.error('File "{}" not present'.format(value))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self._logger.error(
|
||||||
|
'Type for testing "{}" unknown'.format(type_value))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_config(self, conf_data):
|
||||||
|
conf = {}
|
||||||
|
if type(conf_data).__name__ == 'str':
|
||||||
|
try:
|
||||||
|
self._logger.debug('Open file ', conf_data)
|
||||||
|
f = open(conf_data, 'r')
|
||||||
|
json_data = f.read()
|
||||||
|
f.close()
|
||||||
|
conf = json.loads(json_data)
|
||||||
|
except OSError as err:
|
||||||
|
self._logger.error("OS error: {0}".format(err))
|
||||||
|
pass
|
||||||
|
except ValueError as err:
|
||||||
|
self._logger.error('JSON format error: {0}'.format(err))
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
self._logger.error('Load main config file failed')
|
||||||
|
pass
|
||||||
|
elif type(conf).__name__ == 'dict':
|
||||||
|
conf = conf_data
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
self._conf = self._set_conf_params(self._conf, conf, self._params)
|
||||||
|
self._logger.debug('Conf=', self._conf)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return self._conf.keys()
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
return json.dumps(self._conf, sort_keys=True, indent=2)
|
||||||
|
|
||||||
|
def _do_type_set(self, key, value, params):
|
||||||
|
self._logger.error('_do_type_set handler is not defined')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def hash(self):
|
||||||
|
return hashlib.sha1(json.dumps(self._conf, sort_keys=True).encode()).hexdigest()
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
self._conf[key] = value
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.set(key, value)
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return self._conf.get(key, default)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.get(key)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return bool(item in self._conf)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self._conf.__len__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self._conf.__str__()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self._conf.__repr__()
|
||||||
|
|
||||||
|
|
||||||
|
class Config(_ConfigTemplate):
|
||||||
|
_params = {
|
||||||
|
'marathon': {
|
||||||
|
'params': {
|
||||||
|
'force': {
|
||||||
|
'value': True,
|
||||||
|
'type': ['bool']
|
||||||
|
},
|
||||||
|
'host': {
|
||||||
|
'value': 'http://marathon.mesos:8080',
|
||||||
|
'type': ['str']
|
||||||
|
},
|
||||||
|
'enabled': {
|
||||||
|
'value': False,
|
||||||
|
'type': ['bool']
|
||||||
|
},
|
||||||
|
'restart': {
|
||||||
|
'value': False,
|
||||||
|
'type': ['bool']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'type': ['dict']
|
||||||
|
},
|
||||||
|
'mesos': {
|
||||||
|
'params': {
|
||||||
|
"domain": {
|
||||||
|
'value': "marathon.mesos",
|
||||||
|
'type': ['str']
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
'value': False,
|
||||||
|
'type': ['bool']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'type': ['dict']
|
||||||
|
},
|
||||||
|
'memcached': {
|
||||||
|
'params': {
|
||||||
|
'enabled': {
|
||||||
|
'value': False,
|
||||||
|
'type': ['bool']
|
||||||
|
},
|
||||||
|
'discovery': {
|
||||||
|
'params': {
|
||||||
|
'enabled': {
|
||||||
|
'value': False,
|
||||||
|
'type': ['bool']
|
||||||
|
},
|
||||||
|
'service': {
|
||||||
|
'value': 'memcached',
|
||||||
|
'type': ['str']
|
||||||
|
},
|
||||||
|
'group': {
|
||||||
|
'value': 'system',
|
||||||
|
'type': ['str']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'type': ['dict']
|
||||||
|
},
|
||||||
|
'hosts': {
|
||||||
|
'value': ['localhost:11211'],
|
||||||
|
'type': ['list', 'str']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'type': ['dict']
|
||||||
|
},
|
||||||
|
'version': {
|
||||||
|
'value': '0.7',
|
||||||
|
'type': ['str', 'value'],
|
||||||
|
'values': ['0.7', '0.8']
|
||||||
|
},
|
||||||
|
'confd': {
|
||||||
|
'value': '/etc/surok/conf.d',
|
||||||
|
'type': ['str', 'dir']
|
||||||
|
},
|
||||||
|
'wait_time': {
|
||||||
|
'value': 20,
|
||||||
|
'type': ['int']
|
||||||
|
},
|
||||||
|
'lock_dir': {
|
||||||
|
'value': '/var/tmp',
|
||||||
|
'type': ['str', 'dir']
|
||||||
|
},
|
||||||
|
'default_discovery': {
|
||||||
|
'value': 'mesos_dns',
|
||||||
|
'type': ['str', 'value'],
|
||||||
|
'values': ['mesos_dns', 'marathon_api']
|
||||||
|
},
|
||||||
|
'loglevel': {
|
||||||
|
'value': 'info',
|
||||||
|
'type': ['str', 'do'],
|
||||||
|
'do': 'set_loglevel'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __new__(cls, *args):
|
||||||
|
global _config_singleton
|
||||||
|
if _config_singleton is None:
|
||||||
|
_config_singleton = super(Config, cls).__new__(cls)
|
||||||
|
return _config_singleton
|
||||||
|
|
||||||
|
def __init__(self, *conf_data):
|
||||||
|
super().__init__(*conf_data)
|
||||||
|
self.apps = _Apps()
|
||||||
|
|
||||||
|
def set_config(self, conf_data):
|
||||||
|
super().set_config(conf_data)
|
||||||
|
if self.get('version') == '0.7':
|
||||||
|
domain = self.get('domain')
|
||||||
|
if domain is not None and self.get('mesos') is None:
|
||||||
|
self.set('mesos', {'domain': domain, 'enabled': True})
|
||||||
|
|
||||||
|
def _do_type_set(self, key, value, param):
|
||||||
|
if param.get('do') == 'set_loglevel':
|
||||||
|
if self._logger.set_level(value):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_apps(self):
|
||||||
|
self.apps.reset()
|
||||||
|
for app in sorted([os.path.join(self.get('confd'), f) for f in os.listdir(self.get('confd')) if os.path.isfile(os.path.join(self.get('confd'), f))]):
|
||||||
|
self.apps.set(AppConfig(app))
|
||||||
|
|
||||||
|
'''
|
||||||
|
Private Apps object
|
||||||
|
==================================================
|
||||||
|
.get(app) - get _AppConfig object
|
||||||
|
.set(app) - set _AppConfig object
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class _Apps:
|
||||||
|
_apps = {}
|
||||||
|
_items = []
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
return self._apps.get(key)
|
||||||
|
|
||||||
|
def set(self, app):
|
||||||
|
self._apps[app.get('conf_name')] = app
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
keys = [] + list(self.keys())
|
||||||
|
for k in keys:
|
||||||
|
del self._apps[k]
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.get(key)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return bool(item in self._apps)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
self._items = sorted(self.keys())
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
if self._items:
|
||||||
|
return self.get(self._items.pop(0))
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return self._apps.keys()
|
||||||
|
|
||||||
|
'''
|
||||||
|
Public AppConfig object
|
||||||
|
==================================================
|
||||||
|
.set_config(conf_data) - set config data
|
||||||
|
Use: conf_data(str type) - path of json config file
|
||||||
|
conf_data(dict type) - dict with config
|
||||||
|
.set(key,value) - set config key
|
||||||
|
.get(key) - get config key
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class AppConfig(_ConfigTemplate):
|
||||||
|
_params = {
|
||||||
|
'conf_name': {
|
||||||
|
'type': ['str']
|
||||||
|
},
|
||||||
|
'services': {
|
||||||
|
'params': {
|
||||||
|
'name': {
|
||||||
|
'type': ['str']
|
||||||
|
},
|
||||||
|
'ports': {
|
||||||
|
'type': ['list', 'str']
|
||||||
|
},
|
||||||
|
'discovery': {
|
||||||
|
'type': ['str']
|
||||||
|
},
|
||||||
|
'group': {
|
||||||
|
'type': ['str']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'type': ['list', 'dict']
|
||||||
|
},
|
||||||
|
'template': {
|
||||||
|
'type': ['str', 'file']
|
||||||
|
},
|
||||||
|
'dest': {
|
||||||
|
'type': ['str']
|
||||||
|
},
|
||||||
|
'reload_cmd': {
|
||||||
|
'type': ['str']
|
||||||
|
},
|
||||||
|
'discovery': {
|
||||||
|
'type': ['str']
|
||||||
|
},
|
||||||
|
'group': {
|
||||||
|
'type': ['str']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *conf_data):
|
||||||
|
self._config = Config()
|
||||||
|
super().__init__(*conf_data)
|
||||||
|
|
||||||
|
def set_config(self, conf_data):
|
||||||
|
super().set_config(conf_data)
|
||||||
|
self._conf.setdefault(
|
||||||
|
'discovery', self._config.get('default_discovery'))
|
||||||
|
self._conf.setdefault('group', self._get_default_group())
|
||||||
|
if type(conf_data).__name__ == 'str':
|
||||||
|
self._conf.setdefault('conf_name', os.path.basename(conf_data))
|
||||||
|
|
||||||
|
def _get_default_group(self):
|
||||||
|
env = self._config.get('env', dict(os.environ))
|
||||||
|
# Check environment variable
|
||||||
|
if env.get('SUROK_DISCOVERY_GROUP'):
|
||||||
|
return env['SUROK_DISCOVERY_GROUP']
|
||||||
|
# Check marathon environment variable
|
||||||
|
elif env.get('MARATHON_APP_ID'):
|
||||||
|
return ".".join(env['MARATHON_APP_ID'].split('/')[-2:0:-1])
|
@ -1,51 +1,47 @@
|
|||||||
|
# Public names
|
||||||
|
__all__ = ['Discovery', 'DiscoveryMesos', 'DiscoveryMarathon']
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
import dns.query
|
import dns.query
|
||||||
from dns.exception import DNSException
|
import os
|
||||||
from .logger import info, warning, error, debug
|
|
||||||
import sys
|
import sys
|
||||||
|
import requests
|
||||||
|
from dns.exception import DNSException
|
||||||
|
from .config import *
|
||||||
|
from .logger import *
|
||||||
|
|
||||||
|
# Discovery object
|
||||||
|
_discovery_singleton = None
|
||||||
|
|
||||||
|
|
||||||
# Resolve service from mesos-dns SRV record
|
class DiscoveryTemplate:
|
||||||
# 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:
|
def __init__(self):
|
||||||
hosts[service['name']] = {}
|
self._config = Config()
|
||||||
|
self._logger = Logger()
|
||||||
|
|
||||||
group = get_group(service, app)
|
def enabled(self):
|
||||||
if group is False:
|
return self._config[self._config_section].get('enabled', False)
|
||||||
error('Group is not defined in config, SUROK_DISCOVERY_GROUP and MARATHON_APP_ID')
|
|
||||||
error('Not in Mesos launch?')
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
# Port name from app config
|
def update_data(self):
|
||||||
ports = None
|
|
||||||
try:
|
|
||||||
ports = service['ports']
|
|
||||||
except:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# This is fast fix for port naming
|
# Do DNS queries
|
||||||
# Will be rewrite later
|
# Return array:
|
||||||
fqdn = ''
|
# ["10.10.10.1", "10.10.10.2"]
|
||||||
if ports is not None:
|
def do_query_a(self, fqdn):
|
||||||
for port_name in ports:
|
servers = []
|
||||||
fqdn = '_' + port_name + '.' + '_' + service['name'] + '.' + group + '._tcp.' + domain
|
try:
|
||||||
hosts[service['name']][port_name] = do_query(fqdn, conf['loglevel'])
|
resolver = dns.resolver.Resolver()
|
||||||
else:
|
for a_rdata in resolver.query(fqdn, 'A'):
|
||||||
fqdn = '_' + service['name'] + '.' + group + '._tcp.' + domain
|
servers.append(a_rdata.address)
|
||||||
hosts[service['name']] = do_query(fqdn, conf['loglevel'])
|
except DNSException as e:
|
||||||
|
self._logger.error("Could not resolve ", fqdn)
|
||||||
return hosts
|
return servers
|
||||||
|
|
||||||
|
|
||||||
# Do DNS queries
|
# Do DNS queries
|
||||||
# Return array:
|
# Return array:
|
||||||
# [{"name": "f.q.d.n", "port": 8876, "ip": ["10.10.10.1", "10.10.10.2"]}]
|
# [{"name": "f.q.d.n", "port": 8876, "ip": ["10.10.10.1", "10.10.10.2"]}]
|
||||||
def do_query(fqdn, loglevel):
|
def do_query_srv(self, fqdn):
|
||||||
servers = []
|
servers = []
|
||||||
try:
|
try:
|
||||||
resolver = dns.resolver.Resolver()
|
resolver = dns.resolver.Resolver()
|
||||||
@ -54,42 +50,183 @@ def do_query(fqdn, loglevel):
|
|||||||
query = resolver.query(fqdn, 'SRV')
|
query = resolver.query(fqdn, 'SRV')
|
||||||
for rdata in query:
|
for rdata in query:
|
||||||
info = str(rdata).split()
|
info = str(rdata).split()
|
||||||
name = info[3][:-1]
|
servers.append({'name': info[3][:-1], 'port': info[2]})
|
||||||
port = info[2]
|
|
||||||
server = {'name': name, 'port': port, 'ip': []}
|
|
||||||
a_query = resolver.query(name, 'A')
|
|
||||||
for a_rdata in a_query:
|
|
||||||
server['ip'].append(a_rdata.address)
|
|
||||||
servers.append(server)
|
|
||||||
except DNSException as e:
|
except DNSException as e:
|
||||||
if loglevel != 'info':
|
self._logger.warning("Could not resolve ", fqdn)
|
||||||
error("Could not resolve " + fqdn)
|
|
||||||
|
|
||||||
return servers
|
return servers
|
||||||
|
|
||||||
|
|
||||||
# Groups switch
|
class Discovery:
|
||||||
# Priority: config, environment, marathon environment
|
_discoveries = {}
|
||||||
def get_group(service, app):
|
|
||||||
# Check group in app conf
|
def __new__(cls):
|
||||||
if 'group' in service:
|
global _discovery_singleton
|
||||||
return service['group']
|
if _discovery_singleton is None:
|
||||||
# Check environment variable
|
_discovery_singleton = super(Discovery, cls).__new__(cls)
|
||||||
elif app['env'].get('SUROK_DISCOVERY_GROUP'):
|
return _discovery_singleton
|
||||||
return app['env']['SUROK_DISCOVERY_GROUP']
|
|
||||||
# Check marathon environment variable
|
def __init__(self):
|
||||||
elif app['env'].get('MARATHON_APP_ID'):
|
self._config = Config()
|
||||||
group = parse_marathon_app_id(app['env']['MARATHON_APP_ID'])
|
self._logger = Logger()
|
||||||
return group
|
if not self._discoveries.get('mesos_dns'):
|
||||||
|
self._discoveries['mesos_dns'] = DiscoveryMesos()
|
||||||
|
if not self._discoveries.get('marathon_api'):
|
||||||
|
self._discoveries['marathon_api'] = DiscoveryMarathon()
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return self._discoveries.keys()
|
||||||
|
|
||||||
|
def resolve(self, app):
|
||||||
|
discovery = app.get('discovery', self._config.get('default_discovery'))
|
||||||
|
if discovery not in self.keys():
|
||||||
|
self._logger.warning('Discovery "', discovery, '" is not present')
|
||||||
|
return {}
|
||||||
|
if self._discoveries[discovery].enabled():
|
||||||
|
return self.compatible(self._discoveries[discovery].resolve(app))
|
||||||
else:
|
else:
|
||||||
return False
|
self._logger.error('Discovery "', discovery, '" is disabled')
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def update_data(self):
|
||||||
|
self._config.update_apps()
|
||||||
|
for d in self.keys():
|
||||||
|
if self._discoveries[d].enabled():
|
||||||
|
self._discoveries[d].update_data()
|
||||||
|
|
||||||
|
def compatible(self, hosts):
|
||||||
|
compatible_hosts = {}
|
||||||
|
if self._config.get('version') == '0.7':
|
||||||
|
for service in hosts.keys():
|
||||||
|
for host in hosts[service]:
|
||||||
|
ports = host.get('tcp', [])
|
||||||
|
if type(ports).__name__ == 'list':
|
||||||
|
compatible_hosts[service] = []
|
||||||
|
for port in ports:
|
||||||
|
compatible_hosts[service].append(
|
||||||
|
{'name': host['name'],
|
||||||
|
'ip': host['ip'],
|
||||||
|
'port': str(port)})
|
||||||
|
else:
|
||||||
|
compatible_hosts[service] = {}
|
||||||
|
for port in ports.keys():
|
||||||
|
compatible_host = compatible_hosts[
|
||||||
|
service].setdefault(port, [])
|
||||||
|
compatible_host.append({'name': host['name'],
|
||||||
|
'ip': host['ip'],
|
||||||
|
'port': ports[port]})
|
||||||
|
|
||||||
|
return compatible_hosts
|
||||||
|
return hosts
|
||||||
|
|
||||||
|
|
||||||
# Parse MARATHON_APP_ID
|
class DiscoveryMesos(DiscoveryTemplate):
|
||||||
# Return marathon.group
|
_config_section = 'mesos'
|
||||||
def parse_marathon_app_id(marathon_app_id):
|
|
||||||
marathon_app_id = marathon_app_id.split('/')
|
def resolve(self, app):
|
||||||
del(marathon_app_id[-1])
|
hosts = {}
|
||||||
marathon_app_id.reverse()
|
services = app.get('services')
|
||||||
group = ".".join(marathon_app_id)[:-1]
|
domain = self._config[self._config_section].get('domain')
|
||||||
return(group)
|
for service in services:
|
||||||
|
group = service.get('group', app.get('group'))
|
||||||
|
if group is None:
|
||||||
|
self._logger.error('Group for service "{}" of config "{}" not found'.format(
|
||||||
|
service['name'], app.get('conf_name')))
|
||||||
|
continue
|
||||||
|
ports = service.get('ports')
|
||||||
|
name = service['name']
|
||||||
|
hosts[name] = {}
|
||||||
|
serv = hosts[name]
|
||||||
|
self._logger.debug(
|
||||||
|
'group=', group, ' ports=', ports, ' name=', name, ' serv=', serv)
|
||||||
|
for prot in ['tcp', 'udp']:
|
||||||
|
if ports is not None:
|
||||||
|
for port_name in ports:
|
||||||
|
for d in self.do_query_srv('_' + port_name + '._' + name + '.' + group + '._' + prot + '.' + domain):
|
||||||
|
hostname = d['name']
|
||||||
|
serv.setdefault(hostname, {'name': hostname,
|
||||||
|
'ip': self.do_query_a(hostname)})
|
||||||
|
serv[hostname].setdefault(prot, {})
|
||||||
|
serv[hostname][prot][port_name] = d['port']
|
||||||
|
else:
|
||||||
|
for d in self.do_query_srv('_' + name + '.' + group + '._' + prot + '.' + domain):
|
||||||
|
hostname = d['name']
|
||||||
|
if serv.get(hostname) is None:
|
||||||
|
serv[hostname] = {'name': hostname,
|
||||||
|
'ip': self.do_query_a(hostname)}
|
||||||
|
if serv[hostname].get(prot) is None:
|
||||||
|
serv[hostname][prot] = []
|
||||||
|
serv[hostname][prot].extend([d['port']])
|
||||||
|
hosts[name] = list(serv.values())
|
||||||
|
return hosts
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryMarathon(DiscoveryTemplate):
|
||||||
|
_config_section = 'marathon'
|
||||||
|
_tasks = []
|
||||||
|
_ports = {}
|
||||||
|
|
||||||
|
def update_data(self):
|
||||||
|
hostname = self._config[self._config_section].get('host')
|
||||||
|
try:
|
||||||
|
ports = {}
|
||||||
|
for app in requests.get(hostname + '/v2/apps').json()['apps']:
|
||||||
|
ports[app['id']] = {}
|
||||||
|
if app.get('container') is not None and app['container'].get('type') == 'DOCKER':
|
||||||
|
ports[app['id']] = app['container'][
|
||||||
|
'docker'].get('portMappings', [])
|
||||||
|
self._ports = ports
|
||||||
|
except:
|
||||||
|
self._logger.warning(
|
||||||
|
'Apps (', hostname, '/v2/apps) request from Marathon API is failed')
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self._tasks = requests.get(hostname + '/v2/tasks').json()['tasks']
|
||||||
|
except:
|
||||||
|
self._logger.warning(
|
||||||
|
'Tasks (', hostname, '/v2/tasks) request from Marathon API is failed')
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _test_mask(self, mask, value):
|
||||||
|
return (mask.endswith('*') and value.startswith(mask[:-1])) or mask == value
|
||||||
|
|
||||||
|
def resolve(self, app):
|
||||||
|
hosts = {}
|
||||||
|
services = app.get('services')
|
||||||
|
if not services:
|
||||||
|
services = [{'name': '*', 'ports': ['*']}]
|
||||||
|
for service in services:
|
||||||
|
# Convert xxx.yyy.zzz to /zzz/yyy/xxx/ format
|
||||||
|
group = service.get('group', app.get('group'))
|
||||||
|
if group is None:
|
||||||
|
self._logger.error('Group for service "{}" of config "{}" not found'.format(
|
||||||
|
service['name'], app.get('conf_name')))
|
||||||
|
continue
|
||||||
|
group = '/' + '/'.join(group.split('.')[::-1]) + '/'
|
||||||
|
service_mask = group + service['name']
|
||||||
|
for task in self._tasks:
|
||||||
|
if self._test_mask(service_mask, task['appId']):
|
||||||
|
name = '.'.join(
|
||||||
|
task['appId'][len(group):].split('/')[::-1])
|
||||||
|
hosts[name] = {}
|
||||||
|
serv = hosts[name]
|
||||||
|
hostname = task['host']
|
||||||
|
for task_port in self._ports[task['appId']]:
|
||||||
|
prot = task_port['protocol']
|
||||||
|
port_name = task_port['name']
|
||||||
|
port = task['ports'][
|
||||||
|
task['servicePorts'].index(task_port['servicePort'])]
|
||||||
|
if 'ports' in service:
|
||||||
|
for port_mask in service['ports']:
|
||||||
|
if self._test_mask(port_mask, port_name):
|
||||||
|
serv.setdefault(
|
||||||
|
hostname, {'name': hostname,
|
||||||
|
'ip': self.do_query_a(hostname)})
|
||||||
|
serv[hostname].setdefault(prot, {})
|
||||||
|
serv[hostname][prot][port_name] = port
|
||||||
|
else:
|
||||||
|
serv.setdefault(hostname, {'name': hostname,
|
||||||
|
'ip': self.do_query_a(hostname)})
|
||||||
|
serv[hostname].setdefault(prot, [])
|
||||||
|
serv[hostname][prot].extend([port])
|
||||||
|
hosts[name] = list(serv.values())
|
||||||
|
return hosts
|
||||||
|
@ -1,36 +1,86 @@
|
|||||||
|
# Public names
|
||||||
|
__all__ = ['Logger']
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from time import time
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Logger singleton link
|
||||||
|
_logger_singleton = None
|
||||||
|
|
||||||
|
'''
|
||||||
|
Public Logger oblect
|
||||||
|
==================================================
|
||||||
|
.set_level(level) - set level messages
|
||||||
|
level - values 'debug', 'info', 'warning', 'error'
|
||||||
|
* error - write error message
|
||||||
|
* warning - write warning and error message
|
||||||
|
* info - write info, warning and error message
|
||||||
|
* debug - write all message
|
||||||
|
.get_level() - get level messages
|
||||||
|
.error(str) - write error message
|
||||||
|
.warning(str) - write warning message
|
||||||
|
.info(str) - write info message
|
||||||
|
.debug(str)- write error message
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
def make_message(message):
|
class Logger:
|
||||||
cur_time = str(time())
|
_loglevel = 'info'
|
||||||
m = '[' + cur_time + '] ' + message['level'] + ': ' + message['raw'] + "\n"
|
_msg_level = {
|
||||||
return m
|
'debug': 'DEBUG',
|
||||||
|
'info': 'INFO',
|
||||||
|
'warning': 'WARNING',
|
||||||
|
'error': 'ERROR'
|
||||||
|
}
|
||||||
|
|
||||||
|
def __new__(cls, *args):
|
||||||
|
global _logger_singleton
|
||||||
|
if _logger_singleton is None:
|
||||||
|
_logger_singleton = super(Logger, cls).__new__(cls)
|
||||||
|
return _logger_singleton
|
||||||
|
|
||||||
def info(message):
|
def __init__(self, *args):
|
||||||
req = {'level': 'INFO', 'raw': message}
|
if args:
|
||||||
m = make_message(req)
|
self.set_level(args[0])
|
||||||
|
|
||||||
sys.stdout.write(m)
|
def set_level(self, level):
|
||||||
|
if level in ['debug', 'info', 'warning', 'error']:
|
||||||
|
self._loglevel = level
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.warning('Log level "', level, '" not valid')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_level(self):
|
||||||
|
return self._loglevel
|
||||||
|
|
||||||
def warning(message):
|
def _make_message(self, level, message):
|
||||||
req = {'level': 'WARNING', 'raw': message}
|
r = []
|
||||||
m = make_message(req)
|
for m in message:
|
||||||
|
if type(m).__name__ == 'str':
|
||||||
|
r.append(m)
|
||||||
|
else:
|
||||||
|
r.append(json.dumps(m, sort_keys=True, indent=2))
|
||||||
|
return '[' + str(time.time()) + '] ' + self._msg_level[level] + ': ' + ''.join(r) + "\n"
|
||||||
|
|
||||||
sys.stderr.write(m)
|
def debug(self, *message):
|
||||||
|
if self.get_level() in ['debug']:
|
||||||
|
self._log2err(self._make_message('debug', message))
|
||||||
|
|
||||||
|
def info(self, *message):
|
||||||
|
if self.get_level() in ['debug', 'info']:
|
||||||
|
self._log2out(self._make_message('info', message))
|
||||||
|
|
||||||
def error(message):
|
def warning(self, *message):
|
||||||
req = {'level': 'ERROR', 'raw': message}
|
if self.get_level() in ['debug', 'info', 'warning']:
|
||||||
m = make_message(req)
|
self._log2out(self._make_message('warning', message))
|
||||||
|
|
||||||
sys.stderr.write(m)
|
def error(self, *message):
|
||||||
|
self._log2err(self._make_message('error', message))
|
||||||
|
|
||||||
|
def _log2err(self, out):
|
||||||
|
sys.stderr.write(out)
|
||||||
|
|
||||||
def debug(message):
|
def _log2out(self, out):
|
||||||
req = {'level': 'DEBUG', 'raw': message}
|
sys.stdout.write(out)
|
||||||
m = make_message(req)
|
|
||||||
|
|
||||||
sys.stderr.write(m)
|
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import requests
|
import requests
|
||||||
from .discovery import resolve
|
from .discovery import Discovery
|
||||||
from .logger import info, warning, error, debug
|
from .logger import Logger
|
||||||
|
|
||||||
|
logger=Logger()
|
||||||
|
|
||||||
# Get old configuration
|
# Get old configuration
|
||||||
def get_old(name, service_conf):
|
def get_old(name, service_conf):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
path = '/var/tmp/surok.' + name
|
path = '/var/tmp/surok.' + name
|
||||||
f = open(path, 'r')
|
f = open(path, 'r')
|
||||||
old = f.read()
|
old = f.read()
|
||||||
f.close()
|
f.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(str(e))
|
logger.error(str(e))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if old == service_conf:
|
if old == service_conf:
|
||||||
@ -53,7 +53,7 @@ def write_lock(name, service_conf):
|
|||||||
|
|
||||||
|
|
||||||
def do_reload(service_conf, app_conf):
|
def do_reload(service_conf, app_conf):
|
||||||
warning('Write new configuration of ' + app_conf['conf_name'])
|
logger.warning('Write new configuration of ' + app_conf['conf_name'])
|
||||||
|
|
||||||
f = open(app_conf['dest'], 'w')
|
f = open(app_conf['dest'], 'w')
|
||||||
f.write(service_conf)
|
f.write(service_conf)
|
||||||
@ -68,6 +68,7 @@ def do_reload(service_conf, app_conf):
|
|||||||
|
|
||||||
# Discovery memcached servers
|
# Discovery memcached servers
|
||||||
def discovery_memcached(conf):
|
def discovery_memcached(conf):
|
||||||
|
discovery=Discovery()
|
||||||
memcache = conf['memcached']
|
memcache = conf['memcached']
|
||||||
app_conf = {
|
app_conf = {
|
||||||
"services": [
|
"services": [
|
||||||
@ -78,7 +79,7 @@ def discovery_memcached(conf):
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts = resolve(app_conf, conf)
|
hosts = discovery.resolve(app_conf)
|
||||||
mc_servers = []
|
mc_servers = []
|
||||||
|
|
||||||
for server in hosts[memcache['discovery']['service']]:
|
for server in hosts[memcache['discovery']['service']]:
|
||||||
@ -91,7 +92,8 @@ def discovery_memcached(conf):
|
|||||||
# !!! NEED REFACTORING !!!
|
# !!! NEED REFACTORING !!!
|
||||||
def reload_conf(service_conf, app_conf, conf, app_hosts):
|
def reload_conf(service_conf, app_conf, conf, app_hosts):
|
||||||
# Check marathon enabled in configuration
|
# Check marathon enabled in configuration
|
||||||
if conf['marathon']['enabled'] is True:
|
if (conf.get('version','0.7')=='0.8' and conf['marathon'].get('restart',False)) or (
|
||||||
|
conf.get('version','0.7')=='0.7' and conf['marathon'].get('enable',False)):
|
||||||
if get_old(app_conf['conf_name'], service_conf) != 1:
|
if get_old(app_conf['conf_name'], service_conf) != 1:
|
||||||
restart_self_in_marathon(conf['marathon'])
|
restart_self_in_marathon(conf['marathon'])
|
||||||
|
|
||||||
@ -105,47 +107,38 @@ def reload_conf(service_conf, app_conf, conf, app_hosts):
|
|||||||
mc_hosts = None
|
mc_hosts = None
|
||||||
if conf['memcached']['discovery']['enabled'] is True:
|
if conf['memcached']['discovery']['enabled'] is True:
|
||||||
mc_hosts = discovery_memcached(conf)
|
mc_hosts = discovery_memcached(conf)
|
||||||
info('Discovered memcached hosts: ' + str(mc_hosts))
|
logger.info('Discovered memcached hosts: ' + str(mc_hosts))
|
||||||
else:
|
else:
|
||||||
mc_hosts = conf['memcached']['hosts']
|
mc_hosts = conf['memcached']['hosts']
|
||||||
try:
|
try:
|
||||||
mc = memcache.Client(mc_hosts)
|
mc = memcache.Client(mc_hosts)
|
||||||
if get_old_from_memcache(mc, app_conf['conf_name'], app_hosts) != 1:
|
if get_old_from_memcache(mc, app_conf['conf_name'], app_hosts) != 1:
|
||||||
stdout = do_reload(service_conf, app_conf)
|
stdout = do_reload(service_conf, app_conf)
|
||||||
info(stdout)
|
logger.info(stdout)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error('Cannot connect to memcached: ' + str(e))
|
logger.error('Cannot connect to memcached: ' + str(e))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
warning('DEPRECATED main conf file. Please use new syntax!')
|
logger.warning('DEPRECATED main conf file. Please use new syntax!')
|
||||||
# End of memcache block
|
# End of memcache block
|
||||||
#######################
|
#######################
|
||||||
|
|
||||||
if get_old(app_conf['conf_name'], service_conf) != 1:
|
if get_old(app_conf['conf_name'], service_conf) != 1:
|
||||||
stdout = do_reload(service_conf, app_conf)
|
stdout = do_reload(service_conf, app_conf)
|
||||||
info(stdout)
|
logger.info(stdout)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
if conf['loglevel'] == 'debug':
|
logger.debug('Same config ' + app_conf['conf_name'] + ' Skip reload')
|
||||||
debug('Same config ' + app_conf['conf_name'] + ' Skip reload')
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Do POST request to marathon API
|
# Do POST request to marathon API
|
||||||
# /v2/apps//app/name/restart
|
# /v2/apps//app/name/restart
|
||||||
def restart_self_in_marathon(marathon):
|
def restart_self_in_marathon(marathon):
|
||||||
host = marathon['host']
|
|
||||||
|
|
||||||
# Check MARATHON_APP_ID environment varible
|
# Check MARATHON_APP_ID environment varible
|
||||||
if os.environ.get('MARATHON_APP_ID') is not True:
|
if not os.environ.get('MARATHON_APP_ID',False):
|
||||||
error('Cannot find MARATHON_APP_ID. Not in Mesos?')
|
logger.error('Cannot find MARATHON_APP_ID. Not in Mesos?')
|
||||||
sys.exit(2)
|
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
|
# Ok. In this step we made restart request to Marathon
|
||||||
if marathon['force'] is True:
|
r = requests.post('http://'+marathon['host']+'/v2/apps/'+os.environ['MARATHON_APP_ID']+'/restart',
|
||||||
r = requests.post(uri, data = {'force': 'true'})
|
data={'force': marathon.get('force',False)})
|
||||||
else:
|
|
||||||
r = requests.post(uri, data = {'force': 'false'})
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
Summary: Simple service discovery for Apache Mesos clusters
|
Summary: Simple service discovery for Apache Mesos clusters
|
||||||
Name: surok
|
Name: surok
|
||||||
Version: 0.7.4.3
|
Version: 0.8
|
||||||
Release: 1.fc24
|
Release: 1.fc24
|
||||||
License: BSD
|
License: BSD
|
||||||
Group: admin
|
Group: admin
|
||||||
@ -25,10 +25,12 @@ install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/logger.py %{buildr
|
|||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/system.py %{buildroot}/opt/surok/surok
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/system.py %{buildroot}/opt/surok/surok
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/discovery.py %{buildroot}/opt/surok/surok
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/discovery.py %{buildroot}/opt/surok/surok
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/templates.py %{buildroot}/opt/surok/surok
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/templates.py %{buildroot}/opt/surok/surok
|
||||||
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok/config.py %{buildroot}/opt/surok/surok
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok.py %{buildroot}/opt/surok
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/surok.py %{buildroot}/opt/surok
|
||||||
mkdir -p %{buildroot}/etc/surok/{conf,conf.d,templates}
|
mkdir -p %{buildroot}/etc/surok/{conf,conf.d,templates}
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf/surok.json %{buildroot}/etc/surok/conf
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf/surok.json %{buildroot}/etc/surok/conf
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf.d/selfcheck.json %{buildroot}/etc/surok/conf.d
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf.d/self_check.json %{buildroot}/etc/surok/conf.d
|
||||||
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/conf.d/marathon_check.json %{buildroot}/etc/surok/conf.d
|
||||||
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/templates/selfcheck.jj2 %{buildroot}/etc/surok/templates
|
install -p -m 644 /root/rpmbuild/BUILD/surok-%{version}/templates/selfcheck.jj2 %{buildroot}/etc/surok/templates
|
||||||
|
|
||||||
|
|
||||||
@ -43,13 +45,18 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
/opt/surok/surok/discovery.py
|
/opt/surok/surok/discovery.py
|
||||||
/opt/surok/surok/system.py
|
/opt/surok/surok/system.py
|
||||||
/opt/surok/surok/templates.py
|
/opt/surok/surok/templates.py
|
||||||
/etc/surok/conf.d/selfcheck.json
|
/opt/surok/surok/config.py
|
||||||
|
/etc/surok/conf.d/self_check.json
|
||||||
|
/etc/surok/conf.d/marathon_check.json
|
||||||
/etc/surok/templates/selfcheck.jj2
|
/etc/surok/templates/selfcheck.jj2
|
||||||
%defattr(-,root,root,-)
|
%defattr(-,root,root,-)
|
||||||
%doc
|
%doc
|
||||||
|
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Tue Feb 7 2017 Denis Zheleztsov <difrex.punk@gmail.com>
|
||||||
|
- New major release
|
||||||
|
- Discovery over marathon api
|
||||||
* Mon Nov 14 2016 Denis Zheleztsov <difrex.punk@gmail.com> -
|
* Mon Nov 14 2016 Denis Zheleztsov <difrex.punk@gmail.com> -
|
||||||
- Initial build.
|
- Initial build.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user