Skip to content

Commit 1674c96

Browse files
committed
add latency measuring with ping command
1 parent b407336 commit 1674c96

15 files changed

+384
-12
lines changed

Diff for: latency_filter.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from oslo_log import log as logging
2+
import nova.conf
3+
4+
from nova.scheduler.filters import BaseHostFilter
5+
6+
from latency_meter.server import start_server_on_other_thread
7+
8+
LOG = logging.getLogger(__name__)
9+
10+
CONF = nova.conf.CONF
11+
12+
13+
class LatencyAwareFilter(BaseHostFilter):
14+
def __init__(self):
15+
super(LatencyAwareFilter, self).__init__()
16+
LOG.debug("GLLS Starting up")
17+
start_server_on_other_thread(LOG)
18+
19+
def host_passes(self, host_state, spec_obj):
20+
"""
21+
:type host_state: nova.scheduler.host_manager.HostState
22+
:type spec_obj: nova.objects.request_spec.RequestSpec
23+
"""
24+
LOG.debug("GLLS")
25+
LOG.debug("GLLS " + str(host_state))
26+
LOG.debug("GLLS" + str(spec_obj))
27+
return True

Diff for: latency_meter/__init__.py

Whitespace-only changes.

Diff for: latency_meter/client.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from latency_meter.client.api import MeasurementProvider
2+
3+
4+
class Client:
5+
def __init__(self, measurement_provider):
6+
"""
7+
8+
:type measurement_provider: MeasurementProvider
9+
"""
10+
self.measurement_provider = measurement_provider
11+
12+

Diff for: latency_meter/client/__init__.py

Whitespace-only changes.

Diff for: latency_meter/client/api.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class MeasurementProvider():
2+
pass
3+
4+
5+
class MeasuredLatency():
6+
def __init__(self, received_packet_count, average_latency, success, error_code=None):
7+
self.error_code = error_code
8+
self.success = success
9+
self.average_latency = average_latency
10+
self.packet_count = received_packet_count

Diff for: latency_meter/client/ping_measurement_provider.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from subprocess import call, check_output, CalledProcessError, STDOUT
2+
3+
from latency_meter.client.api import MeasuredLatency
4+
import re
5+
6+
7+
class Pinger():
8+
PING_COUNT = 3
9+
ERROR_UKNOWN_HOST = "unknown-host"
10+
ERROR_PACKET_LOSS = "100pc-packet-loss"
11+
ERROR_UNKNOWN = "unknown-error"
12+
13+
def __init__(self, command_runner):
14+
"""
15+
16+
:type command_runner: CommandRunner
17+
"""
18+
self.command_runner = command_runner
19+
self._first_line_regexp = re.compile(".* packets transmitted, (?P<received_count>[\d]*) received, .*")
20+
self._second_line_regexp = re.compile("rtt min\/avg\/max\/mdev \= (.*)/(?P<avg>.*)/(.*)/(.*) ms")
21+
self._uknown_host_regexp = re.compile("(.*)ping: unknown host(.*)")
22+
23+
def get_latency(self, target_host):
24+
# type: (str) -> MeasuredLatency
25+
26+
27+
ping_result = self._ping_host(target_host)
28+
29+
result = self._check_for_errors(ping_result)
30+
if result:
31+
return result
32+
33+
output = ping_result[1]
34+
35+
first_line_match = self._first_line_regexp.match(output[-2])
36+
packet_count = int(first_line_match.groups('received_count')[0])
37+
38+
second_line_match = self._second_line_regexp.match(output[-1])
39+
avg = second_line_match.group('avg')
40+
41+
return MeasuredLatency(
42+
received_packet_count=packet_count,
43+
average_latency=float(avg),
44+
success=True
45+
)
46+
47+
def _ping_host(self, target_host):
48+
return self.command_runner.run(["ping", "-Dn", "-c", self.PING_COUNT, target_host])
49+
50+
def _check_for_errors(self, run_result):
51+
return_code = run_result[0]
52+
output = run_result[1]
53+
54+
first_line_match = self._first_line_regexp.match(output[-1])
55+
56+
if first_line_match:
57+
packet_count = int(first_line_match.groups('received_count')[0])
58+
59+
if (packet_count == 0):
60+
return MeasuredLatency(
61+
received_packet_count=0,
62+
average_latency=None,
63+
success=False,
64+
error_code=self.ERROR_PACKET_LOSS
65+
)
66+
67+
if self._uknown_host_regexp.match(output[-1]):
68+
return MeasuredLatency(
69+
received_packet_count=0,
70+
average_latency=None,
71+
success=False,
72+
error_code=self.ERROR_UKNOWN_HOST
73+
)
74+
75+
if (return_code != 0):
76+
return MeasuredLatency(
77+
received_packet_count=None,
78+
average_latency=None,
79+
success=False,
80+
error_code=self.ERROR_UNKNOWN
81+
)
82+
return None
83+
84+
85+
class CommandRunner():
86+
def run(self, command):
87+
"""
88+
89+
:type command: list
90+
:returns (int, str) (rc,output)
91+
"""
92+
try:
93+
rc = 0
94+
result = check_output([str(c) for c in command], stderr=STDOUT)
95+
except CalledProcessError as e:
96+
rc = e.returncode
97+
result = e.output
98+
99+
return (rc, self._process_lines(result))
100+
101+
def _process_lines(self, result):
102+
return result.strip().splitlines()

