Skip to content

Commit

Permalink
Merge pull request #16 from totem/develop
Browse files Browse the repository at this point in the history
0.1.9 Release
  • Loading branch information
sukrit007 committed Mar 18, 2015
2 parents 14178a9 + 4ed2fd5 commit 669cbd4
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 91 deletions.
2 changes: 1 addition & 1 deletion deployer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '0.1.8'
__version__ = '0.1.9'
__author__ = 'sukrit'

import logging
Expand Down
22 changes: 15 additions & 7 deletions deployer/tasks/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
from fabric.exceptions import NetworkError
from fleet.client.fleet_fabric import FleetExecutionException
from paramiko import SSHException
import sys

from deployer.services.distributed_lock import LockService, \
ResourceLockedException
from deployer.services.security import decrypt_config
from deployer.tasks import notification
from deployer.tasks.exceptions import NodeNotUndeployed, MinNodesNotRunning, \
NodeCheckFailed
from deployer.tasks import util

from deployer.tasks.search import index_deployment, update_deployment_state, \
EVENT_NEW_DEPLOYMENT, \
Expand Down Expand Up @@ -675,10 +678,7 @@ def _deployment_error_event(task_id, deployment, search_params):
return add_search_event.si(
EVENT_DEPLOYMENT_FAILED,
search_params=search_params,
details={
'error': str(output.result),
'traceback': output.traceback
})
details={'deployment-error': util.as_dict(output.result)}).delay()


@app.task
Expand Down Expand Up @@ -773,9 +773,17 @@ def _check_node(self, node, path, attempts, timeout):
check_url = 'http://{0}{1}'.format(node, path)
timeout_ms = to_milliseconds(timeout)
try:
urllib2.urlopen(check_url, None, timeout_ms)
except BaseException as exc:
urllib2.urlopen(check_url, None, timeout_ms/1000)
except IOError as exc:
# Clear the current exception so that celery does not raise original
# exception
reason = exc.reason if hasattr(exc, 'reason') else str(exc)
kwargs = {}
if hasattr(exc, 'read'):
kwargs.update(response={'raw': exc.read()}, status=exc.code)

