Use the mirror as a peer when there are few peers for a file.
[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     # 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     # User name to try and run as
62     'USERNAME': '',
63     
64     # Whether it's OK to use an IP address from a known local/private range
65     'LOCAL_OK': 'no',
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': '57m',
75
76     # Which DHT implementation to use.
77     # It must be possible to do "from <DHT>.DHT import DHT" to get a class that
78     # implements the IDHT interface.
79     'DHT': 'apt_p2p_Khashmir',
80
81     # Whether to only run the DHT (for providing only a bootstrap node)
82     'DHT-ONLY': 'no',
83 }
84
85 DHT_DEFAULTS = {
86     # bootstrap nodes to contact to join the DHT
87     'BOOTSTRAP': """www.camrdale.org:9977""",
88     
89     # whether this node is a bootstrap node
90     'BOOTSTRAP_NODE': "no",
91     
92     # Kademlia "K" constant, this should be an even number
93     'K': '8',
94     
95     # SHA1 is 160 bits long
96     'HASH_LENGTH': '160',
97     
98     # checkpoint every this many seconds
99     'CHECKPOINT_INTERVAL': '5m', # five minutes
100     
101     ### SEARCHING/STORING
102     # concurrent xmlrpc calls per find node/value request!
103     'CONCURRENT_REQS': '4',
104     
105     # how many hosts to post to
106     'STORE_REDUNDANCY': '3',
107     
108     # How many values to attempt to retrieve from the DHT.
109     # Setting this to 0 will try and get all values (which could take a while if
110     # a lot of nodes have values). Setting it negative will try to get that
111     # number of results from only the closest STORE_REDUNDANCY nodes to the hash.
112     # The default is a large negative number so all values from the closest
113     # STORE_REDUNDANCY nodes will be retrieved.
114     'RETRIEVE_VALUES': '-10000',
115
116     ###  ROUTING TABLE STUFF
117     # how many times in a row a node can fail to respond before it's booted from the routing table
118     'MAX_FAILURES': '3',
119     
120     # never ping a node more often than this
121     'MIN_PING_INTERVAL': '15m', # fifteen minutes
122     
123     # refresh buckets that haven't been touched in this long
124     'BUCKET_STALENESS': '1h', # one hour
125     
126     # expire entries older than this
127     'KEY_EXPIRE': '1h', # 60 minutes
128     
129     # whether to spew info about the requests/responses in the protocol
130     'SPEW': 'yes',
131 }
132
133 class AptP2PConfigParser(SafeConfigParser):
134     """Adds 'gettime' and 'getstringlist' to ConfigParser objects.
135     
136     @ivar time_multipliers: the 'gettime' suffixes and the multipliers needed
137         to convert them to seconds
138     """
139     
140     time_multipliers={
141         's': 1,    #seconds
142         'm': 60,   #minutes
143         'h': 3600, #hours
144         'd': 86400,#days
145         }
146
147     def gettime(self, section, option):
148         """Read the config parameter as a time value."""
149         mult = 1
150         value = self.get(section, option)
151         if len(value) == 0:
152             raise ConfigError("Configuration parse error: [%s] %s" % (section, option))
153         suffix = value[-1].lower()
154         if suffix in self.time_multipliers.keys():
155             mult = self.time_multipliers[suffix]
156             value = value[:-1]
157         return int(value)*mult
158     
159     def getstring(self, section, option):
160         """Read the config parameter as a string."""
161         return self.get(section,option)
162     
163     def getstringlist(self, section, option):
164         """Read the multi-line config parameter as a list of strings."""
165         return self.get(section,option).split()
166
167     def optionxform(self, option):
168         """Use all uppercase in the config parameters names."""
169         return option.upper()
170
171 # Initialize the default config parameters
172 config = AptP2PConfigParser(DEFAULTS)
173 config.add_section(config.get('DEFAULT', 'DHT'))
174 for k in DHT_DEFAULTS:
175     config.set(config.get('DEFAULT', 'DHT'), k, DHT_DEFAULTS[k])
176
177 class TestConfigParser(unittest.TestCase):
178     """Unit tests for the config parser."""
179
180     def test_uppercase(self):
181         config.set('DEFAULT', 'case_tester', 'foo')
182         self.failUnless(config.get('DEFAULT', 'CASE_TESTER') == 'foo')
183         self.failUnless(config.get('DEFAULT', 'case_tester') == 'foo')
184         config.set('DEFAULT', 'TEST_CASE', 'bar')
185         self.failUnless(config.get('DEFAULT', 'TEST_CASE') == 'bar')
186         self.failUnless(config.get('DEFAULT', 'test_case') == 'bar')
187         config.set('DEFAULT', 'FINAL_test_CASE', 'foobar')
188         self.failUnless(config.get('DEFAULT', 'FINAL_TEST_CASE') == 'foobar')
189         self.failUnless(config.get('DEFAULT', 'final_test_case') == 'foobar')
190         self.failUnless(config.get('DEFAULT', 'FINAL_test_CASE') == 'foobar')
191         self.failUnless(config.get('DEFAULT', 'final_TEST_case') == 'foobar')
192     
193     def test_time(self):
194         config.set('DEFAULT', 'time_tester_1', '2d')
195         self.failUnless(config.gettime('DEFAULT', 'time_tester_1') == 2*86400)
196         config.set('DEFAULT', 'time_tester_2', '3h')
197         self.failUnless(config.gettime('DEFAULT', 'time_tester_2') == 3*3600)
198         config.set('DEFAULT', 'time_tester_3', '4m')
199         self.failUnless(config.gettime('DEFAULT', 'time_tester_3') == 4*60)
200         config.set('DEFAULT', 'time_tester_4', '37s')
201         self.failUnless(config.gettime('DEFAULT', 'time_tester_4') == 37)
202         
203     def test_string(self):
204         config.set('DEFAULT', 'string_test', 'foobar')
205         self.failUnless(type(config.getstring('DEFAULT', 'string_test')) == str)
206         self.failUnless(config.getstring('DEFAULT', 'string_test') == 'foobar')
207
208     def test_stringlist(self):
209         config.set('DEFAULT', 'stringlist_test', """foo
210         bar   
211         foobar  """)
212         l = config.getstringlist('DEFAULT', 'stringlist_test')
213         self.failUnless(type(l) == list)
214         self.failUnless(len(l) == 3)
215         self.failUnless(l[0] == 'foo')
216         self.failUnless(l[1] == 'bar')
217         self.failUnless(l[2] == 'foobar')
218