diff --git a/docs/config-webapp.rst b/docs/config-webapp.rst index 880738ef1..8daf1adeb 100644 --- a/docs/config-webapp.rst +++ b/docs/config-webapp.rst @@ -323,7 +323,7 @@ Finally, configure the nginx vhost: listen 80; location /static/ { - alias /opt/graphite/webapp/content/; + alias /opt/graphite/webapp/content/ } location / { diff --git a/docs/terminology.rst b/docs/terminology.rst index 8bc2db4e1..06e4cba43 100644 --- a/docs/terminology.rst +++ b/docs/terminology.rst @@ -18,6 +18,9 @@ terms mean in the context of Graphite. metric series See :term:`series` + + node + An element of the name of a :term:`series` separated by periods (``.``). precision See :term:`resolution` @@ -35,7 +38,7 @@ terms mean in the context of Graphite. series A named set of datapoints. A series is identified by a unique name, which is composed of - elements separated by periods (``.``) which are used to display the collection of series + elements (each, a :term:`node`) separated by periods (``.``) which are used to display the collection of series into a hierarchical tree. A series storing system load average on a server called ``apache02`` in datacenter ``metro_east`` might be named as ``metro_east.servers.apache02.system.load_average`` diff --git a/tox.ini b/tox.ini index fd18a64f1..135d1d484 100644 --- a/tox.ini +++ b/tox.ini @@ -62,7 +62,8 @@ deps = git+https://github.com/graphite-project/ceres.git#egg=ceres Django<3.2.99 pyparsing: pyparsing>=2.3.0,<3.0.0 - Sphinx<1.4 + alabaster==0.7.12 + Sphinx==1.3.6 jinja2<3.1.0 sphinx_rtd_theme urllib3 diff --git a/webapp/graphite/render/functions.py b/webapp/graphite/render/functions.py index 14794aac8..2ee2d9eb2 100644 --- a/webapp/graphite/render/functions.py +++ b/webapp/graphite/render/functions.py @@ -4002,9 +4002,11 @@ def holtWintersDeviation(gamma,actual,prediction,last_seasonal_dev): def holtWintersAnalysis(series, seasonality='1d'): alpha = gamma = 0.1 beta = 0.0035 - # season is currently one day seasonality_time = parseTimeOffset(seasonality) season_length = (seasonality_time.seconds + (seasonality_time.days * 86400)) // series.step + # season_length should be 2 or more + if season_length < 2: + season_length = 2 intercept = 0 slope = 0 intercepts = list() @@ -5100,7 +5102,10 @@ def applyByNode(requestContext, seriesList, nodeNum, templateFunction, newName=N """ prefixes = set() for series in seriesList: - prefix = '.'.join(series.name.split('.')[:nodeNum + 1]) + nodes = series.name.split('.') + if nodeNum >= len(nodes): + raise InputParameterError("{} do not contans {} nodes".format(series.name, nodeNum)) + prefix = '.'.join(nodes[:nodeNum + 1]) prefixes.add(prefix) results = [] newContext = requestContext.copy() @@ -5391,6 +5396,9 @@ def summarize(requestContext, seriesList, intervalString, func='sum', alignToFro def _summarizeValues(series, func, interval, newStart=None, newEnd=None): + if interval == 0: + raise InputParameterError("_summarizeValues(): interval parsed to 0") + if newStart is None: newStart = series.start if newEnd is None: diff --git a/webapp/tests/test_functions.py b/webapp/tests/test_functions.py index cc92475ca..509514e6d 100644 --- a/webapp/tests/test_functions.py +++ b/webapp/tests/test_functions.py @@ -18,7 +18,7 @@ except ImportError: # Django < 1.10 from django.core.urlresolvers import reverse -from graphite.errors import NormalizeEmptyResultError +from graphite.errors import NormalizeEmptyResultError, InputParameterError from graphite.functions import _SeriesFunctions, loadFunctions, safe from graphite.render.datalib import TimeSeries from graphite.render import functions @@ -4522,6 +4522,38 @@ def mock_data_fetcher(reqCtx, path_expression): ) self.assertEqual(result, expectedResults) + @patch('graphite.render.evaluator.prefetchData', lambda *_: None) + def test_applyByNode_Overflow(self): + seriesList = self._gen_series_list_with_data( + key=['servers.s1.disk.bytes_used', 'servers.s1.disk.bytes_free','servers.s2.disk.bytes_used','servers.s2.disk.bytes_free'], + start=0, + end=3, + data=[[10, 20, 30], [90, 80, 70], [1, 2, 3], [99, 98, 97]] + ) + + def mock_data_fetcher(reqCtx, path_expression): + rv = [] + for s in seriesList: + if s.name == path_expression or fnmatch(s.name, path_expression): + rv.append(s) + if rv: + return rv + raise KeyError('{} not found!'.format(path_expression)) + + with patch('graphite.render.evaluator.fetchData', mock_data_fetcher): + try: + functions.applyByNode( + self._build_requestContext( + startTime=datetime(1970, 1, 1, 0, 0, 0, 0, pytz.timezone(settings.TIME_ZONE)), + endTime=datetime(1970, 1, 1, 0, 9, 0, 0, pytz.timezone(settings.TIME_ZONE)) + ), + seriesList, 4, + 'divideSeries(%.disk.bytes_used, sumSeries(%.disk.bytes_*))' + ) + self.fail('must raise with InputParameterError') + except InputParameterError: + pass + @patch('graphite.render.evaluator.prefetchData', lambda *_: None) def test_applyByNode_newName(self): seriesList = self._gen_series_list_with_data( diff --git a/webapp/tests/test_readers_multi.py b/webapp/tests/test_readers_multi.py index 76ef5c1a2..6b3503a25 100644 --- a/webapp/tests/test_readers_multi.py +++ b/webapp/tests/test_readers_multi.py @@ -86,8 +86,9 @@ def test_MultiReader_get_intervals(self): reader = MultiReader([node1, node2]) intervals = reader.get_intervals() for interval in intervals: - self.assertEqual(int(interval.start), self.start_ts - 60) + self.assertIn(int(interval.start), [self.start_ts - 60, self.start_ts - 60 - 1]) self.assertIn(int(interval.end), [self.start_ts, self.start_ts - 1]) + self.assertIn(int(interval.end - interval.start), [59,60]) # Confirm fetch works. def test_MultiReader_fetch(self): diff --git a/webapp/tests/test_xss.py b/webapp/tests/test_xss.py new file mode 100644 index 000000000..7a3a2c9b7 --- /dev/null +++ b/webapp/tests/test_xss.py @@ -0,0 +1,42 @@ +import logging +import sys + +try: + from django.urls import reverse +except ImportError: # Django < 1.10 + from django.core.urlresolvers import reverse + +from .base import TestCase + +# Silence logging during tests +LOGGER = logging.getLogger() + +# logging.NullHandler is a python 2.7ism +if hasattr(logging, "NullHandler"): + LOGGER.addHandler(logging.NullHandler()) + +if sys.version_info[0] >= 3: + def resp_text(r): + return r.content.decode('utf-8') +else: + def resp_text(r): + return r.content + + +class RenderXSSTest(TestCase): + def test_render_xss(self): + url = reverse('render') + xssStr = '