gstlal 1.13.0
Loading...
Searching...
No Matches
pipeline.py
1"""
2This modules contains objects that make it simple for the user to
3create python scripts that build Condor DAGs to run code on the LSC
4Data Grid.
5
6This file is part of the Grid LSC User Environment (GLUE)
7
8GLUE is free software: you can redistribute it and/or modify it under the
9terms of the GNU General Public License as published by the Free Software
10Foundation, either version 3 of the License, or (at your option) any later
11version.
12
13This program is distributed in the hope that it will be useful, but WITHOUT
14ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16details.
17
18You should have received a copy of the GNU General Public License along with
19this program. If not, see <http://www.gnu.org/licenses/>.
20"""
21
22from __future__ import print_function
23__author__ = 'Duncan Brown <duncan@gravity.phys.uwm.edu>'
24from gstlal import __date__, __version__
25
26from collections import OrderedDict
27import os
28import sys
29import re
30import time
31import random
32import math
33import stat
34import socket
35import itertools
36import ligo.segments
37from hashlib import md5
38import warnings
39
40try:
41 from cjson import decode
42except ImportError:
43 from json import loads as decode
44
45warnings.warn(
46 "all functionality within this module exists to support deprecated pipeline "
47 "generation programs, and will be removed from gstlal in the future",
48 DeprecationWarning,
49)
50
51
52class CondorError(Exception):
53 """Error thrown by Condor Jobs"""
54 def __init__(self, args=None):
55 self.args = args
58class CondorSubmitError(CondorError):
59 pass
66class SegmentError(Exception):
67 def __init__(self, args=None):
68 self.args = args
69
70
71class CondorJob(object):
72 """
73 Generic condor job class. Provides methods to set the options in the
74 condor submit file for a particular executable
75 """
76 def __init__(self, universe, executable, queue):
77 """
78 @param universe: the condor universe to run the job in.
79 @param executable: the executable to run.
80 @param queue: number of jobs to queue.
81 """
82 self.__universe = universe
83 self.__executable = executable
84 self.__queue = queue
85
86 # These are set by methods in the class
87 self.__options = {}
88 self.__short_options = {}
89 self.__arguments = []
90 self.__condor_cmds = OrderedDict()
91 self.__notification = None
92 self.__log_file = None
93 self.__in_file = None
94 self.__err_file = None
95 self.__out_file = None
96 self.__sub_file_path = None
97 self.__output_files = []
98 self.__input_files = []
99 self.__checkpoint_files = []
100 self.__grid_type = None
101 self.__grid_server = None
102 self.__grid_scheduler = None
103 self.__executable_installed = True
104
105 def get_executable(self):
106 """
107 Return the name of the executable for this job.
108 """
109 return self.__executable
110
111 def set_executable(self, executable):
112 """
113 Set the name of the executable for this job.
114 """
115 self.__executable = executable
116
117 def get_universe(self):
118 """
119 Return the condor universe that the job will run in.
120 """
121 return self.__universe
122
123 def set_universe(self, universe):
124 """
125 Set the condor universe for the job to run in.
126 @param universe: the condor universe to run the job in.
127 """
128 self.__universe = universe
129
130 def get_grid_type(self):
131 """
132 Return the grid type of the job.
133 """
134 return self.__grid_type
135
136 def set_grid_type(self, grid_type):
137 """
138 Set the type of grid resource for the job.
139 @param grid_type: type of grid resource.
140 """
141 self.__grid_type = grid_type
142
144 """
145 Return the grid server on which the job will run.
146 """
147 return self.__grid_server
148
149 def set_grid_server(self, grid_server):
150 """
151 Set the grid server on which to run the job.
152 @param grid_server: grid server on which to run.
153 """
154 self.__grid_server = grid_server
155
157 """
158 Return the grid scheduler.
159 """
160 return self.__grid_scheduler
161
162 def set_grid_scheduler(self, grid_scheduler):
163 """
164 Set the grid scheduler.
165 @param grid_scheduler: grid scheduler on which to run.
166 """
167 self.__grid_scheduler = grid_scheduler
168
169 def set_executable_installed(self,installed):
170 """
171 If executable installed is true, then no copying of the executable is
172 done. If it is false, pegasus stages the executable to the remote site.
173 Default is executable is installed (i.e. True).
174 @param installed: true or fale
175 """
176 self.__executable_installed = installed
177
179 """
180 return whether or not the executable is installed
181 """
182 return self.__executable_installed
183
184 def add_condor_cmd(self, cmd, value):
185 """
186 Add a Condor command to the submit file (e.g. a class add or evironment).
187 @param cmd: Condor command directive.
188 @param value: value for command.
189 """
190 self.__condor_cmds[cmd] = value
191
193 """
194 Return the dictionary of condor keywords to add to the job
195 """
196 return self.__condor_cmds
197
198 def add_input_file(self, filename):
199 """
200 Add filename as a necessary input file for this DAG node.
201
202 @param filename: input filename to add
203 """
204 if filename not in self.__input_files:
205 self.__input_files.append(filename)
206
207 def add_output_file(self, filename):
208 """
209 Add filename as a output file for this DAG node.
210
211 @param filename: output filename to add
212 """
213 if filename not in self.__output_files:
214 self.__output_files.append(filename)
215
216 def add_checkpoint_file(self, filename):
217 """
218 Add filename as a checkpoint file for this DAG job.
219 """
220 if filename not in self.__checkpoint_files:
221 self.__checkpoint_files.append(filename)
222
224 """
225 Return list of input files for this DAG node.
226 """
227 return self.__input_files
228
230 """
231 Return list of output files for this DAG node.
232 """
233 return self.__output_files
234
236 """
237 Return a list of checkpoint files for this DAG node
238 """
239 return self.__checkpoint_files
240
241 def add_arg(self, arg):
242 """
243 Add an argument to the executable. Arguments are appended after any
244 options and their order is guaranteed.
245 @param arg: argument to add.
246 """
247 self.__arguments.append(arg)
248
249 def add_file_arg(self, filename):
250 """
251 Add a file argument to the executable. Arguments are appended after any
252 options and their order is guaranteed. Also adds the file name to the
253 list of required input data for this job.
254 @param filename: file to add as argument.
255 """
256 self.__arguments.append(filename)
257 if filename not in self.__input_files:
258 self.__input_files.append(filename)
259
260 def get_args(self):
261 """
262 Return the list of arguments that are to be passed to the executable.
263 """
264 return self.__arguments
265
266 def add_opt(self, opt, value):
267 """
268 Add a command line option to the executable. The order that the arguments
269 will be appended to the command line is not guaranteed, but they will
270 always be added before any command line arguments. The name of the option
271 is prefixed with double hyphen and the program is expected to parse it
272 with getopt_long().
273 @param opt: command line option to add.
274 @param value: value to pass to the option (None for no argument).
275 """
276 self.__options[opt] = value
277
278 def get_opt( self, opt):
279 """
280 Returns the value associated with the given command line option.
281 Returns None if the option does not exist in the options list.
282 @param opt: command line option
283 """
284 if opt in self.__options:
285 return self.__options[opt]
286 return None
287
288 def add_file_opt(self, opt, filename):
289 """
290 Add a command line option to the executable. The order that the arguments
291 will be appended to the command line is not guaranteed, but they will
292 always be added before any command line arguments. The name of the option
293 is prefixed with double hyphen and the program is expected to parse it
294 with getopt_long().
295 @param opt: command line option to add.
296 @param value: value to pass to the option (None for no argument).
297 """
298 self.__options[opt] = filename
299 if filename not in self.__input_files:
300 self.__input_files.append(filename)
301
302 def get_opts(self):
303 """
304 Return the dictionary of opts for the job.
305 """
306 return self.__options
307
308 def add_short_opt(self, opt, value):
309 """
310 Add a command line option to the executable. The order that the arguments
311 will be appended to the command line is not guaranteed, but they will
312 always be added before any command line arguments. The name of the option
313 is prefixed with single hyphen and the program is expected to parse it
314 with getopt() or getopt_long() (if a single character option), or
315 getopt_long_only() (if multiple characters). Long and (single-character)
316 short options may be mixed if the executable permits this.
317 @param opt: command line option to add.
318 @param value: value to pass to the option (None for no argument).
319 """
320 self.__short_options[opt] = value
321
322 def get_short_opts(self):
323 """
324 Return the dictionary of short options for the job.
325 """
326 return self.__short_options
327
328 def add_ini_opts(self, cp, section):
329 """
330 Parse command line options from a given section in an ini file and
331 pass to the executable.
332 @param cp: ConfigParser object pointing to the ini file.
333 @param section: section of the ini file to add to the options.
334 """
335 for opt in cp.options(section):
336 arg = str(cp.get(section,opt)).strip()
337 self.__options[opt] = arg
338
339 def set_notification(self, value):
340 """
341 Set the email address to send notification to.
342 @param value: email address or never for no notification.
343 """
344 self.__notification = value
345
346 def set_log_file(self, path):
347 """
348 Set the Condor log file.
349 @param path: path to log file.
350 """
351 self.__log_file = path
352
353 def set_stdin_file(self, path):
354 """
355 Set the file from which Condor directs the stdin of the job.
356 @param path: path to stdin file.
357 """
358 self.__in_file = path
359
360 def get_stdin_file(self):
361 """
362 Get the file from which Condor directs the stdin of the job.
363 """
364 return self.__in_file
365
366 def set_stderr_file(self, path):
367 """
368 Set the file to which Condor directs the stderr of the job.
369 @param path: path to stderr file.
370 """
371 self.__err_file = path
372
374 """
375 Get the file to which Condor directs the stderr of the job.
376 """
377 return self.__err_file
378
379 def set_stdout_file(self, path):
380 """
381 Set the file to which Condor directs the stdout of the job.
382 @param path: path to stdout file.
383 """
384 self.__out_file = path
385
387 """
388 Get the file to which Condor directs the stdout of the job.
389 """
390 return self.__out_file
391
392 def set_sub_file(self, path):
393 """
394 Set the name of the file to write the Condor submit file to when
395 write_sub_file() is called.
396 @param path: path to submit file.
397 """
398 self.__sub_file_path = path
399
400 def get_sub_file(self):
401 """
402 Get the name of the file which the Condor submit file will be
403 written to when write_sub_file() is called.
404 """
405 return self.__sub_file_path
406
407 def write_sub_file(self):
408 """
409 Write a submit file for this Condor job.
410 """
411 if not self.__log_file:
412 raise CondorSubmitError("Log file not specified.")
413 if not self.__err_file:
414 raise CondorSubmitError("Error file not specified.")
415 if not self.__out_file:
416 raise CondorSubmitError("Output file not specified.")
417
418 if not self.__sub_file_path:
419 raise CondorSubmitError('No path for submit file.')
420 try:
421 subfile = open(self.__sub_file_path, 'w')
422 except:
423 raise CondorSubmitError("Cannot open file " + self.__sub_file_path)
424
425 if self.__universe == 'grid':
426 if self.__grid_type == None:
427 raise CondorSubmitError('No grid type specified.')
428 elif self.__grid_type == 'gt2':
429 if self.__grid_server == None:
430 raise CondorSubmitError('No server specified for grid resource.')
431 elif self.__grid_type == 'gt4':
432 if self.__grid_server == None:
433 raise CondorSubmitError('No server specified for grid resource.')
434 if self.__grid_scheduler == None:
435 raise CondorSubmitError('No scheduler specified for grid resource.')
436 else:
437 raise CondorSubmitError('Unsupported grid resource.')
438
439 subfile.write( 'universe = ' + self.__universe + '\n' )
440 subfile.write( 'executable = ' + self.__executable + '\n' )
441
442 if self.__universe == 'grid':
443 if self.__grid_type == 'gt2':
444 subfile.write('grid_resource = %s %s\n' % (self.__grid_type,
445 self.__grid_server))
446 if self.__grid_type == 'gt4':
447 subfile.write('grid_resource = %s %s %s\n' % (self.__grid_type,
449
450 if self.__universe == 'grid':
451 subfile.write('when_to_transfer_output = ON_EXIT\n')
452 subfile.write('transfer_output_files = $(macrooutput)\n')
453 subfile.write('transfer_input_files = $(macroinput)\n')
454
455 if list(self.__options.keys()) or list(self.__short_options.keys()) or self.__arguments:
456 subfile.write( 'arguments = "' )
457 for c in self.__arguments:
458 subfile.write( ' ' + c )
459 for c in self.__options.keys():
460 if self.__options[c]:
461 subfile.write( ' --' + c + ' ' + self.__options[c] )
462 else:
463 subfile.write( ' --' + c )
464 for c in self.__short_options.keys():
465 if self.__short_options[c]:
466 subfile.write( ' -' + c + ' ' + self.__short_options[c] )
467 else:
468 subfile.write( ' -' + c )
469 subfile.write( ' "\n' )
470
471 for cmd in self.__condor_cmds.keys():
472 subfile.write( str(cmd) + " = " + str(self.__condor_cmds[cmd]) + '\n' )
473
474 subfile.write( 'log = ' + self.__log_file + '\n' )
475 if self.__in_file is not None:
476 subfile.write( 'input = ' + self.__in_file + '\n' )
477 subfile.write( 'error = ' + self.__err_file + '\n' )
478 subfile.write( 'output = ' + self.__out_file + '\n' )
479 if self.__notification:
480 subfile.write( 'notification = ' + self.__notification + '\n' )
481 subfile.write( 'queue ' + str(self.__queue) + '\n' )
482
483 subfile.close()
484
485
486
488 """
489 A Condor DAG job never notifies the user on completion and can have variable
490 options that are set for a particular node in the DAG. Inherits methods
491 from a CondorJob.
492 """
493 def __init__(self, universe, executable):
494 """
495 universe = the condor universe to run the job in.
496 executable = the executable to run in the DAG.
497 """
498 super(CondorDAGJob,self).__init__(universe, executable, 1)
499 CondorJob.set_notification(self, 'never')
500 self.__var_opts = []
501 self.__arg_index = 0
502 self.__var_args = []
503 self.__var_cmds = []
504 self.__grid_site = None
505 self.__bad_macro_chars = re.compile(r'[_-]')
506
507 def create_node(self):
508 """
509 Create a condor node from this job. This provides a basic interface to
510 the CondorDAGNode class. Most jobs in a workflow will subclass the
511 CondorDAGNode class and overwrite this to give more details when
512 initializing the node. However, this will work fine for jobs with very simp
513 input/output.
514 """
515 return CondorDAGNode(self)
516
517 def set_grid_site(self,site):
518 """
519 Set the grid site to run on. If not specified,
520 will not give hint to Pegasus
521 """
522 self.__grid_site=str(site)
523 if site != 'local':
524 self.set_executable_installed(False)
525
526 def get_grid_site(self):
527 """
528 Return the grid site for this node
529 """
530 return self.__grid_site
531
532 def add_var_opt(self, opt, short=False):
533 """
534 Add a variable (or macro) option to the condor job. The option is added
535 to the submit file and a different argument to the option can be set for
536 each node in the DAG.
537 @param opt: name of option to add.
538 """
539 if opt not in self.__var_opts:
540 self.__var_opts.append(opt)
541 macro = self.__bad_macro_chars.sub( r'', opt )
542 if short:
543 self.add_short_opt(opt,'$(macro' + macro + ')')
544 else:
545 self.add_opt(opt,'$(macro' + macro + ')')
546
547 def add_var_condor_cmd(self, command):
548 """
549 Add a condor command to the submit file that allows variable (macro)
550 arguments to be passes to the executable.
551 """
552 if command not in self.__var_cmds:
553 self.__var_cmds.append(command)
554 macro = self.__bad_macro_chars.sub( r'', command )
555 self.add_condor_cmd(command, '$(macro' + macro + ')')
556
557 def add_var_arg(self,arg_index,quote=False):
558 """
559 Add a command to the submit file to allow variable (macro) arguments
560 to be passed to the executable.
561 """
562 try:
563 self.__var_args[arg_index]
564 except IndexError:
565 if arg_index != self.__arg_index:
566 raise CondorDAGJobError("mismatch between job and node var_arg index")
567 if quote:
568 self.__var_args.append("'$(macroargument%s)'" % str(arg_index))
569 else:
570 self.__var_args.append('$(macroargument%s)' % str(arg_index))
571 self.add_arg(self.__var_args[self.__arg_index])
572 self.__arg_index += 1
573
574
575class CondorDAGManJob(object):
576 """
577 Condor DAGMan job class. Appropriate for setting up DAGs to run within a
578 DAG.
579 """
580 def __init__(self, dag, dir=None):
581 """
582 dag = the name of the condor dag file to run
583 dir = the diretory in which the dag file is located
584 """
585 self.__dag = dag
586 self.__notification = None
587 self.__dag_directory= dir
588
589 def create_node(self):
590 """
591 Create a condor node from this job. This provides a basic interface to
592 the CondorDAGManNode class. Most jobs in a workflow will subclass the
593 CondorDAGManNode class and overwrite this to give more details when
594 initializing the node. However, this will work fine for jobs with very simp
595 input/output.
596 """
597 return CondorDAGManNode(self)
598
599 def set_dag_directory(self, dir):
600 """
601 Set the directory where the dag will be run
602 @param dir: the name of the directory where the dag will be run
603 """
604 self.__dag_directory = dir
605
607 """
608 Get the directory where the dag will be run
609 """
610 return self.__dag_directory
611
612 def set_notification(self, value):
613 """
614 Set the email address to send notification to.
615 @param value: email address or never for no notification.
616 """
617 self.__notification = value
618
619 def get_sub_file(self):
620 """
621 Return the name of the dag as the submit file name for the
622 SUBDAG EXTERNAL command in the uber-dag
623 """
624 return self.__dag
625
626 def write_sub_file(self):
627 """
628 Do nothing as there is not need for a sub file with the
629 SUBDAG EXTERNAL command in the uber-dag
630 """
631 pass
632
633 def get_dag(self):
634 """
635 Return the name of any associated dag file
636 """
637 return self.__dag
638
639
640class CondorDAGNode(object):
641 """
642 A CondorDAGNode represents a node in the DAG. It corresponds to a particular
643 condor job (and so a particular submit file). If the job has variable
644 (macro) options, they can be set here so each nodes executes with the
645 correct options.
646 """
647 def __init__(self, job):
648 """
649 @param job: the CondorJob that this node corresponds to.
650 """
651 if not isinstance(job, CondorDAGJob) and \
652 not isinstance(job,CondorDAGManJob):
653 raise CondorDAGNodeError(
654 "A DAG node must correspond to a Condor DAG job or Condor DAGMan job")
655 self.__name = None
656 self.__job = job
657 self.__category = None
658 self.__priority = None
659 self.__pre_script = None
660 self.__pre_script_args = []
661 self.__post_script = None
662 self.__post_script_args = []
663 self.__macros = {}
664 self.__opts = {}
665 self.__args = []
666 self.__arg_index = 0
667 self.__retry = 0
668 self.__parents = []
669 self.__bad_macro_chars = re.compile(r'[_-]')
670 self.__output_files = []
671 self.__input_files = []
672 self.__checkpoint_files = []
673 self.__vds_group = None
674 if isinstance(job,CondorDAGJob) and job.get_universe()=='standard':
675 self.__grid_start = 'none'
676 else:
677 self.__grid_start = None
678
679 # generate the md5 node name
680 t = str( int( time.time() * 1000 ) )
681 r = str( int( random.random() * 100000000000000000 ) )
682 a = str( self.__class__ )
683 self.__name = md5((t + r + a).encode()).hexdigest()
684 self.__md5name = self.__name
685
686 def __repr__(self):
687 return self.__name
688
689 def job(self):
690 """
691 Return the CondorJob that this node is associated with.
692 """
693 return self.__job
694
695 def set_pre_script(self,script):
696 """
697 Sets the name of the pre script that is executed before the DAG node is
698 run.
699 @param script: path to script
700 """
701 self.__pre_script = script
702
703 def add_pre_script_arg(self,arg):
704 """
705 Adds an argument to the pre script that is executed before the DAG node is
706 run.
707 """
708 self.__pre_script_args.append(arg)
709
710 def set_post_script(self,script):
711 """
712 Sets the name of the post script that is executed before the DAG node is
713 run.
714 @param script: path to script
715 """
716 self.__post_script = script
717
719 """
720 returns the name of the post script that is executed before the DAG node is
721 run.
722 @param script: path to script
723 """
724 return self.__post_script
725
726 def add_post_script_arg(self,arg):
727 """
728 Adds an argument to the post script that is executed before the DAG node is
729 run.
730 """
731 self.__post_script_args.append(arg)
732
734 """
735 Returns and array of arguments to the post script that is executed before
736 the DAG node is run.
737 """
738 return self.__post_script_args
739
740 def set_name(self,name):
741 """
742 Set the name for this node in the DAG.
743 """
744 self.__name = str(name)
745
746 def get_name(self):
747 """
748 Get the name for this node in the DAG.
749 """
750 return self.__name
751
752 def set_category(self,category):
753 """
754 Set the category for this node in the DAG.
755 """
756 self.__category = str(category)
757
758 def get_category(self):
759 """
760 Get the category for this node in the DAG.
761 """
762 return self.__category
763
764 def set_priority(self,priority):
765 """
766 Set the priority for this node in the DAG.
767 """
768 self.__priority = str(priority)
769
770 def get_priority(self):
771 """
772 Get the priority for this node in the DAG.
773 """
774 return self.__priority
775
776 def add_input_file(self, filename):
777 """
778 Add filename as a necessary input file for this DAG node.
779
780 @param filename: input filename to add
781 """
782 if filename not in self.__input_files:
783 self.__input_files.append(filename)
784 if not isinstance(self.job(), CondorDAGManJob):
785 if self.job().get_universe() == 'grid':
786 self.add_input_macro(filename)
787
788 def add_output_file(self, filename):
789 """
790 Add filename as a output file for this DAG node.
791
792 @param filename: output filename to add
793 """
794 if filename not in self.__output_files:
795 self.__output_files.append(filename)
796 if not isinstance(self.job(), CondorDAGManJob):
797 if self.job().get_universe() == 'grid':
798 self.add_output_macro(filename)
799
800 def add_checkpoint_file(self,filename):
801 """
802 Add filename as a checkpoint file for this DAG node
803 @param filename: checkpoint filename to add
804 """
805 if filename not in self.__checkpoint_files:
806 self.__checkpoint_files.append(filename)
807 if not isinstance(self.job(), CondorDAGManJob):
808 if self.job().get_universe() == 'grid':
809 self.add_checkpoint_macro(filename)
810
812 """
813 Return list of input files for this DAG node and its job.
814 """
815 input_files = list(self.__input_files)
816 if isinstance(self.job(), CondorDAGJob):
817 input_files = input_files + self.job().get_input_files()
818 return input_files
819
821 """
822 Return list of output files for this DAG node and its job.
823 """
824 output_files = list(self.__output_files)
825 if isinstance(self.job(), CondorDAGJob):
826 output_files = output_files + self.job().get_output_files()
827 return output_files
828
830 """
831 Return a list of checkpoint files for this DAG node and its job.
832 """
833 checkpoint_files = list(self.__checkpoint_files)
834 if isinstance(self.job(), CondorDAGJob):
835 checkpoint_files = checkpoint_files + self.job().get_checkpoint_files()
836 return checkpoint_files
837
838 def set_vds_group(self,group):
839 """
840 Set the name of the VDS group key when generating a DAX
841 @param group: name of group for thus nore
842 """
843 self.__vds_group = str(group)
844
845 def get_vds_group(self):
846 """
847 Returns the VDS group key for this node
848 """
849 return self.__vds_group
850
851 def add_macro(self,name,value):
852 """
853 Add a variable (macro) for this node. This can be different for
854 each node in the DAG, even if they use the same CondorJob. Within
855 the CondorJob, the value of the macro can be referenced as
856 '$(name)' -- for instance, to define a unique output or error file
857 for each node.
858 @param name: macro name.
859 @param value: value of the macro for this node in the DAG
860 """
861 macro = self.__bad_macro_chars.sub( r'', name )
862 self.__opts[macro] = value
863
864 def add_io_macro(self,io,filename):
865 """
866 Add a variable (macro) for storing the input/output files associated
867 with this node.
868 @param io: macroinput or macrooutput
869 @param filename: filename of input/output file
870 """
871 io = self.__bad_macro_chars.sub( r'', io )
872 if io not in self.__opts:
873 self.__opts[io] = filename
874 else:
875 if filename not in self.__opts[io]:
876 self.__opts[io] += ',%s' % filename
877
878 def add_input_macro(self,filename):
879 """
880 Add a variable (macro) for storing the input files associated with
881 this node.
882 @param filename: filename of input file
883 """
884 self.add_io_macro('macroinput', filename)
885
886 def add_output_macro(self,filename):
887 """
888 Add a variable (macro) for storing the output files associated with
889 this node.
890 @param filename: filename of output file
891 """
892 self.add_io_macro('macrooutput', filename)
893
894 def add_checkpoint_macro(self,filename):
895 self.add_io_macro('macrocheckpoint',filename)
896
897 def get_opts(self):
898 """
899 Return the opts for this node. Note that this returns only
900 the options for this instance of the node and not those
901 associated with the underlying job template.
902 """
903 return self.__opts
904
905 def add_var_condor_cmd(self, command, value):
906 """
907 Add a variable (macro) condor command for this node. If the command
908 specified does not exist in the CondorJob, it is added so the submit file
909 will be correct.
910 PLEASE NOTE: AS with other add_var commands, the variable must be set for
911 all nodes that use the CondorJob instance.
912 @param command: command name
913 @param value: Value of the command for this node in the DAG/DAX.
914 """
915 macro = self.__bad_macro_chars.sub( r'', command )
916 self.__macros['macro' + macro] = value
917 self.__job.add_var_condor_cmd(command)
918
919 def add_var_opt(self,opt,value,short=False):
920 """
921 Add a variable (macro) option for this node. If the option
922 specified does not exist in the CondorJob, it is added so the submit
923 file will be correct when written.
924 @param opt: option name.
925 @param value: value of the option for this node in the DAG.
926 """
927 macro = self.__bad_macro_chars.sub( r'', opt )
928 self.__opts['macro' + macro] = value
929 self.__job.add_var_opt(opt,short)
930
931 def add_file_opt(self,opt,filename,file_is_output_file=False):
932 """
933 Add a variable (macro) option for this node. If the option
934 specified does not exist in the CondorJob, it is added so the submit
935 file will be correct when written. The value of the option is also
936 added to the list of input files for the DAX.
937 @param opt: option name.
938 @param value: value of the option for this node in the DAG.
939 @param file_is_output_file: A boolean if the file will be an output file
940 instead of an input file. The default is to have it be an input.
941 """
942 self.add_var_opt(opt,filename)
943 if file_is_output_file: self.add_output_file(filename)
944 else: self.add_input_file(filename)
945
946 def add_var_arg(self, arg,quote=False):
947 """
948 Add a variable (or macro) argument to the condor job. The argument is
949 added to the submit file and a different value of the argument can be set
950 for each node in the DAG.
951 @param arg: name of option to add.
952 """
953 self.__args.append(arg)
954 self.__job.add_var_arg(self.__arg_index,quote=quote)
955 self.__arg_index += 1
956
957 def add_file_arg(self, filename):
958 """
959 Add a variable (or macro) file name argument to the condor job. The
960 argument is added to the submit file and a different value of the
961 argument can be set for each node in the DAG. The file name is also
962 added to the list of input files for the DAX.
963 @param filename: name of option to add.
964 """
965 self.add_input_file(filename)
966 self.add_var_arg(filename)
967
968 def get_args(self):
969 """
970 Return the arguments for this node. Note that this returns
971 only the arguments for this instance of the node and not those
972 associated with the underlying job template.
973 """
974 return self.__args
975
976 def set_retry(self, retry):
977 """
978 Set the number of times that this node in the DAG should retry.
979 @param retry: number of times to retry node.
980 """
981 self.__retry = retry
982
983 def get_retry(self):
984 """
985 Return the number of times that this node in the DAG should retry.
986 @param retry: number of times to retry node.
987 """
988 return self.__retry
989
990 def write_job(self,fh):
991 """
992 Write the DAG entry for this node's job to the DAG file descriptor.
993 @param fh: descriptor of open DAG file.
994 """
995 if isinstance(self.job(),CondorDAGManJob):
996 # create an external subdag from this dag
997 fh.write( ' '.join(
998 ['SUBDAG EXTERNAL', self.__name, self.__job.get_sub_file()]) )
999 if self.job().get_dag_directory():
1000 fh.write( ' DIR ' + self.job().get_dag_directory() )
1001 else:
1002 # write a regular condor job
1003 fh.write( 'JOB ' + self.__name + ' ' + self.__job.get_sub_file() )
1004 fh.write( '\n')
1005
1006 fh.write( 'RETRY ' + self.__name + ' ' + str(self.__retry) + '\n' )
1007
1008 def write_category(self,fh):
1009 """
1010 Write the DAG entry for this node's category to the DAG file descriptor.
1011 @param fh: descriptor of open DAG file.
1012 """
1013 fh.write( 'CATEGORY ' + self.__name + ' ' + self.__category + '\n' )
1014
1015 def write_priority(self,fh):
1016 """
1017 Write the DAG entry for this node's priority to the DAG file descriptor.
1018 @param fh: descriptor of open DAG file.
1019 """
1020 fh.write( 'PRIORITY ' + self.__name + ' ' + self.__priority + '\n' )
1021
1022 def write_vars(self,fh):
1023 """
1024 Write the variable (macro) options and arguments to the DAG file
1025 descriptor.
1026 @param fh: descriptor of open DAG file.
1027 """
1028 if list(self.__macros.keys()) or list(self.__opts.keys()) or self.__args:
1029 fh.write( 'VARS ' + self.__name )
1030 for k in self.__macros.keys():
1031 fh.write( ' ' + str(k) + '="' + str(self.__macros[k]) + '"' )
1032 for k in self.__opts.keys():
1033 fh.write( ' ' + str(k) + '="' + str(self.__opts[k]) + '"' )
1034 if self.__args:
1035 for i in range(self.__arg_index):
1036 fh.write( ' macroargument' + str(i) + '="' + self.__args[i] + '"' )
1037 fh.write( '\n' )
1038
1039 def write_parents(self,fh):
1040 """
1041 Write the parent/child relations for this job to the DAG file descriptor.
1042 @param fh: descriptor of open DAG file.
1043 """
1044 if len(self.__parents) > 0:
1045 fh.write( 'PARENT ' + " ".join((str(p) for p in self.__parents)) + ' CHILD ' + str(self) + '\n' )
1046
1047 def write_pre_script(self,fh):
1048 """
1049 Write the pre script for the job, if there is one
1050 @param fh: descriptor of open DAG file.
1051 """
1052 if self.__pre_script:
1053 fh.write( 'SCRIPT PRE ' + str(self) + ' ' + self.__pre_script + ' ' +
1054 ' '.join(self.__pre_script_args) + '\n' )
1055
1056 def write_post_script(self,fh):
1057 """
1058 Write the post script for the job, if there is one
1059 @param fh: descriptor of open DAG file.
1060 """
1061 if self.__post_script:
1062 fh.write( 'SCRIPT POST ' + str(self) + ' ' + self.__post_script + ' ' +
1063 ' '.join(self.__post_script_args) + '\n' )
1064
1065 def write_input_files(self, fh):
1066 """
1067 Write as a comment into the DAG file the list of input files
1068 for this DAG node.
1069
1070 @param fh: descriptor of open DAG file.
1071 """
1072 for f in self.__input_files:
1073 fh.write("## Job %s requires input file %s\n" % (self.__name, f))
1074
1075 def write_output_files(self, fh):
1076 """
1077 Write as a comment into the DAG file the list of output files
1078 for this DAG node.
1079
1080 @param fh: descriptor of open DAG file.
1081 """
1082 for f in self.__output_files:
1083 fh.write("## Job %s generates output file %s\n" % (self.__name, f))
1084
1085 def set_log_file(self,log):
1086 """
1087 Set the Condor log file to be used by this CondorJob.
1088 @param log: path of Condor log file.
1089 """
1090 self.__job.set_log_file(log)
1091
1092 def add_parent(self,node):
1093 """
1094 Add a parent to this node. This node will not be executed until the
1095 parent node has run sucessfully.
1096 @param node: CondorDAGNode to add as a parent.
1097 """
1098 if not isinstance(node, (CondorDAGNode,CondorDAGManNode) ):
1099 raise CondorDAGNodeError("Parent must be a CondorDAGNode or a CondorDAGManNode")
1100 self.__parents.append( node )
1101
1103 """
1104 Return a list of tuples containg the command line arguments
1105 """
1106
1107 # pattern to find DAGman macros
1108 pat = re.compile(r'\$\‍((.+)\‍)')
1109 argpat = re.compile(r'\d+')
1110
1111 # first parse the arguments and replace macros with values
1112 args = self.job().get_args()
1113 macros = self.get_args()
1114
1115 cmd_list = []
1116
1117 for a in args:
1118 m = pat.search(a)
1119 if m:
1120 arg_index = int(argpat.findall(a)[0])
1121 try:
1122 cmd_list.append(("%s" % macros[arg_index], ""))
1123 except IndexError:
1124 cmd_list.append("")
1125 else:
1126 cmd_list.append(("%s" % a, ""))
1127
1128 # second parse the options and replace macros with values
1129 options = self.job().get_opts()
1130 macros = self.get_opts()
1131
1132 for k in options:
1133 val = options[k]
1134 m = pat.match(val)
1135 if m:
1136 key = m.group(1)
1137 value = macros[key]
1138
1139 cmd_list.append(("--%s" % k, str(value)))
1140 else:
1141 cmd_list.append(("--%s" % k, str(val)))
1142
1143 # lastly parse the short options and replace macros with values
1144 options = self.job().get_short_opts()
1145
1146 for k in options:
1147 val = options[k]
1148 m = pat.match(val)
1149 if m:
1150 key = m.group(1)
1151 value = macros[key]
1152
1153 cmd_list.append(("-%s" % k, str(value)))
1154 else:
1155 cmd_list.append(("-%s" % k, str(val)))
1156
1157 return cmd_list
1158
1159 def get_cmd_line(self):
1160 """
1161 Return the full command line that will be used when this node
1162 is run by DAGman.
1163 """
1164
1165 cmd = ""
1166 cmd_list = self.get_cmd_tuple_list()
1167 for argument in cmd_list:
1168 cmd += ' '.join(argument) + " "
1169
1170 return cmd
1171
1172 def finalize(self):
1173 """
1174 The finalize method of a node is called before the node is
1175 finally added to the DAG and can be overridden to do any last
1176 minute clean up (such as setting extra command line arguments)
1177 """
1178 pass
1179
1180
1181class CondorDAGManNode(CondorDAGNode):
1182 """
1183 Condor DAGMan node class. Appropriate for setting up DAGs to run within a
1184 DAG. Adds the user-tag functionality to condor_dagman processes running in
1185 the DAG. May also be used to extend dagman-node specific functionality.
1186 """
1187 def __init__(self, job):
1188 """
1189 @job: a CondorDAGNodeJob
1190 """
1191 super(CondorDAGManNode,self).__init__(job)
1192 self.__user_tag = None
1193 self.__maxjobs_categories = []
1194 self.__cluster_jobs = None
1195
1196 def set_user_tag(self,usertag):
1197 """
1198 Set the user tag that is passed to the analysis code.
1199 @param user_tag: the user tag to identify the job
1200 """
1201 self.__user_tag = str(usertag)
1202
1203 def get_user_tag(self):
1204 """
1205 Returns the usertag string
1206 """
1207 return self.__user_tag
1208
1209 def add_maxjobs_category(self,categoryName,maxJobsNum):
1210 """
1211 Add a category to this DAG called categoryName with a maxjobs of maxJobsNum.
1212 @param node: Add (categoryName,maxJobsNum) tuple to CondorDAG.__maxjobs_categories.
1213 """
1214 self.__maxjobs_categories.append((str(categoryName),str(maxJobsNum)))
1215
1217 """
1218 Return an array of tuples containing (categoryName,maxJobsNum)
1219 """
1220 return self.__maxjobs_categories
1221
1222 def set_cluster_jobs(self,cluster):
1223 """
1224 Set the type of job clustering pegasus can use to collapse jobs
1225 @param cluster: clustering type
1226 """
1227 self.__cluster_jobs = str(cluster)
1228
1230 """
1231 Returns the usertag string
1232 """
1233 return self.__cluster_jobs
1234
1235
1236class CondorDAG(object):
1237 """
1238 A CondorDAG is a Condor Directed Acyclic Graph that describes a collection
1239 of Condor jobs and the order in which to run them. All Condor jobs in the
1240 DAG must write their Codor logs to the same file.
1241 NOTE: The log file must not be on an NFS mounted system as the Condor jobs
1242 must be able to get an exclusive file lock on the log file.
1243 """
1244 def __init__(self,log):
1245 """
1246 @param log: path to log file which must not be on an NFS mounted file system.
1247 """
1248 self.__log_file_path = log
1249 self.__dag_file_path = None
1250 self.__jobs = []
1251 self.__nodes = []
1252 self.__maxjobs_categories = []
1253 self.__integer_node_names = 0
1254 self.__node_count = 0
1255 self.__nodes_finalized = 0
1256
1257 def get_nodes(self):
1258 """
1259 Return a list containing all the nodes in the DAG
1260 """
1261 return self.__nodes
1262
1263 def get_jobs(self):
1264 """
1265 Return a list containing all the jobs in the DAG
1266 """
1267 return self.__jobs
1268
1270 """
1271 Use integer node names for the DAG
1272 """
1273 self.__integer_node_names = 1
1274
1275 def set_dag_file(self, path):
1276 """
1277 Set the name of the file into which the DAG is written.
1278 @param path: path to DAG file.
1279 """
1280 self.__dag_file_path = path + '.dag'
1281
1282 def get_dag_file(self):
1283 """
1284 Return the path to the DAG file.
1285 """
1286 if not self.__log_file_path:
1287 raise CondorDAGError("No path for DAG file")
1288 else:
1289 return self.__dag_file_path
1290
1291 def add_node(self,node):
1292 """
1293 Add a CondorDAGNode to this DAG. The CondorJob that the node uses is
1294 also added to the list of Condor jobs in the DAG so that a list of the
1295 submit files needed by the DAG can be maintained. Each unique CondorJob
1296 will be added once to prevent duplicate submit files being written.
1297 @param node: CondorDAGNode to add to the CondorDAG.
1298 """
1299 if not isinstance(node, CondorDAGNode):
1300 raise CondorDAGError("Nodes must be class CondorDAGNode or subclass")
1301 if not isinstance(node.job(), CondorDAGManJob):
1302 node.set_log_file(self.__log_file_path)
1303 self.__nodes.append(node)
1304 if self.__integer_node_names:
1305 node.set_name(str(self.__node_count))
1306 self.__node_count += 1
1307 if node.job() not in self.__jobs:
1308 self.__jobs.append(node.job())
1309
1310 def add_maxjobs_category(self,categoryName,maxJobsNum):
1311 """
1312 Add a category to this DAG called categoryName with a maxjobs of maxJobsNum.
1313 @param node: Add (categoryName,maxJobsNum) tuple to CondorDAG.__maxjobs_categories.
1314 """
1315 self.__maxjobs_categories.append((str(categoryName),str(maxJobsNum)))
1316
1318 """
1319 Return an array of tuples containing (categoryName,maxJobsNum)
1320 """
1321 return self.__maxjobs_categories
1322
1323 def write_maxjobs(self,fh,category):
1324 """
1325 Write the DAG entry for this category's maxjobs to the DAG file descriptor.
1326 @param fh: descriptor of open DAG file.
1327 @param category: tuple containing type of jobs to set a maxjobs limit for
1328 and the maximum number of jobs of that type to run at once.
1329 """
1330 fh.write( 'MAXJOBS ' + str(category[0]) + ' ' + str(category[1]) + '\n' )
1331
1333 """
1334 Write all the submit files used by the dag to disk. Each submit file is
1335 written to the file name set in the CondorJob.
1336 """
1337 if not self.__nodes_finalized:
1338 for node in self.__nodes:
1339 node.finalize()
1340 for job in self.__jobs:
1341 job.write_sub_file()
1342
1344 """
1345 Write all the nodes in the DAG to the DAG file.
1346 """
1347 if not self.__dag_file_path:
1348 raise CondorDAGError("No path for DAG file")
1349 try:
1350 dagfile = open( self.__dag_file_path, 'w' )
1351 except:
1352 raise CondorDAGError("Cannot open file " + self.__dag_file_path)
1353 for node in self.__nodes:
1354 node.write_job(dagfile)
1355 node.write_vars(dagfile)
1356 if node.get_category():
1357 node.write_category(dagfile)
1358 if node.get_priority():
1359 node.write_priority(dagfile)
1360 node.write_pre_script(dagfile)
1361 node.write_post_script(dagfile)
1362 node.write_input_files(dagfile)
1363 node.write_output_files(dagfile)
1364 for node in self.__nodes:
1365 node.write_parents(dagfile)
1366 for category in self.__maxjobs_categories:
1367 self.write_maxjobs(dagfile, category)
1368 dagfile.close()
1369
1370 def write_dag(self):
1371 """
1372 Write a dag.
1373 """
1374 if not self.__nodes_finalized:
1375 for node in self.__nodes:
1376 node.finalize()
1377 self.write_concrete_dag()
1378
1379 def write_script(self):
1380 """
1381 Write the workflow to a script (.sh instead of .dag).
1382
1383 Assuming that parents were added to the DAG before their children,
1384 dependencies should be handled correctly.
1385 """
1386 if not self.__dag_file_path:
1387 raise CondorDAGError("No path for DAG file")
1388 try:
1389 dfp = self.__dag_file_path
1390 outfilename = ".".join(dfp.split(".")[:-1]) + ".sh"
1391 outfile = open(outfilename, "w")
1392 except:
1393 raise CondorDAGError("Cannot open file " + self.__dag_file_path)
1394
1395 for node in self.__nodes:
1396 outfile.write("# Job %s\n" % node.get_name())
1397 # Check if this is a DAGMAN Node
1398 if isinstance(node,CondorDAGManNode):
1399 outfile.write("condor_submit_dag %s\n\n" % (node.job().get_dag()))
1400 else:
1401 outfile.write("%s %s\n\n" % (node.job().get_executable(),
1402 node.get_cmd_line()))
1403 outfile.close()
1404
1405 os.chmod(outfilename, os.stat(outfilename)[0] | stat.S_IEXEC)
add_var_condor_cmd(self, command)
Definition pipeline.py:547
add_var_arg(self, arg_index, quote=False)
Definition pipeline.py:557
__init__(self, universe, executable)
Definition pipeline.py:493
add_var_opt(self, opt, short=False)
Definition pipeline.py:532
__init__(self, dag, dir=None)
Definition pipeline.py:580
add_maxjobs_category(self, categoryName, maxJobsNum)
Definition pipeline.py:1209
set_priority(self, priority)
Definition pipeline.py:764
set_post_script(self, script)
Definition pipeline.py:710
add_var_opt(self, opt, value, short=False)
Definition pipeline.py:919
add_output_macro(self, filename)
Definition pipeline.py:886
add_checkpoint_file(self, filename)
Definition pipeline.py:800
add_var_arg(self, arg, quote=False)
Definition pipeline.py:946
add_input_macro(self, filename)
Definition pipeline.py:878
add_output_file(self, filename)
Definition pipeline.py:788
add_io_macro(self, io, filename)
Definition pipeline.py:864
add_var_condor_cmd(self, command, value)
Definition pipeline.py:905
add_file_arg(self, filename)
Definition pipeline.py:957
add_input_file(self, filename)
Definition pipeline.py:776
add_macro(self, name, value)
Definition pipeline.py:851
set_category(self, category)
Definition pipeline.py:752
add_checkpoint_macro(self, filename)
Definition pipeline.py:894
add_file_opt(self, opt, filename, file_is_output_file=False)
Definition pipeline.py:931
add_maxjobs_category(self, categoryName, maxJobsNum)
Definition pipeline.py:1310
write_maxjobs(self, fh, category)
Definition pipeline.py:1323
add_ini_opts(self, cp, section)
Definition pipeline.py:328
add_output_file(self, filename)
Definition pipeline.py:207
set_notification(self, value)
Definition pipeline.py:339
set_stdin_file(self, path)
Definition pipeline.py:353
__init__(self, universe, executable, queue)
Definition pipeline.py:76
set_grid_type(self, grid_type)
Definition pipeline.py:136
add_checkpoint_file(self, filename)
Definition pipeline.py:216
add_file_arg(self, filename)
Definition pipeline.py:249
set_universe(self, universe)
Definition pipeline.py:123
set_executable(self, executable)
Definition pipeline.py:111
add_short_opt(self, opt, value)
Definition pipeline.py:308
set_stderr_file(self, path)
Definition pipeline.py:366
set_grid_scheduler(self, grid_scheduler)
Definition pipeline.py:162
set_stdout_file(self, path)
Definition pipeline.py:379
add_input_file(self, filename)
Definition pipeline.py:198
add_file_opt(self, opt, filename)
Definition pipeline.py:288
add_condor_cmd(self, cmd, value)
Definition pipeline.py:184
add_opt(self, opt, value)
Definition pipeline.py:266
set_grid_server(self, grid_server)
Definition pipeline.py:149
set_executable_installed(self, installed)
Definition pipeline.py:169