Skip to content

Commit 4c60b01

Browse files
authored
Merge pull request #883 from deniszh/backport/1.1.x/pr-869_pr-872_pr-858_pr-858_pr-858_pr-874_pr-875_pr-880_pr-881_pr-882
[1.1.x] s390x support for travis (#869) | Fix #871: Adjust aggregator-rules input_pattern match greediness to support numeric matching after captured field (#872) | sanitize names when using them as tag value (#858) | sanitize names when us
2 parents a3eea27 + ebed8de commit 4c60b01

File tree

9 files changed

+172
-27
lines changed

9 files changed

+172
-27
lines changed

.travis.yml

+35-11
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
# http://travis-ci.org/#!/graphite-project/carbon
2+
dist: xenial
23
language: python
34
python: 2.7
4-
# sudo is needed to install libboost-python-dev, which is required by pyhash
5-
sudo: required
6-
dist: xenial
75

86
matrix:
97
include:
@@ -17,17 +15,43 @@ matrix:
1715
env:
1816
- TOXENV=py36
1917
- python: 3.7
20-
sudo: true
2118
env:
2219
- TOXENV=py37
23-
- python: "3.8-dev"
24-
sudo: true
20+
- python: 3.8
2521
env:
2622
- TOXENV=py38-pyhash
27-
- python: "3.8-dev"
23+
- python: 3.8
2824
env:
2925
- TOXENV=py38
30-
- python: "3.8-dev"
26+
- python: 3.8
27+
env:
28+
- TOXENV=lint
29+
- python: 2.7
30+
arch: s390x
31+
env:
32+
- TOXENV=py27
33+
- python: 2.7
34+
arch: s390x
35+
env:
36+
- TOXENV=lint
37+
- python: 3.5
38+
arch: s390x
39+
env:
40+
- TOXENV=py35
41+
- python: 3.6
42+
arch: s390x
43+
env:
44+
- TOXENV=py36
45+
- python: 3.7
46+
arch: s390x
47+
env:
48+
- TOXENV=py37
49+
- python: 3.8
50+
arch: s390x
51+
env:
52+
- TOXENV=py38
53+
- python: 3.8
54+
arch: s390x
3155
env:
3256
- TOXENV=lint
3357

@@ -36,10 +60,10 @@ env:
3660
- TOXENV=py27-pyhash
3761
- TOXENV=lint
3862

39-
before_install:
40-
- bash -c "if [[ '$TOXENV' == *pyhash* ]]; then sudo apt-get -qq update; sudo apt-get install -y libboost-python-dev; fi"
41-
4263
install:
64+
- if [[ $(uname -m) == 's390x' ]]; then sudo rm -rf $HOME/.cache/pip; fi
65+
- if echo "$TOXENV" | grep -q 'pyhash' ; then sudo apt-get -q install -y libboost-python-dev; fi
66+
- if echo "$TOXENV" | grep -q '^py2' ; then pip install --upgrade pip virtualenv; fi
4367
- pip install tox
4468

4569
script:

conf/carbon.conf.example

+7
Original file line numberDiff line numberDiff line change
@@ -294,11 +294,18 @@ WHISPER_FALLOCATE_CREATE = True
294294
# you are familiar with the code. If you are not, please don't
295295
# mess with this, you are asking for trouble :)
296296
#
297+
# You need the bcrypt, cryptography and pyasn1 python modules installed for
298+
# manhole to work.
299+
#
300+
# Generate host keys with:
301+
# `ckeygen -t rsa -f /example/host_keys/ssh_host_key_rsa`
302+
#
297303
# ENABLE_MANHOLE = False
298304
# MANHOLE_INTERFACE = 127.0.0.1
299305
# MANHOLE_PORT = 7222
300306
# MANHOLE_USER = admin
301307
# MANHOLE_PUBLIC_KEY = ssh-rsa AAAAB3NzaC1yc2EAAAABiwAaAIEAoxN0sv/e4eZCPpi3N3KYvyzRaBaMeS2RsOQ/cDuKv11dlNzVeiyc3RFmCv5Rjwn/lQ79y0zyHxw67qLyhQ/kDzINc4cY41ivuQXm2tPmgvexdrBv5nsfEpjs3gLZfJnyvlcVyWK/lId8WUvEWSWHTzsbtmXAF2raJMdgLTbQ8wE=
308+
# MANHOLE_HOST_KEY_DIR = /example/host_keys
302309

303310
# Patterns for all of the metrics this machine will store. Read more at
304311
# http://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol#Bindings

lib/carbon/aggregator/rules.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def build_regex(self):
128128
pre = input_part[:i]
129129
post = input_part[j + 2:]
130130
field_name = input_part[i + 2:j]
131-
regex_part = '%s(?P<%s>.+)%s' % (pre, field_name, post)
131+
regex_part = '%s(?P<%s>.+?)%s' % (pre, field_name, post)
132132

133133
else:
134134
i = input_part.find('<')
@@ -137,7 +137,7 @@ def build_regex(self):
137137
pre = input_part[:i]
138138
post = input_part[j + 1:]
139139
field_name = input_part[i + 1:j]
140-
regex_part = '%s(?P<%s>[^.]+)%s' % (pre, field_name, post)
140+
regex_part = '%s(?P<%s>[^.]+?)%s' % (pre, field_name, post)
141141
elif input_part == '*':
142142
regex_part = '[^.]+'
143143
else:

lib/carbon/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
MANHOLE_PORT=7222,
8989
MANHOLE_USER="",
9090
MANHOLE_PUBLIC_KEY="",
91+
MANHOLE_HOST_KEY_DIR="",
9192
RELAY_METHOD='rules',
9293
DYNAMIC_ROUTER=False,
9394
DYNAMIC_ROUTER_MAX_RETRIES=5,

lib/carbon/manhole.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
from twisted.conch.checkers import SSHPublicKeyDatabase
44
from twisted.conch.manhole import Manhole
55
from twisted.conch.manhole_ssh import TerminalRealm, ConchFactory
6+
from twisted.conch.openssh_compat.factory import OpenSSHFactory
67
from twisted.internet import reactor
78
from twisted.application.internet import TCPServer
89

910
from carbon.protocols import CarbonServerProtocol
1011
from carbon.conf import settings
12+
import carbon
1113

14+
from carbon.exceptions import CarbonConfigException
1215

13-
namespace = {}
16+
namespace = {'carbon': carbon}
1417

1518

1619
class PublicKeyChecker(SSHPublicKeyDatabase):
@@ -31,16 +34,27 @@ def createManholeListener():
3134

3235
if settings.MANHOLE_PUBLIC_KEY == 'None':
3336
credChecker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
34-
credChecker.addUser(settings.MANHOLE_USER, '')
37+
credChecker.addUser(settings.MANHOLE_USER.encode('utf-8'),
38+
''.encode('utf-8'))
3539
else:
3640
userKeys = {
37-
settings.MANHOLE_USER: settings.MANHOLE_PUBLIC_KEY,
41+
settings.MANHOLE_USER.encode('utf-8'):
42+
settings.MANHOLE_PUBLIC_KEY.encode('utf-8'),
3843
}
3944
credChecker = PublicKeyChecker(userKeys)
4045

4146
sshPortal = portal.Portal(sshRealm)
4247
sshPortal.registerChecker(credChecker)
4348
sessionFactory = ConchFactory(sshPortal)
49+
50+
# set ssh host keys
51+
if settings.MANHOLE_HOST_KEY_DIR == "":
52+
raise CarbonConfigException("MANHOLE_HOST_KEY_DIR not defined")
53+
openSSHFactory = OpenSSHFactory()
54+
openSSHFactory.dataRoot = settings.MANHOLE_HOST_KEY_DIR
55+
sessionFactory.publicKeys = openSSHFactory.getPublicKeys()
56+
sessionFactory.privateKeys = openSSHFactory.getPrivateKeys()
57+
4458
return sessionFactory
4559

4660

lib/carbon/tests/test_client.py

-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
)
66
from carbon.routers import DatapointRouter
77
from carbon.tests.util import TestSettings
8-
from carbon import instrumentation
98
import carbon.service # NOQA
109