Diff for: latency_meter/server/__init__.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import BaseHTTPServer
2+
import SimpleHTTPServer
3+
import SocketServer
4+
import threading
5+
from BaseHTTPServer import BaseHTTPRequestHandler
6+
from SimpleHTTPServer import SimpleHTTPRequestHandler
7+
from SocketServer import ThreadingTCPServer, TCPServer
8+
9+
10+
class MeasurementHandler():
11+
data = {}
12+
13+
def record_measurement(self, measurement):
14+
"""
15+
16+
:type measurement: Measurement
17+
"""
18+
self.data[measurement.from_host] = {
19+
measurement.to_host: measurement.average_latency
20+
}
21+
22+
def get_measurements_from_host(self, host):
23+
return self.data[host]
24+
25+
26+
class Measurement():
27+
def __init__(self, from_host, to_host, average_latency):
28+
self.average_latency = average_latency
29+
self.to_host = to_host
30+
self.from_host = from_host
31+
32+
33+
class MeasurementRequestHandler(SimpleHTTPRequestHandler):
34+
def __init__(self, request, client_address, server):
35+
BaseHTTPRequestHandler.__init__(self, request, client_address, server)
36+
print("blah")
37+
38+
def handle_one_request(self):
39+
print("handing request")
40+
BaseHTTPRequestHandler.handle_one_request(self)
41+
42+
def do_GET(self):
43+
self.send_response(200)
44+
self.end_headers()
45+
self.wfile.write("hello world")
46+
print(self.raw_requestline)
47+
print(self.request)
48+
self.wfile.close()
49+
50+
51+
class Httpserver(TCPServer):
52+
allow_reuse_address = 1
53+
54+
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, logger=None):
55+
TCPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate)
56+
self.logger = logger
57+
logger.debug("GLLS Inicialized server")
58+
59+
60+
def start_server(port=9913, logger=None):
61+
http_handler = MeasurementRequestHandler
62+
server = Httpserver(('0.0.0.0', port), http_handler, logger=logger)
63+
logger.debug("Listening on " + str(port))
64+
try:
65+
server.serve_forever()
66+
except KeyboardInterrupt as e:
67+
logger.debug("GLLS Shutting down")
68+
logger.debug("GLLS " + e.message)
69+
server.server_close()
70+
server.shutdown()
71+
72+
73+
def start_server_on_other_thread(logger):
74+
logger.debug("GLLS Server started")
75+
thread = threading.Thread(target=lambda: start_server(logger=logger))
76+
thread.start()
77+
78+
79+
class MockLogger():
80+
def debug(self, string):
81+
print(string)

Diff for: latency_meter/server/test_recording_measurements.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from unittest import TestCase
2+
3+
from latency_meter.server import Measurement, MeasurementHandler
4+
5+
6+
class TestMeasurementHandler(TestCase):
7+
def test_given_no_measurements_returns_none(self):
8+
handler = MeasurementHandler()
9+
self.assertIsNone(handler.get_measurements_from_host('blah'))
10+
11+
def test_given_a_server_when_recording_measurements_stores_average_latency(self):
12+
measurement = Measurement(
13+
from_host="192.11.00.12",
14+
to_host="192.00.00.99",
15+
average_latency=5.95
16+
)
17+
handler = MeasurementHandler()
18+
handler.record_measurement(measurement)
19+
got = handler.get_measurements_from_host('192.11.00.12')
20+
self.assertEqual(got, {
21+
'192.00.00.99': 5.95
22+
})

Diff for: latency_meter/tests/__init__.py

Whitespace-only changes.

Diff for: latency_meter/tests/integration/__init__.py

Whitespace-only changes.

