lsst-ctrl-execute 28.2025.500__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lsst/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ import pkgutil
2
+
3
+ __path__ = pkgutil.extend_path(__path__, __name__)
lsst/ctrl/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ import pkgutil
2
+
3
+ __path__ = pkgutil.extend_path(__path__, __name__)
@@ -0,0 +1,27 @@
1
+ #
2
+ # LSST Data Management System
3
+ # Copyright 2008-2016 LSST Corporation.
4
+ #
5
+ # This product includes software developed by the
6
+ # LSST Project (http://www.lsst.org/).
7
+ #
8
+ # This program is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the LSST License Statement and
19
+ # the GNU General Public License along with this program. If not,
20
+ # see <http://www.lsstcorp.org/LegalNotices/>.
21
+ #
22
+ from importlib.metadata import version
23
+
24
+ try:
25
+ from .version import * # type: ignore # noqa: F403
26
+ except ModuleNotFoundError:
27
+ __version__ = version("lsst.ctrl.execute")
@@ -0,0 +1,69 @@
1
+ #
2
+ # LSST Data Management System
3
+ # Copyright 2008-2016 LSST Corporation.
4
+ #
5
+ # This product includes software developed by the
6
+ # LSST Project (http://www.lsst.org/).
7
+ #
8
+ # This program is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the LSST License Statement and
19
+ # the GNU General Public License along with this program. If not,
20
+ # see <http://www.lsstcorp.org/LegalNotices/>.
21
+ #
22
+
23
+ import lsst.pex.config as pexConfig
24
+
25
+
26
+ class AllocatedPlatformConfig(pexConfig.Config):
27
+ """Platform specific information"""
28
+
29
+ queue = pexConfig.Field(
30
+ doc="the scheduler queue to submit to", dtype=str, default="debug"
31
+ )
32
+ email = pexConfig.Field(
33
+ doc="line to add to the scheduler file to get email notification (if supported)",
34
+ dtype=str,
35
+ default=None,
36
+ )
37
+
38
+ scratchDirectory = pexConfig.Field(
39
+ doc="directory on the remote system where the scheduler file is sent",
40
+ dtype=str,
41
+ default=None,
42
+ )
43
+ loginHostName = pexConfig.Field(
44
+ doc="the host to login and copy files to", dtype=str, default=None
45
+ )
46
+ utilityPath = pexConfig.Field(
47
+ doc="the directory containing the scheduler commands", dtype=str, default=None
48
+ )
49
+ totalCoresPerNode = pexConfig.Field(
50
+ doc="the TOTAL number of cores on each node", dtype=int, default=1
51
+ )
52
+ glideinShutdown = pexConfig.Field(
53
+ doc="number of seconds of inactivity before glideins are cancelled",
54
+ dtype=int,
55
+ default=3600,
56
+ )
57
+
58
+
59
+ class AllocationConfig(pexConfig.Config):
60
+ """A pex_config file describing the platform specific information required
61
+ to fill out a scheduler file which will be used to submit a scheduler
62
+ request.
63
+ """
64
+
65
+ # this is done on two levels instead of one for future expansion of this
66
+ # config class, which may require local attributes to be specified.
67
+ platform = pexConfig.ConfigField(
68
+ "platform allocation information", AllocatedPlatformConfig
69
+ )
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env python
2
+
3
+ #
4
+ # LSST Data Management System
5
+ # Copyright 2008-2016 LSST Corporation.
6
+ #
7
+ # This product includes software developed by the
8
+ # LSST Project (http://www.lsst.org/).
9
+ #
10
+ # This program is free software: you can redistribute it and/or modify
11
+ # it under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation, either version 3 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the LSST License Statement and
21
+ # the GNU General Public License along with this program. If not,
22
+ # see <http://www.lsstcorp.org/LegalNotices/>.
23
+ #
24
+
25
+ import logging
26
+ import os
27
+ import pwd
28
+ import sys
29
+ from datetime import datetime
30
+ from string import Template
31
+
32
+ from lsst.ctrl.execute.allocationConfig import AllocationConfig
33
+ from lsst.ctrl.execute.condorInfoConfig import CondorInfoConfig
34
+ from lsst.ctrl.execute.templateWriter import TemplateWriter
35
+ from lsst.resources import ResourcePath, ResourcePathExpression
36
+
37
+ _LOG = logging.getLogger(__name__)
38
+
39
+
40
+ class Allocator:
41
+ """A class which consolidates allocation pex_config information with
42
+ override information (obtained from the command line) and produces a
43
+ PBS file using these values.
44
+
45
+ Parameters
46
+ ----------
47
+ platform : `str`
48
+ the name of the platform to execute on
49
+ opts : `Config`
50
+ Config object containing options
51
+ condorInfoFileName : `lsst.resources.ResourcePathExpression`
52
+ Name of the file containing Config information
53
+
54
+ Raises
55
+ ------
56
+ TypeError
57
+ If the condorInfoFileName is the wrong type.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ platform: str,
63
+ opts,
64
+ configuration,
65
+ condorInfoFileName: ResourcePathExpression,
66
+ ):
67
+ """Constructor
68
+ @param platform: target platform for PBS submission
69
+ @param opts: options to override
70
+ """
71
+ self.opts = opts
72
+ self.defaults = {}
73
+ self.configuration = configuration
74
+
75
+ condorInfoConfig = CondorInfoConfig()
76
+ condorInfoConfig.loadFromStream(ResourcePath(condorInfoFileName).read())
77
+
78
+ self.platform = platform
79
+
80
+ # Look up the user's name and home and scratch directory in the
81
+ # $HOME/.lsst/condor-info.py file
82
+ user_name = None
83
+ user_home = None
84
+ user_scratch = None
85
+ for name in condorInfoConfig.platform:
86
+ if name == self.platform:
87
+ user_name = condorInfoConfig.platform[name].user.name
88
+ user_home = condorInfoConfig.platform[name].user.home
89
+ user_scratch = condorInfoConfig.platform[name].user.scratch
90
+ if user_scratch is None and "SCRATCH" in os.environ:
91
+ user_scratch = os.environ["SCRATCH"]
92
+ if user_name is None:
93
+ raise RuntimeError(
94
+ "error: %s does not specify user name for platform == %s"
95
+ % (condorInfoFileName, self.platform)
96
+ )
97
+ if user_home is None:
98
+ raise RuntimeError(
99
+ "error: %s does not specify user home for platform == %s"
100
+ % (condorInfoFileName, self.platform)
101
+ )
102
+ if user_scratch is None:
103
+ raise RuntimeError(
104
+ "error: %s does not specify user scratch for platform == %s"
105
+ % (condorInfoFileName, self.platform)
106
+ )
107
+ self.defaults["USER_NAME"] = user_name
108
+ self.defaults["USER_HOME"] = user_home
109
+ self.defaults["USER_SCRATCH"] = user_scratch
110
+ self.commandLineDefaults = {}
111
+ self.commandLineDefaults["NODE_COUNT"] = self.opts.nodeCount
112
+ self.commandLineDefaults["COLLECTOR"] = self.opts.collector
113
+ self.commandLineDefaults["CPORT"] = self.opts.collectorport
114
+ self.commandLineDefaults["CPUS"] = self.opts.cpus
115
+ self.commandLineDefaults["WALL_CLOCK"] = self.opts.maximumWallClock
116
+ self.commandLineDefaults["ACCOUNT"] = self.opts.account
117
+ self.commandLineDefaults["MEMPERCORE"] = 4096
118
+ self.commandLineDefaults["ALLOWEDAUTO"] = 500
119
+ self.commandLineDefaults["AUTOCPUS"] = 16
120
+ self.commandLineDefaults["QUEUE"] = self.opts.queue
121
+ self.load()
122
+
123
+ def createUniqueIdentifier(self):
124
+ """Creates a unique file identifier, based on the user's name
125
+ and the time at which this method is invoked.
126
+
127
+ Returns
128
+ -------
129
+ ident : `str`
130
+ the new identifier
131
+ """
132
+ # This naming scheme follows the conventions used for creating
133
+ # RUNID names. We've found this allows these files to be more
134
+ # easily located and shared with other users when debugging
135
+ # The tempfile.mkstemp method restricts the file to only the user,
136
+ # and does not guarantee a file name can that easily be identified.
137
+ now = datetime.now()
138
+ self.defaults["DATE_STRING"] = "%02d_%02d%02d" % (
139
+ now.year,
140
+ now.month,
141
+ now.day,
142
+ )
143
+ username = pwd.getpwuid(os.geteuid()).pw_name
144
+ ident = "%s_%02d_%02d%02d_%02d%02d%02d" % (
145
+ username,
146
+ now.year,
147
+ now.month,
148
+ now.day,
149
+ now.hour,
150
+ now.minute,
151
+ now.second,
152
+ )
153
+ return ident
154
+
155
+ def load(self):
156
+ """Loads all values from configuration and command line overrides into
157
+ data structures suitable for use by the TemplateWriter object.
158
+ """
159
+ tempLocalScratch = Template(self.configuration.platform.localScratch)
160
+ self.defaults["LOCAL_SCRATCH"] = tempLocalScratch.substitute(
161
+ USER_SCRATCH=self.defaults["USER_SCRATCH"]
162
+ )
163
+ self.defaults["SCHEDULER"] = self.configuration.platform.scheduler
164
+
165
+ def loadAllocationConfig(self, name: ResourcePathExpression, suffix):
166
+ """Loads all values from allocationConfig and command line overrides
167
+ into data structures suitable for use by the TemplateWriter object.
168
+ """
169
+ if not (name_ := ResourcePath(name)).exists():
170
+ raise RuntimeError("%s was not found." % name_)
171
+ allocationConfig = AllocationConfig()
172
+ allocationConfig.loadFromStream(name_.read())
173
+
174
+ self.defaults["QUEUE"] = allocationConfig.platform.queue
175
+ self.defaults["EMAIL_NOTIFICATION"] = allocationConfig.platform.email
176
+ self.defaults["HOST_NAME"] = allocationConfig.platform.loginHostName
177
+
178
+ self.defaults["UTILITY_PATH"] = allocationConfig.platform.utilityPath
179
+
180
+ if self.opts.glideinShutdown is None:
181
+ self.defaults["GLIDEIN_SHUTDOWN"] = str(
182
+ allocationConfig.platform.glideinShutdown
183
+ )
184
+ else:
185
+ self.defaults["GLIDEIN_SHUTDOWN"] = str(self.opts.glideinShutdown)
186
+
187
+ if self.opts.outputLog is not None:
188
+ self.defaults["OUTPUT_LOG"] = self.opts.outputLog
189
+ else:
190
+ self.defaults["OUTPUT_LOG"] = "glide.out"
191
+
192
+ if self.opts.errorLog is not None:
193
+ self.defaults["ERROR_LOG"] = self.opts.errorLog
194
+ else:
195
+ self.defaults["ERROR_LOG"] = "glide.err"
196
+
197
+ # This is the TOTAL number of cores in the job, not just the total
198
+ # of the cores you intend to use. In other words, the total available
199
+ # on a machine, times the number of machines.
200
+ totalCoresPerNode = allocationConfig.platform.totalCoresPerNode
201
+ self.commandLineDefaults["TOTAL_CORE_COUNT"] = (
202
+ self.opts.nodeCount * totalCoresPerNode
203
+ )
204
+
205
+ self.uniqueIdentifier = self.createUniqueIdentifier()
206
+
207
+ # write these pbs and config files to {LOCAL_DIR}/configs
208
+ self.configDir = os.path.join(
209
+ self.defaults["LOCAL_SCRATCH"],
210
+ self.defaults["DATE_STRING"],
211
+ self.uniqueIdentifier,
212
+ "configs",
213
+ )
214
+
215
+ self.submitFileName = os.path.join(
216
+ self.configDir, "alloc_%s.%s" % (self.uniqueIdentifier, suffix)
217
+ )
218
+
219
+ self.condorConfigFileName = os.path.join(
220
+ self.configDir, "condor_%s.config" % self.uniqueIdentifier
221
+ )
222
+
223
+ self.defaults["GENERATED_CONFIG"] = os.path.basename(self.condorConfigFileName)
224
+ self.defaults["CONFIGURATION_ID"] = self.uniqueIdentifier
225
+ return allocationConfig
226
+
227
+ def createSubmitFile(self, inputFile):
228
+ """Creates a batch submit file using the file "input" as a Template
229
+
230
+ Returns
231
+ -------
232
+ outfile : `str`
233
+ The newly created file name
234
+ """
235
+ if not os.path.exists(self.configDir):
236
+ os.makedirs(self.configDir)
237
+ outfile = self.createFile(inputFile, self.submitFileName)
238
+ _LOG.debug("Wrote new Slurm submit file to %s", outfile)
239
+ return outfile
240
+
241
+ def createCondorConfigFile(self, input):
242
+ """Creates a Condor config file using the file "input" as a Template
243
+
244
+ Returns
245
+ -------
246
+ outfile : `str`
247
+ The newly created file name
248
+ """
249
+ outfile = self.createFile(input, self.condorConfigFileName)
250
+ _LOG.debug("Wrote new condor configuration file to %s", outfile)
251
+ return outfile
252
+
253
+ def createFile(self, input: ResourcePathExpression, output: ResourcePathExpression):
254
+ """Creates a new file, using "input" as a Template, and writes the
255
+ new file to output.
256
+
257
+ Returns
258
+ -------
259
+ outfile : `str`
260
+ The newly created file name
261
+ """
262
+ _LOG.debug("Creating file from template using %s", input)
263
+ template = TemplateWriter()
264
+ # Uses the associative arrays of "defaults" and "commandLineDefaults"
265
+ # to write out the new file from the template.
266
+ # The commandLineDefaults override values in "defaults"
267
+ substitutes = self.defaults.copy()
268
+ for key in self.commandLineDefaults:
269
+ val = self.commandLineDefaults[key]
270
+ if val is not None:
271
+ substitutes[key] = self.commandLineDefaults[key]
272
+ template.rewrite(input, output, substitutes)
273
+ return output
274
+
275
+ def isVerbose(self):
276
+ """Status of the verbose flag
277
+ @return True if the flag was set, False otherwise
278
+ """
279
+ return self.opts.verbose
280
+
281
+ def isAuto(self):
282
+ """Status of the auto flag
283
+ @return True if the flag was set, False otherwise
284
+ """
285
+ return self.opts.auto
286
+
287
+ def getUserName(self):
288
+ """Accessor for USER_NAME
289
+ @return the value of USER_NAME
290
+ """
291
+ return self.getParameter("USER_NAME")
292
+
293
+ def getUserHome(self):
294
+ """Accessor for USER_HOME
295
+ @return the value of USER_HOME
296
+ """
297
+ return self.getParameter("USER_HOME")
298
+
299
+ def getUserScratch(self):
300
+ """Accessor for USER_SCRATCH
301
+ @return the value of USER_SCRATCH
302
+ """
303
+ return self.getParameter("USER_SCRATCH")
304
+
305
+ def getHostName(self):
306
+ """Accessor for HOST_NAME
307
+ @return the value of HOST_NAME
308
+ """
309
+ return self.getParameter("HOST_NAME")
310
+
311
+ def getUtilityPath(self):
312
+ """Accessor for UTILITY_PATH
313
+ @return the value of UTILITY_PATH
314
+ """
315
+ return self.getParameter("UTILITY_PATH")
316
+
317
+ def getScratchDirectory(self):
318
+ """Accessor for SCRATCH_DIR
319
+ @return the value of SCRATCH_DIR
320
+ """
321
+ return self.getParameter("SCRATCH_DIR")
322
+
323
+ def getLocalScratchDirectory(self):
324
+ """Accessor for LOCAL_SCRATCH
325
+ @return the value of LOCAL_SCRATCH
326
+ """
327
+ return self.getParameter("LOCAL_SCRATCH")
328
+
329
+ def getNodeSetName(self):
330
+ """Accessor for NODE_SET
331
+ @return the value of NODE_SET
332
+ """
333
+ return self.getParameter("NODE_SET")
334
+
335
+ def getNodes(self):
336
+ """Accessor for NODE_COUNT
337
+ @return the value of NODE_COUNT
338
+ """
339
+ return self.getParameter("NODE_COUNT")
340
+
341
+ def getMemoryPerCore(self):
342
+ """Accessor for MemoryPerCore
343
+ @return the value of MemoryPerCore
344
+ """
345
+ return self.getParameter("MEMPERCORE")
346
+
347
+ def getAllowedAutoGlideins(self):
348
+ """Accessor for AllowedAutoGlideins
349
+ @return the value of AllowedAuto
350
+ """
351
+ return self.getParameter("ALLOWEDAUTO")
352
+
353
+ def getQOS(self):
354
+ """Accessor for QOS
355
+ @return the value of QOS
356
+ """
357
+ return self.getParameter("QOS")
358
+
359
+ def getCPUs(self):
360
+ """Accessor for CPUS
361
+ @return the value of CPUS
362
+ """
363
+ return self.getParameter("CPUS")
364
+
365
+ def getAutoCPUs(self):
366
+ """Size of standard glideins for allocateNodes auto
367
+ @return the value of autoCPUs
368
+ """
369
+ return self.getParameter("AUTOCPUS")
370
+
371
+ def getWallClock(self):
372
+ """Accessor for WALL_CLOCK
373
+ @return the value of WALL_CLOCK
374
+ """
375
+ return self.getParameter("WALL_CLOCK")
376
+
377
+ def getScheduler(self):
378
+ """Accessor for SCHEDULER
379
+ @return the value of SCHEDULER
380
+ """
381
+ return self.getParameter("SCHEDULER")
382
+
383
+ def getReservation(self):
384
+ """Accessor for RESERVATION
385
+ @return the value of RESERVATION
386
+ """
387
+ return self.getParameter("RESERVATION")
388
+
389
+ def getParameter(self, value):
390
+ """Accessor for generic value
391
+ @return None if value is not set. Otherwise, use the command line
392
+ override (if set), or the default Config value
393
+ """
394
+ if value in self.commandLineDefaults:
395
+ return self.commandLineDefaults[value]
396
+ if value in self.defaults:
397
+ return self.defaults[value]
398
+ return None
399
+
400
+ def printNodeSetInfo(self):
401
+ nodes = self.getNodes()
402
+ cpus = self.getCPUs()
403
+ wallClock = self.getWallClock()
404
+ nodeString = ""
405
+
406
+ if int(nodes) > 1:
407
+ nodeString = "s"
408
+ if self.opts.dynamic is None:
409
+ print(
410
+ "%s glidein%s will be allocated on %s using default dynamic slots configuration."
411
+ % (nodes, nodeString, self.platform)
412
+ )
413
+ print(
414
+ "There will be %s cores per glidein and a maximum time limit of %s"
415
+ % (cpus, wallClock)
416
+ )
417
+ elif self.opts.dynamic == "__default__":
418
+ print(
419
+ "%s glidein%s will be allocated on %s using default dynamic slots configuration."
420
+ % (nodes, nodeString, self.platform)
421
+ )
422
+ print(
423
+ "There will be %s cores per glidein and a maximum time limit of %s"
424
+ % (cpus, wallClock)
425
+ )
426
+ else:
427
+ print(
428
+ "%s node%s will be allocated on %s using dynamic slot block specified in '%s'"
429
+ % (nodes, nodeString, self.platform, self.opts.dynamic)
430
+ )
431
+ print(
432
+ "There will be %s cores per node and maximum time limit of %s"
433
+ % (cpus, wallClock)
434
+ )
435
+ print("Node set name:")
436
+ print(self.getNodeSetName())
437
+
438
+ def runCommand(self, cmd, verbose):
439
+ cmd_split = cmd.split()
440
+ pid = os.fork()
441
+ if not pid:
442
+ # Methods of file transfer and login may
443
+ # produce different output, depending on how
444
+ # the "gsi" utilities are used. The user can
445
+ # either use grid proxies or ssh, and gsiscp/gsissh
446
+ # does the right thing. Since the output will be
447
+ # different in either case anything potentially parsing this
448
+ # output (like drpRun), would have to go through extra
449
+ # steps to deal with this output, and which ultimately
450
+ # end up not being useful. So we optinally close the i/o output
451
+ # of the executing command down.
452
+ #
453
+ # stdin/stdio/stderr is treated specially
454
+ # by python, so we have to close down
455
+ # both the python objects and the
456
+ # underlying c implementations
457
+ if not verbose:
458
+ # close python i/o
459
+ sys.stdin.close()
460
+ sys.stdout.close()
461
+ sys.stderr.close()
462
+ # close C's i/o
463
+ os.close(0)
464
+ os.close(1)
465
+ os.close(2)
466
+ os.execvp(cmd_split[0], cmd_split)
467
+ pid, status = os.wait()
468
+ # high order bits are status, low order bits are signal.
469
+ exitCode = (status & 0xFF00) >> 8
470
+ return exitCode
471
+
472
+ def submit(self):
473
+ """Submit the glidein jobs to the Batch system."""
474
+ raise NotImplementedError