Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@
Queue('counters', routing_key='counters'),
Queue('events', routing_key='events'),
Queue('triggers', routing_key='triggers'),
Queue('update', routing_key='update'),
)


Expand Down
20 changes: 20 additions & 0 deletions src/sentry/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import time
import uuid
import urlparse
from pkg_resources import parse_version as Version

from datetime import timedelta
from hashlib import md5
Expand Down Expand Up @@ -1177,6 +1178,25 @@ def create_default_project(created_models, verbosity=2, **kwargs):
print 'done!'


def set_sentry_version(latest=None, **kwargs):
import sentry
current = sentry.get_version()

version = Option.objects.get_value(
key='sentry:latest_version',
default=''
)

for ver in (current, version):
if Version(ver) >= Version(latest):
return

Option.objects.set_value(
key='sentry:latest_version',
value=(latest or current)
)


def create_team_and_keys_for_project(instance, created, **kwargs):
if not created or kwargs.get('raw'):
return
Expand Down
1 change: 1 addition & 0 deletions src/sentry/tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""

import sentry.tasks.check_alerts # NOQA
import sentry.tasks.check_update # NOQA
import sentry.tasks.cleanup # NOQA
import sentry.tasks.fetch_source # NOQA
import sentry.tasks.index # NOQA
Expand Down
49 changes: 49 additions & 0 deletions src/sentry/tasks/check_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
sentry.tasks.check_version
~~~~~~~~~~~~~~~~~~~~~~~~~

:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
import json
import logging

from django.utils.simplejson import JSONDecodeError

from sentry.tasks.fetch_source import fetch_url_content, BAD_SOURCE

from celery.task import periodic_task
from celery.task.schedules import crontab

PYPI_URL = 'https://pypi.python.org/pypi/sentry/json'
SENTRY_CHECKUPDATE_TIME = {
'hour': 0,
'minute': 0
}

logger = logging.getLogger(__name__)


@periodic_task(
name='sentry.tasks.check_update',
run_every=crontab(**SENTRY_CHECKUPDATE_TIME), queue='update')
def check_update():
"""
Daily retrieving latest available Sentry version from PyPI
"""
from sentry.models import set_sentry_version

result = fetch_url_content(PYPI_URL)

if result == BAD_SOURCE:
return

try:
(_, _, body) = result

version = json.loads(body)['info']['version']
set_sentry_version(version)
except JSONDecodeError:
logger.warning('Failed parsing data json from PYPI')
except Exception:
logger.warning('Failed update info of latest version Sentry')
35 changes: 23 additions & 12 deletions src/sentry/tasks/fetch_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,12 @@ def discover_sourcemap(result, logger=None):
return sourcemap


def fetch_url(url, logger=None):
def fetch_url_content(url, logger=None):
"""
Pull down a URL, returning a UrlResult object.

Attempts to fetch from the cache.
Pull down a URL, returning a tuple (url, headers, body).
"""
import sentry

cache_key = 'fetch_url:v2:%s' % (
hashlib.md5(url.encode('utf-8')).hexdigest(),)
result = cache.get(cache_key)
if result is not None:
return UrlResult(*result)

try:
opener = urllib2.build_opener()
opener.addheaders = [('User-Agent', 'Sentry/%s' % sentry.VERSION)]
Expand All @@ -124,11 +116,30 @@ def fetch_url(url, logger=None):
logger.error('Unable to fetch remote source for %r', url, exc_info=True)
return BAD_SOURCE

result = (url, headers, body)
return (url, headers, body)


def fetch_url(url, logger=None):
"""
Pull down a URL, returning a UrlResult object.

Attempts to fetch from the cache.
"""

cache_key = 'fetch_url:v2:%s' % (
hashlib.md5(url.encode('utf-8')).hexdigest(),)
result = cache.get(cache_key)
if result is not None:
return UrlResult(*result)

result = fetch_url_content(url, logger)

if result == BAD_SOURCE:
return result

cache.set(cache_key, result, 60 * 5)

return UrlResult(url, headers, body)
return UrlResult(*result)


def fetch_sourcemap(url, logger=None):
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/templates/sentry/admin/status/env.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

{% load i18n %}
{% load sentry_helpers %}
{% get_sentry_version %}

{% block title %}{% trans "Server Status" %} | {{ block.super }}{% endblock %}

Expand All @@ -19,7 +20,7 @@ <h2>{% trans "Environment" %}</h2>
<tbody>
<tr>
<th>{% trans "Server Version" %}</th>
<td class="code">{% sentry_version %}</td>
<td class="code">{{ sentry_version.current }} {% if sentry_version.update_available %}<a href="#" title="You're running a year old version of Sentry, did you know {{ sentry_version.latest }} is available?" class="tip icon-circle-arrow-up">&nbsp;</a>{% endif %}</td>
</tr>
<tr>
<th>{% trans "Python Version" %}</th>
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/templates/sentry/layout.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% load i18n %}
{% load sentry_helpers %}
{% load static_compiler %}

