Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(286)

Side by Side Diff: media/tools/constrained_network_server/cns.py

Issue 8528049: Introduce the constrained network server. (Closed) Base URL: http://git.chromium.org/git/chromium.git@trunk
Patch Set: Fixed nits. Created 9 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 #!/usr/bin/python
2
3 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
6
7 """Constrained Network Server. Serves files with supplied network constraints.
8
9 The CNS exposes a web based API allowing network constraints to be imposed on
10 file serving.
11
12 TODO(dalecurtis): Add some more docs here.
13
14 """
15
16 import optparse
17 import os
18 import signal
19 import sys
20 import threading
21 import time
22
23 try:
24 import cherrypy
25 except ImportError:
26 print ('CNS requires CherryPy v3 or higher to be installed. Please install\n'
27 'and try again. On Linux: sudo apt-get install python-cherrypy3\n')
28 sys.exit(1)
29
30
31 # Default port to serve the CNS on.
32 _DEFAULT_SERVING_PORT = 80
33
34 # Default port range for constrained use.
35 _DEFAULT_CNS_PORT_RANGE = (50000, 51000)
36
37 # Default number of seconds before a port can be torn down.
38 _DEFAULT_PORT_EXPIRY_TIME_SECS = 5 * 60
39
40
41 class PortAllocator(object):
42 """Dynamically allocates/deallocates ports with a given set of constraints."""
43
44 def __init__(self, port_range, expiry_time_secs=5 * 60):
45 """Sets up initial state for the Port Allocator.
46
47 Args:
48 port_range: Range of ports available for allocation.
49 expiry_time_secs: Amount of time in seconds before constrained ports are
50 cleaned up.
51 """
52 self._port_range = port_range
53 self._expiry_time_secs = expiry_time_secs
54
55 # Keeps track of ports we've used, the creation key, and the last request
56 # time for the port so they can be cached and cleaned up later.
57 self._ports = {}
58
59 # Locks port creation and cleanup. TODO(dalecurtis): If performance becomes
60 # an issue a per-port based lock system can be used instead.
61 self._port_lock = threading.Lock()
62
63 def Get(self, key, **kwargs):
64 """Sets up a constrained port using the requested parameters.
65
66 Requests for the same key and constraints will result in a cached port being
67 returned if possible.
68
69 Args:
70 key: Used to cache ports with the given constraints.
71 **kwargs: Constraints to pass into traffic control.
72
73 Returns:
74 None if no port can be setup or the port number of the constrained port.
75 """
76 with self._port_lock:
77 # Check port key cache to see if this port is already setup. Update the
78 # cache time and return the port if so. Performance isn't a concern here,
79 # so just iterate over ports dict for simplicity.
80 full_key = (key,) + tuple(kwargs.values())
81 for port, status in self._ports.iteritems():
82 if full_key == status['key']:
83 self._ports[port]['last_update'] = time.time()
84 return port
85
86 # Cleanup ports on new port requests. Do it after the cache check though
87 # so we don't erase and then setup the same port.
88 self._CleanupLocked(all_ports=False)
89
90 # Performance isn't really an issue here, so just iterate over the port
91 # range to find an unused port. If no port is found, None is returned.
92 for port in xrange(self._port_range[0], self._port_range[1]):
93 if port in self._ports:
94 continue
95
96 # TODO(dalecurtis): Integrate with shadi's scripts.
97 # We've found an open port so call the script and set it up.
98 #Port.Setup(port=port, **kwargs)
99
100 self._ports[port] = {'last_update': time.time(), 'key': full_key}
101 return port
102
103 def _CleanupLocked(self, all_ports):
104 """Internal cleanup method, expects lock to have already been acquired.
105
106 See Cleanup() for more information.
107
108 Args:
109 all_ports: Should all ports be torn down regardless of expiration?
110 """
111 now = time.time()
112 # Use .items() instead of .iteritems() so we can delete keys w/o error.
113 for port, status in self._ports.items():
114 expired = now - status['last_update'] > self._expiry_time_secs
115 if all_ports or expired:
116 cherrypy.log('Cleaning up port %d' % port)
117
118 # TODO(dalecurtis): Integrate with shadi's scripts.
119 #Port.Delete(port=port)
120
121 del self._ports[port]
122
123 def Cleanup(self, all_ports=False):
124 """Cleans up expired ports, or if all_ports=True, all allocated ports.
125
126 By default, ports which haven't been used for self._expiry_time_secs are
127 torn down. If all_ports=True then they are torn down regardless.
128
129 Args:
130 all_ports: Should all ports be torn down regardless of expiration?
131 """
132 with self._port_lock:
133 self._CleanupLocked(all_ports)
134
135
136 class ConstrainedNetworkServer(object):
137 """A CherryPy-based HTTP server for serving files with network constraints."""
138
139 def __init__(self, options, port_allocator):
140 """Sets up initial state for the CNS.
141
142 Args:
143 options: optparse based class returned by ParseArgs()
144 port_allocator: A port allocator instance.
145 """
146 self._options = options
147 self._port_allocator = port_allocator
148
149 @cherrypy.expose
150 def ServeConstrained(self, f=None, bandwidth=None, latency=None, loss=None):
151 """Serves the requested file with the requested constraints.
152
153 Subsequent requests for the same constraints from the same IP will share the
154 previously created port. If no constraints are provided the file is served
155 as is.
156
157 Args:
158 f: path relative to http root of file to serve.
159 bandwidth: maximum allowed bandwidth for the provided port (integer
160 in kbit/s).
161 latency: time to add to each packet (integer in ms).
162 loss: percentage of packets to drop (integer, 0-100).
163 """
164 # CherryPy is a bit wonky at detecting parameters, so just make them all
165 # optional and validate them ourselves.
166 if not f:
167 raise cherrypy.HTTPError(400, 'Invalid request. File must be specified.')
168
169 # Sanitize and check the path to prevent www-root escapes.
170 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f))
171 if not sanitized_path.startswith(self._options.www_root):
172 raise cherrypy.HTTPError(403, 'Invalid file requested.')
173
174 # Check existence early to prevent wasted constraint setup.
175 if not os.path.exists(sanitized_path):
176 raise cherrypy.HTTPError(404, 'File not found.')
177
178 # If there are no constraints, just serve the file.
179 if bandwidth is None and latency is None and loss is None:
180 return cherrypy.lib.static.serve_file(sanitized_path)
181
182 # Validate inputs. isdigit() guarantees a natural number.
183 if bandwidth and not bandwidth.isdigit():
184 raise cherrypy.HTTPError(400, 'Invalid bandwidth constraint.')
185
186 if latency and not latency.isdigit():
187 raise cherrypy.HTTPError(400, 'Invalid latency constraint.')
188
189 if loss and not loss.isdigit() and not int(loss) <= 100:
190 raise cherrypy.HTTPError(400, 'Invalid loss constraint.')
191
192 # Allocate a port using the given constraints. If a port with the requested
193 # key is already allocated, it will be reused.
194 #
195 # TODO(dalecurtis): The key cherrypy.request.remote.ip might not be unique
196 # if build slaves are sharing the same VM.
197 constrained_port = self._port_allocator.Get(
198 cherrypy.request.remote.ip, bandwidth=bandwidth, latency=latency,
199 loss=loss)
200
201 if not constrained_port:
202 raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.')
203
204 # Build constrained URL. Only pass on the file parameter.
205 constrained_url = '%s?file=%s' % (
206 cherrypy.url().replace(
207 ':%d' % self._options.port, ':%d' % constrained_port),
208 f)
209
210 # Redirect request to the constrained port.
211 cherrypy.lib.cptools.redirect(constrained_url, internal=False)
212
213
214 def ParseArgs():
215 """Define and parse the command-line arguments."""
216 parser = optparse.OptionParser()
217
218 parser.add_option('--expiry-time', type='int',
219 default=_DEFAULT_PORT_EXPIRY_TIME_SECS,
220 help=('Number of seconds before constrained ports expire'
221 ' and are cleaned up. Default: %default'))
222 parser.add_option('--port', type='int', default=_DEFAULT_SERVING_PORT,
223 help='Port to serve the API on. Default: %default')
224 parser.add_option('--port-range', default=_DEFAULT_CNS_PORT_RANGE,
225 help=('Range of ports for constrained serving. Specify as'
226 ' a comma separated value pair. Default: %default'))
227 parser.add_option('--eth', default='eth0',
Ami GONE FROM CHROMIUM 2011/11/16 03:11:22 s/--eth/--interface/?
DaleCurtis 2011/11/16 20:20:15 Done.
228 help=('Interface to setup constraints on. Use lo for a'
229 ' local client. Default: %default'))
230 parser.add_option('--threads', type='int',
231 default=cherrypy._cpserver.Server.thread_pool,
232 help=('Number of threads in the thread pool. Default:'
233 ' %default'))
234 parser.add_option('--www-root', default=os.getcwd(),
235 help=('Directory root to serve files from. Defaults to the'
236 ' current directory: %default'))
237
238 options = parser.parse_args()[0]
239
240 # Convert port range into the desired tuple format.
241 try:
242 if isinstance(options.port_range, str):
243 options.port_range = [int(port) for port in options.port_range.split(',')]
244 except ValueError:
245 parser.error('Invalid port range specified.')
246
247 # Normalize the path to remove any . or ..
248 options.www_root = os.path.normpath(options.www_root)
249
250 return options
251
252
253 def Main():
254 """Configure and start the ConstrainedNetworkServer."""
255 options = ParseArgs()
256
257 cherrypy.config.update(
258 {'server.socket_host': '::', 'server.socket_port': options.port})
259
260 if options.threads:
261 cherrypy.config.update({'server.thread_pool': options.threads})
262
263 # Setup port allocator here so we can call cleanup on failures/exit.
264 pa = PortAllocator(options.port_range, expiry_time_secs=options.expiry_time)
265
266 try:
267 cherrypy.quickstart(ConstrainedNetworkServer(options, pa))
268 finally:
269 # Disable Ctrl-C handler to prevent interruption of cleanup.
270 signal.signal(signal.SIGINT, lambda signal, frame: None)
271 pa.Cleanup(all_ports=True)
272
273
274 if __name__ == '__main__':
275 Main()
OLDNEW
« no previous file with comments | « no previous file | media/tools/constrained_network_server/cns_test.py » ('j') | media/tools/constrained_network_server/cns_test.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698