vortex-nwp 2.0.0b1__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.
- vortex/__init__.py +135 -0
- vortex/algo/__init__.py +12 -0
- vortex/algo/components.py +2136 -0
- vortex/algo/mpitools.py +1648 -0
- vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
- vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
- vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
- vortex/algo/serversynctools.py +170 -0
- vortex/config.py +115 -0
- vortex/data/__init__.py +13 -0
- vortex/data/abstractstores.py +1572 -0
- vortex/data/containers.py +780 -0
- vortex/data/contents.py +596 -0
- vortex/data/executables.py +284 -0
- vortex/data/flow.py +113 -0
- vortex/data/geometries.ini +2689 -0
- vortex/data/geometries.py +703 -0
- vortex/data/handlers.py +1021 -0
- vortex/data/outflow.py +67 -0
- vortex/data/providers.py +465 -0
- vortex/data/resources.py +201 -0
- vortex/data/stores.py +1271 -0
- vortex/gloves.py +282 -0
- vortex/layout/__init__.py +27 -0
- vortex/layout/appconf.py +109 -0
- vortex/layout/contexts.py +511 -0
- vortex/layout/dataflow.py +1069 -0
- vortex/layout/jobs.py +1276 -0
- vortex/layout/monitor.py +833 -0
- vortex/layout/nodes.py +1424 -0
- vortex/layout/subjobs.py +464 -0
- vortex/nwp/__init__.py +11 -0
- vortex/nwp/algo/__init__.py +12 -0
- vortex/nwp/algo/assim.py +483 -0
- vortex/nwp/algo/clim.py +920 -0
- vortex/nwp/algo/coupling.py +609 -0
- vortex/nwp/algo/eda.py +632 -0
- vortex/nwp/algo/eps.py +613 -0
- vortex/nwp/algo/forecasts.py +745 -0
- vortex/nwp/algo/fpserver.py +927 -0
- vortex/nwp/algo/ifsnaming.py +403 -0
- vortex/nwp/algo/ifsroot.py +311 -0
- vortex/nwp/algo/monitoring.py +202 -0
- vortex/nwp/algo/mpitools.py +554 -0
- vortex/nwp/algo/odbtools.py +974 -0
- vortex/nwp/algo/oopsroot.py +735 -0
- vortex/nwp/algo/oopstests.py +186 -0
- vortex/nwp/algo/request.py +579 -0
- vortex/nwp/algo/stdpost.py +1285 -0
- vortex/nwp/data/__init__.py +12 -0
- vortex/nwp/data/assim.py +392 -0
- vortex/nwp/data/boundaries.py +261 -0
- vortex/nwp/data/climfiles.py +539 -0
- vortex/nwp/data/configfiles.py +149 -0
- vortex/nwp/data/consts.py +929 -0
- vortex/nwp/data/ctpini.py +133 -0
- vortex/nwp/data/diagnostics.py +181 -0
- vortex/nwp/data/eda.py +148 -0
- vortex/nwp/data/eps.py +383 -0
- vortex/nwp/data/executables.py +1039 -0
- vortex/nwp/data/fields.py +96 -0
- vortex/nwp/data/gridfiles.py +308 -0
- vortex/nwp/data/logs.py +551 -0
- vortex/nwp/data/modelstates.py +334 -0
- vortex/nwp/data/monitoring.py +220 -0
- vortex/nwp/data/namelists.py +644 -0
- vortex/nwp/data/obs.py +748 -0
- vortex/nwp/data/oopsexec.py +72 -0
- vortex/nwp/data/providers.py +182 -0
- vortex/nwp/data/query.py +217 -0
- vortex/nwp/data/stores.py +147 -0
- vortex/nwp/data/surfex.py +338 -0
- vortex/nwp/syntax/__init__.py +9 -0
- vortex/nwp/syntax/stdattrs.py +375 -0
- vortex/nwp/tools/__init__.py +10 -0
- vortex/nwp/tools/addons.py +35 -0
- vortex/nwp/tools/agt.py +55 -0
- vortex/nwp/tools/bdap.py +48 -0
- vortex/nwp/tools/bdcp.py +38 -0
- vortex/nwp/tools/bdm.py +21 -0
- vortex/nwp/tools/bdmp.py +49 -0
- vortex/nwp/tools/conftools.py +1311 -0
- vortex/nwp/tools/drhook.py +62 -0
- vortex/nwp/tools/grib.py +268 -0
- vortex/nwp/tools/gribdiff.py +99 -0
- vortex/nwp/tools/ifstools.py +163 -0
- vortex/nwp/tools/igastuff.py +249 -0
- vortex/nwp/tools/mars.py +56 -0
- vortex/nwp/tools/odb.py +548 -0
- vortex/nwp/tools/partitioning.py +234 -0
- vortex/nwp/tools/satrad.py +56 -0
- vortex/nwp/util/__init__.py +6 -0
- vortex/nwp/util/async.py +184 -0
- vortex/nwp/util/beacon.py +40 -0
- vortex/nwp/util/diffpygram.py +359 -0
- vortex/nwp/util/ens.py +198 -0
- vortex/nwp/util/hooks.py +128 -0
- vortex/nwp/util/taskdeco.py +81 -0
- vortex/nwp/util/usepygram.py +591 -0
- vortex/nwp/util/usetnt.py +87 -0
- vortex/proxy.py +6 -0
- vortex/sessions.py +341 -0
- vortex/syntax/__init__.py +9 -0
- vortex/syntax/stdattrs.py +628 -0
- vortex/syntax/stddeco.py +176 -0
- vortex/toolbox.py +982 -0
- vortex/tools/__init__.py +11 -0
- vortex/tools/actions.py +457 -0
- vortex/tools/addons.py +297 -0
- vortex/tools/arm.py +76 -0
- vortex/tools/compression.py +322 -0
- vortex/tools/date.py +20 -0
- vortex/tools/ddhpack.py +10 -0
- vortex/tools/delayedactions.py +672 -0
- vortex/tools/env.py +513 -0
- vortex/tools/folder.py +663 -0
- vortex/tools/grib.py +559 -0
- vortex/tools/lfi.py +746 -0
- vortex/tools/listings.py +354 -0
- vortex/tools/names.py +575 -0
- vortex/tools/net.py +1790 -0
- vortex/tools/odb.py +10 -0
- vortex/tools/parallelism.py +336 -0
- vortex/tools/prestaging.py +186 -0
- vortex/tools/rawfiles.py +10 -0
- vortex/tools/schedulers.py +413 -0
- vortex/tools/services.py +871 -0
- vortex/tools/storage.py +1061 -0
- vortex/tools/surfex.py +61 -0
- vortex/tools/systems.py +3396 -0
- vortex/tools/targets.py +384 -0
- vortex/util/__init__.py +9 -0
- vortex/util/config.py +1071 -0
- vortex/util/empty.py +24 -0
- vortex/util/helpers.py +184 -0
- vortex/util/introspection.py +63 -0
- vortex/util/iosponge.py +76 -0
- vortex/util/roles.py +51 -0
- vortex/util/storefunctions.py +103 -0
- vortex/util/structs.py +26 -0
- vortex/util/worker.py +150 -0
- vortex_nwp-2.0.0b1.dist-info/LICENSE +517 -0
- vortex_nwp-2.0.0b1.dist-info/METADATA +50 -0
- vortex_nwp-2.0.0b1.dist-info/RECORD +146 -0
- vortex_nwp-2.0.0b1.dist-info/WHEEL +5 -0
- vortex_nwp-2.0.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interface to SMS commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import functools
|
|
7
|
+
|
|
8
|
+
from bronx.fancies import loggers
|
|
9
|
+
import footprints
|
|
10
|
+
|
|
11
|
+
from .services import Service
|
|
12
|
+
|
|
13
|
+
__all__ = []
|
|
14
|
+
|
|
15
|
+
logger = loggers.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Scheduler(Service):
|
|
19
|
+
"""Abstract class for scheduling systems."""
|
|
20
|
+
|
|
21
|
+
_abstract = True
|
|
22
|
+
_footprint = dict(
|
|
23
|
+
info = 'Scheduling service class',
|
|
24
|
+
attr = dict(
|
|
25
|
+
muteset = dict(
|
|
26
|
+
optional = True,
|
|
27
|
+
default = footprints.FPSet(),
|
|
28
|
+
type = footprints.FPSet,
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def __init__(self, *args, **kw):
|
|
34
|
+
logger.debug('Scheduler init %s', self.__class__)
|
|
35
|
+
super().__init__(*args, **kw)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def env(self):
|
|
39
|
+
"""Return the current active environment."""
|
|
40
|
+
return self.sh.env
|
|
41
|
+
|
|
42
|
+
def cmd_rename(self, cmd):
|
|
43
|
+
"""Remap command name. Default is lowercase command name."""
|
|
44
|
+
return cmd.lower()
|
|
45
|
+
|
|
46
|
+
def mute(self, cmd):
|
|
47
|
+
"""Switch off the given command."""
|
|
48
|
+
self.muteset.add(self.cmd_rename(cmd))
|
|
49
|
+
|
|
50
|
+
def play(self, cmd):
|
|
51
|
+
"""Switch on the given command."""
|
|
52
|
+
self.muteset.discard(self.cmd_rename(cmd))
|
|
53
|
+
|
|
54
|
+
def clear(self):
|
|
55
|
+
"""Clear set of mute commands."""
|
|
56
|
+
self.muteset.clear()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EcmwfLikeScheduler(Scheduler):
|
|
60
|
+
"""Abstract class for any ECMWF scheduling systems (SMS, Ecflow)."""
|
|
61
|
+
|
|
62
|
+
_abstract = True
|
|
63
|
+
_footprint = dict(
|
|
64
|
+
attr = dict(
|
|
65
|
+
env_pattern = dict(
|
|
66
|
+
info = 'Scheduler configuration variables start with...',
|
|
67
|
+
),
|
|
68
|
+
non_critical_timeout = dict(
|
|
69
|
+
info = 'Timeout in seconds for non-critical commands',
|
|
70
|
+
type = int,
|
|
71
|
+
default = 5,
|
|
72
|
+
optional = True,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
_KNOWN_CMD = ()
|
|
78
|
+
|
|
79
|
+
def __init__(self, *args, **kw):
|
|
80
|
+
logger.debug('Scheduler init %s', self.__class__)
|
|
81
|
+
super(Scheduler, self).__init__(*args, **kw)
|
|
82
|
+
self._inside_child_session = False
|
|
83
|
+
|
|
84
|
+
def conf(self, kwenv):
|
|
85
|
+
"""Possibly export the provided variables and return a dictionary of positioned variables."""
|
|
86
|
+
if kwenv:
|
|
87
|
+
for schedvar in [x.upper() for x in kwenv.keys() if x.upper().startswith(self.env_pattern)]:
|
|
88
|
+
self.env[schedvar] = str(kwenv[schedvar])
|
|
89
|
+
subenv = dict()
|
|
90
|
+
for schedvar in [x for x in self.env.keys() if x.startswith(self.env_pattern)]:
|
|
91
|
+
subenv[schedvar] = self.env.get(schedvar)
|
|
92
|
+
return subenv
|
|
93
|
+
|
|
94
|
+
def info(self):
|
|
95
|
+
"""Dump current defined variables."""
|
|
96
|
+
for schedvar, schedvalue in self.conf(dict()).items():
|
|
97
|
+
print('{:s}="{!s}"'.format(schedvar, schedvalue))
|
|
98
|
+
|
|
99
|
+
def __call__(self, *args):
|
|
100
|
+
"""By default call the :meth:`info` method."""
|
|
101
|
+
return self.info()
|
|
102
|
+
|
|
103
|
+
@contextlib.contextmanager
|
|
104
|
+
def child_session_setup(self):
|
|
105
|
+
"""This may be customised in order to setup session related stuff."""
|
|
106
|
+
yield True
|
|
107
|
+
|
|
108
|
+
@contextlib.contextmanager
|
|
109
|
+
def child_session(self):
|
|
110
|
+
"""Prepare the environment and possibly setup the session.
|
|
111
|
+
|
|
112
|
+
It will only clone the environment and call child_session_setup
|
|
113
|
+
once (even if child is called again from within the first child call).
|
|
114
|
+
"""
|
|
115
|
+
if not self._inside_child_session:
|
|
116
|
+
self._inside_child_session = True
|
|
117
|
+
try:
|
|
118
|
+
with self.env.clone():
|
|
119
|
+
with self.child_session_setup() as setup_rc:
|
|
120
|
+
yield setup_rc
|
|
121
|
+
finally:
|
|
122
|
+
self._inside_child_session = False
|
|
123
|
+
else:
|
|
124
|
+
yield True
|
|
125
|
+
|
|
126
|
+
def setup_default(self, *args):
|
|
127
|
+
"""Fake method for any missing callback, ie: setup_init, setup_abort, etc."""
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def close_default(self, *args):
|
|
131
|
+
"""Fake method for any missing callback, ie: close_init, close_abort, etc."""
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
@contextlib.contextmanager
|
|
135
|
+
def wrap_actual_child_command(self, kwoptions):
|
|
136
|
+
"""Last minute wrap before binary child command."""
|
|
137
|
+
yield True
|
|
138
|
+
|
|
139
|
+
def child(self, cmd, *options, **kwoptions):
|
|
140
|
+
"""Miscellaneous sms/ecflow child sub-command."""
|
|
141
|
+
rc = None
|
|
142
|
+
cmd = self.cmd_rename(cmd)
|
|
143
|
+
if cmd in self.muteset:
|
|
144
|
+
logger.warning('%s mute command [%s]', self.kind, cmd)
|
|
145
|
+
else:
|
|
146
|
+
with self.child_session() as session_rc:
|
|
147
|
+
if session_rc:
|
|
148
|
+
if getattr(self, 'setup_' + cmd, self.setup_default)(*options):
|
|
149
|
+
wrapp_rc = False
|
|
150
|
+
try:
|
|
151
|
+
with self.wrap_actual_child_command(kwoptions) as wrapp_rc:
|
|
152
|
+
if wrapp_rc:
|
|
153
|
+
rc = self._actual_child(cmd, options, **kwoptions)
|
|
154
|
+
else:
|
|
155
|
+
logger.warning('Actual [%s %s] command wrap failed', self.kind, cmd)
|
|
156
|
+
finally:
|
|
157
|
+
if wrapp_rc:
|
|
158
|
+
getattr(self, 'close_' + cmd, self.close_default)(*options)
|
|
159
|
+
else:
|
|
160
|
+
logger.warning('Actual [%s %s] command skipped due to setup action',
|
|
161
|
+
self.kind, cmd)
|
|
162
|
+
else:
|
|
163
|
+
logger.warning('Actual [%s %s] command skipped session setup failure',
|
|
164
|
+
self.kind, cmd)
|
|
165
|
+
return rc
|
|
166
|
+
|
|
167
|
+
def _actual_child(self, cmd, options, critical=True):
|
|
168
|
+
"""The actual child command implementation."""
|
|
169
|
+
raise NotImplementedError("This an abstract method.")
|
|
170
|
+
|
|
171
|
+
def __getattr__(self, name):
|
|
172
|
+
"""Deal with any known commands."""
|
|
173
|
+
if name in self._KNOWN_CMD:
|
|
174
|
+
return functools.partial(self.child, name)
|
|
175
|
+
else:
|
|
176
|
+
raise AttributeError(name)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class SMS(EcmwfLikeScheduler):
|
|
180
|
+
"""
|
|
181
|
+
Client interface to SMS scheduling and monitoring system.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
_footprint = dict(
|
|
185
|
+
info = 'SMS client service',
|
|
186
|
+
attr = dict(
|
|
187
|
+
kind = dict(
|
|
188
|
+
values = ['sms'],
|
|
189
|
+
),
|
|
190
|
+
rootdir = dict(
|
|
191
|
+
optional = True,
|
|
192
|
+
default = None,
|
|
193
|
+
alias = ('install',),
|
|
194
|
+
),
|
|
195
|
+
env_pattern = dict(
|
|
196
|
+
default = 'SMS',
|
|
197
|
+
optional = True,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
_KNOWN_CMD = ('abort', 'complete', 'event', 'init', 'label', 'meter',
|
|
203
|
+
'msg', 'variable', 'fix')
|
|
204
|
+
|
|
205
|
+
def __init__(self, *args, **kw):
|
|
206
|
+
logger.debug('SMS scheduler client init %s', self)
|
|
207
|
+
super().__init__(*args, **kw)
|
|
208
|
+
self._actual_rootdir = self.rootdir
|
|
209
|
+
if self._actual_rootdir is None:
|
|
210
|
+
self._actual_rootdir = (
|
|
211
|
+
self.env.SMS_INSTALL_ROOT or
|
|
212
|
+
from_config(section="sms", key="rootdir")
|
|
213
|
+
)
|
|
214
|
+
if self._actual_rootdir is None:
|
|
215
|
+
logger.warning(
|
|
216
|
+
'SMS service could not guess install location [%s]',
|
|
217
|
+
str(guesspath)
|
|
218
|
+
)
|
|
219
|
+
if self.sh.path.exists(self.cmdpath('init')):
|
|
220
|
+
self.env.setbinpath(self._actual_rootdir)
|
|
221
|
+
else:
|
|
222
|
+
logger.warning(
|
|
223
|
+
'No SMS client found at init time [rootdir:%s]>',
|
|
224
|
+
self._actual_rootdir
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def cmd_rename(self, cmd):
|
|
228
|
+
"""Remap command name. Strip any sms prefix."""
|
|
229
|
+
cmd = super().cmd_rename(cmd)
|
|
230
|
+
while cmd.startswith('sms'):
|
|
231
|
+
cmd = cmd[3:]
|
|
232
|
+
return cmd
|
|
233
|
+
|
|
234
|
+
def cmdpath(self, cmd):
|
|
235
|
+
"""Return a complete binary path to cmd."""
|
|
236
|
+
cmd = 'sms' + self.cmd_rename(cmd)
|
|
237
|
+
if self._actual_rootdir:
|
|
238
|
+
return self.sh.path.join(self._actual_rootdir, cmd)
|
|
239
|
+
else:
|
|
240
|
+
return cmd
|
|
241
|
+
|
|
242
|
+
def path(self):
|
|
243
|
+
"""Return actual binary path to SMS commands."""
|
|
244
|
+
return self._actual_rootdir
|
|
245
|
+
|
|
246
|
+
@contextlib.contextmanager
|
|
247
|
+
def child_session_setup(self):
|
|
248
|
+
"""Setup the path to the SMS client."""
|
|
249
|
+
with super().child_session_setup() as setup_rc:
|
|
250
|
+
self.env.SMSACTUALPATH = self._actual_rootdir
|
|
251
|
+
yield setup_rc
|
|
252
|
+
|
|
253
|
+
@contextlib.contextmanager
|
|
254
|
+
def wrap_actual_child_command(self, kwoptions):
|
|
255
|
+
"""Last minute wrap before binary child command."""
|
|
256
|
+
with super().wrap_actual_child_command(kwoptions) as wrapp_rc:
|
|
257
|
+
upd_env = dict()
|
|
258
|
+
if not kwoptions.get('critical', True):
|
|
259
|
+
upd_env['SMSDENIED'] = 1
|
|
260
|
+
if self.non_critical_timeout:
|
|
261
|
+
upd_env['SMSTIMEOUT'] = self.non_critical_timeout
|
|
262
|
+
if upd_env:
|
|
263
|
+
with self.env.delta_context(** upd_env):
|
|
264
|
+
yield wrapp_rc
|
|
265
|
+
else:
|
|
266
|
+
yield wrapp_rc
|
|
267
|
+
|
|
268
|
+
def _actual_child(self, cmd, options, critical=True):
|
|
269
|
+
"""Miscellaneous smschild subcommand."""
|
|
270
|
+
args = [self.cmdpath(cmd)]
|
|
271
|
+
args.extend(options)
|
|
272
|
+
return self.sh.spawn(args, output=False, fatal=critical)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class SMSColor(SMS):
|
|
276
|
+
"""
|
|
277
|
+
Default SMS service with some extra colorful features.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
_footprint = dict(
|
|
281
|
+
info = 'SMS color client service',
|
|
282
|
+
attr = dict(
|
|
283
|
+
kind = dict(
|
|
284
|
+
values = ['smscolor'],
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
@contextlib.contextmanager
|
|
290
|
+
def wrap_actual_child_command(self, kwoptions):
|
|
291
|
+
"""Last minute wrap before binary child command."""
|
|
292
|
+
with super().wrap_actual_child_command(kwoptions) as wrapp_rc:
|
|
293
|
+
print("SMS COLOR")
|
|
294
|
+
yield wrapp_rc
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class EcFlow(EcmwfLikeScheduler):
|
|
298
|
+
"""
|
|
299
|
+
Client interface to the ecFlow scheduling and monitoring system.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
_footprint = dict(
|
|
303
|
+
info = 'SMS client service',
|
|
304
|
+
attr = dict(
|
|
305
|
+
kind = dict(
|
|
306
|
+
values = ['ecflow'],
|
|
307
|
+
),
|
|
308
|
+
clientpath = dict(
|
|
309
|
+
info = ("Path to the ecFlow client binary (if omitted, " +
|
|
310
|
+
"it's read in the configuration file)"),
|
|
311
|
+
optional = True,
|
|
312
|
+
default = None,
|
|
313
|
+
),
|
|
314
|
+
env_pattern = dict(
|
|
315
|
+
default = 'ECF_',
|
|
316
|
+
optional = True,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
_KNOWN_CMD = ('abort', 'complete', 'event', 'init', 'label', 'meter', 'msg', 'alter')
|
|
322
|
+
|
|
323
|
+
def __init__(self, *args, **kw):
|
|
324
|
+
logger.debug('EcFlow scheduler client init %s', self)
|
|
325
|
+
super().__init__(*args, **kw)
|
|
326
|
+
self._actual_clientpath = self.clientpath
|
|
327
|
+
|
|
328
|
+
def path(self):
|
|
329
|
+
"""Return the actual binary path to the EcFlow client."""
|
|
330
|
+
if self._actual_clientpath is None:
|
|
331
|
+
thistarget = self.sh.default_target
|
|
332
|
+
guesspath = self.env.ECF_CLIENT_PATH or thistarget.get('ecflow:clientpath')
|
|
333
|
+
ecfversion = self.env.get('ECF_VERSION', 'default')
|
|
334
|
+
guesspath = guesspath.format(version=ecfversion)
|
|
335
|
+
if guesspath is None:
|
|
336
|
+
logger.warning('ecFlow service could not guess the install location [%s]', str(guesspath))
|
|
337
|
+
else:
|
|
338
|
+
self._actual_clientpath = guesspath
|
|
339
|
+
if not self.sh.path.exists(self._actual_clientpath):
|
|
340
|
+
logger.warning('No ecFlow client found at init time [path:%s]>', self._actual_clientpath)
|
|
341
|
+
return self._actual_clientpath
|
|
342
|
+
|
|
343
|
+
@contextlib.contextmanager
|
|
344
|
+
def child_session_setup(self):
|
|
345
|
+
"""Setup a SSH tunnel if necessary."""
|
|
346
|
+
with super().child_session_setup() as setup_rc:
|
|
347
|
+
if setup_rc and not self.sh.default_target.isnetworknode:
|
|
348
|
+
tunnel = None
|
|
349
|
+
# wait and retries from config
|
|
350
|
+
thistarget = self.sh.default_target
|
|
351
|
+
sshwait = float(thistarget.get('ecflow:sshproxy_wait', 6))
|
|
352
|
+
sshretries = float(thistarget.get('ecflow:sshproxy_retries', 2))
|
|
353
|
+
sshretrydelay = float(thistarget.get('ecflow:sshproxy_retrydelay', 1))
|
|
354
|
+
# Build up an SSH tunnel to convey the EcFlow command
|
|
355
|
+
ecconf = self.conf(dict())
|
|
356
|
+
echost = ecconf.get('{:s}HOST'.format(self.env_pattern), None)
|
|
357
|
+
ecport = ecconf.get('{:s}PORT'.format(self.env_pattern), None)
|
|
358
|
+
if not (echost and ecport):
|
|
359
|
+
setup_rc = False
|
|
360
|
+
else:
|
|
361
|
+
sshobj = self.sh.ssh('network', virtualnode=True, mandatory_hostcheck=False,
|
|
362
|
+
maxtries=sshretries, triesdelay=sshretrydelay)
|
|
363
|
+
tunnel = sshobj.tunnel(echost, int(ecport), maxwait=sshwait)
|
|
364
|
+
if not tunnel:
|
|
365
|
+
setup_rc = False
|
|
366
|
+
else:
|
|
367
|
+
newvars = {'{:s}HOST'.format(self.env_pattern): 'localhost',
|
|
368
|
+
'{:s}PORT'.format(self.env_pattern): tunnel.entranceport}
|
|
369
|
+
self.env.update(**newvars)
|
|
370
|
+
try:
|
|
371
|
+
yield setup_rc
|
|
372
|
+
finally:
|
|
373
|
+
# Close the SSH tunnel regardless of the exit status
|
|
374
|
+
if tunnel:
|
|
375
|
+
tunnel.close()
|
|
376
|
+
else:
|
|
377
|
+
yield setup_rc
|
|
378
|
+
|
|
379
|
+
@contextlib.contextmanager
|
|
380
|
+
def wrap_actual_child_command(self, kwoptions):
|
|
381
|
+
"""Last minute wrap before binary child command."""
|
|
382
|
+
with super().wrap_actual_child_command(kwoptions) as wrapp_rc:
|
|
383
|
+
upd_env = dict()
|
|
384
|
+
if not kwoptions.get('critical', True):
|
|
385
|
+
upd_env['{:s}DENIED'.format(self.env_pattern)] = 1
|
|
386
|
+
if self.non_critical_timeout:
|
|
387
|
+
upd_env['{:s}TIMEOUT'.format(self.env_pattern)] = self.non_critical_timeout
|
|
388
|
+
if upd_env:
|
|
389
|
+
with self.env.delta_context(** upd_env):
|
|
390
|
+
yield wrapp_rc
|
|
391
|
+
else:
|
|
392
|
+
yield wrapp_rc
|
|
393
|
+
|
|
394
|
+
def _actual_child(self, cmd, options, critical=True):
|
|
395
|
+
"""Miscellaneous ecFlow sub-command."""
|
|
396
|
+
args = [self.path(), ]
|
|
397
|
+
if options:
|
|
398
|
+
args.append('--{:s}={!s}'.format(cmd, options[0]))
|
|
399
|
+
if len(options) > 1:
|
|
400
|
+
args.extend(options[1:])
|
|
401
|
+
else:
|
|
402
|
+
args.append('--{:s}'.format(cmd))
|
|
403
|
+
args = [str(a) for a in args]
|
|
404
|
+
logger.info('Issuing the ecFlow command: %s', ' '.join(args[1:]))
|
|
405
|
+
return self.sh.spawn(args, output=False, fatal=critical)
|
|
406
|
+
|
|
407
|
+
def abort(self, *opts):
|
|
408
|
+
"""Gateway to :meth:`child` abort method."""
|
|
409
|
+
actual_opts = list(opts)
|
|
410
|
+
if not actual_opts:
|
|
411
|
+
# For backward compatibility with SMS
|
|
412
|
+
actual_opts.append("No abort reason provided")
|
|
413
|
+
return self.child('abort', *actual_opts)
|