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

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: 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
« no previous file with comments | « no previous file | media/tools/constrained_network_server/cns_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
13 import optparse
14 import os
15 import sys
16 import threading
17 import time
18
19 try:
20 import cherrypy
21 except ImportError:
22 print ('CNS requires CherryPy v3 or higher to be installed. Please install\n'
23 'and try again. On Linux: sudo apt-get install python-cherrypy3\n')
24 sys.exit(1)
25
26
27 # Default port to serve the CNS on.
28 _DEFAULT_SERVING_PORT = 80
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 Does this script require running as root? (IWBN no
DaleCurtis 2011/11/15 18:46:00 Technically this script doesn't. However shadi's t
Ami GONE FROM CHROMIUM 2011/11/16 03:11:22 I think suid for tc scripts makes most sense. So d
DaleCurtis 2011/11/16 20:20:15 Done. Used 9000 since layout tests run on 8000. O
29
30 # Default port range for constrained use.
31 _DEFAULT_CNS_PORT_RANGE = (50000, 51000)
32
33 # Default number of seconds before a port can be torn down.
34 _DEFAULT_PORT_EXPIRY_TIME_SECS = 5 * 60
35
36
37 class PortAllocator(object):
38 """Dynamically allocates/deallocates ports with a given set of constraints."""
39
40 def __init__(self, port_range, expiry_time_secs=5 * 60):
41 """Sets up initial state for the Port Allocator.
42
43 Args:
44 port_range: Range of ports available for allocation.
45 expiry_time_secs: Amount of time in seconds before constrained ports are
46 cleaned up.
47 """
48 self._port_range = port_range
49 self._expiry_time_secs = expiry_time_secs
50
51 # Keeps track of ports we've used, the creation key, and the last request
52 # time for the port so they can be cached and cleaned up later.
53 self._ports = {}
54
55 # Locks port creation and cleanup. TODO(dalecurtis): If performance becomes
56 # an issue a per-port based lock system can be used instead.
57 self._port_lock = threading.Lock()
58
59 def Get(self, key, **kwargs):
60 """Sets up a constrained port using the requested parameters.
61
62 Requests for the same key and constraints will result in a cached port being
63 returned if possible.
64
65 Args:
66 key: Used to cache ports with the given constraints.
67 **kwargs: Constraints to pass into ***TODO*** ConstraintGenerator...
scherkus (not reviewing) 2011/11/16 02:25:42 TODO(dalecurtis)?
DaleCurtis 2011/11/16 02:35:56 Done.
68
69 Returns:
70 None if no port can be setup or the port number of the constrained port.
71 """
72 with self._port_lock:
73 # Check port key cache to see if this port is already setup. Update the
74 # cache time and return the port if so. Performance isn't a concern here,
75 # so just iterate over ports dict for simplicity.
76 full_key = (key,) + tuple(kwargs.values())
77 for port, status in self._ports.iteritems():
78 if full_key == status['key']:
79 self._ports[port]['last_update'] = time.time()
80 return port
81
82 # Cleanup ports on new port requests. Do it after the cache check though
83 # so we don't erase and then setup the same port.
84 self.Cleanup(locked=True)
85
86 # Performance isn't really an issue here, so just iterate over the port
87 # range to find an unused port. If no port is found, None is returned.
88 for port in xrange(self._port_range[0], self._port_range[1]):
89 if port in self._ports:
90 continue
91
92 # TODO(dalecurtis): Integrate with shadi's scripts.
93 # We've found an open port so call the script and set it up.
94 #Port.Setup(port=port, **kwargs)
95
96 self._ports[port] = {'last_update': time.time(), 'key': full_key}
97 return port
98
99 def Cleanup(self, all_ports=False, locked=False):
100 """Cleans up expired ports, or if all=True, all allocated ports.
101
102 By default, ports which haven't been used for _PORT_EXPIRATION_TIME_SECS are
103 torn down. If all=True then they are torn down regardless.
104
105 Args:
106 all_ports: Should all ports be torn down regardless of expiration?
107 locked: Has a lock already been acquired in this context?
108 """
109 if not locked:
110 self._port_lock.acquire()
111
112 try:
113 # Use .items() instead of .iteritems() so we can delete keys w/o error.
114 for port, status in self._ports.items():
115 expired = time.time() - status['last_update'] > self._expiry_time_secs
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 Usually calling time.time() once per function (and
DaleCurtis 2011/11/15 18:46:00 Done. You make a good case. On 2011/11/15 17:54:0
116 if all_ports or expired:
117 cherrypy.log('Cleaning up port %d' % port)
118
119 # TODO(dalecurtis): Integrate with shadi's scripts.
120 #Port.Delete(port=port)
121
122 del self._ports[port]
123 finally:
124 if not locked:
125 self._port_lock.release()
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 In English this stanza looks wrong (if not locked,
DaleCurtis 2011/11/15 18:46:00 Done. Agreed on the first solution. On 2011/11/15
126
127
128 class ConstrainedNetworkServer(object):
129 """A CherryPy-based HTTP server for serving files with network constraints."""
130
131 def __init__(self, options, port_allocator):
132 """Sets up initial state for the CNS.
133
134 Args:
135 options: optparse based class returned by ParseArgs()
136 port_allocator: A port allocator instance.
137 """
138 self._options = options
139 self._port_allocator = port_allocator
140
141 @cherrypy.expose
142 def ServeConstrained(self, file=None, bandwidth=None, latency=None,
143 loss=None):
144 """Serves the requested file with the requested constraints.
145
146 Subsequent requests for the same constraints from the same IP will share the
147 previously created port. If no constraints are provided the file is served
148 as is.
149
150 Args:
151 file: path relative to http root of file to serve.
152 bandwidth: maximum allowed bandwidth for the provided port (integer
153 in kbit/s).
154 latency: time to add to each request (integer in ms).
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 Latency in my mind was always the ms to add to eac
DaleCurtis 2011/11/15 18:46:00 Poor phrasing on my part. I'll clean it up a bit.
155 loss: percentage of packets to drop (integer, 0-100).
156 """
157 # CherryPy is a bit wonky at detecting parameters, so just make them all
158 # optional and validate them ourselves.
159 if not file:
160 raise cherrypy.HTTPError(400, 'Invalid request. File must be specified.')
161
162 # Sanitize and check the path to prevent www-root escapes.
163 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, file))
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 Personally I'd overwrite |file| at this point, to
DaleCurtis 2011/11/15 18:46:00 Done.
DaleCurtis 2011/11/16 02:17:39 Actually can't do this because the relative/unsani
164 if not sanitized_path.startswith(self._options.www_root):
165 raise cherrypy.HTTPError(403, 'Invalid file requested.')
166
167 # Check existence early to prevent wasted constraint setup.
168 if not os.path.exists(sanitized_path):
169 raise cherrypy.HTTPError(404, 'File not found.')
170
171 # If there are no constraints, just serve the file.
172 if bandwidth is None and latency is None and loss is None:
173 return cherrypy.lib.static.serve_file(sanitized_path)
174
175 # Validate inputs. isdigit() guarantees a natural number.
176 if bandwidth and not bandwidth.isdigit():
177 raise cherrypy.HTTPError(400, 'Invalid bandwidth constraint.')
178
179 if latency and not latency.isdigit():
180 raise cherrypy.HTTPError(400, 'Invalid latency constraint.')
181
182 if loss and not loss.isdigit() and not int(loss) <= 100:
183 raise cherrypy.HTTPError(400, 'Invalid loss constraint.')
184
185 # Allocate a port using the given constraints. If a port with the requested
186 # key is already allocated, it will be reused.
187 #
188 # TODO(dalecurtis): The key cherrypy.request.remote.ip might not be unique
189 # if build slaves are sharing the same VM.
190 constrained_port = self._port_allocator.Get(
191 cherrypy.request.remote.ip, bandwidth=bandwidth, latency=latency,
192 loss=loss)
193
194 if not constrained_port:
195 raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.')
196
197 # Build constrained URL. Only pass on the file parameter.
198 constrained_url = '%s?f=%s' % (
199 cherrypy.url().replace(
200 ':%d' % self._options.port, ':%d' % constrained_port),
201 file)
202
203 # Redirect request to the constrained port.
204 cherrypy.lib.cptools.redirect(constrained_url, internal=False)
205
206
207 def ParseArgs():
208 """Define and parse the command-line arguments."""
209 parser = optparse.OptionParser()
210
211 parser.add_option('--expiry-time', type='int',
212 default=_DEFAULT_PORT_EXPIRY_TIME_SECS,
213 help=('Number of seconds before constrained ports expire '
214 ' and are cleaned up. Default: %default'))
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 s/' /'/
DaleCurtis 2011/11/15 18:46:00 Done.
Ami GONE FROM CHROMIUM 2011/11/16 03:11:22 Actually you did the opposite of what I suggested.
DaleCurtis 2011/11/16 20:20:15 Ah, I thought you were just pointing out my accide
215 parser.add_option('--port', type='int', default=_DEFAULT_SERVING_PORT,
216 help='Port to serve the API on. Default: %default')
217 parser.add_option('--port-range', default=_DEFAULT_CNS_PORT_RANGE,
218 help=('Range of ports for constrained serving. Specify as '
219 ' a comma separated value pair. Default: %default'))
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 s/' /'/
DaleCurtis 2011/11/15 18:46:00 Done.
220 parser.add_option('--local', action='store_true', default=False,
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 Unused?
DaleCurtis 2011/11/15 18:46:00 Currently, yes. I need to talk to shadi to work th
221 help='Setup server for use with a local client.')
222 parser.add_option('--threads', type='int',
223 help='Number of threads in the thread pool.')
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 doco omission semantics.
DaleCurtis 2011/11/15 18:46:00 Done. I'll forward the default value from: cherryp
224 parser.add_option('--www-root', default=os.getcwd(),
225 help=('Port to serve the API on. Defaults to the current '
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 s/Port to serve the API on./Directory root of serv
DaleCurtis 2011/11/15 18:46:00 Done.
226 'directory: %default'))
227
228 options = parser.parse_args()[0]
229
230 # Convert port range into the desired tuple format.
231 try:
232 if isinstance(options.port_range, str):
233 options.port_range = [int(port) for port in options.port_range.split(',')]
234 except ValueError:
235 parser.error('Invalid port range specified.')
236
237 # Normalize the path to remove any . or ..
238 options.www_root = os.path.normpath(options.www_root)
239
240 return options
241
242
243 def Main():
244 """Configure and start the ConstrainedNetworkServer."""
245 options = ParseArgs()
246
247 cherrypy.config.update(
248 {'server.socket_host': '::', 'server.socket_port': options.port})
249
250 if options.threads:
251 cherrypy.config.update({'server.thread_pool': options.threads})
252
253 # Setup port allocator here so we can call cleanup on failures/exit.
254 pa = PortAllocator(options.port_range, expiry_time_secs=options.expiry_time)
255
256 try:
257 cherrypy.quickstart(ConstrainedNetworkServer(options, pa))
258 finally:
259 pa.Cleanup(all_ports=True)
Ami GONE FROM CHROMIUM 2011/11/15 17:54:07 Does this run in case of ctrl-c? Or in what cases
DaleCurtis 2011/11/15 18:46:00 Yes, it will always run. Although it doesn't disab
Ami GONE FROM CHROMIUM 2011/11/16 03:11:22 I don't think that's needed; a ctrl-c that aborted
DaleCurtis 2011/11/16 20:20:15 Well if cleanup gets aborted the interface will be
Ami GONE FROM CHROMIUM 2011/11/16 20:28:27 That's a good point - we should not let tc/iptable
260
261
262 if __name__ == '__main__':
263 Main()
OLDNEW
« no previous file with comments | « no previous file | media/tools/constrained_network_server/cns_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698