From de10a174ad791b0b563e0d2e64eb83c0a91ecaa4 Mon Sep 17 00:00:00 2001 From: Vasily Ostanin Date: Wed, 25 Jan 2017 13:33:39 +0500 Subject: [PATCH] feature: tcp & udp streams (#6) --- Dockerfile | 4 +- README.md | 8 +- circle.yml | 9 +- nginx/nginx.conf | 12 +- services/nginx.sh | 4 +- src/vergilius/components/port_allocator.py | 20 +++ src/vergilius/config.py | 1 + src/vergilius/loop/service_watcher.py | 3 +- src/vergilius/models/service.py | 130 ++++++++++++------ src/vergilius/templates/service.html | 72 ---------- src/vergilius/templates/service_http.html | 35 +++++ src/vergilius/templates/service_http2.html | 31 +++++ src/vergilius/templates/service_tcp.html | 7 + src/vergilius/templates/service_udp.html | 7 + src/vergilius/templates/service_upstream.html | 8 ++ src/vergilius/templates/service_validate.html | 9 +- tests/base_test.py | 23 +++- tests/test_certificate.py | 9 +- tests/test_service.py | 67 +++++++-- 19 files changed, 312 insertions(+), 147 deletions(-) create mode 100644 src/vergilius/components/port_allocator.py delete mode 100644 src/vergilius/templates/service.html create mode 100644 src/vergilius/templates/service_http.html create mode 100644 src/vergilius/templates/service_http2.html create mode 100644 src/vergilius/templates/service_tcp.html create mode 100644 src/vergilius/templates/service_udp.html create mode 100644 src/vergilius/templates/service_upstream.html diff --git a/Dockerfile b/Dockerfile index 67b2f0d..ced6d4a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,8 @@ ADD src /opt/vergilius RUN cd /opt/vergilius/ && python setup.py install WORKDIR /opt/vergilius/ -EXPOSE 80 443 +EXPOSE 80 443 7000-8000 + +ENV DHPARAM_LENGTH 4096 RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/README.md b/README.md index 76f90a9..5856e5d 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,16 @@ Consul service config example } ``` -Vergilius looks for registered services with tags `http` and `http2`, creates upstream with all containers of this service, +Vergilius looks for registered services with tags `http` and `http2` creates upstream with all containers of this service, routes requests from `(www.)?service.example.com` and `*.(www.)?service.example.com` to containers using nginx `least_conn` balancing algorithm. +You can also add `tcp` and `udp` tags to service, vergilus will stream this protocols too. + External ports for this services are stored in consul KV at `vergilius/ports/%service_name%`. + You can configure external ports range with `PROXY_PORTS` env, for ex.: `5000-6000`. + It's strongly recommended to use vergilius in `net=host` mode or disable `userland-proxy`, + because docker will create as much userland proxies as `PROXY_PORTS` you have. + #### how http2 works To use `http2` proxy, use `http2` tag instead of `http` or use both. Vergilius will try to acquire certificate from diff --git a/circle.yml b/circle.yml index 2c63ac9..cca56e2 100644 --- a/circle.yml +++ b/circle.yml @@ -20,9 +20,14 @@ test: pre: - docker run -d -p 8500:8500 e96tech/consul-server -advertise 127.0.0.1 -bootstrap -dc circle -domain local - curl --retry 10 --retry-delay 5 -v http://localhost:8500 + - sed -i '1i load_module "modules/ngx_stream_module.so";' /home/ubuntu/vergilius/src/vergilius/templates/service_validate.html deployment: - dockerhub: + dockerhub_master: branch: master commands: - - 'curl -H "Content-Type: application/json" --data "{\"source_type\": \"Branch\", \"source_name\": \"master\"}" -X POST https://registry.hub.docker.com/u/devopsftw/vergilius/trigger/ea3f932c-49b9-47e8-af0c-ec1d8615cda4/' \ No newline at end of file + - 'curl -H "Content-Type: application/json" --data "{\"source_type\": \"Branch\", \"source_name\": \"master\"}" -X POST https://registry.hub.docker.com/u/devopsftw/vergilius/trigger/ea3f932c-49b9-47e8-af0c-ec1d8615cda4/' + dockerhub_tag: + tag: /.*/ + commands: + - 'curl -H "Content-Type: application/json" --data "{\"source_type\": \"Tag\", \"source_name\": \"$CIRCLE_TAG\"}" -X POST https://registry.hub.docker.com/u/devopsftw/vergilius/trigger/ea3f932c-49b9-47e8-af0c-ec1d8615cda4/' \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 8c8d6db..f681d50 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -41,6 +41,16 @@ http { ssl_dhparam /etc/nginx/dhparam/dhparam.pem; - include /etc/nginx/conf.d/*.conf; + include /etc/nginx/conf.d/*.upstream.conf; + include /etc/nginx/conf.d/*.http.conf; + include /etc/nginx/conf.d/*.http2.conf; + include /etc/nginx/conf.d/default.conf; include /etc/nginx/sites-enabled/*.conf; } + +stream { + include /etc/nginx/conf.d/*.upstream.conf; + include /etc/nginx/conf.d/*.tcp.conf; + include /etc/nginx/conf.d/*.udp.conf; +} + diff --git a/services/nginx.sh b/services/nginx.sh index b96ddf4..4893aa4 100755 --- a/services/nginx.sh +++ b/services/nginx.sh @@ -7,8 +7,8 @@ function die { if [ ! -f /etc/nginx/dhparam/dhparam.pem ]; then mkdir -p /etc/nginx/dhparam/ - echo "dhparam file /etc/nginx/dhparam/dhparam.pem does not exist. Generating one with 4086 bit. This will take a while..." - openssl dhparam -out /etc/nginx/dhparam/dhparam.pem 4096 || die "Could not generate dhparam file" + echo "dhparam file /etc/nginx/dhparam/dhparam.pem does not exist. Generating one with $DHPARAM_LENGTH bit. This will take a while..." + openssl dhparam -out /etc/nginx/dhparam/dhparam.pem $DHPARAM_LENGTH || die "Could not generate dhparam file" echo "Finished. Starting nginx now..." fi diff --git a/src/vergilius/components/port_allocator.py b/src/vergilius/components/port_allocator.py new file mode 100644 index 0000000..74e21d6 --- /dev/null +++ b/src/vergilius/components/port_allocator.py @@ -0,0 +1,20 @@ +from vergilius.config import PROXY_PORTS + +allocated = set() + + +def allocate(): + min_port = PROXY_PORTS[0] + max_port = PROXY_PORTS[1] + + while min_port < max_port: + if min_port not in allocated: + allocated.add(min_port) + return min_port + min_port += 1 + + raise Exception('Failed to allocate port') + + +def release(port): + allocated.discard(int(port)) diff --git a/src/vergilius/config.py b/src/vergilius/config.py index 0dda231..ea4975a 100644 --- a/src/vergilius/config.py +++ b/src/vergilius/config.py @@ -8,6 +8,7 @@ NGINX_BINARY = os.environ.get('NGINX_BINARY', '/usr/sbin/nginx') NGINX_HTTP_PORT = os.environ.get('NGINX_HTTP_PORT', 80) NGINX_HTTP2_PORT = os.environ.get('NGINX_HTTP2_PORT', 443) +PROXY_PORTS = [int(s) for s in os.environ.get('PROXY_PORTS', '7000-8000').split('-')] ACME_DIRECTORY_URL = os.environ.get('ACME_DIRECTORY_URL', 'https://acme-staging.api.letsencrypt.org/directory') diff --git a/src/vergilius/loop/service_watcher.py b/src/vergilius/loop/service_watcher.py index 2c19ecf..cb7ff42 100644 --- a/src/vergilius/loop/service_watcher.py +++ b/src/vergilius/loop/service_watcher.py @@ -23,7 +23,8 @@ def watch_services(self): def check_services(self, data): # check if service has any of our tags - services_to_publish = dict((k, v) for k, v in data.items() if any(x in v for x in [u'http', u'http2'])) + services_to_publish = dict( + (k, v) for k, v in data.items() if any(x in v for x in [u'http', u'http2', u'tcp', u'udp'])) for service_name in services_to_publish: if service_name not in self.services: vergilius.logger.info('[service watcher]: new service: %s' % service_name) diff --git a/src/vergilius/models/service.py b/src/vergilius/models/service.py index 2e8afcf..177f9f0 100644 --- a/src/vergilius/models/service.py +++ b/src/vergilius/models/service.py @@ -4,8 +4,12 @@ import tempfile import unicodedata +import itertools from consul import tornado, base, ConsulException +from shutil import rmtree + from vergilius import config, consul_tornado, consul, logger, template_loader +from vergilius.components import port_allocator from vergilius.loop.nginx_reloader import NginxReloader from vergilius.models.certificate import Certificate @@ -20,9 +24,12 @@ def __init__(self, name): logger.info('[service][%s]: new and loading' % self.name) self.allow_crossdomain = False self.nodes = {} - self.domains = { + self.port = None + self.binds = { u'http': set(), - u'http2': set() + u'http2': set(), + u'tcp': set(), + u'udp': set() } self.active = True @@ -35,7 +42,7 @@ def __init__(self, name): self.watch() def fetch(self): - index, data = consul.health.service(self.name, passing=True) + index, data = consul.health.service(self.id, passing=True) self.parse_data(data) @tornado.gen.coroutine @@ -43,7 +50,7 @@ def watch(self): index = None while True and self.active: try: - index, data = yield consul_tornado.health.service(self.name, index, wait=None, passing=True) + index, data = yield consul_tornado.health.service(self.id, index, wait=None, passing=True) self.parse_data(data) except ConsulException as e: logger.error('consul exception: %s' % e) @@ -55,18 +62,18 @@ def parse_data(self, data): :type data: set[] """ - for protocol in self.domains.iterkeys(): - self.domains[protocol].clear() + for protocol in self.binds.iterkeys(): + self.binds[protocol].clear() allow_crossdomain = False self.nodes = {} for node in data: if not node[u'Service'][u'Port']: - logger.warn('[service][%s]: Node %s is ignored due no ServicePort' % (self.id, node[u'Node'])) + logger.warn('[service][%s]: Node %s is ignored due no Service Port' % (self.id, node[u'Node'][u'Node'])) continue if node[u'Service'][u'Tags'] is None: - logger.warn('[service][%s]: Node %s is ignored due no ServiceTags' % (self.id, node[u'Node'])) + logger.warn('[service][%s]: Node %s is ignored due no Service Tags' % (self.id, node[u'Node'][u'Node'])) continue self.nodes[node['Node']['Node']] = { @@ -80,66 +87,89 @@ def parse_data(self, data): for protocol in [u'http', u'http2']: if protocol in node[u'Service'][u'Tags']: - self.domains[protocol].update( + self.binds[protocol].update( tag.replace(protocol + ':', '') for tag in node[u'Service'][u'Tags'] if tag.startswith(protocol + ':') ) + for protocol in ['tcp', 'udp']: + self.binds[protocol].update({node[u'Service'][u'Port']}) + self.allow_crossdomain = allow_crossdomain self.flush_nginx_config() - def get_nginx_config(self): + def get_nginx_config(self, config_type): """ Generate nginx config from service attributes + :param config_type: string """ - if self.domains[u'http2']: + if config_type == 'http2' and len(self.binds['http2']): self.check_certificate() - return template_loader.load('service.html').generate(service=self, config=config) + + if config_type in ['tcp', 'udp']: + self.check_port() + + return template_loader.load('service_%s.html' % config_type).generate(service=self, config=config) def flush_nginx_config(self): if not self.validate(): logger.error('[service][%s]: failed to validate nginx config!' % self.id) return False - nginx_config = self.get_nginx_config() - deployed_nginx_config = None + has_changes = False - try: - deployed_nginx_config = self.read_nginx_config_file() - except IOError: - pass + for config_type in self.get_config_types(): + nginx_config = self.get_nginx_config(config_type) + deployed_nginx_config = None - if deployed_nginx_config != nginx_config: - config_file = open(self.get_nginx_config_path(), 'w+') - config_file.write(nginx_config) - config_file.close() - logger.info('[service][%s]: got new nginx config %s' % (self.name, self.get_nginx_config_path())) + try: + deployed_nginx_config = self.read_nginx_config_file(config_type) + except IOError: + pass + + if deployed_nginx_config != nginx_config: + config_file = open(self.get_nginx_config_path(config_type), 'w+') + config_file.write(nginx_config) + config_file.close() + has_changes = True + + if has_changes: NginxReloader.queue_reload() + logger.info('[service][%s]: got new nginx config' % self.name) - def get_nginx_config_path(self): - return os.path.join(config.NGINX_CONFIG_PATH, self.id + '.conf') + def get_nginx_config_path(self, config_type): + return os.path.join(config.NGINX_CONFIG_PATH, '%s.%s.conf' % (self.id, config_type)) - def read_nginx_config_file(self): - with open(self.get_nginx_config_path(), 'r') as config_file: + def read_nginx_config_file(self, config_type): + with open(self.get_nginx_config_path(config_type), 'r') as config_file: config_content = config_file.read() config_file.close() return config_content + def get_config_types(self): + return itertools.chain(self.binds.keys(), ['upstream']) + def validate(self): """ Deploy temporary service & nginx config and validate it with nginx :return: bool """ - service_config_file = tempfile.NamedTemporaryFile(delete=False) - service_config_file.write(self.get_nginx_config()) - service_config_file.close() - - nginx_config_file = tempfile.NamedTemporaryFile(delete=False) - nginx_config_file.write(template_loader.load('service_validate.html') - .generate(service_config=service_config_file.name, - pid_file='%s.pid' % service_config_file.name) - ) + + temp_dir = tempfile.mkdtemp() + + files = {} + for config_type in self.get_config_types(): + path = os.path.join(temp_dir, config_type) + config_file = open(path, 'w+') + config_file.write(self.get_nginx_config(config_type)) + config_file.close() + files['service_%s' % config_type] = path + + files['pid_file'] = os.path.join(temp_dir, 'pid') + + nginx_config_file = open(os.path.join(temp_dir, 'service'), 'w+') + nginx_config_file.write(template_loader.load('service_validate.html').generate(**files)) nginx_config_file.close() try: @@ -147,9 +177,7 @@ def validate(self): except subprocess.CalledProcessError: return_code = 1 finally: - os.unlink(service_config_file.name) - os.unlink('%s.pid' % service_config_file.name) - os.unlink(nginx_config_file.name) + rmtree(temp_dir, ignore_errors=True) return return_code == 0 @@ -161,10 +189,14 @@ def delete(self): logger.info('[service][%s]: deleting' % self.name) self.active = False - try: - os.remove(self.get_nginx_config_path()) - except OSError: - pass + if self.port: + self.release_port() + + for config_type in self.get_config_types(): + try: + os.remove(self.get_nginx_config_path(config_type)) + except OSError: + pass def __del__(self): if self.active: @@ -182,4 +214,14 @@ def slugify(cls, string): def check_certificate(self): if not self.certificate: - self.certificate = Certificate(service=self, domains=self.domains[u'http2']) + self.certificate = Certificate(service=self, domains=self.binds['http2']) + + def check_port(self): + if not self.port: + self.port = port_allocator.allocate() + consul.kv.put('vergilius/ports/%s' % self.name, str(self.port)) + + def release_port(self): + if self.port: + port_allocator.release(self.port) + consul.kv.delete('vergilius/ports/%s' % self.name) diff --git a/src/vergilius/templates/service.html b/src/vergilius/templates/service.html deleted file mode 100644 index 63adc26..0000000 --- a/src/vergilius/templates/service.html +++ /dev/null @@ -1,72 +0,0 @@ -{% whitespace all%} -upstream {{service.id}} { - least_conn; - {% for node_name in service.nodes %}server {{ service.nodes[node_name]['address'] }}:{{ service.nodes[node_name]['port'] }}; -{% end %} - {% if not service.nodes %}server 127.0.0.1:6666;{% end %} -} - -{% if len(service.domains['http2']) and service.certificate.private_key and service.certificate.public_key %} -server { - server_name{% for domain in service.domains['http2'] %} {{ domain }} *.{{ domain }}{% end %}; - listen {{config.NGINX_HTTP2_PORT}} ssl http2; - - ssl_certificate {{service.certificate.get_cert_path()}}; - ssl_certificate_key {{service.certificate.get_key_path()}}; - - location / { - proxy_pass http://{{service.id}}; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - {% if service.allow_crossdomain %} - proxy_hide_header 'Access-Control-Allow-Origin'; - add_header 'Access-Control-Allow-Origin' "$http_origin"; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cookie,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With'; - - if ($request_method = 'OPTIONS' ) { - # if request method is options we immediately return with 200 OK. - return 200; - } - {% end %} - } -} -{% end %} -{% if len(service.domains['http']) %} -server { - server_name{% for domain in service.domains['http'] %} {{ domain }} *.{{ domain }}{% end %}; - listen {{config.NGINX_HTTP_PORT}}; - - {% if len(service.domains['http']) %} - location / { - proxy_pass http://{{service.id}}; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - {% if service.allow_crossdomain %} - proxy_hide_header 'Access-Control-Allow-Origin'; - add_header 'Access-Control-Allow-Origin' "$http_origin"; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cookie,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With'; - - if ($request_method = 'OPTIONS' ) { - # if request method is options we immediately return with 200 OK. - return 200; - } - {% end %} - } - {% else %} - location / { - return 301 https://$server_name$request_uri; - } - {% end %} -} -{% end %} \ No newline at end of file diff --git a/src/vergilius/templates/service_http.html b/src/vergilius/templates/service_http.html new file mode 100644 index 0000000..60cde2d --- /dev/null +++ b/src/vergilius/templates/service_http.html @@ -0,0 +1,35 @@ +{% whitespace all%} +{% if len(service.binds['http']) %} +server { + server_name{% for domain in service.binds['http'] %} {{ domain }} *.{{ domain }}{% end %}; + listen {{config.NGINX_HTTP_PORT}}; + + {% if len(service.binds['http']) %} + location / { + proxy_pass http://{{service.id}}; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + {% if service.allow_crossdomain %} + proxy_hide_header 'Access-Control-Allow-Origin'; + add_header 'Access-Control-Allow-Origin' "$http_origin"; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Credentials' 'true'; + add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cookie,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With'; + + if ($request_method = 'OPTIONS' ) { + # if request method is options we immediately return with 200 OK. + return 200; + } + {% end %} + } + {% else %} + location / { + return 301 https://$server_name$request_uri; + } + {% end %} +} +{% end %} \ No newline at end of file diff --git a/src/vergilius/templates/service_http2.html b/src/vergilius/templates/service_http2.html new file mode 100644 index 0000000..ed37205 --- /dev/null +++ b/src/vergilius/templates/service_http2.html @@ -0,0 +1,31 @@ +{% whitespace all%} +{% if len(service.binds['http2']) and service.certificate.private_key and service.certificate.public_key %} +server { + server_name{% for domain in service.binds['http2'] %} {{ domain }} *.{{ domain }}{% end %}; + listen {{config.NGINX_HTTP2_PORT}} ssl http2; + + ssl_certificate {{service.certificate.get_cert_path()}}; + ssl_certificate_key {{service.certificate.get_key_path()}}; + + location / { + proxy_pass http://{{service.id}}; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + {% if service.allow_crossdomain %} + proxy_hide_header 'Access-Control-Allow-Origin'; + add_header 'Access-Control-Allow-Origin' "$http_origin"; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Credentials' 'true'; + add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cookie,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With'; + + if ($request_method = 'OPTIONS' ) { + # if request method is options we immediately return with 200 OK. + return 200; + } + {% end %} + } +} +{% end %} \ No newline at end of file diff --git a/src/vergilius/templates/service_tcp.html b/src/vergilius/templates/service_tcp.html new file mode 100644 index 0000000..8a889cf --- /dev/null +++ b/src/vergilius/templates/service_tcp.html @@ -0,0 +1,7 @@ +{% whitespace all%} +{% if len(service.binds['tcp']) %} +server { + listen {{service.port}}; + proxy_pass {{service.id}}; +} +{% end %} \ No newline at end of file diff --git a/src/vergilius/templates/service_udp.html b/src/vergilius/templates/service_udp.html new file mode 100644 index 0000000..fa52607 --- /dev/null +++ b/src/vergilius/templates/service_udp.html @@ -0,0 +1,7 @@ +{% whitespace all%} +{% if len(service.binds['udp']) %} +server { + listen {{service.port}} udp; + proxy_pass {{service.id}}; +} +{% end %} \ No newline at end of file diff --git a/src/vergilius/templates/service_upstream.html b/src/vergilius/templates/service_upstream.html new file mode 100644 index 0000000..d6812bf --- /dev/null +++ b/src/vergilius/templates/service_upstream.html @@ -0,0 +1,8 @@ +{% whitespace all%} +upstream {{service.id}} { + hash $remote_addr consistent; + + {% for node_name in service.nodes %}server {{ service.nodes[node_name]['address'] }}:{{ service.nodes[node_name]['port'] }}; + {% end %} + {% if not service.nodes %}server 127.0.0.1:6666;{% end %} +} diff --git a/src/vergilius/templates/service_validate.html b/src/vergilius/templates/service_validate.html index 06b2d9c..ad4cf71 100644 --- a/src/vergilius/templates/service_validate.html +++ b/src/vergilius/templates/service_validate.html @@ -8,6 +8,13 @@ http { - include {{service_config}}; + include {{service_upstream}}; + include {{service_http}}; + include {{service_http2}}; } +stream { + include {{service_upstream}}; + include {{service_tcp}}; + include {{service_udp}}; +} diff --git a/tests/base_test.py b/tests/base_test.py index 9ecca6e..417eae8 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -9,13 +9,21 @@ import vergilius from vergilius import consul, logger +from vergilius.components import port_allocator from vergilius.loop.service_watcher import ServiceWatcher -from vergilius.models.identity import Identity + +# for requests headers: +# import httplib as http_client +# http_client.HTTPConnection.debuglevel = 1 out_hdlr = logging.StreamHandler(sys.stdout) out_hdlr.setFormatter(logging.Formatter('%(asctime)s %(message)s')) out_hdlr.setLevel(logging.DEBUG) +requests_log = logging.getLogger("requests.packages.urllib3") +requests_log.setLevel(logging.DEBUG) +requests_log.propagate = True + logger.addHandler(out_hdlr) logger.setLevel(logging.DEBUG) @@ -44,7 +52,12 @@ def setUp(self): try: os.mkdir(vergilius.config.DATA_PATH) + except OSError as e: + print(e) + + try: os.mkdir(vergilius.config.NGINX_CONFIG_PATH) + os.mkdir(os.path.join(vergilius.config.NGINX_CONFIG_PATH, 'certs')) except OSError as e: print(e) @@ -53,6 +66,10 @@ def setUp(self): def tearDown(self): super(BaseTest, self).tearDown() consul.kv.delete('vergilius', True) + port_allocator.allocated = set() - shutil.rmtree(vergilius.config.NGINX_CONFIG_PATH) - shutil.rmtree(vergilius.config.DATA_PATH) + try: + shutil.rmtree(vergilius.config.NGINX_CONFIG_PATH) + shutil.rmtree(vergilius.config.DATA_PATH) + except OSError as e: + print(e) diff --git a/tests/test_certificate.py b/tests/test_certificate.py index 710fe4c..3e8d4b0 100644 --- a/tests/test_certificate.py +++ b/tests/test_certificate.py @@ -1,7 +1,7 @@ from mock import mock from base_test import BaseTest -from vergilius import consul +from vergilius import consul, DummyCertificateProvider from vergilius.models.certificate import Certificate from vergilius.models.service import Service @@ -10,6 +10,7 @@ class Test(BaseTest): def __init__(self, methodName='runTest'): super(Test, self).__init__(methodName) self.service = Service('test') + self.service.binds['http2'] = {'example.com'} def setUp(self): super(Test, self).setUp() @@ -19,6 +20,6 @@ def test_keys_request(self): cert = Certificate(service=self.service, domains={'example.com'}) self.assertTrue(cert.validate(), 'got valid keys') - with mock.patch.object(Certificate, 'request_certificate', return_value={}) as mock_method: - Certificate(service=self.service, domains={'example.com'}) - self.assertFalse(mock_method.called, 'check if existing keys are not requested from provider') + with mock.patch.object(DummyCertificateProvider, 'get_certificate', return_value={}) as mock_method: + Certificate(service=self.service, domains={'example.com'}) + self.assertFalse(mock_method.called, 'existing keys are not requested from provider') diff --git a/tests/test_service.py b/tests/test_service.py index 19ccff1..b06b590 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -1,14 +1,12 @@ +from mock import mock from base_test import BaseTest from vergilius import consul +from vergilius.components import port_allocator from vergilius.models.service import Service class Test(BaseTest): - def setUp(self): - super(Test, self).setUp() - consul.kv.delete('vergilius', True) - def test_watcher(self): pass @@ -16,9 +14,10 @@ def test_base(self): service = Service(name='test service') service.flush_nginx_config() - config_file = service.get_nginx_config_path() - self.assertNotEqual(service.read_nginx_config_file().find('server 127.0.0.1:6666'), -1, - 'config written and has backup 503') + config_file = service.get_nginx_config_path('upstream') + self.assertNotEqual( + service.read_nginx_config_file('upstream').find('server 127.0.0.1:6666'), + -1, 'config written and has backup 503') self.assertTrue(service.validate(), 'nginx config is valid') service.delete() @@ -27,24 +26,62 @@ def test_base(self): def test_http(self): service = Service(name='test service') + service.binds['http'] = {'example.com'} - service.domains[u'http'] = ('example.com',) - - self.assertNotEqual(service.get_nginx_config().find('server_name example.com *.example.com;'), -1, - 'server_name and wildcard present') + self.assertNotEqual( + service.get_nginx_config('http').find('server_name example.com *.example.com;'), -1, + 'server_name and wildcard present') self.assertTrue(service.validate(), 'nginx config is valid') def test_http2(self): service = Service(name='test service') - service.domains[u'http2'] = ('example.com',) + service.binds['http2'] = {'example.com'} + self.assertTrue(service.validate(), 'nginx config is valid') - def test_upstream_nodes(self): + @mock.patch.object(Service, 'watch') + def test_upstream_nodes(self, _): service = Service(name='test service') - service.domains[u'http'] = ('example.com',) + + service.binds['http'] = {'example.com'} service.nodes['test_node'] = {'address': '127.0.0.1', 'port': '10000'} + self.assertTrue(service.validate(), 'nginx config is valid') + config = service.get_nginx_config('upstream') - config = service.get_nginx_config() self.assertNotEqual(config.find('server 127.0.0.1:10000;'), -1, 'upstream node present') self.assertEqual(config.find('server 127.0.0.1:6666'), -1, 'backup node deleted') + + @mock.patch.object(Service, 'watch') + def test_tcp(self, _): + service = Service(name='test service') + service.binds['tcp'] = {'10000'} + service.nodes['test_node'] = {'address': '127.0.0.1', 'port': '10000'} + + self.assertTrue(service.validate(), 'nginx config is valid') + config = service.get_nginx_config('tcp') + self.assertNotEqual(config.find('listen %s;' % service.port), -1, 'tcp listen valid') + + @mock.patch.object(Service, 'watch') + def test_udp(self, _): + service = Service(name='test service') + service.binds['udp'] = {'10000'} + service.nodes['test_node'] = {'address': '127.0.0.1', 'port': '10000'} + + self.assertTrue(service.validate(), 'nginx config is valid') + config = service.get_nginx_config('udp') + self.assertNotEqual(config.find('listen %s udp;' % service.port), -1, 'udp listen valid') + + def test_port_allocate(self): + service = Service(name='test service') + + service.check_port() + self.assertEqual(service.port, 7000) + consul_port_data = consul.kv.get('vergilius/ports/test service') + self.assertIsNotNone(consul_port_data) + self.assertEqual('7000', consul_port_data[1][u'Value']) + + service.delete() + self.assertFalse(7000 in port_allocator.allocated) + consul_port_data = consul.kv.get('vergilius/ports/test service') + self.assertIsNotNone(consul_port_data)