Diff for: latency_meter/tests/integration/test_pinger.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import unittest
2+
3+
from latency_meter.client.ping_measurement_provider import Pinger, CommandRunner
4+
5+
6+
class TestPingerIntegration(unittest.TestCase):
7+
def test_given_a_real_command_runner_can_ping_localhost(self):
8+
pinger = Pinger(CommandRunner())
9+
result = pinger.get_latency("localhost")
10+
self.assertTrue(result.success)
11+
self.assertEqual(Pinger.PING_COUNT, result.packet_count)
12+
13+
def test_given_a_made_up_ip_100_pc_packet_loss(self):
14+
pinger = Pinger(CommandRunner())
15+
result = pinger.get_latency("240.0.0.1") #unallocated block
16+
self.assertFalse(result.success)
17+
self.assertEqual(Pinger.ERROR_PACKET_LOSS, result.error_code)
18+
self.assertEqual(0, result.packet_count)
19+
20+
def test_given_an_unknown_host_returns_unknown_host_error(self):
21+
pinger = Pinger(CommandRunner())
22+
result = pinger.get_latency("thisisanunknown.host")
23+
self.assertFalse(result.success)
24+
self.assertEqual(Pinger.ERROR_UKNOWN_HOST, result.error_code)
25+
self.assertEqual(0, result.packet_count)

Diff for: latency_meter/tests/test_pinger.py

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from unittest import TestCase
2+
3+
from latency_meter.client.api import MeasuredLatency
4+
from latency_meter.client.ping_measurement_provider import Pinger, CommandRunner
5+
6+
7+
class TestPinger(TestCase):
8+
def test_given_a_ping_calls_command_runner_with_host(self):
9+
command_runner = MockCommandRunner(
10+
command_output="3 packets transmitted, 3 received, 0% packet loss, time 1998ms\n"
11+
"rtt min/avg/max/mdev = 0.402/0.580/0.701/0.128 ms"
12+
)
13+
pinger = Pinger(command_runner=command_runner)
14+
pinger.get_latency("test-host")
15+
self.assertEqual(
16+
command_runner.was_called_with_command,
17+
["ping", "-Dn", "-c", Pinger.PING_COUNT, "test-host"]
18+
)
19+
20+
def test_given_a_ping_parses_output_and_returns_measured_latency(self):
21+
got = self.run_pinger_with_output(
22+
"3 packets transmitted, 3 received, 0% packet loss, time 1998ms\n"
23+
"rtt min/avg/max/mdev = 0.402/0.580/0.701/0.128 ms"
24+
)
25+
expected = MeasuredLatency(3, 0.58, success=True)
26+
self.assertEqual(expected.packet_count, got.packet_count)
27+
self.assertEqual(expected.average_latency, got.average_latency)
28+
self.assertTrue(got.success)
29+
30+
def test_given_more_ping_output_parses_correctly(self):
31+
ping_output = """PING index.hu (217.20.130.99) 56(84) bytes of data.
32+
[1491753086.899682] 64 bytes from 217.20.130.99: icmp_seq=1 ttl=57 time=9.43 ms
33+
[1491753087.900707] 64 bytes from 217.20.130.99: icmp_seq=2 ttl=57 time=8.77 ms
34+
[1491753088.902781] 64 bytes from 217.20.130.99: icmp_seq=3 ttl=57 time=8.90 ms
35+
36+
--- index.hu ping statistics ---
37+
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
38+
rtt min/avg/max/mdev = 8.773/9.039/9.437/0.286 ms
39+
"""
40+
got = self.run_pinger_with_output(ping_output)
41+
expected = MeasuredLatency(3, 9.039, True)
42+
self.assertEqual(expected.packet_count, got.packet_count)
43+
self.assertEqual(expected.average_latency, got.average_latency)
44+
self.assertTrue(got.success)
45+
46+
def test_given_100_percent_packet_loss_return_error(self):
47+
got = self.run_pinger_with_output("""
48+
PING 240.0.0.1 (240.0.0.1) 56(84) bytes of data.
49+
50+
--- 240.0.0.1 ping statistics ---
51+
3 packets transmitted, 0 received, 100% packet loss, time 1999ms
52+
53+
""")
54+
self.assertFalse(got.success)
55+
self.assertEqual(Pinger.ERROR_PACKET_LOSS, got.error_code)
56+
self.assertIsNone(got.average_latency)
57+
self.assertEqual(0, got.packet_count)
58+
59+
def test_given_a_command_returns_with_nonzero_return_code_but_the_error_doesnt_match_return_unknown_error(self):
60+
failing_runner = MockCommandRunner("This error was very unexpected.", return_code=1)
61+
got = self._run_with_command_runner(failing_runner)
62+
self.assertFalse(got.success)
63+
self.assertEqual(Pinger.ERROR_UNKNOWN, got.error_code)
64+
65+
def test_given_a_command_returns_unknown_host_expect_error_unknown_host(self):
66+
failing_runner = MockCommandRunner(
67+
"""ping: unknown host a-host.unknown
68+
"""
69+
, return_code=2)
70+
got = self._run_with_command_runner(failing_runner)
71+
self.assertFalse(got.success)
72+
self.assertEqual(Pinger.ERROR_UKNOWN_HOST, got.error_code)
73+
74+
def run_pinger_with_output(self, ping_output):
75+
command_runner = MockCommandRunner(
76+
command_output=ping_output
77+
)
78+
got = self._run_with_command_runner(command_runner)
79+
return got
80+
81+
def _run_with_command_runner(self, command_runner):
82+
pinger = Pinger(command_runner=command_runner)
83+
got = pinger.get_latency("test-host")
84+
return got
85+
86+
87+
class MockCommandRunner(CommandRunner):
88+
def __init__(self, command_output, return_code=0):
89+
self.rc = return_code
90+
self.was_called_with_command = None
91+
self.command_output = command_output
92+
93+
def run(self, command):
94+
self.was_called_with_command = command
95+
return (self.rc, self._process_lines(self.command_output))

