commit 8ea32382cc467202707c0161d05b271846978669
Author: Damian Johnson <atagar(a)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')