##// END OF EJS Templates
Backport PR #5486: disambiguate to location when no IPs can be determined...
Thomas Kluyver -
Show More
@@ -1,380 +1,390
1 """Some generic utilities for dealing with classes, urls, and serialization.
1 """Some generic utilities for dealing with classes, urls, and serialization."""
2 2
3 Authors:
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 5
5 * Min RK
6 """
7 #-----------------------------------------------------------------------------
8 # Copyright (C) 2010-2011 The IPython Development Team
9 #
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
12 #-----------------------------------------------------------------------------
13
14 #-----------------------------------------------------------------------------
15 # Imports
16 #-----------------------------------------------------------------------------
17
18 # Standard library imports.
19 6 import logging
20 7 import os
21 8 import re
22 9 import stat
23 10 import socket
24 11 import sys
12 import warnings
25 13 from signal import signal, SIGINT, SIGABRT, SIGTERM
26 14 try:
27 15 from signal import SIGKILL
28 16 except ImportError:
29 17 SIGKILL=None
30 18 from types import FunctionType
31 19
32 20 try:
33 21 import cPickle
34 22 pickle = cPickle
35 23 except:
36 24 cPickle = None
37 25 import pickle
38 26
39 # System library imports
40 27 import zmq
41 28 from zmq.log import handlers
42 29
43 30 from IPython.external.decorator import decorator
44 31
45 # IPython imports
46 32 from IPython.config.application import Application
47 33 from IPython.utils.localinterfaces import localhost, is_public_ip, public_ips
48 34 from IPython.utils.py3compat import string_types, iteritems, itervalues
49 35 from IPython.kernel.zmq.log import EnginePUBHandler
50 36 from IPython.kernel.zmq.serialize import (
51 37 unserialize_object, serialize_object, pack_apply_message, unpack_apply_message
52 38 )
53 39
54 40 #-----------------------------------------------------------------------------
55 41 # Classes
56 42 #-----------------------------------------------------------------------------
57 43
58 44 class Namespace(dict):
59 45 """Subclass of dict for attribute access to keys."""
60 46
61 47 def __getattr__(self, key):
62 48 """getattr aliased to getitem"""
63 49 if key in self:
64 50 return self[key]
65 51 else:
66 52 raise NameError(key)
67 53
68 54 def __setattr__(self, key, value):
69 55 """setattr aliased to setitem, with strict"""
70 56 if hasattr(dict, key):
71 57 raise KeyError("Cannot override dict keys %r"%key)
72 58 self[key] = value
73 59
74 60
75 61 class ReverseDict(dict):
76 62 """simple double-keyed subset of dict methods."""
77 63
78 64 def __init__(self, *args, **kwargs):
79 65 dict.__init__(self, *args, **kwargs)
80 66 self._reverse = dict()
81 67 for key, value in iteritems(self):
82 68 self._reverse[value] = key
83 69
84 70 def __getitem__(self, key):
85 71 try:
86 72 return dict.__getitem__(self, key)
87 73 except KeyError:
88 74 return self._reverse[key]
89 75
90 76 def __setitem__(self, key, value):
91 77 if key in self._reverse:
92 78 raise KeyError("Can't have key %r on both sides!"%key)
93 79 dict.__setitem__(self, key, value)
94 80 self._reverse[value] = key
95 81
96 82 def pop(self, key):
97 83 value = dict.pop(self, key)
98 84 self._reverse.pop(value)
99 85 return value
100 86
101 87 def get(self, key, default=None):
102 88 try:
103 89 return self[key]
104 90 except KeyError:
105 91 return default
106 92
107 93 #-----------------------------------------------------------------------------
108 94 # Functions
109 95 #-----------------------------------------------------------------------------
110 96
111 97 @decorator
112 98 def log_errors(f, self, *args, **kwargs):
113 99 """decorator to log unhandled exceptions raised in a method.
114 100
115 101 For use wrapping on_recv callbacks, so that exceptions
116 102 do not cause the stream to be closed.
117 103 """
118 104 try:
119 105 return f(self, *args, **kwargs)
120 106 except Exception:
121 107 self.log.error("Uncaught exception in %r" % f, exc_info=True)
122 108
123 109
124 110 def is_url(url):
125 111 """boolean check for whether a string is a zmq url"""
126 112 if '://' not in url:
127 113 return False
128 114 proto, addr = url.split('://', 1)
129 115 if proto.lower() not in ['tcp','pgm','epgm','ipc','inproc']:
130 116 return False
131 117 return True
132 118
133 119 def validate_url(url):
134 120 """validate a url for zeromq"""
135 121 if not isinstance(url, string_types):
136 122 raise TypeError("url must be a string, not %r"%type(url))
137 123 url = url.lower()
138 124
139 125 proto_addr = url.split('://')
140 126 assert len(proto_addr) == 2, 'Invalid url: %r'%url
141 127 proto, addr = proto_addr
142 128 assert proto in ['tcp','pgm','epgm','ipc','inproc'], "Invalid protocol: %r"%proto
143 129
144 130 # domain pattern adapted from http://www.regexlib.com/REDetails.aspx?regexp_id=391
145 131 # author: Remi Sabourin
146 132 pat = re.compile(r'^([\w\d]([\w\d\-]{0,61}[\w\d])?\.)*[\w\d]([\w\d\-]{0,61}[\w\d])?$')
147 133
148 134 if proto == 'tcp':
149 135 lis = addr.split(':')
150 136 assert len(lis) == 2, 'Invalid url: %r'%url
151 137 addr,s_port = lis
152 138 try:
153 139 port = int(s_port)
154 140 except ValueError:
155 141 raise AssertionError("Invalid port %r in url: %r"%(port, url))
156 142
157 143 assert addr == '*' or pat.match(addr) is not None, 'Invalid url: %r'%url
158 144
159 145 else:
160 146 # only validate tcp urls currently
161 147 pass
162 148
163 149 return True
164 150
165 151
166 152 def validate_url_container(container):
167 153 """validate a potentially nested collection of urls."""
168 154 if isinstance(container, string_types):
169 155 url = container
170 156 return validate_url(url)
171 157 elif isinstance(container, dict):
172 158 container = itervalues(container)
173 159
174 160 for element in container:
175 161 validate_url_container(element)
176 162
177 163
178 164 def split_url(url):
179 165 """split a zmq url (tcp://ip:port) into ('tcp','ip','port')."""
180 166 proto_addr = url.split('://')
181 167 assert len(proto_addr) == 2, 'Invalid url: %r'%url
182 168 proto, addr = proto_addr
183 169 lis = addr.split(':')
184 170 assert len(lis) == 2, 'Invalid url: %r'%url
185 171 addr,s_port = lis
186 172 return proto,addr,s_port
187
173
174
188 175 def disambiguate_ip_address(ip, location=None):
189 """turn multi-ip interfaces '0.0.0.0' and '*' into connectable
190 ones, based on the location (default interpretation of location is localhost)."""
191 if ip in ('0.0.0.0', '*'):
192 if location is None or is_public_ip(location) or not public_ips():
193 # If location is unspecified or cannot be determined, assume local
176 """turn multi-ip interfaces '0.0.0.0' and '*' into a connectable address
177
178 Explicit IP addresses are returned unmodified.
179
180 Parameters
181 ----------
182
183 ip : IP address
184 An IP address, or the special values 0.0.0.0, or *
185 location: IP address, optional
186 A public IP of the target machine.
187 If location is an IP of the current machine,
188 localhost will be returned,
189 otherwise location will be returned.
190 """
191 if ip in {'0.0.0.0', '*'}:
192 if not location:
193 # unspecified location, localhost is the only choice
194 ip = localhost()
195 elif is_public_ip(location):
196 # location is a public IP on this machine, use localhost
194 197 ip = localhost()
195 elif location:
196 return location
198 elif not public_ips():
199 # this machine's public IPs cannot be determined,
200 # assume `location` is not this machine
201 warnings.warn("IPython could not determine public IPs", RuntimeWarning)
202 ip = location
203 else:
204 # location is not this machine, do not use loopback
205 ip = location
197 206 return ip
198 207
208
199 209 def disambiguate_url(url, location=None):
200 210 """turn multi-ip interfaces '0.0.0.0' and '*' into connectable
201 211 ones, based on the location (default interpretation is localhost).
202 212
203 213 This is for zeromq urls, such as ``tcp://*:10101``.
204 214 """
205 215 try:
206 216 proto,ip,port = split_url(url)
207 217 except AssertionError:
208 218 # probably not tcp url; could be ipc, etc.
209 219 return url
210 220
211 221 ip = disambiguate_ip_address(ip,location)
212 222
213 223 return "%s://%s:%s"%(proto,ip,port)
214 224
215 225
216 226 #--------------------------------------------------------------------------
217 227 # helpers for implementing old MEC API via view.apply
218 228 #--------------------------------------------------------------------------
219 229
220 230 def interactive(f):
221 231 """decorator for making functions appear as interactively defined.
222 232 This results in the function being linked to the user_ns as globals()
223 233 instead of the module globals().
224 234 """
225 235
226 236 # build new FunctionType, so it can have the right globals
227 237 # interactive functions never have closures, that's kind of the point
228 238 if isinstance(f, FunctionType):
229 239 mainmod = __import__('__main__')
230 240 f = FunctionType(f.__code__, mainmod.__dict__,
231 241 f.__name__, f.__defaults__,
232 242 )
233 243 # associate with __main__ for uncanning
234 244 f.__module__ = '__main__'
235 245 return f
236 246
237 247 @interactive
238 248 def _push(**ns):
239 249 """helper method for implementing `client.push` via `client.apply`"""
240 250 user_ns = globals()
241 251 tmp = '_IP_PUSH_TMP_'
242 252 while tmp in user_ns:
243 253 tmp = tmp + '_'
244 254 try:
245 255 for name, value in ns.items():
246 256 user_ns[tmp] = value
247 257 exec("%s = %s" % (name, tmp), user_ns)
248 258 finally:
249 259 user_ns.pop(tmp, None)
250 260
251 261 @interactive
252 262 def _pull(keys):
253 263 """helper method for implementing `client.pull` via `client.apply`"""
254 264 if isinstance(keys, (list,tuple, set)):
255 265 return [eval(key, globals()) for key in keys]
256 266 else:
257 267 return eval(keys, globals())
258 268
259 269 @interactive
260 270 def _execute(code):
261 271 """helper method for implementing `client.execute` via `client.apply`"""
262 272 exec(code, globals())
263 273
264 274 #--------------------------------------------------------------------------
265 275 # extra process management utilities
266 276 #--------------------------------------------------------------------------
267 277
268 278 _random_ports = set()
269 279
270 280 def select_random_ports(n):
271 281 """Selects and return n random ports that are available."""
272 282 ports = []
273 283 for i in range(n):
274 284 sock = socket.socket()
275 285 sock.bind(('', 0))
276 286 while sock.getsockname()[1] in _random_ports:
277 287 sock.close()
278 288 sock = socket.socket()
279 289 sock.bind(('', 0))
280 290 ports.append(sock)
281 291 for i, sock in enumerate(ports):
282 292 port = sock.getsockname()[1]
283 293 sock.close()
284 294 ports[i] = port
285 295 _random_ports.add(port)
286 296 return ports
287 297
288 298 def signal_children(children):
289 299 """Relay interupt/term signals to children, for more solid process cleanup."""
290 300 def terminate_children(sig, frame):
291 301 log = Application.instance().log
292 302 log.critical("Got signal %i, terminating children..."%sig)
293 303 for child in children:
294 304 child.terminate()
295 305
296 306 sys.exit(sig != SIGINT)
297 307 # sys.exit(sig)
298 308 for sig in (SIGINT, SIGABRT, SIGTERM):
299 309 signal(sig, terminate_children)
300 310
301 311 def generate_exec_key(keyfile):
302 312 import uuid
303 313 newkey = str(uuid.uuid4())
304 314 with open(keyfile, 'w') as f:
305 315 # f.write('ipython-key ')
306 316 f.write(newkey+'\n')
307 317 # set user-only RW permissions (0600)
308 318 # this will have no effect on Windows
309 319 os.chmod(keyfile, stat.S_IRUSR|stat.S_IWUSR)
310 320
311 321
312 322 def integer_loglevel(loglevel):
313 323 try:
314 324 loglevel = int(loglevel)
315 325 except ValueError:
316 326 if isinstance(loglevel, str):
317 327 loglevel = getattr(logging, loglevel)
318 328 return loglevel
319 329
320 330 def connect_logger(logname, context, iface, root="ip", loglevel=logging.DEBUG):
321 331 logger = logging.getLogger(logname)
322 332 if any([isinstance(h, handlers.PUBHandler) for h in logger.handlers]):
323 333 # don't add a second PUBHandler
324 334 return
325 335 loglevel = integer_loglevel(loglevel)
326 336 lsock = context.socket(zmq.PUB)
327 337 lsock.connect(iface)
328 338 handler = handlers.PUBHandler(lsock)
329 339 handler.setLevel(loglevel)
330 340 handler.root_topic = root
331 341 logger.addHandler(handler)
332 342 logger.setLevel(loglevel)
333 343
334 344 def connect_engine_logger(context, iface, engine, loglevel=logging.DEBUG):
335 345 logger = logging.getLogger()
336 346 if any([isinstance(h, handlers.PUBHandler) for h in logger.handlers]):
337 347 # don't add a second PUBHandler
338 348 return
339 349 loglevel = integer_loglevel(loglevel)
340 350 lsock = context.socket(zmq.PUB)
341 351 lsock.connect(iface)
342 352 handler = EnginePUBHandler(engine, lsock)
343 353 handler.setLevel(loglevel)
344 354 logger.addHandler(handler)
345 355 logger.setLevel(loglevel)
346 356 return logger
347 357
348 358 def local_logger(logname, loglevel=logging.DEBUG):
349 359 loglevel = integer_loglevel(loglevel)
350 360 logger = logging.getLogger(logname)
351 361 if any([isinstance(h, logging.StreamHandler) for h in logger.handlers]):
352 362 # don't add a second StreamHandler
353 363 return
354 364 handler = logging.StreamHandler()
355 365 handler.setLevel(loglevel)
356 366 formatter = logging.Formatter("%(asctime)s.%(msecs).03d [%(name)s] %(message)s",
357 367 datefmt="%Y-%m-%d %H:%M:%S")
358 368 handler.setFormatter(formatter)
359 369
360 370 logger.addHandler(handler)
361 371 logger.setLevel(loglevel)
362 372 return logger
363 373
364 374 def set_hwm(sock, hwm=0):
365 375 """set zmq High Water Mark on a socket
366 376
367 377 in a way that always works for various pyzmq / libzmq versions.
368 378 """
369 379 import zmq
370 380
371 381 for key in ('HWM', 'SNDHWM', 'RCVHWM'):
372 382 opt = getattr(zmq, key, None)
373 383 if opt is None:
374 384 continue
375 385 try:
376 386 sock.setsockopt(opt, hwm)
377 387 except zmq.ZMQError:
378 388 pass
379 389
380 390 No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now