Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bde6de0
Initial support for chronos 3.x for #33
Mar 7, 2017
a68659e
Make warnings throw an exception instead
Mar 15, 2017
ccc48a9
Line breaking < 120c and default to v1
Mar 15, 2017
538d9bb
Chronos API removed PUT method here
Mar 15, 2017
5adc62d
async is no longer a valid attribute
Mar 15, 2017
1a3bc93
Should account for whatever prefix client has
Mar 15, 2017
25f2c8d
Simplify the test environment by a lot
Mar 15, 2017
c8a337c
Put all the test requirements in a single file
Mar 15, 2017
2a4a3e7
Using the docker service provided by travis
Mar 15, 2017
de0188e
Update to chronos 3.0.2
Mar 15, 2017
97e90d4
Update the docker images before building if needed
Mar 15, 2017
1f91834
Trying to bring back legacy support
Mar 22, 2017
5a37d15
Revert "Chronos API removed PUT method here"
Mar 22, 2017
c31e8ae
Working legacy support back in
Mar 22, 2017
da54db4
We need to raise a proper exception on http errors
Mar 22, 2017
4795d0b
Work in environment variables to control behavior
Mar 22, 2017
103fb95
Use travis' matrix support
Mar 22, 2017
496aac7
Use behave's tag support to only run some tests
Mar 22, 2017
65cf3ac
3xx are potentially good status codes
Mar 23, 2017
1ba92a9
Adding a test for all known status codes
Mar 23, 2017
34ee57f
Small update to docs
Mar 23, 2017
c378464
I think this is causing travis failures
Mar 23, 2017
3abaa15
For flake8's sake
Mar 23, 2017
f0c3405
Fixing typo in {env} and passing CHRONOSVERSION
Mar 23, 2017
787ddd8
Using the environment is kind of feeble I think
Mar 23, 2017
2d598b2
s/scheduler_version/scheduler_api_version/
Mar 24, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
env:
- CHRONOSVERSION: 2.4.0

language: python
python:
- 2.7
- 3.5
sudo: true
matrix:
include:
- os: linux
sudo: required
services: docker
python: 2.7
env: CHRONOSVERSION=2.4.0
- os: linux
sudo: required
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:
- pip install tox
script:
- ./itests/install-chronos.sh
- ./itests/start-chronos.sh
- make test && make itests
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ 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:

make itests

To run against a different version of Chronos:

CHRONOSVERSION=2.4.0 make itests
CHRONOSVERSION=3.0.2 make itests
68 changes: 55 additions & 13 deletions chronos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@
import socket
import json
import logging
import warnings

# Python 3 changed the submodule for quote
try:
from urllib import quote
except ImportError:
from urllib.parse import quote

SCHEDULER_API_VERSIONS = ('v1',)


class ChronosAPIError(Exception):
pass
Expand All @@ -54,7 +57,11 @@ 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_api_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
Expand All @@ -63,6 +70,14 @@ 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_api_version is None:
warnings.warn("Chronos >=3.x requires scheduler_api_version set", FutureWarning)
self._prefix = ''
else:
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."""
Expand All @@ -89,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_api_version is None:
if update:
method = "PUT"
else:
method = "POST"
return self._call(path, method, json.dumps(job_def))

def update(self, job_def):
Expand Down Expand Up @@ -125,14 +144,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("Calling: %s %s" % (method, _url))
if body:
self.logger.debug("Body: %s" % body)
conn = httplib2.Http(disable_ssl_certificate_validation=True)
Expand All @@ -145,7 +169,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:
Expand Down Expand Up @@ -178,10 +202,23 @@ 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment re < 3. You're not really allowing for backwards compat here.

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'][:120],)
raise ChronosAPIError(message)

return payload

def _check_fields(self, job):
for k in ChronosJob.fields:
fields = ChronosJob.fields
if self.scheduler_api_version is None:
fields.extend(ChronosJob.legacy_fields)
for k in fields:
if k not in job:
raise MissingFieldError("missing required field %s" % k)

Expand All @@ -202,18 +239,23 @@ def _check_fields(self, job):

class ChronosJob(object):
fields = [
"async",
"command",
"name",
"owner",
"disabled"
]
legacy_fields = [
"async",
]
one_of = ["schedule", "parents"]
container_fields = [
"type",
"image"
]


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_api_version='v1'):
return ChronosClient(
servers, proto=proto, username=username, password=password,
extra_headers=extra_headers, scheduler_api_version=scheduler_api_version
)
11 changes: 0 additions & 11 deletions itests/Dockerfile

This file was deleted.

15 changes: 12 additions & 3 deletions itests/chronos-python.feature
Original file line number Diff line number Diff line change
@@ -1,17 +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 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 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"
Expand All @@ -20,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
1 change: 0 additions & 1 deletion itests/chronos-version

This file was deleted.

82 changes: 77 additions & 5 deletions itests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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

3 changes: 1 addition & 2 deletions itests/environment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import time

from itest_utils import wait_for_chronos


Expand All @@ -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)
18 changes: 0 additions & 18 deletions itests/install-chronos.sh

This file was deleted.

Loading