##// END OF EJS Templates
ipcluster implemented with new subcommands
MinRK -
Show More
@@ -82,6 +82,11 b' class ClusterDir(Configurable):'
82 'default'. The cluster directory is resolve this way if the
82 'default'. The cluster directory is resolve this way if the
83 `cluster_dir` option is not used.""", config=True
83 `cluster_dir` option is not used.""", config=True
84 )
84 )
85 auto_create = Bool(False,
86 help="""Whether to automatically create the ClusterDirectory if it does
87 not exist""")
88 overwrite = Bool(False,
89 help="""Whether to overwrite existing config files""")
85
90
86 _location_isset = Bool(False) # flag for detecting multiply set location
91 _location_isset = Bool(False) # flag for detecting multiply set location
87 _new_dir = Bool(False) # flag for whether a new dir was created
92 _new_dir = Bool(False) # flag for whether a new dir was created
@@ -96,10 +101,14 b' class ClusterDir(Configurable):'
96 raise RuntimeError("Cannot set ClusterDir more than once.")
101 raise RuntimeError("Cannot set ClusterDir more than once.")
97 self._location_isset = True
102 self._location_isset = True
98 if not os.path.isdir(new):
103 if not os.path.isdir(new):
99 os.makedirs(new)
104 if self.auto_create:
100 self._new_dir = True
105 os.makedirs(new)
106 self._new_dir = True
107 else:
108 raise ClusterDirError('Directory not found: %s' % new)
109
101 # ensure config files exist:
110 # ensure config files exist:
102 self.copy_all_config_files(overwrite=False)
111 self.copy_all_config_files(overwrite=self.overwrite)
103 self.security_dir = os.path.join(new, self.security_dir_name)
112 self.security_dir = os.path.join(new, self.security_dir_name)
104 self.log_dir = os.path.join(new, self.log_dir_name)
113 self.log_dir = os.path.join(new, self.log_dir_name)
105 self.pid_dir = os.path.join(new, self.pid_dir_name)
114 self.pid_dir = os.path.join(new, self.pid_dir_name)
@@ -289,22 +298,23 b' class ClusterDirCrashHandler(CrashHandler):'
289 base_aliases = {
298 base_aliases = {
290 'profile' : "ClusterDir.profile",
299 'profile' : "ClusterDir.profile",
291 'cluster_dir' : 'ClusterDir.location',
300 'cluster_dir' : 'ClusterDir.location',
292 'log_level' : 'Application.log_level',
301 'auto_create' : 'ClusterDirApplication.auto_create',
293 'work_dir' : 'ClusterDirApplicaiton.work_dir',
302 'log_level' : 'ClusterApplication.log_level',
294 'log_to_file' : 'ClusterDirApplicaiton.log_to_file',
303 'work_dir' : 'ClusterApplication.work_dir',
295 'clean_logs' : 'ClusterDirApplicaiton.clean_logs',
304 'log_to_file' : 'ClusterApplication.log_to_file',
296 'log_url' : 'ClusterDirApplicaiton.log_url',
305 'clean_logs' : 'ClusterApplication.clean_logs',
306 'log_url' : 'ClusterApplication.log_url',
297 }
307 }
298
308
299 base_flags = {
309 base_flags = {
300 'debug' : ( {"Application" : {"log_level" : logging.DEBUG}}, "set loglevel to DEBUG"),
310 'debug' : ( {"ClusterApplication" : {"log_level" : logging.DEBUG}}, "set loglevel to DEBUG"),
301 'clean-logs' : ( {"ClusterDirApplication" : {"clean_logs" : True}}, "cleanup old logfiles"),
311 'quiet' : ( {"ClusterApplication" : {"log_level" : logging.CRITICAL}}, "set loglevel to CRITICAL (minimal output)"),
302 'log-to-file' : ( {"ClusterDirApplication" : {"log_to_file" : True}}, "log to a file")
312 'log-to-file' : ( {"ClusterApplication" : {"log_to_file" : True}}, "redirect log output to a file"),
303 }
313 }
304 for k,v in base_flags.iteritems():
314 for k,v in base_flags.iteritems():
305 base_flags[k] = (Config(v[0]),v[1])
315 base_flags[k] = (Config(v[0]),v[1])
306
316
307 class ClusterDirApplication(BaseIPythonApplication):
317 class ClusterApplication(BaseIPythonApplication):
308 """An application that puts everything into a cluster directory.
318 """An application that puts everything into a cluster directory.
309
319
310 Instead of looking for things in the ipython_dir, this type of application
320 Instead of looking for things in the ipython_dir, this type of application
@@ -326,9 +336,12 b' class ClusterDirApplication(BaseIPythonApplication):'
326 crash_handler_class = ClusterDirCrashHandler
336 crash_handler_class = ClusterDirCrashHandler
327 auto_create_cluster_dir = Bool(True, config=True,
337 auto_create_cluster_dir = Bool(True, config=True,
328 help="whether to create the cluster_dir if it doesn't exist")
338 help="whether to create the cluster_dir if it doesn't exist")
329 # temporarily override default_log_level to INFO
330 default_log_level = logging.INFO
331 cluster_dir = Instance(ClusterDir)
339 cluster_dir = Instance(ClusterDir)
340 classes = [ClusterDir]
341
342 def _log_level_default(self):
343 # temporarily override default_log_level to INFO
344 return logging.INFO
332
345
333 work_dir = Unicode(os.getcwdu(), config=True,
346 work_dir = Unicode(os.getcwdu(), config=True,
334 help='Set the working dir for the process.'
347 help='Set the working dir for the process.'
@@ -339,7 +352,7 b' class ClusterDirApplication(BaseIPythonApplication):'
339 log_to_file = Bool(config=True,
352 log_to_file = Bool(config=True,
340 help="whether to log to a file")
353 help="whether to log to a file")
341
354
342 clean_logs = Bool(True, shortname='--clean-logs', config=True,
355 clean_logs = Bool(False, shortname='--clean-logs', config=True,
343 help="whether to cleanup old logfiles before starting")
356 help="whether to cleanup old logfiles before starting")
344
357
345 log_url = CStr('', shortname='--log-url', config=True,
358 log_url = CStr('', shortname='--log-url', config=True,
@@ -349,6 +362,11 b' class ClusterDirApplication(BaseIPythonApplication):'
349 help="""Path to ipcontroller configuration file. The default is to use
362 help="""Path to ipcontroller configuration file. The default is to use
350 <appname>_config.py, as found by cluster-dir."""
363 <appname>_config.py, as found by cluster-dir."""
351 )
364 )
365
366 loop = Instance('zmq.eventloop.ioloop.IOLoop')
367 def _loop_default(self):
368 from zmq.eventloop.ioloop import IOLoop
369 return IOLoop.instance()
352
370
353 aliases = Dict(base_aliases)
371 aliases = Dict(base_aliases)
354 flags = Dict(base_flags)
372 flags = Dict(base_flags)
@@ -370,14 +388,30 b' class ClusterDirApplication(BaseIPythonApplication):'
370 ``True``, then create the new cluster dir in the IPython directory.
388 ``True``, then create the new cluster dir in the IPython directory.
371 4. If all fails, then raise :class:`ClusterDirError`.
389 4. If all fails, then raise :class:`ClusterDirError`.
372 """
390 """
373 self.cluster_dir = ClusterDir(config=self.config)
391 self.cluster_dir = ClusterDir(config=self.config, auto_create=self.auto_create_cluster_dir)
374 if self.cluster_dir._new_dir:
392 if self.cluster_dir._new_dir:
375 self.log.info('Creating new cluster dir: %s' % \
393 self.log.info('Creating new cluster dir: %s' % \
376 self.cluster_dir.location)
394 self.cluster_dir.location)
377 else:
395 else:
378 self.log.info('Using existing cluster dir: %s' % \
396 self.log.info('Using existing cluster dir: %s' % \
379 self.cluster_dir.location)
397 self.cluster_dir.location)
380
398
399 def initialize(self, argv=None):
400 """initialize the app"""
401 self.parse_command_line(argv)
402 cl_config = self.config
403 self.init_clusterdir()
404 if self.config_file:
405 self.load_config_file(self.config_file)
406 else:
407 self.load_config_file(self.default_config_file_name, path=self.cluster_dir.location)
408 # command-line should *override* config file, but command-line is necessary
409 # to determine clusterdir, etc.
410 self.update_config(cl_config)
411 self.reinit_logging()
412
413 self.to_work_dir()
414
381 def to_work_dir(self):
415 def to_work_dir(self):
382 wd = self.work_dir
416 wd = self.work_dir
383 if unicode(wd) != os.getcwdu():
417 if unicode(wd) != os.getcwdu():
@@ -386,6 +420,9 b' class ClusterDirApplication(BaseIPythonApplication):'
386
420
387 def load_config_file(self, filename, path=None):
421 def load_config_file(self, filename, path=None):
388 """Load a .py based config file by filename and path."""
422 """Load a .py based config file by filename and path."""
423 # use config.application.Application.load_config
424 # instead of inflexible
425 # core.newapplication.BaseIPythonApplication.load_config
389 return Application.load_config_file(self, filename, path=path)
426 return Application.load_config_file(self, filename, path=path)
390 #
427 #
391 # def load_default_config_file(self):
428 # def load_default_config_file(self):
@@ -393,30 +430,26 b' class ClusterDirApplication(BaseIPythonApplication):'
393 # return BaseIPythonApplication.load_config_file(self)
430 # return BaseIPythonApplication.load_config_file(self)
394
431
395 # disable URL-logging
432 # disable URL-logging
396 # def init_logging(self):
433 def reinit_logging(self):
397 # # Remove old log files
434 # Remove old log files
398 # if self.master_config.Global.clean_logs:
435 log_dir = self.cluster_dir.log_dir
399 # log_dir = self.master_config.Global.log_dir
436 if self.clean_logs:
400 # for f in os.listdir(log_dir):
437 for f in os.listdir(log_dir):
401 # if re.match(r'%s-\d+\.(log|err|out)'%self.name,f):
438 if re.match(r'%s-\d+\.(log|err|out)'%self.name,f):
402 # # if f.startswith(self.name + u'-') and f.endswith('.log'):
439 os.remove(os.path.join(log_dir, f))
403 # os.remove(os.path.join(log_dir, f))
440 if self.log_to_file:
404 # # Start logging to the new log file
441 # Start logging to the new log file
405 # if self.master_config.Global.log_to_file:
442 log_filename = self.name + u'-' + str(os.getpid()) + u'.log'
406 # log_filename = self.name + u'-' + str(os.getpid()) + u'.log'
443 logfile = os.path.join(log_dir, log_filename)
407 # logfile = os.path.join(self.log_dir, log_filename)
444 open_log_file = open(logfile, 'w')
408 # open_log_file = open(logfile, 'w')
445 else:
409 # elif self.master_config.Global.log_url:
446 open_log_file = None
410 # open_log_file = None
447 if open_log_file is not None:
411 # else:
448 self.log.removeHandler(self._log_handler)
412 # open_log_file = sys.stdout
449 self._log_handler = logging.StreamHandler(open_log_file)
413 # if open_log_file is not None:
450 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
414 # self.log.removeHandler(self._log_handler)
451 self._log_handler.setFormatter(self._log_formatter)
415 # self._log_handler = logging.StreamHandler(open_log_file)
452 self.log.addHandler(self._log_handler)
416 # self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
417 # self._log_handler.setFormatter(self._log_formatter)
418 # self.log.addHandler(self._log_handler)
419 # # log.startLogging(open_log_file)
420
453
421 def write_pid_file(self, overwrite=False):
454 def write_pid_file(self, overwrite=False):
422 """Create a .pid file in the pid_dir with my pid.
455 """Create a .pid file in the pid_dir with my pid.
This diff has been collapsed as it changes many lines, (706 lines changed) Show them Hide them
@@ -25,14 +25,16 b' from subprocess import check_call, CalledProcessError, PIPE'
25 import zmq
25 import zmq
26 from zmq.eventloop import ioloop
26 from zmq.eventloop import ioloop
27
27
28 from IPython.config.application import Application, boolean_flag
28 from IPython.config.loader import Config
29 from IPython.config.loader import Config
30 from IPython.core.newapplication import BaseIPythonApplication
29 from IPython.utils.importstring import import_item
31 from IPython.utils.importstring import import_item
30 from IPython.utils.traitlets import Int, CStr, CUnicode, Str, Bool, CFloat, Dict, List
32 from IPython.utils.traitlets import Int, CStr, CUnicode, Str, Bool, CFloat, Dict, List
31
33
32 from IPython.parallel.apps.clusterdir import (
34 from IPython.parallel.apps.clusterdir import (
33 ClusterDirApplication, ClusterDirError,
35 ClusterApplication, ClusterDirError, ClusterDir,
34 PIDFileError,
36 PIDFileError,
35 base_flags,
37 base_flags, base_aliases
36 )
38 )
37
39
38
40
@@ -52,8 +54,8 b' This command automates the startup of these processes using a wide'
52 range of startup methods (SSH, local processes, PBS, mpiexec,
54 range of startup methods (SSH, local processes, PBS, mpiexec,
53 Windows HPC Server 2008). To start a cluster with 4 engines on your
55 Windows HPC Server 2008). To start a cluster with 4 engines on your
54 local host simply do 'ipcluster start n=4'. For more complex usage
56 local host simply do 'ipcluster start n=4'. For more complex usage
55 you will typically do 'ipcluster --create profile=mycluster', then edit
57 you will typically do 'ipcluster create profile=mycluster', then edit
56 configuration files, followed by 'ipcluster --start -p mycluster -n 4'.
58 configuration files, followed by 'ipcluster start profile=mycluster n=4'.
57 """
59 """
58
60
59
61
@@ -76,158 +78,65 b' NO_CLUSTER = 12'
76 #-----------------------------------------------------------------------------
78 #-----------------------------------------------------------------------------
77 # Main application
79 # Main application
78 #-----------------------------------------------------------------------------
80 #-----------------------------------------------------------------------------
79 start_help = """Start an ipython cluster by its profile name or cluster
81 start_help = """
80 directory. Cluster directories contain configuration, log and
82 Start an ipython cluster by its profile name or cluster
81 security related files and are named using the convention
83 directory. Cluster directories contain configuration, log and
82 'cluster_<profile>' and should be creating using the 'start'
84 security related files and are named using the convention
83 subcommand of 'ipcluster'. If your cluster directory is in
85 'cluster_<profile>' and should be creating using the 'start'
84 the cwd or the ipython directory, you can simply refer to it
86 subcommand of 'ipcluster'. If your cluster directory is in
85 using its profile name, 'ipcluster start -n 4 -p <profile>`,
87 the cwd or the ipython directory, you can simply refer to it
86 otherwise use the '--cluster-dir' option.
88 using its profile name, 'ipcluster start n=4 profile=<profile>`,
87 """
89 otherwise use the 'cluster_dir' option.
88 stop_help = """Stop a running ipython cluster by its profile name or cluster
90 """
89 directory. Cluster directories are named using the convention
91 stop_help = """
90 'cluster_<profile>'. If your cluster directory is in
92 Stop a running ipython cluster by its profile name or cluster
91 the cwd or the ipython directory, you can simply refer to it
93 directory. Cluster directories are named using the convention
92 using its profile name, 'ipcluster stop -p <profile>`, otherwise
94 'cluster_<profile>'. If your cluster directory is in
93 use the '--cluster-dir' option.
95 the cwd or the ipython directory, you can simply refer to it
94 """
96 using its profile name, 'ipcluster stop profile=<profile>`, otherwise
95 engines_help = """Start one or more engines to connect to an existing Cluster
97 use the 'cluster_dir' option.
96 by profile name or cluster directory.
98 """
97 Cluster directories contain configuration, log and
99 engines_help = """
98 security related files and are named using the convention
100 Start one or more engines to connect to an existing Cluster
99 'cluster_<profile>' and should be creating using the 'start'
101 by profile name or cluster directory.
100 subcommand of 'ipcluster'. If your cluster directory is in
102 Cluster directories contain configuration, log and
101 the cwd or the ipython directory, you can simply refer to it
103 security related files and are named using the convention
102 using its profile name, 'ipcluster --engines -n 4 -p <profile>`,
104 'cluster_<profile>' and should be creating using the 'start'
103 otherwise use the 'cluster_dir' option.
105 subcommand of 'ipcluster'. If your cluster directory is in
104 """
106 the cwd or the ipython directory, you can simply refer to it
105 create_help = """Create an ipython cluster directory by its profile name or
107 using its profile name, 'ipcluster engines n=4 profile=<profile>`,
106 cluster directory path. Cluster directories contain
108 otherwise use the 'cluster_dir' option.
107 configuration, log and security related files and are named
109 """
108 using the convention 'cluster_<profile>'. By default they are
110 create_help = """
109 located in your ipython directory. Once created, you will
111 Create an ipython cluster directory by its profile name or
110 probably need to edit the configuration files in the cluster
112 cluster directory path. Cluster directories contain
111 directory to configure your cluster. Most users will create a
113 configuration, log and security related files and are named
112 cluster directory by profile name,
114 using the convention 'cluster_<profile>'. By default they are
113 'ipcluster create -p mycluster', which will put the directory
115 located in your ipython directory. Once created, you will
114 in '<ipython_dir>/cluster_mycluster'.
116 probably need to edit the configuration files in the cluster
115 """
117 directory to configure your cluster. Most users will create a
118 cluster directory by profile name,
119 `ipcluster create profile=mycluster`, which will put the directory
120 in `<ipython_dir>/cluster_mycluster`.
121 """
116 list_help = """List all available clusters, by cluster directory, that can
122 list_help = """List all available clusters, by cluster directory, that can
117 be found in the current working directly or in the ipython
123 be found in the current working directly or in the ipython
118 directory. Cluster directories are named using the convention
124 directory. Cluster directories are named using the convention
119 'cluster_<profile>'."""
125 'cluster_<profile>'.
120
126 """
121
122 flags = {}
123 flags.update(base_flags)
124 flags.update({
125 'start' : ({ 'IPClusterApp': Config({'subcommand' : 'start'})} , start_help),
126 'stop' : ({ 'IPClusterApp': Config({'subcommand' : 'stop'})} , stop_help),
127 'create' : ({ 'IPClusterApp': Config({'subcommand' : 'create'})} , create_help),
128 'engines' : ({ 'IPClusterApp': Config({'subcommand' : 'engines'})} , engines_help),
129 'list' : ({ 'IPClusterApp': Config({'subcommand' : 'list'})} , list_help),
130
131 })
132
133 class IPClusterApp(ClusterDirApplication):
134
135 name = u'ipcluster'
136 description = _description
137 usage = None
138 default_config_file_name = default_config_file_name
139 default_log_level = logging.INFO
140 auto_create_cluster_dir = False
141 classes = List()
142 def _classes_default(self,):
143 from IPython.parallel.apps import launcher
144 return launcher.all_launchers
145
146 n = Int(0, config=True,
147 help="The number of engines to start.")
148 signal = Int(signal.SIGINT, config=True,
149 help="signal to use for stopping. [default: SIGINT]")
150 delay = CFloat(1., config=True,
151 help="delay (in s) between starting the controller and the engines")
152
153 subcommand = Str('', config=True,
154 help="""ipcluster has a variety of subcommands. The general way of
155 running ipcluster is 'ipcluster --<cmd> [options]'."""
156 )
157
158 controller_launcher_class = Str('IPython.parallel.apps.launcher.LocalControllerLauncher',
159 config=True,
160 help="The class for launching a Controller."
161 )
162 engine_launcher_class = Str('IPython.parallel.apps.launcher.LocalEngineSetLauncher',
163 config=True,
164 help="The class for launching Engines."
165 )
166 reset = Bool(False, config=True,
167 help="Whether to reset config files as part of '--create'."
168 )
169 daemonize = Bool(False, config=True,
170 help='Daemonize the ipcluster program. This implies --log-to-file')
171
172 def _daemonize_changed(self, name, old, new):
173 if new:
174 self.log_to_file = True
175
176 def _n_changed(self, name, old, new):
177 # propagate n all over the place...
178 # TODO make this clean
179 # ensure all classes are covered.
180 self.config.LocalEngineSetLauncher.n=new
181 self.config.MPIExecEngineSetLauncher.n=new
182 self.config.SSHEngineSetLauncher.n=new
183 self.config.PBSEngineSetLauncher.n=new
184 self.config.SGEEngineSetLauncher.n=new
185 self.config.WinHPEngineSetLauncher.n=new
186
187 aliases = Dict(dict(
188 n='IPClusterApp.n',
189 signal = 'IPClusterApp.signal',
190 delay = 'IPClusterApp.delay',
191 clauncher = 'IPClusterApp.controller_launcher_class',
192 elauncher = 'IPClusterApp.engine_launcher_class',
193 ))
194 flags = Dict(flags)
195
127
196 def init_clusterdir(self):
197 subcommand = self.subcommand
198 if subcommand=='list':
199 self.list_cluster_dirs()
200 self.exit(0)
201 if subcommand=='create':
202 reset = self.reset_config
203 self.auto_create_cluster_dir = True
204 super(IPClusterApp, self).init_clusterdir()
205 self.log.info('Copying default config files to cluster directory '
206 '[overwrite=%r]' % (reset,))
207 self.cluster_dir.copy_all_config_files(overwrite=reset)
208 elif subcommand=='start' or subcommand=='stop':
209 self.auto_create_cluster_dir = True
210 try:
211 super(IPClusterApp, self).init_clusterdir()
212 except ClusterDirError:
213 raise ClusterDirError(
214 "Could not find a cluster directory. A cluster dir must "
215 "be created before running 'ipcluster start'. Do "
216 "'ipcluster create -h' or 'ipcluster list -h' for more "
217 "information about creating and listing cluster dirs."
218 )
219 elif subcommand=='engines':
220 self.auto_create_cluster_dir = False
221 try:
222 super(IPClusterApp, self).init_clusterdir()
223 except ClusterDirError:
224 raise ClusterDirError(
225 "Could not find a cluster directory. A cluster dir must "
226 "be created before running 'ipcluster start'. Do "
227 "'ipcluster create -h' or 'ipcluster list -h' for more "
228 "information about creating and listing cluster dirs."
229 )
230
128
129 class IPClusterList(BaseIPythonApplication):
130 name = u'ipcluster-list'
131 description = list_help
132
133 # empty aliases
134 aliases=Dict()
135 flags = Dict(base_flags)
136
137 def _log_level_default(self):
138 return 20
139
231 def list_cluster_dirs(self):
140 def list_cluster_dirs(self):
232 # Find the search paths
141 # Find the search paths
233 cluster_dir_paths = os.environ.get('IPCLUSTER_DIR_PATH','')
142 cluster_dir_paths = os.environ.get('IPCLUSTER_DIR_PATH','')
@@ -235,12 +144,10 b' class IPClusterApp(ClusterDirApplication):'
235 cluster_dir_paths = cluster_dir_paths.split(':')
144 cluster_dir_paths = cluster_dir_paths.split(':')
236 else:
145 else:
237 cluster_dir_paths = []
146 cluster_dir_paths = []
238 try:
147
239 ipython_dir = self.ipython_dir
148 ipython_dir = self.ipython_dir
240 except AttributeError:
149
241 ipython_dir = self.ipython_dir
150 paths = [os.getcwd(), ipython_dir] + cluster_dir_paths
242 paths = [os.getcwd(), ipython_dir] + \
243 cluster_dir_paths
244 paths = list(set(paths))
151 paths = list(set(paths))
245
152
246 self.log.info('Searching for cluster dirs in paths: %r' % paths)
153 self.log.info('Searching for cluster dirs in paths: %r' % paths)
@@ -250,135 +157,195 b' class IPClusterApp(ClusterDirApplication):'
250 full_path = os.path.join(path, f)
157 full_path = os.path.join(path, f)
251 if os.path.isdir(full_path) and f.startswith('cluster_'):
158 if os.path.isdir(full_path) and f.startswith('cluster_'):
252 profile = full_path.split('_')[-1]
159 profile = full_path.split('_')[-1]
253 start_cmd = 'ipcluster --start profile=%s n=4' % profile
160 start_cmd = 'ipcluster start profile=%s n=4' % profile
254 print start_cmd + " ==> " + full_path
161 print start_cmd + " ==> " + full_path
162
163 def start(self):
164 self.list_cluster_dirs()
255
165
256 def init_launchers(self):
166 create_flags = {}
257 config = self.config
167 create_flags.update(base_flags)
258 subcmd = self.subcommand
168 create_flags.update(boolean_flag('reset', 'IPClusterCreate.reset',
259 if subcmd =='start':
169 "reset config files to defaults", "leave existing config files"))
260 self.start_logging()
170
261 self.loop = ioloop.IOLoop.instance()
171 class IPClusterCreate(ClusterApplication):
262 # reactor.callWhenRunning(self.start_launchers)
172 name = u'ipcluster'
263 dc = ioloop.DelayedCallback(self.start_launchers, 0, self.loop)
173 description = create_help
264 dc.start()
174 auto_create_cluster_dir = Bool(True,
265 if subcmd == 'engines':
175 help="whether to create the cluster_dir if it doesn't exist")
266 self.start_logging()
176 default_config_file_name = default_config_file_name
267 self.loop = ioloop.IOLoop.instance()
177
268 # reactor.callWhenRunning(self.start_launchers)
178 reset = Bool(False, config=True,
269 engine_only = lambda : self.start_launchers(controller=False)
179 help="Whether to reset config files as part of 'create'."
270 dc = ioloop.DelayedCallback(engine_only, 0, self.loop)
180 )
271 dc.start()
181
182 flags = Dict(create_flags)
183
184 aliases = Dict(dict(profile='ClusterDir.profile'))
185
186 classes = [ClusterDir]
187
188 def init_clusterdir(self):
189 super(IPClusterCreate, self).init_clusterdir()
190 self.log.info('Copying default config files to cluster directory '
191 '[overwrite=%r]' % (self.reset,))
192 self.cluster_dir.copy_all_config_files(overwrite=self.reset)
193
194 def initialize(self, argv=None):
195 self.parse_command_line(argv)
196 self.init_clusterdir()
197
198 stop_aliases = dict(
199 signal='IPClusterStop.signal',
200 profile='ClusterDir.profile',
201 cluster_dir='ClusterDir.location',
202 )
203
204 class IPClusterStop(ClusterApplication):
205 name = u'ipcluster'
206 description = stop_help
207 auto_create_cluster_dir = Bool(False,
208 help="whether to create the cluster_dir if it doesn't exist")
209 default_config_file_name = default_config_file_name
210
211 signal = Int(signal.SIGINT, config=True,
212 help="signal to use for stopping processes.")
213
214 aliases = Dict(stop_aliases)
272
215
273 def start_launchers(self, controller=True):
216 def start(self):
274 config = self.config
217 """Start the app for the stop subcommand."""
275
218 try:
276 # Create the launchers. In both bases, we set the work_dir of
219 pid = self.get_pid_from_file()
277 # the launcher to the cluster_dir. This is where the launcher's
220 except PIDFileError:
278 # subprocesses will be launched. It is not where the controller
221 self.log.critical(
279 # and engine will be launched.
222 'Could not read pid file, cluster is probably not running.'
280 if controller:
281 clsname = self.controller_launcher_class
282 if '.' not in clsname:
283 clsname = 'IPython.parallel.apps.launcher.'+clsname
284 cl_class = import_item(clsname)
285 self.controller_launcher = cl_class(
286 work_dir=self.cluster_dir.location, config=config,
287 logname=self.log.name
288 )
223 )
289 # Setup the observing of stopping. If the controller dies, shut
224 # Here I exit with a unusual exit status that other processes
290 # everything down as that will be completely fatal for the engines.
225 # can watch for to learn how I existed.
291 self.controller_launcher.on_stop(self.stop_launchers)
226 self.remove_pid_file()
292 # But, we don't monitor the stopping of engines. An engine dying
227 self.exit(ALREADY_STOPPED)
293 # is just fine and in principle a user could start a new engine.
294 # Also, if we did monitor engine stopping, it is difficult to
295 # know what to do when only some engines die. Currently, the
296 # observing of engine stopping is inconsistent. Some launchers
297 # might trigger on a single engine stopping, other wait until
298 # all stop. TODO: think more about how to handle this.
299 else:
300 self.controller_launcher = None
301
228
302 clsname = self.engine_launcher_class
229 if not self.check_pid(pid):
303 if '.' not in clsname:
230 self.log.critical(
304 # not a module, presume it's the raw name in apps.launcher
231 'Cluster [pid=%r] is not running.' % pid
305 clsname = 'IPython.parallel.apps.launcher.'+clsname
232 )
306 print repr(clsname)
233 self.remove_pid_file()
307 el_class = import_item(clsname)
234 # Here I exit with a unusual exit status that other processes
235 # can watch for to learn how I existed.
236 self.exit(ALREADY_STOPPED)
237
238 elif os.name=='posix':
239 sig = self.signal
240 self.log.info(
241 "Stopping cluster [pid=%r] with [signal=%r]" % (pid, sig)
242 )
243 try:
244 os.kill(pid, sig)
245 except OSError:
246 self.log.error("Stopping cluster failed, assuming already dead.",
247 exc_info=True)
248 self.remove_pid_file()
249 elif os.name=='nt':
250 try:
251 # kill the whole tree
252 p = check_call(['taskkill', '-pid', str(pid), '-t', '-f'], stdout=PIPE,stderr=PIPE)
253 except (CalledProcessError, OSError):
254 self.log.error("Stopping cluster failed, assuming already dead.",
255 exc_info=True)
256 self.remove_pid_file()
257
258 engine_aliases = {}
259 engine_aliases.update(base_aliases)
260 engine_aliases.update(dict(
261 n='IPClusterEngines.n',
262 elauncher = 'IPClusterEngines.engine_launcher_class',
263 ))
264 class IPClusterEngines(ClusterApplication):
265
266 name = u'ipcluster'
267 description = engines_help
268 usage = None
269 default_config_file_name = default_config_file_name
270 default_log_level = logging.INFO
271 auto_create_cluster_dir = Bool(False)
272 classes = List()
273 def _classes_default(self):
274 from IPython.parallel.apps import launcher
275 launchers = launcher.all_launchers
276 eslaunchers = [ l for l in launchers if 'EngineSet' in l.__name__]
277 return [ClusterDir]+eslaunchers
278
279 n = Int(2, config=True,
280 help="The number of engines to start.")
308
281
309 self.engine_launcher = el_class(
282 engine_launcher_class = Str('LocalEngineSetLauncher',
310 work_dir=self.cluster_dir.location, config=config, logname=self.log.name
283 config=True,
284 help="The class for launching a set of Engines."
311 )
285 )
286 daemonize = Bool(False, config=True,
287 help='Daemonize the ipcluster program. This implies --log-to-file')
312
288
289 def _daemonize_changed(self, name, old, new):
290 if new:
291 self.log_to_file = True
292
293 aliases = Dict(engine_aliases)
294 # flags = Dict(flags)
295 _stopping = False
296
297 def initialize(self, argv=None):
298 super(IPClusterEngines, self).initialize(argv)
299 self.init_signal()
300 self.init_launchers()
301
302 def init_launchers(self):
303 self.engine_launcher = self.build_launcher(self.engine_launcher_class)
304 self.engine_launcher.on_stop(lambda r: self.loop.stop())
305
306 def init_signal(self):
313 # Setup signals
307 # Setup signals
314 signal.signal(signal.SIGINT, self.sigint_handler)
308 signal.signal(signal.SIGINT, self.sigint_handler)
309
310 def build_launcher(self, clsname):
311 """import and instantiate a Launcher based on importstring"""
312 if '.' not in clsname:
313 # not a module, presume it's the raw name in apps.launcher
314 clsname = 'IPython.parallel.apps.launcher.'+clsname
315 # print repr(clsname)
316 klass = import_item(clsname)
315
317
316 # Start the controller and engines
318 launcher = klass(
317 self._stopping = False # Make sure stop_launchers is not called 2x.
319 work_dir=self.cluster_dir.location, config=self.config, logname=self.log.name
318 if controller:
319 self.start_controller()
320 dc = ioloop.DelayedCallback(self.start_engines, 1000*self.delay*controller, self.loop)
321 dc.start()
322 self.startup_message()
323
324 def startup_message(self, r=None):
325 self.log.info("IPython cluster: started")
326 return r
327
328 def start_controller(self, r=None):
329 # self.log.info("In start_controller")
330 config = self.config
331 d = self.controller_launcher.start(
332 cluster_dir=self.cluster_dir.location
333 )
320 )
334 return d
321 return launcher
335
322
336 def start_engines(self, r=None):
323 def start_engines(self):
337 # self.log.info("In start_engines")
324 self.log.info("Starting %i engines"%self.n)
338 config = self.config
325 self.engine_launcher.start(
339
340 d = self.engine_launcher.start(
341 self.n,
326 self.n,
342 cluster_dir=self.cluster_dir.location
327 cluster_dir=self.cluster_dir.location
343 )
328 )
344 return d
345
329
346 def stop_controller(self, r=None):
330 def stop_engines(self):
347 # self.log.info("In stop_controller")
331 self.log.info("Stopping Engines...")
348 if self.controller_launcher and self.controller_launcher.running:
349 return self.controller_launcher.stop()
350
351 def stop_engines(self, r=None):
352 # self.log.info("In stop_engines")
353 if self.engine_launcher.running:
332 if self.engine_launcher.running:
354 d = self.engine_launcher.stop()
333 d = self.engine_launcher.stop()
355 # d.addErrback(self.log_err)
356 return d
334 return d
357 else:
335 else:
358 return None
336 return None
359
337
360 def log_err(self, f):
361 self.log.error(f.getTraceback())
362 return None
363
364 def stop_launchers(self, r=None):
338 def stop_launchers(self, r=None):
365 if not self._stopping:
339 if not self._stopping:
366 self._stopping = True
340 self._stopping = True
367 # if isinstance(r, failure.Failure):
368 # self.log.error('Unexpected error in ipcluster:')
369 # self.log.info(r.getTraceback())
370 self.log.error("IPython cluster: stopping")
341 self.log.error("IPython cluster: stopping")
371 # These return deferreds. We are not doing anything with them
342 self.stop_engines()
372 # but we are holding refs to them as a reminder that they
373 # do return deferreds.
374 d1 = self.stop_engines()
375 d2 = self.stop_controller()
376 # Wait a few seconds to let things shut down.
343 # Wait a few seconds to let things shut down.
377 dc = ioloop.DelayedCallback(self.loop.stop, 4000, self.loop)
344 dc = ioloop.DelayedCallback(self.loop.stop, 4000, self.loop)
378 dc.start()
345 dc.start()
379 # reactor.callLater(4.0, reactor.stop)
380
346
381 def sigint_handler(self, signum, frame):
347 def sigint_handler(self, signum, frame):
348 self.log.debug("SIGINT received, stopping launchers...")
382 self.stop_launchers()
349 self.stop_launchers()
383
350
384 def start_logging(self):
351 def start_logging(self):
@@ -392,25 +359,105 b' class IPClusterApp(ClusterDirApplication):'
392 # super(IPClusterApp, self).start_logging()
359 # super(IPClusterApp, self).start_logging()
393
360
394 def start(self):
361 def start(self):
395 """Start the application, depending on what subcommand is used."""
362 """Start the app for the engines subcommand."""
396 subcmd = self.subcommand
363 self.log.info("IPython cluster: started")
397 if subcmd=='create':
364 # First see if the cluster is already running
398 # init_clusterdir step completed create action
365
399 return
366 # Now log and daemonize
400 elif subcmd=='start':
367 self.log.info(
401 self.start_app_start()
368 'Starting engines with [daemon=%r]' % self.daemonize
402 elif subcmd=='stop':
369 )
403 self.start_app_stop()
370 # TODO: Get daemonize working on Windows or as a Windows Server.
404 elif subcmd=='engines':
371 if self.daemonize:
405 self.start_app_engines()
372 if os.name=='posix':
406 else:
373 from twisted.scripts._twistd_unix import daemonize
407 self.log.fatal("one command of '--start', '--stop', '--list', '--create', '--engines'"
374 daemonize()
408 " must be specified")
375
409 self.exit(-1)
376 dc = ioloop.DelayedCallback(self.start_engines, 0, self.loop)
377 dc.start()
378 # Now write the new pid file AFTER our new forked pid is active.
379 # self.write_pid_file()
380 try:
381 self.loop.start()
382 except KeyboardInterrupt:
383 pass
384 except zmq.ZMQError as e:
385 if e.errno == errno.EINTR:
386 pass
387 else:
388 raise
389
390 start_aliases = {}
391 start_aliases.update(engine_aliases)
392 start_aliases.update(dict(
393 delay='IPClusterStart.delay',
394 clean_logs='IPClusterStart.clean_logs',
395 ))
396
397 class IPClusterStart(IPClusterEngines):
398
399 name = u'ipcluster'
400 description = start_help
401 usage = None
402 default_config_file_name = default_config_file_name
403 default_log_level = logging.INFO
404 auto_create_cluster_dir = Bool(True, config=True,
405 help="whether to create the cluster_dir if it doesn't exist")
406 classes = List()
407 def _classes_default(self,):
408 from IPython.parallel.apps import launcher
409 return [ClusterDir]+launcher.all_launchers
410
411 clean_logs = Bool(True, config=True,
412 help="whether to cleanup old logs before starting")
413
414 delay = CFloat(1., config=True,
415 help="delay (in s) between starting the controller and the engines")
416
417 controller_launcher_class = Str('LocalControllerLauncher',
418 config=True,
419 help="The class for launching a Controller."
420 )
421 reset = Bool(False, config=True,
422 help="Whether to reset config files as part of '--create'."
423 )
424
425 # flags = Dict(flags)
426 aliases = Dict(start_aliases)
427
428 def init_clusterdir(self):
429 try:
430 super(IPClusterStart, self).init_clusterdir()
431 except ClusterDirError:
432 raise ClusterDirError(
433 "Could not find a cluster directory. A cluster dir must "
434 "be created before running 'ipcluster start'. Do "
435 "'ipcluster create -h' or 'ipcluster list -h' for more "
436 "information about creating and listing cluster dirs."
437 )
438
439 def init_launchers(self):
440 self.controller_launcher = self.build_launcher(self.controller_launcher_class)
441 self.engine_launcher = self.build_launcher(self.engine_launcher_class)
442 self.controller_launcher.on_stop(self.stop_launchers)
443
444 def start_controller(self):
445 self.controller_launcher.start(
446 cluster_dir=self.cluster_dir.location
447 )
448
449 def stop_controller(self):
450 # self.log.info("In stop_controller")
451 if self.controller_launcher and self.controller_launcher.running:
452 return self.controller_launcher.stop()
453
454 def stop_launchers(self, r=None):
455 if not self._stopping:
456 self.stop_controller()
457 super(IPClusterStart, self).stop_launchers()
410
458
411 def start_app_start(self):
459 def start(self):
412 """Start the app for the start subcommand."""
460 """Start the app for the start subcommand."""
413 config = self.config
414 # First see if the cluster is already running
461 # First see if the cluster is already running
415 try:
462 try:
416 pid = self.get_pid_from_file()
463 pid = self.get_pid_from_file()
@@ -439,6 +486,10 b' class IPClusterApp(ClusterDirApplication):'
439 from twisted.scripts._twistd_unix import daemonize
486 from twisted.scripts._twistd_unix import daemonize
440 daemonize()
487 daemonize()
441
488
489 dc = ioloop.DelayedCallback(self.start_controller, 0, self.loop)
490 dc.start()
491 dc = ioloop.DelayedCallback(self.start_engines, 1000*self.delay, self.loop)
492 dc.start()
442 # Now write the new pid file AFTER our new forked pid is active.
493 # Now write the new pid file AFTER our new forked pid is active.
443 self.write_pid_file()
494 self.write_pid_file()
444 try:
495 try:
@@ -453,95 +504,36 b' class IPClusterApp(ClusterDirApplication):'
453 finally:
504 finally:
454 self.remove_pid_file()
505 self.remove_pid_file()
455
506
456 def start_app_engines(self):
507 base='IPython.parallel.apps.ipclusterapp.IPCluster'
457 """Start the app for the start subcommand."""
458 config = self.config
459 # First see if the cluster is already running
460
461 # Now log and daemonize
462 self.log.info(
463 'Starting engines with [daemon=%r]' % self.daemonize
464 )
465 # TODO: Get daemonize working on Windows or as a Windows Server.
466 if self.daemonize:
467 if os.name=='posix':
468 from twisted.scripts._twistd_unix import daemonize
469 daemonize()
470
508
471 # Now write the new pid file AFTER our new forked pid is active.
509 class IPClusterApp(Application):
472 # self.write_pid_file()
510 name = u'ipcluster'
473 try:
511 description = _description
474 self.loop.start()
475 except KeyboardInterrupt:
476 pass
477 except zmq.ZMQError as e:
478 if e.errno == errno.EINTR:
479 pass
480 else:
481 raise
482 # self.remove_pid_file()
483
512
484 def start_app_stop(self):
513 subcommands = {'create' : (base+'Create', create_help),
485 """Start the app for the stop subcommand."""
514 'list' : (base+'List', list_help),
486 config = self.config
515 'start' : (base+'Start', start_help),
487 try:
516 'stop' : (base+'Stop', stop_help),
488 pid = self.get_pid_from_file()
517 'engines' : (base+'Engines', engines_help),
489 except PIDFileError:
518 }
490 self.log.critical(
519
491 'Could not read pid file, cluster is probably not running.'
520 # no aliases or flags for parent App
492 )
521 aliases = Dict()
493 # Here I exit with a unusual exit status that other processes
522 flags = Dict()
494 # can watch for to learn how I existed.
523
495 self.remove_pid_file()
524 def start(self):
496 self.exit(ALREADY_STOPPED)
525 if self.subapp is None:
497
526 print "No subcommand specified! Must specify one of: %s"%(self.subcommands.keys())
498 if not self.check_pid(pid):
527 print
499 self.log.critical(
528 self.print_subcommands()
500 'Cluster [pid=%r] is not running.' % pid
529 self.exit(1)
501 )
530 else:
502 self.remove_pid_file()
531 return self.subapp.start()
503 # Here I exit with a unusual exit status that other processes
504 # can watch for to learn how I existed.
505 self.exit(ALREADY_STOPPED)
506
507 elif os.name=='posix':
508 sig = self.signal
509 self.log.info(
510 "Stopping cluster [pid=%r] with [signal=%r]" % (pid, sig)
511 )
512 try:
513 os.kill(pid, sig)
514 except OSError:
515 self.log.error("Stopping cluster failed, assuming already dead.",
516 exc_info=True)
517 self.remove_pid_file()
518 elif os.name=='nt':
519 try:
520 # kill the whole tree
521 p = check_call(['taskkill', '-pid', str(pid), '-t', '-f'], stdout=PIPE,stderr=PIPE)
522 except (CalledProcessError, OSError):
523 self.log.error("Stopping cluster failed, assuming already dead.",
524 exc_info=True)
525 self.remove_pid_file()
526
527
532
528 def launch_new_instance():
533 def launch_new_instance():
529 """Create and run the IPython cluster."""
534 """Create and run the IPython cluster."""
530 app = IPClusterApp()
535 app = IPClusterApp()
531 app.parse_command_line()
536 app.initialize()
532 cl_config = app.config
533 app.init_clusterdir()
534 if app.config_file:
535 app.load_config_file(app.config_file)
536 else:
537 app.load_config_file(app.default_config_file_name, path=app.cluster_dir.location)
538 # command-line should *override* config file, but command-line is necessary
539 # to determine clusterdir, etc.
540 app.update_config(cl_config)
541
542 app.to_work_dir()
543 app.init_launchers()
544
545 app.start()
537 app.start()
546
538
547
539
@@ -38,7 +38,7 b' from IPython.parallel import factory'
38
38
39 from IPython.parallel.apps.clusterdir import (
39 from IPython.parallel.apps.clusterdir import (
40 ClusterDir,
40 ClusterDir,
41 ClusterDirApplication,
41 ClusterApplication,
42 base_flags
42 base_flags
43 # ClusterDirConfigLoader
43 # ClusterDirConfigLoader
44 )
44 )
@@ -104,7 +104,7 b' flags.update({'
104
104
105 flags.update()
105 flags.update()
106
106
107 class IPControllerApp(ClusterDirApplication):
107 class IPControllerApp(ClusterApplication):
108
108
109 name = u'ipcontroller'
109 name = u'ipcontroller'
110 description = _description
110 description = _description
@@ -361,6 +361,12 b' class IPControllerApp(ClusterDirApplication):'
361 # handler.setLevel(self.log_level)
361 # handler.setLevel(self.log_level)
362 # self.log.addHandler(handler)
362 # self.log.addHandler(handler)
363 # #
363 # #
364
365 def initialize(self, argv=None):
366 super(IPControllerApp, self).initialize(argv)
367 self.init_hub()
368 self.init_schedulers()
369
364 def start(self):
370 def start(self):
365 # Start the subprocesses:
371 # Start the subprocesses:
366 self.factory.start()
372 self.factory.start()
@@ -380,27 +386,13 b' class IPControllerApp(ClusterDirApplication):'
380 self.factory.loop.start()
386 self.factory.loop.start()
381 except KeyboardInterrupt:
387 except KeyboardInterrupt:
382 self.log.critical("Interrupted, Exiting...\n")
388 self.log.critical("Interrupted, Exiting...\n")
389
383
390
384
391
385 def launch_new_instance():
392 def launch_new_instance():
386 """Create and run the IPython controller"""
393 """Create and run the IPython controller"""
387 app = IPControllerApp()
394 app = IPControllerApp()
388 app.parse_command_line()
395 app.initialize()
389 cl_config = app.config
390 # app.load_config_file()
391 app.init_clusterdir()
392 if app.config_file:
393 app.load_config_file(app.config_file)
394 else:
395 app.load_config_file(app.default_config_file_name, path=app.cluster_dir.location)
396 # command-line should *override* config file, but command-line is necessary
397 # to determine clusterdir, etc.
398 app.update_config(cl_config)
399
400 app.to_work_dir()
401 app.init_hub()
402 app.init_schedulers()
403
404 app.start()
396 app.start()
405
397
406
398
@@ -23,7 +23,7 b' import zmq'
23 from zmq.eventloop import ioloop
23 from zmq.eventloop import ioloop
24
24
25 from IPython.parallel.apps.clusterdir import (
25 from IPython.parallel.apps.clusterdir import (
26 ClusterDirApplication,
26 ClusterApplication,
27 ClusterDir,
27 ClusterDir,
28 base_aliases,
28 base_aliases,
29 # ClusterDirConfigLoader
29 # ClusterDirConfigLoader
@@ -99,13 +99,16 b' class MPI(Configurable):'
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101
101
102 class IPEngineApp(ClusterDirApplication):
102 class IPEngineApp(ClusterApplication):
103
103
104 app_name = Unicode(u'ipengine')
104 app_name = Unicode(u'ipengine')
105 description = Unicode(_description)
105 description = Unicode(_description)
106 default_config_file_name = default_config_file_name
106 default_config_file_name = default_config_file_name
107 classes = List([ClusterDir, StreamSession, EngineFactory, Kernel, MPI])
107 classes = List([ClusterDir, StreamSession, EngineFactory, Kernel, MPI])
108
108
109 auto_create_cluster_dir = Bool(False, config=True,
110 help="whether to create the cluster_dir if it doesn't exist")
111
109 startup_script = Unicode(u'', config=True,
112 startup_script = Unicode(u'', config=True,
110 help='specify a script to be run at startup')
113 help='specify a script to be run at startup')
111 startup_command = Str('', config=True,
114 startup_command = Str('', config=True,
@@ -262,7 +265,11 b' class IPEngineApp(ClusterDirApplication):'
262 else:
265 else:
263 mpi = None
266 mpi = None
264
267
265
268 def initialize(self, argv=None):
269 super(IPEngineApp, self).initialize(argv)
270 self.init_mpi()
271 self.init_engine()
272
266 def start(self):
273 def start(self):
267 self.engine.start()
274 self.engine.start()
268 try:
275 try:
@@ -274,25 +281,7 b' class IPEngineApp(ClusterDirApplication):'
274 def launch_new_instance():
281 def launch_new_instance():
275 """Create and run the IPython engine"""
282 """Create and run the IPython engine"""
276 app = IPEngineApp()
283 app = IPEngineApp()
277 app.parse_command_line()
284 app.initialize()
278 cl_config = app.config
279 app.init_clusterdir()
280 # app.load_config_file()
281 # print app.config
282 if app.config_file:
283 app.load_config_file(app.config_file)
284 else:
285 app.load_config_file(app.default_config_file_name, path=app.cluster_dir.location)
286
287 # command-line should *override* config file, but command-line is necessary
288 # to determine clusterdir, etc.
289 app.update_config(cl_config)
290
291 # print app.config
292 app.to_work_dir()
293 app.init_mpi()
294 app.init_engine()
295 print app.config
296 app.start()
285 app.start()
297
286
298
287
@@ -21,7 +21,7 b' import sys'
21 import zmq
21 import zmq
22
22
23 from IPython.parallel.apps.clusterdir import (
23 from IPython.parallel.apps.clusterdir import (
24 ClusterDirApplication,
24 ClusterApplication,
25 ClusterDirConfigLoader
25 ClusterDirConfigLoader
26 )
26 )
27 from IPython.parallel.apps.logwatcher import LogWatcher
27 from IPython.parallel.apps.logwatcher import LogWatcher
@@ -74,7 +74,7 b' class IPLoggerAppConfigLoader(ClusterDirConfigLoader):'
74 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
75
75
76
76
77 class IPLoggerApp(ClusterDirApplication):
77 class IPLoggerApp(ClusterApplication):
78
78
79 name = u'iploggerz'
79 name = u'iploggerz'
80 description = _description
80 description = _description
General Comments 0
You need to be logged in to leave comments. Login now