1110
from twisted.internet import reactor
@@ -44,7 +43,6 @@ def getDestinations(self, key):
4443
yield destination
4544

4645

47-
@patch('carbon.state.instrumentation', Mock(spec=instrumentation))
4846
class ConnectedCarbonClientProtocolTest(TestCase):
4947
def setUp(self):
5048
self.router_mock = Mock(spec=DatapointRouter)
@@ -88,7 +86,6 @@ def test_send_datapoints(self):
8886
self.protocol.sendLine.assert_called_with(expected_line_to_send)
8987

9088

91-
@patch('carbon.state.instrumentation', Mock(spec=instrumentation))
9289
class CarbonClientFactoryTest(TestCase):
9390
def setUp(self):
9491
self.router_mock = Mock(spec=DatapointRouter)
@@ -127,7 +124,6 @@ def test_send_queued_should_call_protocol_send_queued(self):
127124
self.protocol_mock.sendQueued.assert_called_once_with()
128125

129126

130-
@patch('carbon.state.instrumentation', Mock(spec=instrumentation))
131127
class CarbonClientManagerTest(TestCase):
132128
timeout = 1.0
133129

@@ -190,7 +186,6 @@ def test_stop_client_removes_destination_from_router(self):
190186
self.router_mock.removeDestination.assert_called_once_with(dest)
191187

