commit 8ea32382cc467202707c0161d05b271846978669 Author: Damian Johnson atagar@torproject.org Date: Fri Sep 1 11:39:06 2017 -0700
Implement relay cache
There, that's more like what I was after. Adding methods to read and write relay data to the cache along with tests. --- nyx/__init__.py | 72 +++++++++++++++++++++++++++++++++++++++++++++-- test/cache.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 144 insertions(+), 15 deletions(-)
diff --git a/nyx/__init__.py b/nyx/__init__.py index 3c3c939..39c1e5d 100644 --- a/nyx/__init__.py +++ b/nyx/__init__.py @@ -31,6 +31,7 @@ Tor curses monitoring application. +- halt - stops daemon panels """
+import contextlib import distutils.spawn import os import sqlite3 @@ -42,8 +43,10 @@ import stem import stem.connection import stem.control import stem.util.conf +import stem.util.connection import stem.util.log import stem.util.system +import stem.util.tor_tools
__version__ = '1.4.6-dev' __release_date__ = 'April 28, 2011' @@ -339,7 +342,8 @@ def join(entries, joiner = ' ', size = None):
class Cache(object): """ - Cache for frequently used information. + Cache for frequently needed information. This persists to disk if we can, and + otherwise is an in-memory cache. """
def __init__(self): @@ -374,7 +378,43 @@ class Cache(object): for cmd in SCHEMA: self._conn.execute(cmd)
- def query(self, query, *param): + @contextlib.contextmanager + def write(self): + """ + Provides a context in which we can modify the cache. + + :returns: :class:`~nyx.CacheWriter` that can modify the cache + """ + + with self._conn: + yield CacheWriter(self) + + def relay_nickname(self, fingerprint): + """ + Provides the nickname associated with the given relay. + + :param str fingerprint: relay to look up + + :returns: **str** with the nickname ("Unnamed" if unset), and **None** if + no such relay exists + """ + + result = self._query('SELECT nickname FROM relays WHERE fingerprint=?', fingerprint).fetchone() + return result[0] if result else None + + def relay_address(self, fingerprint): + """ + Provides the (address, port) tuple where a relay is running. + + :param str fingerprint: fingerprint to be checked + + :returns: **tuple** with a **str** address and **int** port + """ + + result = self._query('SELECT address, or_port FROM relays WHERE fingerprint=?', fingerprint).fetchone() + return result if result else None # TODO: does this raise if fingerprint doesn't exist? + + def _query(self, query, *param): """ Performs a query on our cache. """ @@ -383,6 +423,34 @@ class Cache(object): return self._conn.execute(query, param)
+class CacheWriter(object): + def __init__(self, cache): + self._cache = cache + + def record_relay(self, fingerprint, address, or_port, nickname): + """ + Records relay metadata. + + :param str fingerprint: relay fingerprint + :param str address: ipv4 or ipv6 address + :param int or_port: ORPort of the relay + :param str nickname: relay nickname + + :raises: **ValueError** if provided data is malformed + """ + + if not stem.util.tor_tools.is_valid_fingerprint(fingerprint): + raise ValueError("'%s' isn't a valid fingerprint" % fingerprint) + elif not stem.util.tor_tools.is_valid_nickname(nickname): + raise ValueError("'%s' isn't a valid nickname" % nickname) + elif not stem.util.connection.is_valid_ipv4_address(address) and not stem.util.connection.is_valid_ipv6_address(address): + raise ValueError("'%s' isn't a valid address" % address) + elif not stem.util.connection.is_valid_port(or_port): + raise ValueError("'%s' isn't a valid port" % or_port) + + self._cache._query('INSERT OR REPLACE INTO relays(fingerprint, address, or_port, nickname) VALUES (?,?,?,?)', fingerprint, address, or_port, nickname) + + class Interface(object): """ Overall state of the nyx interface. diff --git a/test/cache.py b/test/cache.py index 91e9ab8..dd76c00 100644 --- a/test/cache.py +++ b/test/cache.py @@ -2,6 +2,7 @@ Unit tests for nyx.cache. """
+import re import tempfile import unittest
@@ -9,11 +10,6 @@ import nyx
from mock import Mock, patch
-FINGERPRINT = '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66' -ADDRESS = '208.113.165.162' -PORT = 1443 -NICKNAME = 'caersidi' -
class TestCache(unittest.TestCase): def setUp(self): @@ -26,10 +22,12 @@ class TestCache(unittest.TestCase): """
cache = nyx.cache() + self.assertEqual((0, 'main', ''), cache._query('PRAGMA database_list').fetchone()) + + with cache.write() as writer: + writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi')
- self.assertEqual((0, 'main', ''), cache.query('PRAGMA database_list').fetchone()) - cache.query('INSERT INTO relays(fingerprint, address, or_port, nickname) VALUES (?,?,?,?)', FINGERPRINT, ADDRESS, PORT, NICKNAME) - self.assertEqual(NICKNAME, cache.query('SELECT nickname FROM relays WHERE fingerprint=?', FINGERPRINT).fetchone()[0]) + self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66'))
def test_file_cache(self): """ @@ -39,12 +37,75 @@ class TestCache(unittest.TestCase): with tempfile.NamedTemporaryFile(suffix = '.sqlite') as tmp: with patch('nyx.data_directory', Mock(return_value = tmp.name)): cache = nyx.cache() + self.assertEqual((0, 'main', tmp.name), cache._query('PRAGMA database_list').fetchone())
- self.assertEqual((0, 'main', tmp.name), cache.query('PRAGMA database_list').fetchone()) - cache.query('INSERT INTO relays(fingerprint, address, or_port, nickname) VALUES (?,?,?,?)', FINGERPRINT, ADDRESS, PORT, NICKNAME) - cache._conn.commit() + with cache.write() as writer: + writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi')
nyx.CACHE = None - cache = nyx.cache() - self.assertEqual(NICKNAME, cache.query('SELECT nickname FROM relays WHERE fingerprint=?', FINGERPRINT).fetchone()[0]) + self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) + + @patch('nyx.data_directory', Mock(return_value = None)) + def test_relay_nickname(self): + """ + Basic checks for registering and fetching nicknames. + """ + + cache = nyx.cache() + + with cache.write() as writer: + writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi') + writer.record_relay('9695DFC35FFEB861329B9F1AB04C46397020CE31', '128.31.0.34', 9101, 'moria1') + writer.record_relay('74A910646BCEEFBCD2E874FC1DC997430F968145', '199.254.238.53', 443, 'longclaw') + + self.assertEqual('moria1', cache.relay_nickname('9695DFC35FFEB861329B9F1AB04C46397020CE31')) + self.assertEqual('longclaw', cache.relay_nickname('74A910646BCEEFBCD2E874FC1DC997430F968145')) + self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) + + self.assertEqual(None, cache.relay_nickname('66E1D8F00C49820FE8AA26003EC49B6F069E8AE3')) + + @patch('nyx.data_directory', Mock(return_value = None)) + def test_relay_address(self): + """ + Basic checks for registering and fetching nicknames. + """ + + cache = nyx.cache() + + with cache.write() as writer: + writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi') + writer.record_relay('9695DFC35FFEB861329B9F1AB04C46397020CE31', '128.31.0.34', 9101, 'moria1') + writer.record_relay('74A910646BCEEFBCD2E874FC1DC997430F968145', '199.254.238.53', 443, 'longclaw') + + self.assertEqual(('128.31.0.34', 9101), cache.relay_address('9695DFC35FFEB861329B9F1AB04C46397020CE31')) + self.assertEqual(('199.254.238.53', 443), cache.relay_address('74A910646BCEEFBCD2E874FC1DC997430F968145')) + self.assertEqual(('208.113.165.162', 1443), cache.relay_address('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) + + self.assertEqual(None, cache.relay_address('66E1D8F00C49820FE8AA26003EC49B6F069E8AE3')) + + @patch('nyx.data_directory', Mock(return_value = None)) + def test_record_relay_when_updating(self): + cache = nyx.cache() + + with cache.write() as writer: + writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi') + + self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) + + with cache.write() as writer: + writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '128.31.0.34', 9101, 'moria1') + + self.assertEqual('moria1', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) + + @patch('nyx.data_directory', Mock(return_value = None)) + def test_record_relay_when_invalid(self): + """ + Provide malformed information to record_relay. + """ + + with nyx.cache().write() as writer: + self.assertRaisesRegexp(ValueError, re.escape("'blarg' isn't a valid fingerprint"), writer.record_relay, 'blarg', '208.113.165.162', 1443, 'caersidi') + self.assertRaisesRegexp(ValueError, re.escape("'blarg' isn't a valid address"), writer.record_relay, '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', 'blarg', 1443, 'caersidi') + self.assertRaisesRegexp(ValueError, re.escape("'blarg' isn't a valid port"), writer.record_relay, '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 'blarg', 'caersidi') + self.assertRaisesRegexp(ValueError, re.escape("'~blarg' isn't a valid nickname"), writer.record_relay, '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, '~blarg')