Updated and added a lot of unittests.
[quix0rs-apt-p2p.git] / apt_p2p / apt_p2p_conf.py
1
2 """Loading of configuration files and parameters.
3
4 @type version: L{twisted.python.versions.Version}
5 @var version: the version of this program
6 @type DEFAULT_CONFIG_FILES: C{list} of C{string}
7 @var DEFAULT_CONFIG_FILES: the default config files to load (in order)
8 @var DEFAULTS: the default config parameter values for the main program
9 @var DHT_DEFAULTS: the default config parameter values for the default DHT
10
11 """
12
13 import os, sys
14 from ConfigParser import SafeConfigParser
15
16 from twisted.python import log, versions
17 from twisted.trial import unittest
18
19 class ConfigError(Exception):
20     """Errors that occur in the loading of configuration variables."""
21     def __init__(self, message):
22         self.message = message
23     def __str__(self):
24         return repr(self.message)
25
26 version = versions.Version('apt-p2p', 0, 0, 0)
27
28 # Set the home parameter
29 home = os.path.expandvars('${HOME}')
30 if home == '${HOME}' or not os.path.isdir(home):
31     home = os.path.expanduser('~')
32     if not os.path.isdir(home):
33         home = os.path.abspath(os.path.dirname(sys.argv[0]))
34
35 DEFAULT_CONFIG_FILES=['/etc/apt-p2p/apt-p2p.conf',
36                       home + '/.apt-p2p/apt-p2p.conf']
37
38 DEFAULTS = {
39
40     # Port to listen on for all requests (TCP and UDP)
41     'PORT': '9977',
42     
43     # The rate to limit sending data to peers to, in KBytes/sec.
44     # Set this to 0 to not limit the upload bandwidth.
45     'UPLOAD_LIMIT': '0',
46
47     # Directory to store the downloaded files in
48     'CACHE_DIR': home + '/.apt-p2p/cache',
49     
50     # Other directories containing packages to share with others
51     # WARNING: all files in these directories will be hashed and available
52     #          for everybody to download
53     'OTHER_DIRS': """""",
54     
55     # User name to try and run as
56     'USERNAME': '',
57     
58     # Whether it's OK to use an IP address from a known local/private range
59     'LOCAL_OK': 'no',
60
61     # Unload the packages cache after an interval of inactivity this long.
62     # The packages cache uses a lot of memory, and only takes a few seconds
63     # to reload when a new request arrives.
64     'UNLOAD_PACKAGES_CACHE': '5m',
65
66     # Refresh the DHT keys after this much time has passed.
67     # This should be a time slightly less than the DHT's KEY_EXPIRE value.
68     'KEY_REFRESH': '57m',
69
70     # Which DHT implementation to use.
71     # It must be possible to do "from <DHT>.DHT import DHT" to get a class that
72     # implements the IDHT interface.
73     'DHT': 'apt_p2p_Khashmir',
74
75     # Whether to only run the DHT (for providing only a bootstrap node)
76     'DHT-ONLY': 'no',
77 }
78
79 DHT_DEFAULTS = {
80     # bootstrap nodes to contact to join the DHT
81     'BOOTSTRAP': """www.camrdale.org:9977""",
82     
83     # whether this node is a bootstrap node
84     'BOOTSTRAP_NODE': "no",
85     
86     # Kademlia "K" constant, this should be an even number
87     'K': '8',
88     
89     # SHA1 is 160 bits long
90     'HASH_LENGTH': '160',
91     
92     # checkpoint every this many seconds
93     'CHECKPOINT_INTERVAL': '5m', # five minutes
94     
95     ### SEARCHING/STORING
96     # concurrent xmlrpc calls per find node/value request!
97     'CONCURRENT_REQS': '4',
98     
99     # how many hosts to post to
100     'STORE_REDUNDANCY': '3',
101     
102     # How many values to attempt to retrieve from the DHT.
103     # Setting this to 0 will try and get all values (which could take a while if
104     # a lot of nodes have values). Setting it negative will try to get that
105     # number of results from only the closest STORE_REDUNDANCY nodes to the hash.
106     # The default is a large negative number so all values from the closest
107     # STORE_REDUNDANCY nodes will be retrieved.
108     'RETRIEVE_VALUES': '-10000',
109
110     ###  ROUTING TABLE STUFF
111     # how many times in a row a node can fail to respond before it's booted from the routing table
112     'MAX_FAILURES': '3',
113     
114     # never ping a node more often than this
115     'MIN_PING_INTERVAL': '15m', # fifteen minutes
116     
117     # refresh buckets that haven't been touched in this long
118     'BUCKET_STALENESS': '1h', # one hour
119     
120     # expire entries older than this
121     'KEY_EXPIRE': '1h', # 60 minutes
122     
123     # whether to spew info about the requests/responses in the protocol
124     'SPEW': 'yes',
125 }
126
127 class AptP2PConfigParser(SafeConfigParser):
128     """Adds 'gettime' and 'getstringlist' to ConfigParser objects.
129     
130     @ivar time_multipliers: the 'gettime' suffixes and the multipliers needed
131         to convert them to seconds
132     """
133     
134     time_multipliers={
135         's': 1,    #seconds
136         'm': 60,   #minutes
137         'h': 3600, #hours
138         'd': 86400,#days
139         }
140
141     def gettime(self, section, option):
142         """Read the config parameter as a time value."""
143         mult = 1
144         value = self.get(section, option)
145         if len(value) == 0:
146             raise ConfigError("Configuration parse error: [%s] %s" % (section, option))
147         suffix = value[-1].lower()
148         if suffix in self.time_multipliers.keys():
149             mult = self.time_multipliers[suffix]
150             value = value[:-1]
151         return int(value)*mult
152     
153     def getstring(self, section, option):
154         """Read the config parameter as a string."""
155         return self.get(section,option)
156     
157     def getstringlist(self, section, option):
158         """Read the multi-line config parameter as a list of strings."""
159         return self.get(section,option).split()
160
161     def optionxform(self, option):
162         """Use all uppercase in the config parameters names."""
163         return option.upper()
164
165 # Initialize the default config parameters
166 config = AptP2PConfigParser(DEFAULTS)
167 config.add_section(config.get('DEFAULT', 'DHT'))
168 for k in DHT_DEFAULTS:
169     config.set(config.get('DEFAULT', 'DHT'), k, DHT_DEFAULTS[k])
170
171 class TestConfigParser(unittest.TestCase):
172     """Unit tests for the config parser."""
173
174     def test_uppercase(self):
175         config.set('DEFAULT', 'case_tester', 'foo')
176         self.failUnless(config.get('DEFAULT', 'CASE_TESTER') == 'foo')
177         self.failUnless(config.get('DEFAULT', 'case_tester') == 'foo')
178         config.set('DEFAULT', 'TEST_CASE', 'bar')
179         self.failUnless(config.get('DEFAULT', 'TEST_CASE') == 'bar')
180         self.failUnless(config.get('DEFAULT', 'test_case') == 'bar')
181         config.set('DEFAULT', 'FINAL_test_CASE', 'foobar')
182         self.failUnless(config.get('DEFAULT', 'FINAL_TEST_CASE') == 'foobar')
183         self.failUnless(config.get('DEFAULT', 'final_test_case') == 'foobar')
184         self.failUnless(config.get('DEFAULT', 'FINAL_test_CASE') == 'foobar')
185         self.failUnless(config.get('DEFAULT', 'final_TEST_case') == 'foobar')
186     
187     def test_time(self):
188         config.set('DEFAULT', 'time_tester_1', '2d')
189         self.failUnless(config.gettime('DEFAULT', 'time_tester_1') == 2*86400)
190         config.set('DEFAULT', 'time_tester_2', '3h')
191         self.failUnless(config.gettime('DEFAULT', 'time_tester_2') == 3*3600)
192         config.set('DEFAULT', 'time_tester_3', '4m')
193         self.failUnless(config.gettime('DEFAULT', 'time_tester_3') == 4*60)
194         config.set('DEFAULT', 'time_tester_4', '37s')
195         self.failUnless(config.gettime('DEFAULT', 'time_tester_4') == 37)
196         
197     def test_string(self):
198         config.set('DEFAULT', 'string_test', 'foobar')
199         self.failUnless(type(config.getstring('DEFAULT', 'string_test')) == str)
200         self.failUnless(config.getstring('DEFAULT', 'string_test') == 'foobar')
201
202     def test_stringlist(self):
203         config.set('DEFAULT', 'stringlist_test', """foo
204         bar   
205         foobar  """)
206         l = config.getstringlist('DEFAULT', 'stringlist_test')
207         self.failUnless(type(l) == list)
208         self.failUnless(len(l) == 3)
209         self.failUnless(l[0] == 'foo')
210         self.failUnless(l[1] == 'bar')
211         self.failUnless(l[2] == 'foobar')
212