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