From bde6de0a7a6da44bf2cb87455ce82ddc20ce450b Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 7 Mar 2017 11:56:22 -0600 Subject: [PATCH 01/26] Initial support for chronos 3.x for #33 This is the initial draft I wrote for support of chronos >= 3.x It tries to play nice with versions < 3.x as well and it doesn't keep you from shooting yourself in the foot. I am undecided at which point you draw the line in the sand and only allow valid values sent. Tested vs chronos 2.4 and 3.0.x --- chronos/__init__.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index e89f394..df78a14 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -26,6 +26,8 @@ import socket import json import logging +import re +import warnings # Python 3 changed the submodule for quote try: @@ -49,12 +51,13 @@ class MissingFieldError(Exception): class OneOfViolationError(Exception): pass +SCHEDULER_VERSIONS = ('v1',) class ChronosClient(object): _user = None _password = None - def __init__(self, servers, proto="http", username=None, password=None, extra_headers=None, level='WARN'): + def __init__(self, servers, proto="http", username=None, password=None, extra_headers=None, level='WARN', scheduler_version=None): server_list = servers if isinstance(servers, list) else [servers] self.servers = ["%s://%s" % (proto, server) for server in server_list] self.extra_headers = extra_headers @@ -63,6 +66,13 @@ def __init__(self, servers, proto="http", username=None, password=None, extra_he self._password = password logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=level) self.logger = logging.getLogger(__name__) + if scheduler_version is None: + warnings.warn("Chronos >=3.x requires scheduler_version set to 'v1', in the future the constructor will default to 'v1' as well", FutureWarning) + self._prefix = "" + else: + if scheduler_version not in SCHEDULER_VERSIONS: + raise ValueError('Wrong scheduler_version provided') + self._prefix = "/%s" % (scheduler_version,) def list(self): """List all jobs on Chronos.""" @@ -125,14 +135,19 @@ def scheduler_stat_mean(self): return self._call('/scheduler/stats/mean', 'GET') def metrics(self): - return self._call('/metrics', 'GET') + # for some reason, /metrics is not prefixed with the version + return self._call('/metrics', 'GET', prefix=False) - def _call(self, url, method="GET", body=None, headers={}): + def _call(self, url, method="GET", body=None, headers={}, prefix=True): hdrs = {} if body: hdrs['Content-Type'] = "application/json" hdrs.update(headers) - self.logger.debug("Fetch: %s %s" % (method, url)) + if prefix: + _url = '%s%s' % (self._prefix, url, ) + else: + _url = url + self.logger.debug("Fetch: %s %s" % (method, _url)) if body: self.logger.debug("Body: %s" % body) conn = httplib2.Http(disable_ssl_certificate_validation=True) @@ -145,7 +160,7 @@ def _call(self, url, method="GET", body=None, headers={}): servers = list(self.servers) while servers: server = servers.pop(0) - endpoint = "%s%s" % (server, quote(url)) + endpoint = "%s%s" % (server, quote(_url)) try: resp, content = conn.request(endpoint, method, body=body, headers=hdrs) except (socket.error, httplib2.ServerNotFoundError) as e: @@ -185,6 +200,11 @@ def _check_fields(self, job): if k not in job: raise MissingFieldError("missing required field %s" % k) + # Chronos v3.x rejects job names with spaces in them + regex = re.compile(r'^[\w.-]+$') + # We're not sure if we're running chronos < 3.x at this point, so just throw a warning + if not regex.match(job["name"]): + warnings.warn('Chronos >= 3.x rejects job names with spaces in them, "%(name)s" might be rejected' % job) if any(field in job for field in ChronosJob.one_of): if len([field for field in ChronosJob.one_of if field in job]) > 1: raise OneOfViolationError("Job must only include 1 of %s" % ChronosJob.one_of) @@ -215,5 +235,5 @@ class ChronosJob(object): ] -def connect(servers, proto="http", username=None, password=None, extra_headers=None): - return ChronosClient(servers, proto=proto, username=username, password=password, extra_headers=extra_headers) +def connect(servers, proto="http", username=None, password=None, extra_headers=None, scheduler_version=None): + return ChronosClient(servers, proto=proto, username=username, password=password, extra_headers=extra_headers, scheduler_version=scheduler_version) From a68659ea537be06bdbdc4aeb3f4cc83b61cb7363 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:32:53 -0600 Subject: [PATCH 02/26] Make warnings throw an exception instead --- chronos/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index df78a14..8c156aa 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -27,7 +27,6 @@ import json import logging import re -import warnings # Python 3 changed the submodule for quote try: @@ -67,11 +66,10 @@ def __init__(self, servers, proto="http", username=None, password=None, extra_he logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=level) self.logger = logging.getLogger(__name__) if scheduler_version is None: - warnings.warn("Chronos >=3.x requires scheduler_version set to 'v1', in the future the constructor will default to 'v1' as well", FutureWarning) - self._prefix = "" + raise ChronosAPIError("Chronos >=3.x requires scheduler_version set") else: if scheduler_version not in SCHEDULER_VERSIONS: - raise ValueError('Wrong scheduler_version provided') + raise ChronosAPIError('Wrong scheduler_version provided') self._prefix = "/%s" % (scheduler_version,) def list(self): @@ -202,9 +200,11 @@ def _check_fields(self, job): # Chronos v3.x rejects job names with spaces in them regex = re.compile(r'^[\w.-]+$') - # We're not sure if we're running chronos < 3.x at this point, so just throw a warning - if not regex.match(job["name"]): - warnings.warn('Chronos >= 3.x rejects job names with spaces in them, "%(name)s" might be rejected' % job) + if "name" in job and not regex.match(job["name"]): + raise ChronosAPIError( + 'Chronos >= 3.x only allows job names that match "[\w.-]+", ' + '"%(name)s" is invalid' % job + ) if any(field in job for field in ChronosJob.one_of): if len([field for field in ChronosJob.one_of if field in job]) > 1: raise OneOfViolationError("Job must only include 1 of %s" % ChronosJob.one_of) From ccc48a9ec082ad426fb354913a9dee497cca49bb Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:33:41 -0600 Subject: [PATCH 03/26] Line breaking < 120c and default to v1 --- chronos/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index 8c156aa..2b0c091 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -34,6 +34,8 @@ except ImportError: from urllib.parse import quote +SCHEDULER_VERSIONS = ('v1',) + class ChronosAPIError(Exception): pass @@ -50,13 +52,16 @@ class MissingFieldError(Exception): class OneOfViolationError(Exception): pass -SCHEDULER_VERSIONS = ('v1',) class ChronosClient(object): _user = None _password = None - def __init__(self, servers, proto="http", username=None, password=None, extra_headers=None, level='WARN', scheduler_version=None): + def __init__( + self, servers, proto="http", username=None, + password=None, extra_headers=None, level='WARN', + scheduler_version='v1' + ): server_list = servers if isinstance(servers, list) else [servers] self.servers = ["%s://%s" % (proto, server) for server in server_list] self.extra_headers = extra_headers @@ -235,5 +240,8 @@ class ChronosJob(object): ] -def connect(servers, proto="http", username=None, password=None, extra_headers=None, scheduler_version=None): - return ChronosClient(servers, proto=proto, username=username, password=password, extra_headers=extra_headers, scheduler_version=scheduler_version) +def connect(servers, proto="http", username=None, password=None, extra_headers=None, scheduler_version='v1'): + return ChronosClient( + servers, proto=proto, username=username, password=password, + extra_headers=extra_headers, scheduler_version=scheduler_version + ) From 538d9bb635274ff2842fe51701b2b3dbd4728a70 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:34:28 -0600 Subject: [PATCH 04/26] Chronos API removed PUT method here Which is pretty fantastic --- chronos/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index 2b0c091..36289b2 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -96,21 +96,21 @@ def run(self, name): path = "/scheduler/job/%s" % name return self._call(path, "PUT") - def add(self, job_def, update=False): - """Schedule a new job""" + def set(self, job_def): + """Add or update a job""" path = "/scheduler/iso8601" self._check_fields(job_def) if "parents" in job_def: path = "/scheduler/dependency" - if update: - method = "PUT" - else: - method = "POST" - return self._call(path, method, json.dumps(job_def)) + return self._call(path, "POST", json.dumps(job_def)) + + def add(self, job_def): + """Add a new job""" + return self.set(job_def) def update(self, job_def): """Update an existing job by name""" - return self.add(job_def, update=True) + return self.set(job_def) def job_stat(self, name): """ List stats for a job """ From 5adc62d69bf7268d70d7f30168ebbcec7284567e Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:34:49 -0600 Subject: [PATCH 05/26] async is no longer a valid attribute --- chronos/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index 36289b2..2a4933f 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -227,7 +227,6 @@ def _check_fields(self, job): class ChronosJob(object): fields = [ - "async", "command", "name", "owner", From 1a3bc93432bf941c1ce1c636bf17bdd5d00865be Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:35:11 -0600 Subject: [PATCH 06/26] Should account for whatever prefix client has --- tests/test_chronos_init.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_chronos_init.py b/tests/test_chronos_init.py index db3c6b1..a542c01 100644 --- a/tests/test_chronos_init.py +++ b/tests/test_chronos_init.py @@ -94,7 +94,6 @@ def test_check_missing_container_fields(): container_without_field = {x: 'foo' for x in filter(lambda y: y != field, chronos.ChronosJob.container_fields)} job_def = { "container": container_without_field, - "async": False, "command": "while sleep 10; do date =u %T; done", "schedule": "R/2014-09-25T17:22:00Z/PT2M", "name": "dockerjob", @@ -162,6 +161,6 @@ def test_call_retries_on_http_error(mock_http): ) client = chronos.ChronosClient(servers=['1.2.3.4', '1.2.3.5', '1.2.3.6']) client._call("/foo") - mock_call.assert_any_call('http://1.2.3.4/foo', 'GET', body=None, headers={}) - mock_call.assert_any_call('http://1.2.3.5/foo', 'GET', body=None, headers={}) - mock_call.assert_any_call('http://1.2.3.6/foo', 'GET', body=None, headers={}) + mock_call.assert_any_call('http://1.2.3.4%s/foo' % client._prefix, 'GET', body=None, headers={}) + mock_call.assert_any_call('http://1.2.3.5%s/foo' % client._prefix, 'GET', body=None, headers={}) + mock_call.assert_any_call('http://1.2.3.6%s/foo' % client._prefix, 'GET', body=None, headers={}) From 25f2c8d119850092105acef6964b26cc5842ee99 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:36:55 -0600 Subject: [PATCH 07/26] Simplify the test environment by a lot Now it is a matter of passing the appropriate docker node defined in the docker-compose.yml file. All chronos nodes will expose chronos on port 4400 so all client calls will be done to localhost port 4400. No need to figure out magically which port docker-compose is exposing or anything (at least in theory) --- itests/Dockerfile | 11 ----- itests/chronos-python.feature | 5 +-- itests/chronos-version | 1 - itests/docker-compose.yml | 82 ++++++++++++++++++++++++++++++++--- itests/environment.py | 3 +- itests/install-chronos.sh | 18 -------- itests/itest_utils.py | 39 +++-------------- itests/start-chronos.sh | 4 -- itests/steps/chronos_steps.py | 31 +++++++++---- 9 files changed, 108 insertions(+), 86 deletions(-) delete mode 100644 itests/Dockerfile delete mode 100644 itests/chronos-version delete mode 100755 itests/install-chronos.sh delete mode 100755 itests/start-chronos.sh diff --git a/itests/Dockerfile b/itests/Dockerfile deleted file mode 100644 index 3a90cb4..0000000 --- a/itests/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM ubuntu-debootstrap:14.04 -RUN apt-get update && apt-get -y install sudo lsb-release - -# Setup -ADD ./chronos-version /root/chronos-version -ADD ./install-chronos.sh /root/install-chronos.sh -RUN /root/install-chronos.sh - -EXPOSE 4400 -ADD ./start-chronos.sh /root/start-chronos.sh -CMD /etc/init.d/zookeeper start && /root/start-chronos.sh && sleep infinity diff --git a/itests/chronos-python.feature b/itests/chronos-python.feature index fccc631..50e4524 100644 --- a/itests/chronos-python.feature +++ b/itests/chronos-python.feature @@ -8,9 +8,8 @@ Feature: chronos-python can interact with chronos Scenario: Handling spaces in job names Given a working chronos instance When we create a trivial chronos job named "job with spaces" - Then we should be able to see the job named "job with spaces" when we list jobs - And we should be able to delete the job named "job with spaces" - And we should not be able to see the job named "job with spaces" when we list jobs + Then the job "job with spaces" failed to be created + And we should not see the job named "job with spaces" when we list jobs Scenario: Gathering job stats for job Given a working chronos instance diff --git a/itests/chronos-version b/itests/chronos-version deleted file mode 100644 index 3249e96..0000000 --- a/itests/chronos-version +++ /dev/null @@ -1 +0,0 @@ -CHRONOSVERSION= diff --git a/itests/docker-compose.yml b/itests/docker-compose.yml index c8a9d63..71cb228 100644 --- a/itests/docker-compose.yml +++ b/itests/docker-compose.yml @@ -1,5 +1,77 @@ ---- -chronos: - build: . - ports: - - 4400 +version: "2" + +services: + zookeeper: + image: zookeeper:3.4.9 + container_name: zookeeper + environment: + ZOO_MY_ID: 1 + ZK_INIT_LIMIT: 10 + ZK_SYNC_LIMIT: 5 + + master: + image: mesosphere/mesos-master:1.0.3 + container_name: mesos-master + environment: + MESOS_ZK: zk://zookeeper:2181/mesos + MESOS_QUORUM: 1 + MESOS_CLUSTER: chronos-python + MESOS_REGISTRY: in_memory + MESOS_HOSTNAME: localhost + ports: + - 5050:5050 + depends_on: + - zookeeper + links: + - zookeeper + + agent: + image: mesosphere/mesos-slave:1.0.3 + container_name: mesos-agent + pid: host + environment: + MESOS_MASTER: zk://zookeeper:2181/mesos + MESOS_WORK_DIR: /tmp/mesos + MESOS_PORT: 5051 + MESOS_RESOURCES: ports(*):[11000-11999] + MESOS_CONTAINERIZERS: mesos + MESOS_HOSTNAME: localhost + ports: + - 5051:5051 + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup + depends_on: + - zookeeper + links: + - zookeeper + - master + + chronos-2.4.0: + image: mesosphere/chronos:chronos-2.4.0-0.1.20150828104228.ubuntu1404-mesos-0.27.0-0.2.190.ubuntu1404 + container_name: chronos-2.4.0 + command: '/usr/bin/chronos run_jar --http_port 4400 --zk_hosts zookeeper:2181 --master zk://zookeeper:2181/mesos' + ports: + - 4400:4400 + depends_on: + - zookeeper + - master + links: + - zookeeper + - master + + chronos-3.0.2: + image: mesosphere/chronos:v3.0.2 + container_name: chronos-3.0.2 + command: '--zk_hosts zookeeper:2181 --master zk://zookeeper:2181/mesos' + environment: + PORT0: 4400 + PORT1: 8080 + ports: + - 4400:4400 + depends_on: + - zookeeper + - master + links: + - zookeeper + - master + diff --git a/itests/environment.py b/itests/environment.py index 8cea4bd..5169e3e 100644 --- a/itests/environment.py +++ b/itests/environment.py @@ -1,5 +1,4 @@ import time - from itest_utils import wait_for_chronos @@ -16,4 +15,4 @@ def after_scenario(context, scenario): break for job in jobs: context.client.delete(job['name']) - time.sleep(0.5) + time.sleep(1.0) diff --git a/itests/install-chronos.sh b/itests/install-chronos.sh deleted file mode 100755 index a70766e..0000000 --- a/itests/install-chronos.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -set -e - -# Default version of chronos to test against if not set by the user -[[ -f /root/chronos-version ]] && source /root/chronos-version -CHRONOSVERSION="${CHRONOSVERSION:-2.4.0}" - -sudo apt-key adv --keyserver keyserver.ubuntu.com --recv E56151BF -DISTRO=$(lsb_release -is | tr '[:upper:]' '[:lower:]') -CODENAME=$(lsb_release -cs) - -# Add the repository -echo "deb http://repos.mesosphere.com/${DISTRO} ${CODENAME} main" | - sudo tee /etc/apt/sources.list.d/mesosphere.list - -sudo apt-get -qq update - -sudo apt-get -y --force-yes install mesos chronos=$CHRONOSVERSION* diff --git a/itests/itest_utils.py b/itests/itest_utils.py index e1062fd..ac84a86 100644 --- a/itests/itest_utils.py +++ b/itests/itest_utils.py @@ -1,12 +1,11 @@ +#!/usr/bin/env python import errno import os import signal import time from functools import wraps -from urlparse import urlparse import requests -from compose.cli import command class TimeoutError(Exception): @@ -34,12 +33,13 @@ def wrapper(*args, **kwargs): @timeout(60) def wait_for_chronos(): - """Blocks until marathon is up""" - chronos_service = get_chronos_connection_string() + """Blocks until chronos is up""" + # we start chronos always on port 4400 + chronos_service = 'localhost:4400' while True: print 'Connecting to chronos on %s' % chronos_service try: - response = requests.get('http://%s/' % chronos_service, timeout=2) + response = requests.get('http://%s/ping' % chronos_service, timeout=2) except ( requests.exceptions.ConnectionError, requests.exceptions.Timeout, @@ -49,32 +49,3 @@ def wait_for_chronos(): if response.status_code == 200: print "Chronos is up and running!" break - - -def get_compose_service(service_name): - """Returns a compose object for the service""" - project = command.get_project('.') - return project.get_service(service_name) - - -def get_chronos_connection_string(): - # only reliable way I can detect travis.. - if '/travis/' in os.environ.get('PATH'): - return 'localhost:4400' - else: - service_port = get_service_internal_port('chronos') - connection_string = get_compose_service('chronos').get_container().get_local_port(service_port) - - # Check for non-local docker host - if 'DOCKER_HOST' in os.environ.keys() and os.environ.get('DOCKER_HOST').startswith("tcp"): - host = urlparse(os.environ.get('DOCKER_HOST')).hostname - port = connection_string.split(":")[1] - return "%s:%s" % (host, port) - else: - return connection_string - - -def get_service_internal_port(service_name): - """Gets the exposed port for service_name from docker-compose.yml. If there are - multiple ports. It returns the first one.""" - return get_compose_service(service_name).options['ports'][0] diff --git a/itests/start-chronos.sh b/itests/start-chronos.sh deleted file mode 100755 index a9b419d..0000000 --- a/itests/start-chronos.sh +++ /dev/null @@ -1,4 +0,0 @@ -sudo /etc/init.d/zookeeper start -sleep 5 -sudo /etc/init.d/chronos start -sleep 5 diff --git a/itests/steps/chronos_steps.py b/itests/steps/chronos_steps.py index bb6035d..436e909 100644 --- a/itests/steps/chronos_steps.py +++ b/itests/steps/chronos_steps.py @@ -2,11 +2,9 @@ import logging import sys from behave import given, when, then +import time import chronos -from itest_utils import get_chronos_connection_string - -sys.path.append('../') log = logging.getLogger('chronos') log.addHandler(logging.StreamHandler(sys.stdout)) @@ -18,22 +16,31 @@ def working_chronos(context): """Adds a working chronos client as context.client for the purposes of interacting with it in the test.""" if not hasattr(context, 'client'): - chronos_connection_string = get_chronos_connection_string() - context.client = chronos.connect(chronos_connection_string) + chronos_servers = ['127.0.0.1:4400'] + context.client = chronos.connect(chronos_servers) @when(u'we create a trivial chronos job named "{job_name}"') def create_trivial_chronos_job(context, job_name): job = { - 'async': False, 'command': 'echo 1', - 'epsilon': 'PT1M', 'name': job_name, 'owner': '', 'disabled': False, 'schedule': 'R0/2014-01-01T00:00:00Z/PT60M', } - context.client.add(job) + try: + context.client.add(job) + # give it a bit of time to reflect the job in ZK + time.sleep(0.5) + context.created = True + except: + context.created = False + + +@then(u'the job "{job_name}" failed to be created') +def job_creation_failed(context, job_name): + assert context.created == False @then(u'we should be able to see the job named "{job_name}" when we list jobs') @@ -43,9 +50,17 @@ def list_chronos_jobs_has_trivial_job(context, job_name): assert job_name in job_names +@then(u'we should not see the job named "{job_name}" when we list jobs') +def list_chronos_jobs_hasnt_trivial_job(context, job_name): + jobs = context.client.list() + job_names = [job['name'] for job in jobs] + assert job_name not in job_names + + @then(u'we should be able to delete the job named "{job_name}"') def delete_job_with_spaces(context, job_name): context.client.delete(job_name) + time.sleep(0.5) @then(u'we should not be able to see the job named "{job_name}" when we list jobs') From c8a337c122cc12e9837f3522b141d6cffa8f39d3 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:39:24 -0600 Subject: [PATCH 08/26] Put all the test requirements in a single file Instead of on various files --- test-requirements.txt | 7 +++++++ tox.ini | 24 ++++++------------------ 2 files changed, 13 insertions(+), 18 deletions(-) create mode 100644 test-requirements.txt diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..a74becc --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,7 @@ +-r requirements.txt +docker-compose==1.8.1 +pep8==1.5.7 +behave +flake8 +pytest +mock diff --git a/tox.ini b/tox.ini index 9503a2d..a9fd022 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,4 @@ [tox] -passenv = TRAVIS usedevelop=True basepython = python2.7 envlist = py @@ -12,32 +11,21 @@ max-line-length = 120 [testenv:py] basepython = python2.7 deps = - -rrequirements.txt - pep8==1.5.7 - flake8 - pytest - mock + -rtest-requirements.txt commands = flake8 chronos tests - py.test -s {posargs:tests} + py.test -v {posargs:tests} [testenv:itests] -passenv = TRAVIS DOCKER_TLS_VERIFY DOCKER_HOST DOCKER_CERT_PATH +passenv = DOCKER_TLS_VERIFY DOCKER_HOST DOCKER_CERT_PATH basepython = python2.7 whitelist_externals=/bin/bash skipsdist=True changedir=itests/ deps = - docker-compose==1.8.1 - -rrequirements.txt - behave - mock + -rtest-requirements.txt commands = - /bin/bash -c "[[ -n $TRAVIS ]] || echo CHRONOSVERSION=$CHRONOSVERSION > chronos-version" - /bin/bash -c "[[ -n $TRAVIS ]] || docker-compose build" - /bin/bash -c "[[ -n $TRAVIS ]] || docker-compose pull" - /bin/bash -c "[[ -n $TRAVIS ]] || docker-compose up -d" + /bin/bash -c "docker-compose up -d chronos-{env:CHRONOSVERSION:3.0.2}" behave --no-capture --no-capture-stderr {posargs} - /bin/bash -c "[[ -n $TRAVIS ]] || docker-compose stop" - /bin/bash -c "[[ -n $TRAVIS ]] || docker-compose rm --force" + /bin/bash -c "docker-compose down" From 2a4a3e7c1a5f8f95556ff4782b72cde9250e7262 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:40:05 -0600 Subject: [PATCH 09/26] Using the docker service provided by travis Taken straight from https://docs.travis-ci.com/user/docker/ --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index dfdedda..e794f97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,10 @@ language: python python: - 2.7 - 3.5 -sudo: true +sudo: required +services: + - docker install: - pip install tox script: - - ./itests/install-chronos.sh - - ./itests/start-chronos.sh - make test && make itests From de0188ead5871e742164ffb7a6ed979232d3af3f Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:42:21 -0600 Subject: [PATCH 10/26] Update to chronos 3.0.2 --- .travis.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e794f97..d8870fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ env: - - CHRONOSVERSION: 2.4.0 + - CHRONOSVERSION: 3.0.2 language: python python: diff --git a/README.md b/README.md index 033b759..f9f8e4c 100644 --- a/README.md +++ b/README.md @@ -78,4 +78,4 @@ To run the tests: To run against a different version of Chronos: - CHRONOSVERSION=2.4.0 make itests + CHRONOSVERSION=3.0.2 make itests From 97e90d48eef16df2bc96fe1f7139f072f6c788de Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Tue, 14 Mar 2017 18:45:04 -0600 Subject: [PATCH 11/26] Update the docker images before building if needed --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index d8870fb..477ca28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ python: sudo: required services: - docker +before_install: + - docker-compose -f itests/docker-compose.yml pull install: - pip install tox script: From 1f91834f5a7e341074e6962663cdb9aa81d0c7c1 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Wed, 22 Mar 2017 09:59:29 -0600 Subject: [PATCH 12/26] Trying to bring back legacy support --- chronos/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index 2a4933f..bd35ad0 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -26,7 +26,7 @@ import socket import json import logging -import re +import warnings # Python 3 changed the submodule for quote try: @@ -71,11 +71,13 @@ def __init__( logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=level) self.logger = logging.getLogger(__name__) if scheduler_version is None: - raise ChronosAPIError("Chronos >=3.x requires scheduler_version set") + warnings.warn("Chronos >=3.x requires scheduler_version set", FutureWarning) + self._prefix = '' else: if scheduler_version not in SCHEDULER_VERSIONS: raise ChronosAPIError('Wrong scheduler_version provided') self._prefix = "/%s" % (scheduler_version,) + self.scheduler_version = scheduler_version def list(self): """List all jobs on Chronos.""" @@ -199,17 +201,13 @@ def _check(self, resp, content): return payload def _check_fields(self, job): - for k in ChronosJob.fields: + fields = ChronosJob.fields + if self.scheduler_version is None: + fields.extend(ChronosJob.legacy_fields) + for k in fields: if k not in job: raise MissingFieldError("missing required field %s" % k) - # Chronos v3.x rejects job names with spaces in them - regex = re.compile(r'^[\w.-]+$') - if "name" in job and not regex.match(job["name"]): - raise ChronosAPIError( - 'Chronos >= 3.x only allows job names that match "[\w.-]+", ' - '"%(name)s" is invalid' % job - ) if any(field in job for field in ChronosJob.one_of): if len([field for field in ChronosJob.one_of if field in job]) > 1: raise OneOfViolationError("Job must only include 1 of %s" % ChronosJob.one_of) @@ -232,6 +230,9 @@ class ChronosJob(object): "owner", "disabled" ] + legacy_fields = [ + "async", + ] one_of = ["schedule", "parents"] container_fields = [ "type", From 5a37d1534737697657a57beaeab16b7f1fae3739 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Wed, 22 Mar 2017 09:59:42 -0600 Subject: [PATCH 13/26] Revert "Chronos API removed PUT method here" This reverts commit 538d9bb635274ff2842fe51701b2b3dbd4728a70. --- chronos/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index bd35ad0..e611b12 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -98,21 +98,21 @@ def run(self, name): path = "/scheduler/job/%s" % name return self._call(path, "PUT") - def set(self, job_def): - """Add or update a job""" + def add(self, job_def, update=False): + """Schedule a new job""" path = "/scheduler/iso8601" self._check_fields(job_def) if "parents" in job_def: path = "/scheduler/dependency" - return self._call(path, "POST", json.dumps(job_def)) - - def add(self, job_def): - """Add a new job""" - return self.set(job_def) + if update: + method = "PUT" + else: + method = "POST" + return self._call(path, method, json.dumps(job_def)) def update(self, job_def): """Update an existing job by name""" - return self.set(job_def) + return self.add(job_def, update=True) def job_stat(self, name): """ List stats for a job """ From c31e8aeec2cfff1c19183bcb899c0b8e1711b2b7 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Wed, 22 Mar 2017 10:45:43 -0600 Subject: [PATCH 14/26] Working legacy support back in --- chronos/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index e611b12..6bd49b9 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -104,10 +104,14 @@ def add(self, job_def, update=False): self._check_fields(job_def) if "parents" in job_def: path = "/scheduler/dependency" - if update: - method = "PUT" - else: - method = "POST" + # Cool story: chronos >= 3.0 ditched PUT and only allows POST here, + # trying to maintain backwards compat with < 3.0 + method = "POST" + if self.scheduler_version is None: + if update: + method = "PUT" + else: + method = "POST" return self._call(path, method, json.dumps(job_def)) def update(self, job_def): From da54db41cf3eba93a7a8aa96ccb9d1467496814f Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Wed, 22 Mar 2017 10:46:11 -0600 Subject: [PATCH 15/26] We need to raise a proper exception on http errors This way we'll properly raise an exception when the API returns a non OK status code. --- chronos/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index 6bd49b9..3d9da53 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -156,7 +156,7 @@ def _call(self, url, method="GET", body=None, headers={}, prefix=True): _url = '%s%s' % (self._prefix, url, ) else: _url = url - self.logger.debug("Fetch: %s %s" % (method, _url)) + self.logger.debug("Calling: %s %s" % (method, _url)) if body: self.logger.debug("Body: %s" % body) conn = httplib2.Http(disable_ssl_certificate_validation=True) @@ -202,6 +202,16 @@ def _check(self, resp, content): if payload is None and status != 204: raise ChronosAPIError("Request to Chronos API failed: status: %d, response: %s" % (status, content)) + # if the status returned is not an OK status, raise an exception + if status > 299: + message = "API returned status %d" % (status,) + # newer chronos does return the full stack trace in a message field, + # grabbing the first 160 chars from it + if 'message' in payload: + self.logger.debug(payload['message']) + message = '%s (...)' % (payload['message'][:160],) + raise ChronosAPIError(message) + return payload def _check_fields(self, job): From 4795d0be835011b694e22b32cac352c5f44d1333 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Wed, 22 Mar 2017 10:48:52 -0600 Subject: [PATCH 16/26] Work in environment variables to control behavior This way we can test on multiple versions again --- itests/steps/chronos_steps.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/itests/steps/chronos_steps.py b/itests/steps/chronos_steps.py index 436e909..eb365c7 100644 --- a/itests/steps/chronos_steps.py +++ b/itests/steps/chronos_steps.py @@ -3,12 +3,15 @@ import sys from behave import given, when, then import time +import os import chronos log = logging.getLogger('chronos') log.addHandler(logging.StreamHandler(sys.stdout)) log.setLevel(logging.DEBUG) +CHRONOSVERSION = os.getenv('CHRONOSVERSION', '3.0.2') +LEGACY_VERSIONS = ('2.4.0',) @given('a working chronos instance') @@ -17,7 +20,11 @@ def working_chronos(context): interacting with it in the test.""" if not hasattr(context, 'client'): chronos_servers = ['127.0.0.1:4400'] - context.client = chronos.connect(chronos_servers) + if CHRONOSVERSION in LEGACY_VERSIONS: + scheduler_version = None + else: + scheduler_version = 'v1' + context.client = chronos.connect(chronos_servers, scheduler_version=scheduler_version) @when(u'we create a trivial chronos job named "{job_name}"') @@ -29,18 +36,15 @@ def create_trivial_chronos_job(context, job_name): 'disabled': False, 'schedule': 'R0/2014-01-01T00:00:00Z/PT60M', } + if CHRONOSVERSION in LEGACY_VERSIONS: + job['async'] = False try: context.client.add(job) - # give it a bit of time to reflect the job in ZK - time.sleep(0.5) context.created = True except: context.created = False - - -@then(u'the job "{job_name}" failed to be created') -def job_creation_failed(context, job_name): - assert context.created == False + # give it a bit of time to reflect the job in ZK + time.sleep(0.5) @then(u'we should be able to see the job named "{job_name}" when we list jobs') From 103fb959ceb8992529db94cc41e1333e23dcb45a Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Wed, 22 Mar 2017 10:54:44 -0600 Subject: [PATCH 17/26] Use travis' matrix support as per https://docs.travis-ci.com/user/multi-os/ --- .travis.yml | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 477ca28..a866958 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,22 @@ -env: - - CHRONOSVERSION: 3.0.2 - language: python -python: - - 2.7 - - 3.5 -sudo: required -services: - - docker +matrix: + include: + - os: linux + sudo: required + services: + - docker + python: + - 2.7 + - 3.5 + env: CHRONOSVERSION=3.0.2 + - os: linux + sudo: required + services: + - docker + python: + - 2.7 + - 3.5 + env: CHRONOSVERSION=2.4.0 before_install: - docker-compose -f itests/docker-compose.yml pull install: From 496aac7451a1dc1c21463f86b4519e48220a3761 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Wed, 22 Mar 2017 10:55:10 -0600 Subject: [PATCH 18/26] Use behave's tag support to only run some tests This way we can support more than 1 version --- itests/chronos-python.feature | 14 ++++++++++++-- tox.ini | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/itests/chronos-python.feature b/itests/chronos-python.feature index 50e4524..e3c7b02 100644 --- a/itests/chronos-python.feature +++ b/itests/chronos-python.feature @@ -1,16 +1,24 @@ Feature: chronos-python can interact with chronos + @all Scenario: Trivial chronos interaction Given a working chronos instance When we create a trivial chronos job named "myjob" Then we should be able to see the job named "myjob" when we list jobs + @3.0.2 Scenario: Handling spaces in job names Given a working chronos instance When we create a trivial chronos job named "job with spaces" - Then the job "job with spaces" failed to be created - And we should not see the job named "job with spaces" when we list jobs + Then we should not see the job named "job with spaces" when we list jobs + @2.4.0 + Scenario: Handling spaces in job names + Given a working chronos instance + When we create a trivial chronos job named "job with spaces" + Then we should be able to see the job named "job with spaces" when we list jobs + + @all Scenario: Gathering job stats for job Given a working chronos instance When we create a trivial chronos job named "myjob" @@ -19,12 +27,14 @@ Feature: chronos-python can interact with chronos And we should be able to see the median timing for all jobs And we should be able to see the mean timing for all jobs + @all Scenario: Gathering scheduler graph Given a working chronos instance When we create a trivial chronos job named "myjob" And we create a trivial chronos job named "myotherjob" Then we should be able to see 2 jobs in the job graph + @all Scenario: Getting metrics Given a working chronos instance Then we should be able to see metrics diff --git a/tox.ini b/tox.ini index a9fd022..c22a891 100644 --- a/tox.ini +++ b/tox.ini @@ -27,5 +27,5 @@ deps = -rtest-requirements.txt commands = /bin/bash -c "docker-compose up -d chronos-{env:CHRONOSVERSION:3.0.2}" - behave --no-capture --no-capture-stderr {posargs} + behave --tags all,${env:CHRONOSVERSION:3.0.2} --no-capture --no-capture-stderr {posargs} /bin/bash -c "docker-compose down" From 65cf3ac1a579001e567514132285e749f29481b5 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Thu, 23 Mar 2017 13:40:08 -0600 Subject: [PATCH 19/26] 3xx are potentially good status codes Also the payload message might be too long as is --- chronos/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index 3d9da53..5e682af 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -203,13 +203,13 @@ def _check(self, resp, content): raise ChronosAPIError("Request to Chronos API failed: status: %d, response: %s" % (status, content)) # if the status returned is not an OK status, raise an exception - if status > 299: - message = "API returned status %d" % (status,) + if status >= 400: + message = "API returned status %d, content: %s" % (status, payload,) # newer chronos does return the full stack trace in a message field, # grabbing the first 160 chars from it if 'message' in payload: self.logger.debug(payload['message']) - message = '%s (...)' % (payload['message'][:160],) + message = '%s (...)' % (payload['message'][:120],) raise ChronosAPIError(message) return payload From 1ba92a988648d74d32afc85cab770a17d8cd02d4 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Thu, 23 Mar 2017 13:41:22 -0600 Subject: [PATCH 20/26] Adding a test for all known status codes And also since we're throwing exceptions, make sure to catch them properly --- tests/test_chronos_init.py | 43 ++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/tests/test_chronos_init.py b/tests/test_chronos_init.py index a542c01..d750624 100644 --- a/tests/test_chronos_init.py +++ b/tests/test_chronos_init.py @@ -30,13 +30,42 @@ def test_check_accepts_json(): assert actual == json.loads(fake_content) +def test_http_codes(): + client = chronos.ChronosClient("localhost") + fake_response = mock.Mock() + # all status codes 2xx and 3xx are potentially valid + valid_codes = range(200, 399) + for status in valid_codes: + fake_response.status = status + fake_content = '{ "foo": "bar" }' + actual = client._check(fake_response, fake_content) + assert actual == json.loads(fake_content) + # we treat 401 in a special way, so we skip it (add 400 at the beginning) + invalid_codes = range(402, 550) + invalid_codes.insert(0, 400) + for status in invalid_codes: + fake_response.status = status + fake_content = '{ "foo": "bar" }' + with pytest.raises(chronos.ChronosAPIError): + actual = client._check(fake_response, fake_content) + # let's test 401 finally + fake_response.status = 401 + fake_content = '{ "foo": "bar" }' + with pytest.raises(chronos.UnauthorizedError): + actual = client._check(fake_response, fake_content) + + def test_check_returns_raw_response_when_not_json(): client = chronos.ChronosClient("localhost") fake_response = mock.Mock(__getitem__=mock.Mock(return_value="not-json")) fake_response.status = 400 fake_content = 'foo bar baz' - actual = client._check(fake_response, fake_content) - assert actual == fake_content + try: + actual = client._check(fake_response, fake_content) + except chronos.ChronosAPIError as cap: + actual = cap.message + # on exceptions, the content is passed on the exception's message + assert actual == "API returned status 400, content: %s" % (fake_content, ) def test_check_does_not_log_error_when_content_type_is_not_json(): @@ -45,7 +74,10 @@ def test_check_does_not_log_error_when_content_type_is_not_json(): fake_response = mock.Mock(__getitem__=mock.Mock(return_value="not-json")) fake_response.status = 400 fake_content = 'foo bar baz' - client._check(fake_response, fake_content) + try: + client._check(fake_response, fake_content) + except chronos.ChronosAPIError as cap: + pass assert mock_log().error.call_count == 0 @@ -55,7 +87,10 @@ def test_check_logs_error_when_invalid_json(): fake_response = mock.Mock(__getitem__=mock.Mock(return_value="application/json")) fake_response.status = 400 fake_content = 'foo bar baz' - client._check(fake_response, fake_content) + try: + client._check(fake_response, fake_content) + except chronos.ChronosAPIError as cap: + pass mock_log().error.assert_called_once_with("Response not valid json: %s" % fake_content) From 34ee57f5b566ac3c6dfde321ad32085730f6c446 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Thu, 23 Mar 2017 14:26:45 -0600 Subject: [PATCH 21/26] Small update to docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9f8e4c..7532ce7 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Delete all the in flight tasks for a job: ## Testing ``chronos-python`` uses Travis to test against multiple versions of Chronos. You can run the tests locally on any machine -with [Docker](https://www.docker.com/) on it. +with [Docker](https://www.docker.com/) and [Docker compose](https://docs.docker.com/compose/) on it. To run the tests: From c3784646d412b56c94bad0c7a1e6a8361ea0d33d Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Thu, 23 Mar 2017 14:57:49 -0600 Subject: [PATCH 22/26] I think this is causing travis failures --- .travis.yml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index a866958..572a5a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,20 +3,24 @@ matrix: include: - os: linux sudo: required - services: - - docker - python: - - 2.7 - - 3.5 - env: CHRONOSVERSION=3.0.2 + services: docker + python: 2.7 + env: CHRONOSVERSION=2.4.0 - os: linux sudo: required - services: - - docker - python: - - 2.7 - - 3.5 + services: docker + python: 3.5 env: CHRONOSVERSION=2.4.0 + - os: linux + sudo: required + services: docker + python: 2.7 + env: CHRONOSVERSION=3.0.2 + - os: linux + sudo: required + services: docker + python: 3.5 + env: CHRONOSVERSION=3.0.2 before_install: - docker-compose -f itests/docker-compose.yml pull install: From 3abaa15da54685b56495a1f19d373c32cd7ba855 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Thu, 23 Mar 2017 15:04:28 -0600 Subject: [PATCH 23/26] For flake8's sake --- tests/test_chronos_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_chronos_init.py b/tests/test_chronos_init.py index d750624..66a0e34 100644 --- a/tests/test_chronos_init.py +++ b/tests/test_chronos_init.py @@ -76,7 +76,7 @@ def test_check_does_not_log_error_when_content_type_is_not_json(): fake_content = 'foo bar baz' try: client._check(fake_response, fake_content) - except chronos.ChronosAPIError as cap: + except chronos.ChronosAPIError: pass assert mock_log().error.call_count == 0 @@ -89,7 +89,7 @@ def test_check_logs_error_when_invalid_json(): fake_content = 'foo bar baz' try: client._check(fake_response, fake_content) - except chronos.ChronosAPIError as cap: + except chronos.ChronosAPIError: pass mock_log().error.assert_called_once_with("Response not valid json: %s" % fake_content) From f0c34053d519c6fbe8e170ec7e79f67b5b03b8cb Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Thu, 23 Mar 2017 15:17:11 -0600 Subject: [PATCH 24/26] Fixing typo in {env} and passing CHRONOSVERSION --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index c22a891..36af0e6 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ commands = [testenv:itests] -passenv = DOCKER_TLS_VERIFY DOCKER_HOST DOCKER_CERT_PATH +passenv = CHRONOSVERSION DOCKER_TLS_VERIFY DOCKER_HOST DOCKER_CERT_PATH basepython = python2.7 whitelist_externals=/bin/bash skipsdist=True @@ -27,5 +27,5 @@ deps = -rtest-requirements.txt commands = /bin/bash -c "docker-compose up -d chronos-{env:CHRONOSVERSION:3.0.2}" - behave --tags all,${env:CHRONOSVERSION:3.0.2} --no-capture --no-capture-stderr {posargs} + behave --tags all,{env:CHRONOSVERSION:3.0.2} --no-capture --no-capture-stderr {posargs} /bin/bash -c "docker-compose down" From 787ddd8232d5e910192061cc78fcd23f45852607 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Thu, 23 Mar 2017 15:35:18 -0600 Subject: [PATCH 25/26] Using the environment is kind of feeble I think --- itests/steps/chronos_steps.py | 9 +++++---- tox.ini | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/itests/steps/chronos_steps.py b/itests/steps/chronos_steps.py index eb365c7..e212ba9 100644 --- a/itests/steps/chronos_steps.py +++ b/itests/steps/chronos_steps.py @@ -3,15 +3,14 @@ import sys from behave import given, when, then import time -import os import chronos log = logging.getLogger('chronos') log.addHandler(logging.StreamHandler(sys.stdout)) log.setLevel(logging.DEBUG) -CHRONOSVERSION = os.getenv('CHRONOSVERSION', '3.0.2') LEGACY_VERSIONS = ('2.4.0',) +DEFAULT_CHRONOS_VERSION = '3.0.2' @given('a working chronos instance') @@ -20,7 +19,8 @@ def working_chronos(context): interacting with it in the test.""" if not hasattr(context, 'client'): chronos_servers = ['127.0.0.1:4400'] - if CHRONOSVERSION in LEGACY_VERSIONS: + chronos_version = context.config.userdata.get('chronos_version', DEFAULT_CHRONOS_VERSION) + if chronos_version in LEGACY_VERSIONS: scheduler_version = None else: scheduler_version = 'v1' @@ -36,7 +36,8 @@ def create_trivial_chronos_job(context, job_name): 'disabled': False, 'schedule': 'R0/2014-01-01T00:00:00Z/PT60M', } - if CHRONOSVERSION in LEGACY_VERSIONS: + chronos_version = context.config.userdata.get('chronos_version', DEFAULT_CHRONOS_VERSION) + if chronos_version in LEGACY_VERSIONS: job['async'] = False try: context.client.add(job) diff --git a/tox.ini b/tox.ini index 36af0e6..84f474e 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ commands = [testenv:itests] -passenv = CHRONOSVERSION DOCKER_TLS_VERIFY DOCKER_HOST DOCKER_CERT_PATH +passenv = DOCKER_TLS_VERIFY DOCKER_HOST DOCKER_CERT_PATH basepython = python2.7 whitelist_externals=/bin/bash skipsdist=True @@ -27,5 +27,5 @@ deps = -rtest-requirements.txt commands = /bin/bash -c "docker-compose up -d chronos-{env:CHRONOSVERSION:3.0.2}" - behave --tags all,{env:CHRONOSVERSION:3.0.2} --no-capture --no-capture-stderr {posargs} + behave --tags all,{env:CHRONOSVERSION:3.0.2} --no-capture --no-capture-stderr -Dchronos_version={env:CHRONOSVERSION:3.0.2} {posargs} /bin/bash -c "docker-compose down" From 2d598b2d03951962f50944a4342915182f8b3c19 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Fri, 24 Mar 2017 11:22:48 -0600 Subject: [PATCH 26/26] s/scheduler_version/scheduler_api_version/ --- chronos/__init__.py | 24 ++++++++++++------------ itests/steps/chronos_steps.py | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/chronos/__init__.py b/chronos/__init__.py index 5e682af..2665844 100755 --- a/chronos/__init__.py +++ b/chronos/__init__.py @@ -34,7 +34,7 @@ except ImportError: from urllib.parse import quote -SCHEDULER_VERSIONS = ('v1',) +SCHEDULER_API_VERSIONS = ('v1',) class ChronosAPIError(Exception): @@ -60,7 +60,7 @@ class ChronosClient(object): def __init__( self, servers, proto="http", username=None, password=None, extra_headers=None, level='WARN', - scheduler_version='v1' + scheduler_api_version='v1' ): server_list = servers if isinstance(servers, list) else [servers] self.servers = ["%s://%s" % (proto, server) for server in server_list] @@ -70,14 +70,14 @@ def __init__( self._password = password logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=level) self.logger = logging.getLogger(__name__) - if scheduler_version is None: - warnings.warn("Chronos >=3.x requires scheduler_version set", FutureWarning) + if scheduler_api_version is None: + warnings.warn("Chronos >=3.x requires scheduler_api_version set", FutureWarning) self._prefix = '' else: - if scheduler_version not in SCHEDULER_VERSIONS: - raise ChronosAPIError('Wrong scheduler_version provided') - self._prefix = "/%s" % (scheduler_version,) - self.scheduler_version = scheduler_version + if scheduler_api_version not in SCHEDULER_API_VERSIONS: + raise ChronosAPIError('Wrong scheduler_api_version provided') + self._prefix = "/%s" % (scheduler_api_version,) + self.scheduler_api_version = scheduler_api_version def list(self): """List all jobs on Chronos.""" @@ -107,7 +107,7 @@ def add(self, job_def, update=False): # Cool story: chronos >= 3.0 ditched PUT and only allows POST here, # trying to maintain backwards compat with < 3.0 method = "POST" - if self.scheduler_version is None: + if self.scheduler_api_version is None: if update: method = "PUT" else: @@ -216,7 +216,7 @@ def _check(self, resp, content): def _check_fields(self, job): fields = ChronosJob.fields - if self.scheduler_version is None: + if self.scheduler_api_version is None: fields.extend(ChronosJob.legacy_fields) for k in fields: if k not in job: @@ -254,8 +254,8 @@ class ChronosJob(object): ] -def connect(servers, proto="http", username=None, password=None, extra_headers=None, scheduler_version='v1'): +def connect(servers, proto="http", username=None, password=None, extra_headers=None, scheduler_api_version='v1'): return ChronosClient( servers, proto=proto, username=username, password=password, - extra_headers=extra_headers, scheduler_version=scheduler_version + extra_headers=extra_headers, scheduler_api_version=scheduler_api_version ) diff --git a/itests/steps/chronos_steps.py b/itests/steps/chronos_steps.py index e212ba9..7708ba3 100644 --- a/itests/steps/chronos_steps.py +++ b/itests/steps/chronos_steps.py @@ -21,10 +21,10 @@ def working_chronos(context): chronos_servers = ['127.0.0.1:4400'] chronos_version = context.config.userdata.get('chronos_version', DEFAULT_CHRONOS_VERSION) if chronos_version in LEGACY_VERSIONS: - scheduler_version = None + scheduler_api_version = None else: - scheduler_version = 'v1' - context.client = chronos.connect(chronos_servers, scheduler_version=scheduler_version) + scheduler_api_version = 'v1' + context.client = chronos.connect(chronos_servers, scheduler_api_version=scheduler_api_version) @when(u'we create a trivial chronos job named "{job_name}"')