{% get_sentry_version %}
<!DOCTYPE html>
<html lang="en">
<head>
Expand Down Expand Up @@ -156,7 +156,7 @@ <h1>{% block heading %}{% endblock %}</h1>
{% block footer %}
<div class="contribute">Sentry is <a href="http://en.wikipedia.org/wiki/Open-source_software">Open Source Software</a>
<a href="https://github.com/getsentry/sentry" class="btn btn-small">{% trans "Contribute" %}</a> <a href="https://www.transifex.com/projects/p/sentry/" class="btn btn-small">{% trans "Translate" %}</a></div>
<div class="version">Sentry {% sentry_version %}</div>
<div class="version">Sentry {{ sentry_version.current }} {% if sentry_version.update_available %}<a href="#" title="You're running a year old version of Sentry, did you know {{ sentry_version.latest }} is available?" class="tip icon-circle-arrow-up">&nbsp;</a>{% endif %}</div>
<div class="credits">Handcrafted by the <a href="https://www.getsentry.com">Sentry team</a> and its <a href="https://github.com/getsentry/sentry/contributors">many contributors</a></div>
{% endblock %}
</div>
Expand Down
23 changes: 18 additions & 5 deletions src/sentry/templatetags/sentry_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from paging.helpers import paginate as paginate_func
from sentry.conf import settings
from sentry.constants import STATUS_MUTED
from sentry.models import Group
from sentry.models import Group, Option
from sentry.web.helpers import group_is_public
from sentry.utils import to_unicode
from sentry.utils.avatar import get_gravatar_url
Expand All @@ -27,8 +27,13 @@
from sentry.utils.strings import truncatechars
from templatetag_sugar.register import tag
from templatetag_sugar.parser import Name, Variable, Constant, Optional

from collections import namedtuple
import datetime
from pkg_resources import parse_version as Version

SentryVersion = namedtuple('SentryVersion', ['current', 'latest',
'update_available'])


register = template.Library()

Expand Down Expand Up @@ -117,10 +122,18 @@ def is_none(value):
return value is None


@register.simple_tag
def sentry_version():
@register.simple_tag(takes_context=True)
def get_sentry_version(context):
import sentry
return sentry.get_version()
current = sentry.get_version()

latest = Option.objects.get_value('sentry:latest_version', current)
update_available = Version(latest) > Version(current)

context['sentry_version'] = SentryVersion(
current, latest, update_available
)
return ''


@register.filter
Expand Down
Empty file.
59 changes: 59 additions & 0 deletions tests/sentry/tasks/check_update/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import json

import mock

from sentry.plugins.helpers import get_option, set_option
from sentry.testutils import TestCase
from sentry.models import set_sentry_version, Option
from sentry.tasks.check_update import check_update, PYPI_URL


class CheckUpdateTest(TestCase):

OLD = '5.0.0'
CURRENT = '5.5.0-DEV'
NEW = '5.5.1'

KEY = 'sentry:latest_version'

def test_run_check_update_task(self):
with mock.patch('sentry.tasks.check_update.fetch_url_content') as fetch:
fetch.return_value = (
None, None, json.dumps({'info': {'version': self.NEW}})
)
check_update() # latest_version > current_version
fetch.assert_called_once_with(PYPI_URL)

self.assertEqual(get_option(key=self.KEY), self.NEW)

def test_run_check_update_task_with_bad_response(self):
with mock.patch('sentry.tasks.check_update.fetch_url_content') as fetch:
fetch.return_value = (None, None, '')
check_update() # latest_version == current_version
fetch.assert_called_once_with(PYPI_URL)

self.assertEqual(get_option(key=self.KEY), None)

def test_set_sentry_version_empty_latest(self):
set_sentry_version(latest=self.NEW)
self.assertEqual(get_option(key=self.KEY), self.NEW)

def test_set_sentry_version_new(self):
set_option(self.KEY, self.OLD)

with mock.patch('sentry.get_version') as get_version:
get_version.return_value = self.CURRENT

set_sentry_version(latest=self.NEW)

self.assertEqual(Option.objects.get_value(key=self.KEY), self.NEW)

def test_set_sentry_version_old(self):
set_option(self.KEY, self.NEW)

with mock.patch('sentry.get_version') as get_version:
get_version.return_value = self.CURRENT

set_sentry_version(latest=self.OLD)

self.assertEqual(Option.objects.get_value(key=self.KEY), self.NEW)
27 changes: 27 additions & 0 deletions tests/sentry/web/frontend/generic/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from sentry.models import Team
from sentry.testutils import TestCase, fixture, before
from sentry.plugins.helpers import set_option


class DashboardTest(TestCase):
Expand Down Expand Up @@ -52,3 +53,29 @@ def test_shows_error_when_no_teams_and_cannot_create(self):
self.assertTemplateUsed(resp, 'sentry/generic_error.html')
assert 'title' in resp.context
assert 'message' in resp.context


class UpdateAvailableTest(TestCase):

UPDATE_MESSAGE = "You're running a year old version of Sentry"

@before
def login_user(self):
Team.objects.create(name='test', owner=self.user)
self.login()

@fixture
def path(self):
return reverse('sentry')

def test_update_is_available(self):
resp = self.client.get(self.path)
assert self.UPDATE_MESSAGE not in resp.content

def test_update_is_not_available(self):
set_option('sentry:latest_version', '5.5.1')

with mock.patch('sentry.get_version') as get_version:
get_version.return_value = '5.5.0'
resp = self.client.get(self.path)
assert self.UPDATE_MESSAGE in resp.content