3198be173223d5f978062e0cd4dff7146f683178
[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, 1, 3)
27
28 mapbase64 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-'
29 versionID = 'A'
30 for subver in version.base().split('.', 2):
31     while type(subver) != int and len(subver) > 0:
32         try:
33             subver = int(subver)
34         except:
35             subver = subver[:-1]
36     if type(subver) != int or subver >= 64:
37         subver = 0
38     versionID += mapbase64[subver]
39
40 # Set the home parameter
41 home = os.path.expandvars('${HOME}')
42 if home == '${HOME}' or not os.path.isdir(home):
43     home = os.path.expanduser('~')
44     if not os.path.isdir(home):
45         home = os.path.abspath(os.path.dirname(sys.argv[0]))
46
47 DEFAULT_CONFIG_FILES=['/etc/apt-p2p/apt-p2p.conf',
48                       home + '/.apt-p2p/apt-p2p.conf']
49
50 DEFAULTS = {
51
52     # Port to listen on for all requests (TCP and UDP)
53     'PORT': '9977',
54     
55     # The rate to limit sending data to peers to, in KBytes/sec.
56     # Set this to 0 to not limit the upload bandwidth.
57     'UPLOAD_LIMIT': '0',
58
59     # The minimum number of peers before the mirror is not used.
60     # If there are fewer peers than this for a file, the mirror will also be
61     # used to speed up the download. Set to 0 to never use the mirror if
62     # there are peers.
63     'MIN_DOWNLOAD_PEERS': '3',
64
65     # Directory to store the downloaded files in
66     'CACHE_DIR': home + '/.apt-p2p/cache',
67     
68     # Other directories containing packages to share with others
69     # WARNING: all files in these directories will be hashed and available
70     #          for everybody to download
71     'OTHER_DIRS': """""",
72     
73     # Whether it's OK to use an IP address from a known local/private range
74     'LOCAL_OK': 'no',
75
76     # Whether a remote peer can access the statistics page
77     'REMOTE_STATS': 'yes',
78
79     # Unload the packages cache after an interval of inactivity this long.
80     # The packages cache uses a lot of memory, and only takes a few seconds
81     # to reload when a new request arrives.
82     'UNLOAD_PACKAGES_CACHE': '5m',
83
84     # Refresh the DHT keys after this much time has passed.
85     # This should be a time slightly less than the DHT's KEY_EXPIRE value.
86     'KEY_REFRESH': '2.5h',
87
88     # The user name to try and run as (leave blank to run as current user)
89     'USERNAME': 'apt-p2p',
90     
91     # Which DHT implementation to use.
92     # It must be possible to do "from <DHT>.DHT import DHT" to get a class that
93     # implements the IDHT interface.
94     'DHT': 'apt_p2p_Khashmir',
95
96     # Whether to only run the DHT (for providing only a bootstrap node)
97     'DHT-ONLY': 'no',
98 }
99
100 DHT_DEFAULTS = {
101     # bootstrap nodes to contact to join the DHT
102     'BOOTSTRAP': """www.camrdale.org:9977""",
103     
104     # whether this node is a bootstrap node
105     'BOOTSTRAP_NODE': "no",
106     
107     # checkpoint every this many seconds
108     'CHECKPOINT_INTERVAL': '5m', # five minutes
109     
110     ### SEARCHING/STORING
111     # concurrent xmlrpc calls per find node/value request!
112     'CONCURRENT_REQS': '8',
113     
114     # how many hosts to post to
115     'STORE_REDUNDANCY': '6',
116     
117     # How many values to attempt to retrieve from the DHT.
118     # Setting this to 0 will try and get all values (which could take a while if
119     # a lot of nodes have values). Setting it negative will try to get that
120     # number of results from only the closest STORE_REDUNDANCY nodes to the hash.
121     # The default is a large negative number so all values from the closest
122     # STORE_REDUNDANCY nodes will be retrieved.
123     'RETRIEVE_VALUES': '-10000',
124
125     ###  ROUTING TABLE STUFF
126     # how many times in a row a node can fail to respond before it's booted from the routing table
127     'MAX_FAILURES': '3',
128     
129     # never ping a node more often than this
130     'MIN_PING_INTERVAL': '15m', # fifteen minutes
131     
132     # refresh buckets that haven't been touched in this long
133     'BUCKET_STALENESS': '1h', # one hour
134     
135     # expire entries older than this
136     'KEY_EXPIRE': '3h', # 3 hours
137     
138     # Timeout KRPC requests to nodes after this time.
139     'KRPC_TIMEOUT': '9s',
140     
141     # KRPC requests are resent using exponential backoff starting with this delay.
142     # The request will first be resent after the delay set here.
143     # The request will be resent again after twice the delay set here. etc.
144     # e.g. if TIMEOUT is 9 sec., and INITIAL_DELAY is 2 sec., then requests will
145     # be resent at times 0, 2 (2 sec. later), and 6 (4 sec. later), and then will
146     # timeout at 9.
147     'KRPC_INITIAL_DELAY': '2s',
148
149     # whether to spew info about the requests/responses in the protocol
150     'SPEW': 'no',
151 }
152
153 class AptP2PConfigParser(SafeConfigParser):
154     """Adds 'gettime' and 'getstringlist' to ConfigParser objects.
155     
156     @ivar time_multipliers: the 'gettime' suffixes and the multipliers needed
157         to convert them to seconds
158     """
159     
160     time_multipliers={
161         's': 1,    #seconds
162         'm': 60,   #minutes
163         'h': 3600, #hours
164         'd': 86400,#days
165         }
166
167     def gettime(self, section, option):
168         """Read the config parameter as a time value."""
169         mult = 1
170         value = self.get(section, option)
171         if len(value) == 0:
172             raise ConfigError("Configuration parse error: [%s] %s" % (section, option))
173         suffix = value[-1].lower()
174         if suffix in self.time_multipliers.keys():
175             mult = self.time_multipliers[suffix]
176             value = value[:-1]
177         return int(float(value)*mult)
178     
179     def getstring(self, section, option):
180         """Read the config parameter as a string."""
181         return self.get(section,option)
182     
183     def getstringlist(self, section, option):
184         """Read the multi-line config parameter as a list of strings."""
185         return self.get(section,option).split()
186
187     def optionxform(self, option):
188         """Use all uppercase in the config parameters names."""
189         return option.upper()
190
191 # Initialize the default config parameters
192 config = AptP2PConfigParser(DEFAULTS)
193 config.add_section(config.get('DEFAULT', 'DHT'))
194 for k in DHT_DEFAULTS:
195     config.set(config.get('DEFAULT', 'DHT'), k, DHT_DEFAULTS[k])
196
197 class TestConfigParser(unittest.TestCase):
198     """Unit tests for the config parser."""
199
200     def test_uppercase(self):
201         config.set('DEFAULT', 'case_tester', 'foo')
202         self.failUnless(config.get('DEFAULT', 'CASE_TESTER') == 'foo')
203         self.failUnless(config.get('DEFAULT', 'case_tester') == 'foo')
204         config.set('DEFAULT', 'TEST_CASE', 'bar')
205         self.failUnless(config.get('DEFAULT', 'TEST_CASE') == 'bar')
206         self.failUnless(config.get('DEFAULT', 'test_case') == 'bar')
207         config.set('DEFAULT', 'FINAL_test_CASE', 'foobar')
208         self.failUnless(config.get('DEFAULT', 'FINAL_TEST_CASE') == 'foobar')
209         self.failUnless(config.get('DEFAULT', 'final_test_case') == 'foobar')
210         self.failUnless(config.get('DEFAULT', 'FINAL_test_CASE') == 'foobar')
211         self.failUnless(config.get('DEFAULT', 'final_TEST_case') == 'foobar')
212     
213     def test_time(self):
214         config.set('DEFAULT', 'time_tester_1', '2d')
215         self.failUnless(config.gettime('DEFAULT', 'time_tester_1') == 2*86400)
216         config.set('DEFAULT', 'time_tester_2', '3h')
217         self.failUnless(config.gettime('DEFAULT', 'time_tester_2') == 3*3600)
218         config.set('DEFAULT', 'time_tester_3', '4m')
219         self.failUnless(config.gettime('DEFAULT', 'time_tester_3') == 4*60)
220         config.set('DEFAULT', 'time_tester_4', '37s')
221         self.failUnless(config.gettime('DEFAULT', 'time_tester_4') == 37)
222         
223     def test_floating_time(self):
224         config.set('DEFAULT', 'time_float_tester_1', '2.5d')
225         self.failUnless(config.gettime('DEFAULT', 'time_float_tester_1') == int(2.5*86400))
226         config.set('DEFAULT', 'time_float_tester_2', '0.5h')
227         self.failUnless(config.gettime('DEFAULT', 'time_float_tester_2') == int(0.5*3600))
228         config.set('DEFAULT', 'time_float_tester_3', '4.3333m')
229         self.failUnless(config.gettime('DEFAULT', 'time_float_tester_3') == int(4.3333*60))
230         
231     def test_string(self):
232         config.set('DEFAULT', 'string_test', 'foobar')
233         self.failUnless(type(config.getstring('DEFAULT', 'string_test')) == str)
234         self.failUnless(config.getstring('DEFAULT', 'string_test') == 'foobar')
235
236     def test_stringlist(self):
237         config.set('DEFAULT', 'stringlist_test', """foo
238         bar   
239         foobar  """)
240         l = config.getstringlist('DEFAULT', 'stringlist_test')
241         self.failUnless(type(l) == list)
242         self.failUnless(len(l) == 3)
243         self.failUnless(l[0] == 'foo')
244         self.failUnless(l[1] == 'bar')
245         self.failUnless(l[2] == 'foobar')
246