192188

193-
@patch('carbon.state.instrumentation', Mock(spec=instrumentation))
194189
class RelayProcessorTest(TestCase):
195190
timeout = 1.0
196191

lib/carbon/tests/test_protobuf.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import carbon.client as carbon_client
22
from carbon.routers import DatapointRouter
33
from carbon.tests.util import TestSettings
4-
from carbon import instrumentation
54
import carbon.service # NOQA
65

76
from carbon.carbon_pb2 import Payload
@@ -14,7 +13,7 @@
1413
from twisted.trial.unittest import TestCase
1514
from twisted.test.proto_helpers import StringTransport
1615

17-
from mock import Mock, patch
16+
from mock import Mock
1817
from struct import unpack, calcsize
1918

2019

@@ -35,7 +34,6 @@ def decode_sent(data):
3534
return datapoints
3635

3736

38-
@patch('carbon.state.instrumentation', Mock(spec=instrumentation))
3937
class ConnectedCarbonClientProtocolTest(TestCase):
4038
def setUp(self):
4139
self.router_mock = Mock(spec=DatapointRouter)

lib/carbon/tests/test_util.py

+67
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from carbon.util import parseDestinations
66
from carbon.util import enableTcpKeepAlive
7+
from carbon.util import TaggedSeries
78

89

910
class UtilTest(TestCase):
@@ -21,6 +22,72 @@ def setTcpKeepAlive(self, value):
2122
enableTcpKeepAlive(_Transport(), True, None)
2223
self.assertEquals(s.getsockopt(socket.SOL_TCP, socket.SO_KEEPALIVE), 1)
2324

25+
def test_sanitizing_name_as_tag_value(self):
26+
test_cases = [
27+
{
28+
'original': "my~.test.abc",
29+
'expected': "my~.test.abc",
30+
}, {
31+
'original': "a.b.c",
32+
'expected': "a.b.c",
33+
}, {
34+
'original': "~~a~~.~~~b~~~.~~~c~~~",
35+
'expected': "a~~.~~~b~~~.~~~c~~~",
36+
}, {
37+
'original': "a.b.c~",
38+
'expected': "a.b.c~",
39+
}, {
40+
'original': "~a.b.c",
41+
'expected': "a.b.c",
42+
}, {
43+
'original': "~a~",
44+
'expected': "a~",
45+
}, {
46+
'original': "~~~",
47+
'raises': True,
48+
}, {
49+
'original': "~",
50+
'raises': True,
51+
},
52+
]
53+
54+
for test_case in test_cases:
55+
if test_case.get('raises', False):
56+
self.assertRaises(
57+
Exception,
58+
TaggedSeries.sanitize_name_as_tag_value,
59+
test_case['original'],
60+
)
61+
else:
62+
result = TaggedSeries.sanitize_name_as_tag_value(test_case['original'])
63+
self.assertEquals(result, test_case['expected'])
64+
65+
def test_validate_tag_key_and_value(self):
66+
# assert that it raises exception when sanitized name is still not valid
67+
with self.assertRaises(Exception):
68+
# sanitized name is going to be '', which is not a valid tag value
69+
TaggedSeries.sanitize_name_as_tag_value('~~~~')
70+
71+
with self.assertRaises(Exception):
72+
# given tag value is invalid because it has length 0
73+
TaggedSeries.validateTagAndValue('metric.name;tag=')
74+
75+
with self.assertRaises(Exception):
76+
# given tag key is invalid because it has length 0
77+
TaggedSeries.validateTagAndValue('metric.name;=value')
78+
79+
with self.assertRaises(Exception):
80+
# given tag is missing =
81+
TaggedSeries.validateTagAndValue('metric.name;tagvalue')
82+
83+
with self.assertRaises(Exception):
84+
# given tag value is invalid because it starts with ~
85+
TaggedSeries.validateTagAndValue('metric.name;tag=~value')
86+
87+
with self.assertRaises(Exception):
88+
# given tag key is invalid because it contains !
89+
TaggedSeries.validateTagAndValue('metric.name;ta!g=value')
90+
2491