sys.exc_clear()
raise self.retry(
exc=NodeCheckFailed(node, str(exc)),
exc=NodeCheckFailed(check_url, reason, **kwargs),
max_retries=attempts-1,
countdown=TASK_SETTINGS['CHECK_NODE_RETRY_DELAY'])
21 changes: 14 additions & 7 deletions deployer/tasks/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,23 +147,30 @@ class NodeCheckFailed(Exception):
Exception corresponding to failed deployment check for a given node.
"""

def __init__(self, node, reason):
self.node = node
self.message = 'Deployment check failed for node: {0} due to: {1}'\
.format(node, reason)
super(NodeCheckFailed, self).__init__(node, reason)
def __init__(self, url, reason, status=None, response=None):
self.url = url
self.message = 'Deployment check failed for url: {0} due to: {1}'\
.format(url, reason)
self.status = status
self.response = response
super(NodeCheckFailed, self).__init__(url, reason, status, response)

def to_dict(self):
return {
'message': self.message,
'code': 'NODE_CHECK_FAILED',
'details': {
'node': self.node
'url': self.url,
'status': self.status,
'response': self.response
}
}

def __str__(self):
return self.message

def __eq__(self, other):
return self.node == other.node and self.message == other.message
return self.status == other.status and \
self.message == other.message and \
self.response == other.response and \
self.url == other.url
18 changes: 3 additions & 15 deletions deployer/tasks/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from deployer import templatefactory
from deployer.celery import app
from deployer.services.security import decrypt_config
from deployer.tasks import util


@app.task
Expand All @@ -38,27 +39,14 @@ def notify(obj, ctx=None, level=LEVEL_FAILED,
obj, ctx, level, notification, security_profile).delay()


def _as_dict(obj):
if isinstance(obj, dict):
return obj
elif getattr(obj, 'to_dict', None):
obj_dict = obj.to_dict()
return obj_dict
else:
return {
'message': repr(obj),
'code': 'INTERNAL'
}


@app.task
def notify_hipchat(obj, ctx, level, config, security_profile):
config = decrypt_config(config, profile=security_profile)
api_url = config.get('url') or 'https://api.hipchat.com'
room_url = '{0}/v2/room/{1}/notification'.format(
api_url, config.get('room'))
msg = templatefactory.render_template(
'hipchat.html', notification=_as_dict(obj), ctx=ctx, level=level)
'hipchat.html', notification=util.as_dict(obj), ctx=ctx, level=level)
headers = {
'content-type': 'application/json',
'Authorization': 'Bearer {0}'.format(
Expand All @@ -83,7 +71,7 @@ def notify_github(obj, ctx, level, config, security_profile):
git_type = git.get('type', 'github')
token = config.get('token') or DEFAULT_GITHUB_TOKEN
if owner and repo and commit and token and git_type == 'github':
desc = _as_dict(obj).get('message', str(obj))
desc = util.as_dict(obj).get('message', str(obj))
# Max 140 characters allowed for description
use_desc = desc[:137] + '...' if len(desc) > 140 else desc

Expand Down
19 changes: 19 additions & 0 deletions deployer/tasks/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,24 @@ def simple_result(result):
return result


def as_dict(error):
"""
Creates a dictionary representation for a given error.
:param error: Object representing error
:type error: dict or object
"""
if isinstance(error, dict):
return error
elif getattr(error, 'to_dict', None):
obj_dict = error.to_dict()
return obj_dict
else:
return {
'message': repr(error),
'code': 'INTERNAL'
}


class TaskNotReadyException(Exception):
pass
14 changes: 10 additions & 4 deletions tests/unit/tasks/test_deployment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import urllib2

from freezegun import freeze_time
from mock import patch, ANY, MagicMock
Expand Down Expand Up @@ -734,7 +735,7 @@ def test_check_node(m_urlopen):
_check_node('localhost:8080', '/mock', 5, '5s')

# Then: Http URL Check is performed for given path
m_urlopen.assert_called_once_with('http://localhost:8080/mock', None, 5000)
m_urlopen.assert_called_once_with('http://localhost:8080/mock', None, 5)


@patch('urllib2.urlopen')
Expand All @@ -747,7 +748,7 @@ def test_check_node_for_path_not_beginning_with_forward_slash(m_urlopen):
_check_node('localhost:8080', 'mock', 5, '5s')

# Then: Http URL Check is performed for given path
m_urlopen.assert_called_once_with('http://localhost:8080/mock', None, 5000)
m_urlopen.assert_called_once_with('http://localhost:8080/mock', None, 5)


@patch('urllib2.urlopen')
Expand All @@ -757,7 +758,10 @@ def test_check_node_for_unhealthy_node(m_urlopen):
"""

# Given: Unhealthy node
m_urlopen.side_effect = RuntimeError('Mock')
fp = MagicMock()
fp.read.return_value = 'MockResponse'
m_urlopen.side_effect = urllib2.HTTPError(
'http://mockurl', 500, 'MockError', None, fp)

# And: Mock Implementation for retry
_check_node.retry = MagicMock()
Expand All @@ -772,7 +776,9 @@ def retry(*args, **kwargs):
_check_node('localhost:8080', 'mock', 5, '5s')

# Then: NodeCheckFailed exception is raised
eq_(cm.exception, NodeCheckFailed('localhost:8080', 'Mock'))
eq_(cm.exception, NodeCheckFailed(
'http://localhost:8080/mock', 'MockError', status=500,
response={'raw': 'MockResponse'}))


@patch('deployer.tasks.deployment._check_node')
Expand Down
20 changes: 17 additions & 3 deletions tests/unit/tasks/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from nose.tools import eq_
from deployer.tasks.exceptions import MinNodesNotRunning, MinNodesNotDiscovered, \
NodeCheckFailed
from tests.helper import dict_compare
Expand Down Expand Up @@ -46,14 +47,27 @@ def test_dict_repr_for_min_nodes_not_discovered_exception():
def test_dict_repr_for_node_check_failed():

