[0-9.]+) ms')
19 |
20 |
21 | ProbeResults = collections.namedtuple(
22 | 'ProbeResults', ['loss', 'avg', 'target'])
23 |
24 |
25 | def hping3(target, count=128, *args, **kwargs):
26 | """Sends TCP SYN traffic to a target host.
27 |
28 | Note: Using hping3 requires not only hping3 be installed on the host
29 | system, but access as `root` (or sudo equivalent).
30 |
31 | Args:
32 | target: hostname or IP address of target
33 | count: number of datagrams to send
34 | args: catch for args not yet supported by this method
35 | kwargs: catch for kwargs not yet supported by this method
36 |
37 | Returns:
38 | a tuple containing (loss %, RTT average, target host)
39 | """
40 | cmd = 'sudo hping3 --interval u10000 --count %s --syn %s' % (
41 | count, target)
42 | code, out, err = util.runcmd(cmd)
43 | for line in err.split('\n'):
44 | logging.debug(line)
45 | match_loss = RE_LOSS.search(err)
46 | match_stats = RE_STATS.search(err)
47 | if match_loss and match_stats:
48 | results = ProbeResults(match_loss.group('loss'),
49 | match_stats.group('avg'),
50 | target)
51 | else:
52 | results = ProbeResults(None, None, target)
53 | return results
54 |
55 |
56 | def send_udp(target, count=500, port=util.DEFAULT_DST_PORT, tos=0x00,
57 | timeout=util.DEFAULT_TIMEOUT):
58 | """Sends UDP datagrams crafted for LLAMA reflectors to target host.
59 |
60 | Note: Using this method does NOT require `root` privileges.
61 |
62 | Args:
63 | target: hostname or IP address of target
64 | count: number of datagrams to send
65 | port: destination port to use for probes
66 | tos: hex type-of-service to use for probes
67 | timeout: seconds to wait for probe to return
68 |
69 | Returns:
70 | a tuple containing (loss %, RTT average, target host)
71 | """
72 | sender = udp.Sender(target, port, count, tos, timeout)
73 | sender.run()
74 | return ProbeResults(sender.stats.loss, sender.stats.rtt_avg, target)
75 |
--------------------------------------------------------------------------------
/llama/scraper.py:
--------------------------------------------------------------------------------
1 | """LLAMA TSDB Scraper
2 |
3 | This binary scrapes the LLAMA collectors for latency statistics and shovels
4 | them into a timeseries database.
5 | """
6 |
7 | import httplib
8 | import influxdb
9 | import json
10 | import logging
11 | import socket
12 |
13 |
14 | class Error(Exception):
15 | """Top-level error."""
16 |
17 |
18 | def http_get(server, port, uri, **headers):
19 | """Generic HTTP GET request.
20 |
21 | Args:
22 | uri: string containing the URI to query
23 | headers: HTTP headers to inject into request
24 |
25 | Returns:
26 | a tuple, (status_code, data_as_string)
27 | """
28 | # TODO(): Move this to requests library.
29 | httpconn = httplib.HTTPConnection(server, port)
30 | try:
31 | httpconn.request('GET', uri, "", headers)
32 | except socket.error as exc:
33 | raise Error('Could not connect to %s:%s (%s)' % (server, port, exc))
34 | response = httpconn.getresponse()
35 | return response.status, response.read()
36 |
37 |
38 | class CollectorClient(object):
39 | """A client for moving data from Collector to TSDB."""
40 |
41 | def __init__(self, server, port):
42 | """Constructor.
43 |
44 | Args:
45 | server: (str) collector server hostname or IP
46 | port: (int) collector TCP port
47 | """
48 | logging.info('Created a %s for %s:%s', self, server, port)
49 | self.server = server
50 | self.port = port
51 |
52 | def get_latency(self):
53 | """Gets /influxdata stats from collector.
54 |
55 | Returns:
56 | list of dictionary data (latency JSON)
57 |
58 | Raises:
59 | Error: if status code from collector is not 200
60 | """
61 | status, data = http_get(self.server, self.port, '/influxdata')
62 | # TODO(): this would be obviated by the requests library.
63 | if status < 200 or status > 299:
64 | logging.error('Error received getting latency from collector: '
65 | '%s:%s, code=%s' % (self.server, self.port, status))
66 | return json.loads(data)
67 |
68 | def push_tsdb(self, server, port, database, points):
69 | """Push latest datapoints to influxDB server.
70 |
71 | Args:
72 | server: (str) influxDB server hostname or IP
73 | port: (int) influxDB server TCP port
74 | database: (str) name of LLAMA database
75 | points: (list) dicts containing InfluxDB formatted datapoints
76 | """
77 | client = influxdb.InfluxDBClient(
78 | server, port, database=database)
79 | client.write_points(points)
80 |
81 | def run(self, server, port, database):
82 | """Get and push stats to TSDB."""
83 | try:
84 | points = self.get_latency()
85 | except Error as exc:
86 | logging.error(exc)
87 | return
88 | logging.info('Pulled %s datapoints from collector: %s',
89 | len(points), self.server)
90 | self.push_tsdb(server, port, database, points)
91 | logging.info('Pushed %s datapoints to TSDB: %s', len(points), server)
92 |
--------------------------------------------------------------------------------
/llama/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 | LAMA collector agent
3 | LAMA collector agent
4 |
5 | status |
6 | latency |
7 | influxdata
8 |
9 |
10 | Process uptime: {{ uptime }}
11 | Polling interval: {{ interval }} seconds
12 |
13 | Targets:
14 |
15 | {% for hostname, tags in targets -%}
16 | {{ hostname }}:
17 | {% for key, value in tags -%}
18 | {{key}}={{value}}
19 | {% endfor %}
20 | {% endfor %}
21 |
22 |
23 |
--------------------------------------------------------------------------------
/llama/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dropbox/llama-archive/afe310d762f444b6f8c4a445651f4bbfd9c7471d/llama/tests/__init__.py
--------------------------------------------------------------------------------
/llama/tests/metrics_test.py:
--------------------------------------------------------------------------------
1 | """Unittests for metrics lib."""
2 |
3 | from llama import metrics
4 | import pytest
5 | import time
6 |
7 |
8 | @pytest.fixture
9 | def two_things():
10 | class Thing(object):
11 | dp1 = metrics.Datapoint('dp1')
12 | dp2 = metrics.Datapoint('dp2')
13 | return Thing(), Thing()
14 |
15 |
16 | class TestDatapoint(object):
17 | """Let's test the Datapoint descriptor."""
18 |
19 | def test_setget_single(self, two_things, monkeypatch):
20 | obj1, obj2 = two_things
21 | monkeypatch.setattr(time, 'time', lambda: 100)
22 | obj1.dp1 = 1241
23 | assert obj1.dp1 == ('dp1', 1241, 100)
24 |
25 | def test_setget_multidatapoint(self, two_things, monkeypatch):
26 | obj1, obj2 = two_things
27 | monkeypatch.setattr(time, 'time', lambda: 999)
28 | obj1.dp1 = 1241
29 | obj1.dp2 = 0.1111
30 | assert obj1.dp1 == ('dp1', 1241, 999)
31 | assert obj1.dp2 == ('dp2', 0.1111, 999)
32 |
33 | def test_setget_multiclass(self, two_things, monkeypatch):
34 | obj1, obj2 = two_things
35 | monkeypatch.setattr(time, 'time', lambda: 12345)
36 | obj1.dp1 = 1241
37 | obj1.dp2 = 0.1111
38 | obj2.dp1 = 3000
39 | obj2.dp2 = 1234567890123456
40 | assert obj1.dp2 == ('dp2', 0.1111, 12345)
41 | assert obj2.dp1 == ('dp1', 3000, 12345)
42 | assert obj2.dp2 == ('dp2', 1234567890123456, 12345)
43 | # another round
44 | obj1.dp1 = 0
45 | obj1.dp2 = 10.1111
46 | assert obj1.dp1 == ('dp1', 0, 12345)
47 | assert obj1.dp2 == ('dp2', 10.1111, 12345)
48 |
49 |
50 | @pytest.fixture
51 | def m1():
52 | m = metrics.Metrics(src='host2',
53 | metro='iad', facility='iad2', cluster='iad2a')
54 | return m
55 |
56 |
57 | class TestMetrics(object):
58 |
59 | def test_constructor(self, m1):
60 | assert m1
61 |
62 | def test_tags(self, m1):
63 | assert m1.tags == {
64 | 'src': 'host2',
65 | 'metro': 'iad',
66 | 'facility': 'iad2',
67 | 'cluster': 'iad2a'
68 | }
69 |
70 | def test_data(self, m1):
71 | assert len(m1.data) == 2
72 |
73 | def test_as_dict(self, m1, monkeypatch):
74 | monkeypatch.setattr(time, 'time', lambda: 100)
75 | m1.rtt = 1
76 | m1.loss = 2
77 | assert m1.as_dict['tags'] == {
78 | 'src': 'host2',
79 | 'metro': 'iad',
80 | 'facility': 'iad2',
81 | 'cluster': 'iad2a'
82 | }
83 | assert ('rtt', 1, 100) in m1.as_dict['data']
84 | assert ('loss', 2, 100) in m1.as_dict['data']
85 |
86 | def test_as_influx(self, monkeypatch):
87 | monkeypatch.setattr(time, 'time', lambda: 100)
88 | m1 = metrics.Metrics(src='a', dst='b')
89 | m1.rtt = 70
90 | m1.loss = 1.2
91 | point1 = {
92 | 'measurement': 'rtt',
93 | 'tags': {
94 | 'src': 'a',
95 | 'dst': 'b',
96 | },
97 | 'time': 100000000000,
98 | 'fields': {
99 | 'value': 70
100 | }
101 | }
102 | point2 = {
103 | 'measurement': 'loss',
104 | 'tags': {
105 | 'src': 'a',
106 | 'dst': 'b',
107 | },
108 | 'time': 100000000000,
109 | 'fields': {
110 | 'value': 1.2
111 | }
112 | }
113 | assert type(m1.as_influx) is list
114 | assert point1 in m1.as_influx
115 | assert point2 in m1.as_influx
116 |
--------------------------------------------------------------------------------
/llama/tests/ping_test.py:
--------------------------------------------------------------------------------
1 | """Unittests for metrics lib."""
2 |
3 | from llama import ping
4 | from llama import util
5 | import pytest
6 |
7 |
8 | def fake_runcmd(cmd):
9 | stderr = '''
10 | --- shelby hping statistic ---
11 | 5 packets transmitted, 5 packets received, 0% packet loss
12 | round-trip min/avg/max = 0.1/0.1/0.2 ms
13 | '''
14 | stdout = '''
15 | HPING shelby (eth0 108.160.167.85): S set, 40 headers + 0 data bytes
16 | len=46 ip=1.1.7.5 ttl=61 DF id=4696 sport=0 flags=RA seq=0 win=0 rtt=0.1 ms
17 | len=46 ip=1.1.7.5 ttl=61 DF id=4699 sport=0 flags=RA seq=1 win=0 rtt=0.1 ms
18 | len=46 ip=1.1.7.5 ttl=61 DF id=4701 sport=0 flags=RA seq=2 win=0 rtt=0.1 ms
19 | len=46 ip=1.1.7.5 ttl=61 DF id=4702 sport=0 flags=RA seq=3 win=0 rtt=0.1 ms
20 | len=46 ip=1.1.7.5 ttl=61 DF id=4704 sport=0 flags=RA seq=4 win=0 rtt=0.1 ms
21 | '''
22 | return 0, stdout, stderr
23 |
24 |
25 | class TestHping3(object):
26 |
27 | def silence_pyflakes(self):
28 | """PyFlakes complains because we don't explicitly use the module."""
29 | dir(pytest)
30 |
31 | def test_good(self, monkeypatch):
32 | monkeypatch.setattr(util, 'runcmd', fake_runcmd)
33 | assert ping.hping3('somehost', count=5) == ('0', '0.1', 'somehost')
34 |
--------------------------------------------------------------------------------
/llama/tests/udp_test.py:
--------------------------------------------------------------------------------
1 | """Unittests for udp lib"""
2 |
3 | from llama.udp import UdpData, UdpStats, Sender, Ipv4UdpSocket
4 | import pytest
5 |
6 | class TestSender(object):
7 |
8 | def test_stats(self):
9 | mock_results = [
10 | UdpData(
11 | Ipv4UdpSocket.SIGNATURE, # Signature
12 | 0x00, # ToS
13 | 1496847778307.926, # Sent timestamp
14 | 1496847778320.653, # Rcvd timestamp
15 | 12.727022171020508, # RTT
16 | False, # Lost
17 | ),
18 | UdpData(
19 | Ipv4UdpSocket.SIGNATURE, # Signature
20 | 0x00, # ToS
21 | 0, # Sent timestamp
22 | 0, # Rcvd timestamp
23 | 0, # RTT
24 | True, # Lost
25 | ),
26 | UdpData(
27 | Ipv4UdpSocket.SIGNATURE, # Signature
28 | 0x00, # ToS
29 | 1496847925937.957, # Sent timestamp
30 | 1496847925952.936, # Rcvd timestamp
31 | 14.978885650634766, # RTT
32 | False, # Lost
33 | ),
34 | ]
35 | # TODO: This should be updated to reflect the commented/correct
36 | # values once #27 has been resolved.
37 | mock_stats = UdpStats(
38 | 3, # sent
39 | 1, # lost
40 | 33.33333333333333, # loss
41 | 14.978885650634766, # rtt_max
42 | # 12.727022171020508, # rtt_min
43 | 0, # rtt_min - This is due to #27
44 | # 13.852953910827637, # rtt_avg
45 | 9.235302607218424, # rtt_avg - This is due to #27
46 | )
47 | sender = Sender('127.0.0.1', 60000, 3, tos=0x00, timeout=0.2)
48 | sender.results = mock_results
49 | stats = sender.stats
50 | assert stats == mock_stats
51 |
--------------------------------------------------------------------------------
/llama/tests/util_test.py:
--------------------------------------------------------------------------------
1 | """Unittests for util lib."""
2 |
3 | from llama import util
4 | import pytest # noqa
5 |
6 |
7 | class TestUtil(object):
8 |
9 | def test_mean(self):
10 | """Test ``util.mean()``"""
11 | items = [2, 4, 6, 8, 10]
12 | expected = 6
13 | result = util.mean(items)
14 | assert expected == result
15 |
16 | def test_array_split(self):
17 | """Test ``util.array_split()``"""
18 | items = range(90)
19 | expected_lengths = (50, 40)
20 |
21 | batches = util.array_split(items, 50)
22 | for idx, batch in enumerate(batches):
23 | assert len(batch) == expected_lengths[idx]
24 |
25 | def test_runcmd(self):
26 | """Test ``util.runcmd()``"""
27 | results = util.runcmd('echo something')
28 | assert results.returncode == 0
29 | assert results.stdout == 'something\n'
30 | assert not results.stderr
31 | results = util.runcmd('ls /somethingthatdoesntexist__16481916571')
32 | assert results.returncode == 2
33 | assert results.stderr
34 | assert not results.stdout
35 |
--------------------------------------------------------------------------------
/llama/udp.py:
--------------------------------------------------------------------------------
1 | """UDP Packet Library for LLAMA
2 |
3 | This library provides several functions:
4 | * Custom IPv4 UDP sockets
5 | * Sender class
6 | * Reflector class
7 |
8 | The custom socket class provides the capability to carry timestamps, TOS
9 | markings, and other data encoding. Specialty methods send and receive these
10 | pieces of data. Usually, setting IP_TOS in the IP header is simple, but in
11 | Python reading TOS bits becomes difficult unless using raw sockets. We'd like
12 | to avoid raw sockets so our application doesn't need to run as `root`. To
13 | solve this we encode TOS bits into the payload of the datagrams.
14 |
15 | The Sender class sends large quantities of UDP probes in batches.
16 |
17 | The Reflector class runs a simple loop: receive, decode TOS, set timestamp,
18 | encode TOS, send back.
19 |
20 | TOS is encoded as 8-bits (1-byte, 2-hex digits). See
21 | https://www.tucny.com/Home/dscp-tos for a reference.
22 | """
23 |
24 | import collections
25 | import concurrent.futures
26 | import logging
27 | import socket
28 | import struct
29 | import time
30 |
31 | from llama import util
32 |
33 |
34 | # Data payload structure for probes
35 | # Encoding timestamps in packet data reduces the amount of tracking we have to
36 | # do in code. TOS bits can be set on outbound UDP packets, but are difficult
37 | # to read back with getsockopt() -- placing in payload helps this as well.
38 | # We include a 'signature' to help reject non-LLAMA related datagrams.
39 | UdpData = collections.namedtuple(
40 | 'UdpData', ['signature', # Unique LLAMA signature
41 | 'tos', # TOS bits, expressed as 1 byte in hex
42 | 'sent', # Time datagram was placed on wire in ms
43 | 'rcvd', # Time datagram was returned to sender in ms
44 | 'rtt', # Total round-trip time in ms
45 | 'lost']) # Boolean, was our packet returned to sender?
46 |
47 |
48 | # UDP statistics returned at the end of each probe cycle.
49 | UdpStats = collections.namedtuple(
50 | 'UdpStats', ['sent', # How many datagrams were sent
51 | 'lost', # How many datagrams were not returned
52 | 'loss', # Loss, expressed as a percentage
53 | 'rtt_max', # Maximum round trip time
54 | 'rtt_min', # Minimum round trip time
55 | 'rtt_avg']) # Average (mean) round trip time
56 |
57 |
58 | class Ipv4UdpSocket(socket.socket):
59 | """Custom IPv4 UDP socket which tracks TOS and timestamps.
60 |
61 | Note: We inherit from the socket.socket() class for ease of use, but due
62 | to restrictions in C bindings, we cannot override builtin methods
63 | like sendto() and recvfrom(). For those methods, we make special
64 | methods below: tos_sendto() and tos_recvfrom().
65 | """
66 |
67 | SIGNATURE = '__llama__' # Identify LLAMA packets from other UDP
68 | FORMAT = '<10sBddd?' # Used to pack/unpack struct data
69 |
70 | def __init__(self, tos=0x00, timeout=util.DEFAULT_TIMEOUT):
71 | """Constructor.
72 |
73 | Args:
74 | tos: (hex) TOS bits expressed as 2-bytes
75 | timeout: (float) Number of seconds to block/wait socket operation
76 | """
77 | super(Ipv4UdpSocket, self).__init__(socket.AF_INET, socket.SOCK_DGRAM,
78 | socket.IPPROTO_UDP)
79 | self._tos = tos & 0xff # [6-bits TOS] [2-bits ECN]
80 | self.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, self._tos)
81 | self.settimeout(timeout)
82 | self.processed = 0
83 |
84 | def tos_sendto(self, ip, port):
85 | """Mimic the behavior of socket.sendto() with special behavior.
86 |
87 | Note: Data is excluded from arguments since we encode our own.
88 |
89 | Args:
90 | ip: (str) destination IP address
91 | port: (int) destination UDP port
92 |
93 | Returns:
94 | (int) the number of bytes sent on the socket
95 | """
96 | return self.sendto(struct.pack(self.FORMAT, self.SIGNATURE, self._tos,
97 | time.time() * 1000, 0, 0, False),
98 | (ip, port))
99 |
100 | def tos_recvfrom(self, bufsize=512):
101 | """Mimic the behavior of socket.recvfrom() with special behavior.
102 |
103 | Args:
104 | bufsize: (int) number of bytes to read from socket
105 | It's not advisable to change this.
106 |
107 | Returns:
108 | (UdpData) namedtuple containing timestamps
109 | """
110 | try:
111 | data, addr = self.recvfrom(bufsize)
112 | rcvd = time.time() * 1000
113 | results = UdpData._make(struct.unpack(self.FORMAT, data))
114 | rtt = rcvd - results.sent
115 | return results._replace(rcvd=rcvd, rtt=rtt, lost=False)
116 | except socket.timeout:
117 | logging.debug('Timed out after {}s waiting to receive'.format(
118 | self.gettimeout()))
119 | return UdpData(self.SIGNATURE, self._tos, 0, 0, 0, True)
120 |
121 | def tos_reflect(self, bufsize=512):
122 | """Intended to be the sole operation on a LLAMA reflector.
123 |
124 | Args:
125 | bufsize: (int) number of bytes to read from socket
126 | It's not advisable to change this.
127 | """
128 | data, addr = self.recvfrom(bufsize)
129 | try:
130 | udpdata = UdpData._make(struct.unpack(self.FORMAT, data))
131 | except struct.error:
132 | logging.warn('Received malformed datagram of %s bytes. '
133 | 'Discarding.', len(data))
134 | # Don't reflect invalid data
135 | return
136 | self.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, udpdata.tos)
137 | self.sendto(data, addr)
138 | self.processed += 1
139 | if self.processed % 512 == 0:
140 | logging.info('Processed packets: %s', self.processed)
141 |
142 |
143 | class Sender(object):
144 | """UDP Sender class capable of sending/receiving UDP probes."""
145 |
146 | def __init__(self, target, port, count, tos=0x00,
147 | timeout=util.DEFAULT_TIMEOUT):
148 | """Constructor.
149 |
150 | Args:
151 | target: (str) IP address or hostname of destination
152 | port: (int) UDP port of destination
153 | count: (int) number of UDP datagram probes to send
154 | tos: (hex) TOS bits
155 | timeout: (float) in seconds
156 | """
157 | self.target = target
158 | self.port = port
159 | sockets = []
160 | for x in range(0, count):
161 | sock = Ipv4UdpSocket(tos=tos, timeout=timeout)
162 | sock.bind(('', 0))
163 | sockets.append(sock)
164 | self.batches = util.array_split(sockets, 50)
165 |
166 | def send_and_recv(self, batch):
167 | """Send and receive a single datagram and store results.
168 |
169 | Args:
170 | batch: (list of socket objects) for sending/receiving
171 | """
172 | for sock in batch:
173 | sock.tos_sendto(self.target, self.port)
174 | self.results.append(sock.tos_recvfrom())
175 |
176 | def run(self):
177 | """Run the sender."""
178 | self.results = []
179 | exception_jobs = []
180 | jobs = []
181 | with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
182 | for batch in self.batches:
183 | jobs.append(executor.submit(self.send_and_recv, batch))
184 | for job in concurrent.futures.as_completed(jobs):
185 | # Results should be getting collected as part of the job
186 | # So just handle logging any exceptions.
187 | if job.exception():
188 | exception_jobs.append(job)
189 | for result in self.results:
190 | logging.debug(result)
191 | if len(exception_jobs) > 0:
192 | logging.critical("Encountered {} exceptions while running Sender. "
193 | "Logging one such exception as an "
194 | "example.".format(len(exception_jobs)))
195 | try:
196 | exception_jobs[0].result()
197 | except Exception as e:
198 | logging.exception(e)
199 |
200 | @property
201 | def stats(self):
202 | """Returns a namedtuple containing UDP loss/latency results."""
203 | sent = len(self.results)
204 | if sent is 0:
205 | logging.critical('Sender has zero results, likely as a '
206 | 'result of exceptions during probing')
207 | # TODO: Better handling for this requires a greater refactor
208 | return UdpStats(0, 0, 0.0, 0.0, 0.0, 0.0)
209 | lost = sum(x.lost for x in self.results)
210 | loss = (float(lost) / float(sent)) * 100
211 | # TODO: This includes 0 values for instances of loss
212 | # Handling this requires more work around null
213 | # values along the various components and DB
214 | rtt_values = [x.rtt for x in self.results]
215 | rtt_min = min(rtt_values)
216 | rtt_max = max(rtt_values)
217 | rtt_avg = util.mean(rtt_values)
218 | return UdpStats(sent, lost, loss, rtt_max, rtt_min, rtt_avg)
219 |
220 |
221 | class Reflector(object):
222 | """Simple Reflector class."""
223 |
224 | def __init__(self, port):
225 | self.sock = Ipv4UdpSocket()
226 | self.sock.bind(('', port))
227 | sockname = self.sock.getsockname()
228 | logging.info('LLAMA reflector listening on %s udp/%s',
229 | sockname[0], sockname[1])
230 | self.sock.setblocking(1)
231 |
232 | def run(self):
233 | while True:
234 | self.sock.tos_reflect()
235 |
--------------------------------------------------------------------------------
/llama/util.py:
--------------------------------------------------------------------------------
1 | """Utility library for LLAMA.
2 |
3 | This provides utility functions used across the project.
4 | """
5 |
6 | import collections
7 | import logging
8 | import shlex
9 | import subprocess
10 |
11 |
12 | # Default port for targets
13 | # Primarily used for UDP, as dst for the collector, and
14 | # the listening port on the reflector
15 | DEFAULT_DST_PORT = 60000
16 | # Default timeout for probes
17 | # Determines how long to wait until counting it as a loss
18 | DEFAULT_TIMEOUT = 0.2
19 |
20 | CommandResults = collections.namedtuple(
21 | 'CommandResults', ['returncode', 'stdout', 'stderr'])
22 |
23 |
24 | def array_split(iterable, n):
25 | """Split a list into chunks of ``n`` length."""
26 | for i in range(0, len(iterable), n):
27 | yield iterable[i:i + n]
28 |
29 |
30 | def mean(iterable):
31 | """Returns the average of the list of items."""
32 | return sum(iterable) / len(iterable)
33 |
34 |
35 | def runcmd(command):
36 | """Runs a command in sub-shell.
37 |
38 | Args:
39 | command: string containing the command
40 |
41 | Returns:
42 | a namedtuple containing (returncode, stdout, stderr)
43 | """
44 | stdout = ''
45 | cmd = shlex.split(command)
46 | runner = subprocess.Popen(
47 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
48 | while True:
49 | out = runner.stdout.readline()
50 | if out == '' and runner.poll() is not None:
51 | break
52 | if out:
53 | logging.debug(out.strip())
54 | stdout += out
55 | return CommandResults(runner.returncode, stdout, runner.stderr.read())
56 |
--------------------------------------------------------------------------------
/llama/version.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.1.1'
2 |
--------------------------------------------------------------------------------
/requirements-ci.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 | pytest==2.7.0
3 | Sphinx==1.3.6
4 | sphinx-autobuild==0.6.0
5 | sphinx-rtd-theme==0.1.9
6 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | apscheduler>=3.2.0
2 | docopt>=0.6.2
3 | flask>=0.10.1
4 | futures>=3.0.3
5 | humanfriendly>=1.44.3
6 | influxdb>=2.12.0
7 | pyyaml>=3.11
8 | ipaddress>=1.0.14
9 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | from setuptools import find_packages, setup
5 |
6 | execfile('llama/version.py')
7 |
8 | with open('requirements.txt') as fh:
9 | required = fh.read().splitlines()
10 |
11 | setup(
12 | name='llama',
13 | version=str(__version__),
14 | description='LLAMA - Loss & LAtency MAtrix',
15 | url='https://github.com/dropbox/llama',
16 | author='Bryan Reed',
17 | maintainer='Daniel Martin',
18 | author_email='breed@dropbox.com',
19 | maintainer_email='dmar@dropbox.com',
20 | license='Apache',
21 | classifiers=[
22 | 'Development Status :: 1 - Planning',
23 | 'Intended Audience :: System Administrators',
24 | 'License :: OSI Approved :: Apache Software License',
25 | 'Operating System :: POSIX :: Linux',
26 | 'Programming Language :: Python :: 2.7',
27 | 'Topic :: System :: Networking :: Monitoring',
28 | ],
29 | keywords='llama udp loss latency matrix probe packet',
30 | scripts=['bin/llama_collector'],
31 | packages=find_packages(exclude=['docs', 'tests*']),
32 | include_package_data=True,
33 | zip_safe=False,
34 | install_requires=required,
35 | )
36 |
--------------------------------------------------------------------------------