2592
# Destinations have the form:
2693
# <host> ::= <string without colons> | "[" <string> "]"

lib/carbon/util.py

+42-3
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,28 @@ def __init__(classObj, name, bases, members):
336336

337337

338338
class TaggedSeries(object):
339+
prohibitedTagChars = ';!^='
340+
341+
@classmethod
342+
def validateTagAndValue(cls, tag, value):
343+
"""validate the given tag / value based on the specs in the documentation"""
344+
if len(tag) == 0:
345+
raise Exception('Tag may not be empty')
346+
if len(value) == 0:
347+
raise Exception('Value for tag "{tag}" may not be empty'.format(tag=tag))
348+
349+
for char in cls.prohibitedTagChars:
350+
if char in tag:
351+
raise Exception(
352+
'Character "{char}" is not allowed in tag "{tag}"'.format(char=char, tag=tag))
353+
354+
if ';' in value:
355+
raise Exception(
356+
'Character ";" is not allowed in value "{value}" of tag {tag}'.format(value=value, tag=tag))
357+
358+
if value[0] == '~':
359+
raise Exception('Tag values are not allowed to start with "~" in tag "{tag}"'.format(tag=tag))
360+
339361
@classmethod
340362
def parse(cls, path):
341363
# if path is in openmetrics format: metric{tag="value",...}
@@ -362,10 +384,15 @@ def parse_openmetrics(cls, path):
362384
if not m:
363385
raise Exception('Cannot parse path %s, invalid segment %s' % (path, rawtags))
364386

365-
tags[m.group(1)] = m.group(2).replace(r'\"', '"').replace(r'\\', '\\')
387+
tag = m.group(1)
388+
value = m.group(2).replace(r'\"', '"').replace(r'\\', '\\')
389+
390+
cls.validateTagAndValue(tag, value)
391+
392+
tags[tag] = value
366393
rawtags = rawtags[len(m.group(0)):]
367394

368-
tags['name'] = metric
395+
tags['name'] = cls.sanitize_name_as_tag_value(metric)
369396
return cls(metric, tags)
370397

371398
@classmethod
@@ -384,11 +411,23 @@ def parse_carbon(cls, path):
384411
if len(tag) != 2 or not tag[0]:
385412
raise Exception('Cannot parse path %s, invalid segment %s' % (path, segment))
386413

414+
cls.validateTagAndValue(*tag)
415+
387416
tags[tag[0]] = tag[1]
388417

389-
tags['name'] = metric
418+
tags['name'] = cls.sanitize_name_as_tag_value(metric)
390419
return cls(metric, tags)
391420

421+
@staticmethod
422+
def sanitize_name_as_tag_value(name):
423+
"""take a metric name and sanitize it so it is guaranteed to be a valid tag value"""
424+
sanitized = name.lstrip('~')
425+
426+
if len(sanitized) == 0:
427+
raise Exception('Cannot use metric name %s as tag value, results in emptry string' % (name))
428+
429+
return sanitized
430+
392431
@staticmethod
393432
def format(tags):
394433
return tags.get('name', '') + ''.join(sorted([

0 commit comments

Comments
 (0)