# When: I call to_dict for NodeCheckFailed exception
result = NodeCheckFailed('localhost:8080', 'MockReason').to_dict()
result = NodeCheckFailed('http://localhost:8080', 'MockReason',
status=500, response={'raw': 'Mock'}).to_dict()

# Then: Expected result is returned
dict_compare(result, {
'message': 'Deployment check failed for node: localhost:8080 '
'message': 'Deployment check failed for url: http://localhost:8080 '
'due to: MockReason',
'code': 'NODE_CHECK_FAILED',
'details': {
'node': 'localhost:8080'
'url': 'http://localhost:8080',
'status': 500,
'response': {'raw': 'Mock'}
}
})


def test_str_repr_for_node_check_failed():

# When: I call str representation for NodeCheckFailed exception
result = str(NodeCheckFailed('http://localhost:8080', 'MockReason'))

# Then: Expected result is returned
eq_(result, 'Deployment check failed for url: http://localhost:8080 due '
'to: MockReason')
56 changes: 2 additions & 54 deletions tests/unit/tasks/test_notification.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from mock import patch, MagicMock
from nose.tools import eq_
from mock import patch

from conf.appconfig import LEVEL_FAILED, LEVEL_FAILED_WARN, LEVEL_SUCCESS
from deployer.tasks import notification

Expand Down Expand Up @@ -72,58 +72,6 @@ def test_notify(m_notify_hipchat):
m_notify_hipchat.si.assert_called_once()


def test_as_dict_for_dictionary_type():
"""
Should return the input dictionary
"""
# Given: Input dictionary
input = {
'mockkey': 'mockvalue'
}

# When: I invoke _as_dict with dict type
output = notification._as_dict(input)

# Then: Input dictionary is returned
eq_(output, input)


def test_as_dict_for_obj_with_to_dict_method():
"""
Should return the dict representation
"""
# Given: Input Object
input = MagicMock()
input.to_dict.return_value = {
'mockkey': 'mockvalue'
}

# When: I invoke _as_dict with dict type
output = notification._as_dict(input)

# Then: Dictionary representation is returned
eq_(output, {
'mockkey': 'mockvalue'
})


def test_as_dict_for_obj_with_no_to_dict_method():
"""
Should return the dict representation
"""
# Given: Input object
input = 'test'

# When: I invoke _as_dict with dict type
output = notification._as_dict(input)

# Then: Dictionary representation is returned
eq_(output, {
'code': 'INTERNAL',
'message': repr(input)
})


@patch('deployer.tasks.notification.requests')
@patch('deployer.tasks.notification.templatefactory')
@patch('deployer.tasks.notification.json')
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/tasks/test_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from mock import MagicMock
from nose.tools import eq_
from deployer.tasks import util


def test_as_dict_for_dictionary_type():
"""
Should return the input dictionary
"""
# Given: Input dictionary
input = {
'mockkey': 'mockvalue'
}

# When: I invoke as_dict with dict type
output = util.as_dict(input)

# Then: Input dictionary is returned
eq_(output, input)


def test_as_dict_for_obj_with_to_dict_method():
"""
Should return the dict representation
"""
# Given: Input Object
input = MagicMock()
input.to_dict.return_value = {
'mockkey': 'mockvalue'
}

# When: I invoke as_dict with dict type
output = util.as_dict(input)

# Then: Dictionary representation is returned
eq_(output, {
'mockkey': 'mockvalue'
})


def test_as_dict_for_obj_with_no_to_dict_method():
"""
Should return the dict representation
"""
# Given: Input object
input = 'test'

# When: I invoke as_dict with dict type
output = util.as_dict(input)

# Then: Dictionary representation is returned
eq_(output, {
'code': 'INTERNAL',
'message': repr(input)
})

0 comments on commit 669cbd4

Please sign in to comment.