Diff for: latency_scheduler.py

-12
This file was deleted.

Diff for: scheduler_context_spec.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
nova-scheduler.log:2017-04-08 18:42:00.917 8932 DEBUG nova.scheduler.filter_scheduler [req-e3a713de-66ec-4b7a-a83f-c67ace2f669a 5f14c47f62e2481ba6b79534a82bcda4 329b3f3bab484a62bc176caf142389b2 - - -] <Context {'domain': None, 'project_name': u'admin', 'project_domain': None, 'timestamp': '2017-04-08T18:41:59.543412', 'auth_token': u'gAAAAABY6SKmFcU6odoBvbO0NxGrDVd4lvz0OMHtGGAa1qRby8jT_2WhWUiC75uY_NXu133yq0wVntnBJTp2ZB9X9t_UlbbtyunNHvKoYaLA3bZjHt6dqZLU4tTOOdmvvmAVR9-NCfHahIkCQspzu_xNYb589s5TBf__L6aJgm5frnrkESg_1kfqq-nuDcXLXmSnTACl1at7', 'remote_address': u'10.199.0.11', 'quota_class': None, 'resource_uuid': None, 'is_admin': True, 'user': u'5f14c47f62e2481ba6b79534a82bcda4', 'service_catalog': [{u'endpoints': [{u'adminURL': u'http://controller:8778', u'region': u'RegionOne', u'internalURL': u'http://', u'publicURL': u'http://controller:8778'}], u'type': u'placement', u'name': u'placement'}], 'tenant': u'329b3f3bab484a62bc176caf142389b2', 'read_only': False, 'project_id': u'329b3f3bab484a62bc176caf142389b2', 'user_id': u'5f14c47f62e2481ba6b79534a82bcda4', 'show_deleted': False, 'roles': [u'admin'], 'user_identity': u'5f14c47f62e2481ba6b79534a82bcda4 329b3f3bab484a62bc176caf142389b2 - - -', 'is_admin_project': True, 'read_deleted': u'no', 'request_id': u'req-e3a713de-66ec-4b7a-a83f-c67ace2f669a', 'instance_lock_checked': False, 'user_domain': None, 'user_name': u'admin'}> select_destinations /usr/lib/python2.7/dist-packages/nova/scheduler/filter_scheduler.py:56
2+
nova-scheduler.log:2017-04-08 18:42:00.917 8932 DEBUG nova.scheduler.filter_scheduler [req-e3a713de-66ec-4b7a-a83f-c67ace2f669a 5f14c47f62e2481ba6b79534a82bcda4 329b3f3bab484a62bc176caf142389b2 - - -] RequestSpec(availability_zone='nova',flavor=Flavor(2),force_hosts=None,force_nodes=None,id=<?>,ignore_hosts=None,image=ImageMeta(93c6eb95-8456-4a56-8785-86539eddb50e),instance_group=None,instance_uuid=d63eaf51-defc-4772-94f9-a5a4529ab8c3,limits=SchedulerLimits,num_instances=1,numa_topology=None,pci_requests=InstancePCIRequests,project_id='329b3f3bab484a62bc176caf142389b2',requested_destination=None,retry=None,scheduler_hints={},security_groups=<?>) select_destinations /usr/lib/python2.7/dist-packages/nova/scheduler/filter_scheduler.py:57

0 commit comments

Comments
 (0)