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
vortex/tools/services.py
ADDED
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standard services to be used by user defined actions.
|
|
3
|
+
|
|
4
|
+
With the abstract class Service (inheritating from FootprintBase)
|
|
5
|
+
a default Mail Service is provided.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from configparser import NoOptionError, NoSectionError
|
|
9
|
+
import contextlib
|
|
10
|
+
import hashlib
|
|
11
|
+
import pprint
|
|
12
|
+
from string import Template
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import footprints
|
|
16
|
+
from bronx.fancies import loggers
|
|
17
|
+
from bronx.fancies.display import print_tablelike
|
|
18
|
+
from bronx.stdtypes import date
|
|
19
|
+
from bronx.stdtypes.dictionaries import UpperCaseDict
|
|
20
|
+
from bronx.syntax.pretty import EncodedPrettyPrinter
|
|
21
|
+
from vortex import sessions
|
|
22
|
+
from vortex.util.config import GenericConfigParser, load_template, LegacyTemplatingAdapter
|
|
23
|
+
|
|
24
|
+
#: No automatic export
|
|
25
|
+
__all__ = []
|
|
26
|
+
|
|
27
|
+
logger = loggers.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# See logging.handlers.SysLogHandler.priority_map
|
|
30
|
+
criticals = ['debug', 'info', 'error', 'warning', 'critical']
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Service(footprints.FootprintBase):
|
|
34
|
+
"""
|
|
35
|
+
Abstract base class for services.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_abstract = True
|
|
39
|
+
_collector = ('service',)
|
|
40
|
+
_footprint = dict(
|
|
41
|
+
info = 'Abstract services class',
|
|
42
|
+
attr = dict(
|
|
43
|
+
kind = dict(),
|
|
44
|
+
level = dict(
|
|
45
|
+
optional = True,
|
|
46
|
+
default = 'info',
|
|
47
|
+
values = criticals,
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def __init__(self, *args, **kw):
|
|
53
|
+
logger.debug('Abstract service init %s', self.__class__)
|
|
54
|
+
t = sessions.current()
|
|
55
|
+
glove = kw.pop('glove', t.glove)
|
|
56
|
+
sh = kw.pop('sh', t.system())
|
|
57
|
+
super().__init__(*args, **kw)
|
|
58
|
+
self._glove = glove
|
|
59
|
+
self._sh = sh
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def realkind(self):
|
|
63
|
+
return 'service'
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def sh(self):
|
|
67
|
+
return self._sh
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def env(self):
|
|
71
|
+
return self._sh.env
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def glove(self):
|
|
75
|
+
return self._glove
|
|
76
|
+
|
|
77
|
+
def actual_value(self, key, as_var=None, as_conf=None, default=None):
|
|
78
|
+
"""
|
|
79
|
+
Return for a given ``attr`` a value from several sources in turn:
|
|
80
|
+
- a defined attribute value (e.g. from the footprint)
|
|
81
|
+
- a shell environment variable
|
|
82
|
+
- a variable from an ini file section
|
|
83
|
+
- a default value as specified.
|
|
84
|
+
"""
|
|
85
|
+
if as_var is None:
|
|
86
|
+
as_var = key.upper()
|
|
87
|
+
value = getattr(self, key, None)
|
|
88
|
+
if not value:
|
|
89
|
+
value = self.env.get(as_var, None)
|
|
90
|
+
if not value:
|
|
91
|
+
if as_conf is None:
|
|
92
|
+
as_conf = 'services:' + key.lower()
|
|
93
|
+
value = self.sh.default_target.get(as_conf, default)
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
def __call__(self, *args):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MailService(Service):
|
|
101
|
+
"""
|
|
102
|
+
Class responsible for handling email data.
|
|
103
|
+
This class should not be called directly.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
_footprint = dict(
|
|
107
|
+
info = 'Mail services class',
|
|
108
|
+
attr = dict(
|
|
109
|
+
kind = dict(
|
|
110
|
+
values = ['sendmail'],
|
|
111
|
+
),
|
|
112
|
+
sender = dict(
|
|
113
|
+
optional = True,
|
|
114
|
+
default = '[glove::xmail]',
|
|
115
|
+
),
|
|
116
|
+
to = dict(
|
|
117
|
+
alias = ('receiver', 'recipients'),
|
|
118
|
+
),
|
|
119
|
+
replyto = dict(
|
|
120
|
+
optional = True,
|
|
121
|
+
alias = ('reply', 'reply_to'),
|
|
122
|
+
default = None,
|
|
123
|
+
),
|
|
124
|
+
message = dict(
|
|
125
|
+
optional = True,
|
|
126
|
+
default = '',
|
|
127
|
+
alias = ('contents', 'body'),
|
|
128
|
+
type = str,
|
|
129
|
+
),
|
|
130
|
+
filename = dict(
|
|
131
|
+
optional = True,
|
|
132
|
+
default = None,
|
|
133
|
+
),
|
|
134
|
+
attachments = dict(
|
|
135
|
+
type = footprints.FPList,
|
|
136
|
+
optional = True,
|
|
137
|
+
default = footprints.FPList(),
|
|
138
|
+
alias = ('files', 'attach'),
|
|
139
|
+
),
|
|
140
|
+
subject = dict(
|
|
141
|
+
type = str,
|
|
142
|
+
),
|
|
143
|
+
smtpserver = dict(
|
|
144
|
+
optional = True,
|
|
145
|
+
),
|
|
146
|
+
smtpport = dict(
|
|
147
|
+
type = int,
|
|
148
|
+
optional = True,
|
|
149
|
+
),
|
|
150
|
+
smtpuser = dict(
|
|
151
|
+
optional = True,
|
|
152
|
+
),
|
|
153
|
+
smtppass = dict(
|
|
154
|
+
optional = True,
|
|
155
|
+
),
|
|
156
|
+
charset = dict(
|
|
157
|
+
info = 'The encoding that should be used when sending the email',
|
|
158
|
+
optional = True,
|
|
159
|
+
default = 'utf-8',
|
|
160
|
+
),
|
|
161
|
+
inputs_charset = dict(
|
|
162
|
+
info = 'The encoding that should be used when reading input files',
|
|
163
|
+
optional = True,
|
|
164
|
+
),
|
|
165
|
+
commaspace = dict(
|
|
166
|
+
optional = True,
|
|
167
|
+
default = ', '
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def attach(self, *args):
|
|
173
|
+
"""Extend the internal attachments of the next mail to send."""
|
|
174
|
+
self.attachments.extend(args)
|
|
175
|
+
return len(self.attachments)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def is_not_plain_ascii(string):
|
|
179
|
+
"""Return True if any character in string is not ascii-7."""
|
|
180
|
+
return not all(ord(c) < 128 for c in string)
|
|
181
|
+
|
|
182
|
+
def get_message_body(self):
|
|
183
|
+
"""Returns the internal body contents as a MIMEText object."""
|
|
184
|
+
body = self.message
|
|
185
|
+
if self.filename:
|
|
186
|
+
with open(self.filename, encoding=self.inputs_charset) as tmp:
|
|
187
|
+
body += tmp.read()
|
|
188
|
+
from email.message import EmailMessage
|
|
189
|
+
msg = EmailMessage()
|
|
190
|
+
msg.set_content(body, subtype="plain",
|
|
191
|
+
charset=(self.charset if self.is_not_plain_ascii(body) else 'us-ascii'))
|
|
192
|
+
return msg
|
|
193
|
+
|
|
194
|
+
def as_multipart(self, msg):
|
|
195
|
+
"""Build a new multipart mail with default text contents and attachments."""
|
|
196
|
+
from email.message import MIMEPart
|
|
197
|
+
for xtra in self.attachments:
|
|
198
|
+
if isinstance(xtra, MIMEPart):
|
|
199
|
+
msg.add_attachment(xtra)
|
|
200
|
+
elif self.sh.path.isfile(xtra):
|
|
201
|
+
import mimetypes
|
|
202
|
+
ctype, encoding = mimetypes.guess_type(xtra)
|
|
203
|
+
if ctype is None or encoding is not None:
|
|
204
|
+
# No guess could be made, or the file is encoded
|
|
205
|
+
# (compressed), so use a generic bag-of-bits type.
|
|
206
|
+
ctype = 'application/octet-stream'
|
|
207
|
+
maintype, subtype = ctype.split('/', 1)
|
|
208
|
+
with open(xtra, 'rb') as fp:
|
|
209
|
+
msg.add_attachment(fp.read(), maintype, subtype,
|
|
210
|
+
cte="base64", filename=xtra)
|
|
211
|
+
return msg
|
|
212
|
+
|
|
213
|
+
def _set_header(self, msg, header, value):
|
|
214
|
+
msg[header] = value
|
|
215
|
+
|
|
216
|
+
def set_headers(self, msg):
|
|
217
|
+
"""Put on the current message the header items associated to footprint attributes."""
|
|
218
|
+
self._set_header(msg, 'From', self.sender)
|
|
219
|
+
self._set_header(msg, 'To', self.commaspace.join(self.to.split()))
|
|
220
|
+
self._set_header(msg, 'Subject', self.subject)
|
|
221
|
+
if self.replyto is not None:
|
|
222
|
+
self._set_header(msg, 'Reply-To', self.commaspace.join(self.replyto.split()))
|
|
223
|
+
|
|
224
|
+
@contextlib.contextmanager
|
|
225
|
+
def smtp_entrypoints(self):
|
|
226
|
+
import smtplib
|
|
227
|
+
my_smtpserver = self.actual_value('smtpserver',
|
|
228
|
+
as_var='VORTEX_SMTPSERVER',
|
|
229
|
+
default='localhost')
|
|
230
|
+
my_smtpport = self.actual_value('smtpport',
|
|
231
|
+
as_var='VORTEX_SMTPPORT',
|
|
232
|
+
default=smtplib.SMTP_PORT)
|
|
233
|
+
if not self.sh.default_target.isnetworknode:
|
|
234
|
+
sshobj = self.sh.ssh('network', virtualnode=True, mandatory_hostcheck=False)
|
|
235
|
+
with sshobj.tunnel(my_smtpserver, my_smtpport) as tun:
|
|
236
|
+
yield 'localhost', tun.entranceport
|
|
237
|
+
else:
|
|
238
|
+
yield my_smtpserver, my_smtpport
|
|
239
|
+
|
|
240
|
+
def __call__(self):
|
|
241
|
+
"""Main action: pack the message body, add the attachments, and send via SMTP."""
|
|
242
|
+
msg = self.get_message_body()
|
|
243
|
+
if self.attachments:
|
|
244
|
+
msg = self.as_multipart(msg)
|
|
245
|
+
self.set_headers(msg)
|
|
246
|
+
msgcorpus = msg.as_string()
|
|
247
|
+
with self.smtp_entrypoints() as (smtpserver, smtpport):
|
|
248
|
+
import smtplib
|
|
249
|
+
extras = dict()
|
|
250
|
+
if smtpport:
|
|
251
|
+
extras['port'] = smtpport
|
|
252
|
+
smtp = smtplib.SMTP(smtpserver, **extras)
|
|
253
|
+
if self.smtpuser and self.smtppass:
|
|
254
|
+
smtp.login(self.smtpuser, self.smtppass)
|
|
255
|
+
smtp.sendmail(self.sender, self.to.split(), msgcorpus)
|
|
256
|
+
smtp.quit()
|
|
257
|
+
return len(msgcorpus)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class ReportService(Service):
|
|
261
|
+
"""
|
|
262
|
+
Class responsible for handling report data.
|
|
263
|
+
This class should not be called directly.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
_abstract = True
|
|
267
|
+
_footprint = dict(
|
|
268
|
+
info = 'Report services class',
|
|
269
|
+
attr = dict(
|
|
270
|
+
kind = dict(
|
|
271
|
+
values = ['sendreport']
|
|
272
|
+
),
|
|
273
|
+
sender = dict(
|
|
274
|
+
optional = True,
|
|
275
|
+
default = '[glove::user]',
|
|
276
|
+
),
|
|
277
|
+
subject = dict(
|
|
278
|
+
optional = True,
|
|
279
|
+
default = 'Test'
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def __call__(self, *args):
|
|
285
|
+
"""Main action: ..."""
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class FileReportService(ReportService):
|
|
290
|
+
"""Building the report as a simple file."""
|
|
291
|
+
|
|
292
|
+
_abstract = True
|
|
293
|
+
_footprint = dict(
|
|
294
|
+
info = 'File Report services class',
|
|
295
|
+
attr = dict(
|
|
296
|
+
kind = dict(
|
|
297
|
+
values = ['sendreport', 'sendfilereport'],
|
|
298
|
+
remap = dict(sendfilereport = 'sendreport'),
|
|
299
|
+
),
|
|
300
|
+
filename = dict(),
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class SSHProxy(Service):
|
|
306
|
+
"""Remote execution via ssh on a generic target.
|
|
307
|
+
|
|
308
|
+
If ``node`` is the specified :attr:`hostname` value, some target hostname
|
|
309
|
+
will be built on the basis of attributes, :attr:`genericnode`,
|
|
310
|
+
and :attr:`nodetype`.
|
|
311
|
+
|
|
312
|
+
In this case, if :attr:`genericnode` is defined it will be used. If not,
|
|
313
|
+
the configuration file will be checked for a configuration key matching
|
|
314
|
+
the :attr:`nodetype`.
|
|
315
|
+
|
|
316
|
+
When several nodes are available, the first responding ``hostname`` will be
|
|
317
|
+
selected.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
_footprint = dict(
|
|
321
|
+
info = 'Remote command proxy',
|
|
322
|
+
attr = dict(
|
|
323
|
+
kind = dict(
|
|
324
|
+
values = ['ssh', 'ssh_proxy'],
|
|
325
|
+
remap = dict(autoremap = 'first'),
|
|
326
|
+
),
|
|
327
|
+
hostname = dict(),
|
|
328
|
+
genericnode = dict(
|
|
329
|
+
optional = True,
|
|
330
|
+
default = None,
|
|
331
|
+
access = 'rwx',
|
|
332
|
+
),
|
|
333
|
+
nodetype = dict(
|
|
334
|
+
optional = True,
|
|
335
|
+
values = ['login', 'transfer', 'transfert', 'network', 'agt', 'syslog'],
|
|
336
|
+
default = 'network',
|
|
337
|
+
remap = dict(transfer = 'transfert'),
|
|
338
|
+
),
|
|
339
|
+
permut = dict(
|
|
340
|
+
type = bool,
|
|
341
|
+
optional = True,
|
|
342
|
+
default = True,
|
|
343
|
+
),
|
|
344
|
+
maxtries = dict(
|
|
345
|
+
type = int,
|
|
346
|
+
optional = True,
|
|
347
|
+
default = 2,
|
|
348
|
+
),
|
|
349
|
+
sshopts = dict(
|
|
350
|
+
optional = True,
|
|
351
|
+
type = footprints.FPList,
|
|
352
|
+
default = None,
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
def __init__(self, *args, **kw):
|
|
358
|
+
logger.debug('Remote command proxy init %s', self.__class__)
|
|
359
|
+
super().__init__(*args, **kw)
|
|
360
|
+
hostname, virtualnode = self._actual_hostname()
|
|
361
|
+
extra_sshopts = None if self.sshopts is None else ' '.join(self.sshopts)
|
|
362
|
+
self._sshobj = self.sh.ssh(hostname, sshopts=extra_sshopts,
|
|
363
|
+
maxtries=self.maxtries, virtualnode=virtualnode,
|
|
364
|
+
permut=self.permut, mandatory_hostcheck=False)
|
|
365
|
+
|
|
366
|
+
def _actual_hostname(self):
|
|
367
|
+
"""Build a list of candidate target hostnames."""
|
|
368
|
+
myhostname = self.hostname.strip().lower()
|
|
369
|
+
virtualnode = False
|
|
370
|
+
if myhostname == 'node':
|
|
371
|
+
if self.genericnode is not None and self.genericnode != 'no_generic':
|
|
372
|
+
myhostname = self.genericnode
|
|
373
|
+
else:
|
|
374
|
+
myhostname = self.nodetype
|
|
375
|
+
virtualnode = True
|
|
376
|
+
return myhostname, virtualnode
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def retries(self):
|
|
380
|
+
return self._sshobj.retries
|
|
381
|
+
|
|
382
|
+
def __call__(self, *args):
|
|
383
|
+
"""Remote execution."""
|
|
384
|
+
return self._sshobj.execute(' '.join(args))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class JeevesService(Service):
|
|
388
|
+
"""
|
|
389
|
+
Class acting as a standard Bertie asking Jeeves to do something.
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
_footprint = dict(
|
|
393
|
+
info = 'Jeeves services class',
|
|
394
|
+
attr = dict(
|
|
395
|
+
kind = dict(
|
|
396
|
+
values = ['askjeeves']
|
|
397
|
+
),
|
|
398
|
+
todo = dict(),
|
|
399
|
+
jname = dict(
|
|
400
|
+
optional = True,
|
|
401
|
+
default = 'test',
|
|
402
|
+
),
|
|
403
|
+
juser = dict(
|
|
404
|
+
optional = True,
|
|
405
|
+
default = '[glove::user]',
|
|
406
|
+
),
|
|
407
|
+
jpath = dict(
|
|
408
|
+
optional = True,
|
|
409
|
+
default = None,
|
|
410
|
+
access = 'rwx',
|
|
411
|
+
),
|
|
412
|
+
jfile = dict(
|
|
413
|
+
optional = True,
|
|
414
|
+
default = 'vortex',
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def __call__(self, *args):
|
|
420
|
+
"""Main action: ..."""
|
|
421
|
+
if self.jpath is None:
|
|
422
|
+
self.jpath = self.sh.path.join(self.env.HOME, 'jeeves', self.jname, 'depot')
|
|
423
|
+
if self.sh.path.isdir(self.jpath):
|
|
424
|
+
from jeeves import bertie
|
|
425
|
+
data = dict()
|
|
426
|
+
for arg in args:
|
|
427
|
+
data.update(arg)
|
|
428
|
+
fulltalk = dict(
|
|
429
|
+
user=self.juser,
|
|
430
|
+
jtag=self.sh.path.join(self.jpath, self.jfile),
|
|
431
|
+
todo=self.todo,
|
|
432
|
+
mail=data.pop('mail', self.glove.xmail),
|
|
433
|
+
apps=data.pop('apps', (self.glove.vapp,)),
|
|
434
|
+
conf=data.pop('conf', (self.glove.vconf,)),
|
|
435
|
+
task=self.env.get('JOBNAME') or self.env.get('SMSNAME', 'interactif'),
|
|
436
|
+
)
|
|
437
|
+
fulltalk.update(
|
|
438
|
+
data=data,
|
|
439
|
+
)
|
|
440
|
+
jr = bertie.ask(**fulltalk)
|
|
441
|
+
return jr.todo, jr.last
|
|
442
|
+
else:
|
|
443
|
+
logger.error('No valid path to jeeves <%s>', self.jpath)
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class HideService(Service):
|
|
448
|
+
"""
|
|
449
|
+
A service to hide data.
|
|
450
|
+
|
|
451
|
+
Mainly used to store files to be handled asynchronously
|
|
452
|
+
(and then deleted) by Jeeves.
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
_footprint = dict(
|
|
456
|
+
info = 'Hide a given object on current filesystem',
|
|
457
|
+
attr = dict(
|
|
458
|
+
kind = dict(
|
|
459
|
+
values = ['hidden', 'hide', 'hiddencache'],
|
|
460
|
+
remap = dict(autoremap = 'first'),
|
|
461
|
+
),
|
|
462
|
+
rootdir = dict(
|
|
463
|
+
optional = True,
|
|
464
|
+
default = None,
|
|
465
|
+
),
|
|
466
|
+
headdir = dict(
|
|
467
|
+
optional = True,
|
|
468
|
+
default = 'hidden',
|
|
469
|
+
),
|
|
470
|
+
asfmt = dict(
|
|
471
|
+
optional = True,
|
|
472
|
+
default = None,
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def find_rootdir(self, filename):
|
|
478
|
+
"""Find a path for hiding files on the same filesystem."""
|
|
479
|
+
username = self.sh.getlogname()
|
|
480
|
+
work_dir = self.sh.path.join(self.sh.find_mount_point(filename), 'work')
|
|
481
|
+
if not self.sh.path.exists(work_dir):
|
|
482
|
+
logger.warning("path <%s> doesn't exist", work_dir)
|
|
483
|
+
fullpath = self.sh.path.realpath(filename)
|
|
484
|
+
if username not in fullpath:
|
|
485
|
+
logger.error('No login <%s> in path <%s>', username, fullpath)
|
|
486
|
+
raise ValueError('Login name not in actual path for hiding data')
|
|
487
|
+
work_dir = fullpath.partition(username)[0]
|
|
488
|
+
logger.warning("using work_dir = <%s>", work_dir)
|
|
489
|
+
hidden_path = self.sh.path.join(work_dir, username, self.headdir)
|
|
490
|
+
return hidden_path
|
|
491
|
+
|
|
492
|
+
def __call__(self, filename):
|
|
493
|
+
"""Main action: hide a cheap copy of this file under a unique name."""
|
|
494
|
+
|
|
495
|
+
rootdir = self.rootdir
|
|
496
|
+
if rootdir is None:
|
|
497
|
+
rootdir = self.sh.default_target.get('hidden_rootdir', None)
|
|
498
|
+
if rootdir is not None:
|
|
499
|
+
rootdir = self.sh.path.expanduser(rootdir)
|
|
500
|
+
|
|
501
|
+
actual_rootdir = rootdir or self.find_rootdir(filename)
|
|
502
|
+
destination = self.sh.path.join(
|
|
503
|
+
actual_rootdir,
|
|
504
|
+
'.'.join((
|
|
505
|
+
'HIDDEN',
|
|
506
|
+
date.now().strftime('%Y%m%d%H%M%S.%f'),
|
|
507
|
+
'P{:06d}'.format(self.sh.getpid()),
|
|
508
|
+
hashlib.md5(self.sh.path.abspath(filename).encode(encoding='utf-8')).hexdigest()
|
|
509
|
+
))
|
|
510
|
+
)
|
|
511
|
+
self.sh.cp(filename, destination, intent='in', fmt=self.asfmt)
|
|
512
|
+
return destination
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class Directory:
|
|
516
|
+
"""
|
|
517
|
+
A class to represent and use mail aliases.
|
|
518
|
+
|
|
519
|
+
Directory (en) means Annuaire (fr).
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
def __init__(self, inifile, domain='meteo.fr', encoding=None):
|
|
523
|
+
"""Keep aliases in memory, as a dict of sets."""
|
|
524
|
+
config = GenericConfigParser(inifile, encoding=encoding)
|
|
525
|
+
try:
|
|
526
|
+
self.domain = config.get('general', 'default_domain')
|
|
527
|
+
except NoOptionError:
|
|
528
|
+
self.domain = domain
|
|
529
|
+
self.aliases = {
|
|
530
|
+
k.lower(): set(v.lower().replace(',', ' ').split())
|
|
531
|
+
for (k, v) in config.items('aliases')
|
|
532
|
+
}
|
|
533
|
+
count = self._flatten()
|
|
534
|
+
logger.debug('opmail aliases flattened in %d iterations:\n%s',
|
|
535
|
+
count, str(self))
|
|
536
|
+
|
|
537
|
+
def get_addresses(self, definition, add_domain=True):
|
|
538
|
+
"""
|
|
539
|
+
Build a space separated list of unique mail addresses from a string that
|
|
540
|
+
may reference aliases.
|
|
541
|
+
"""
|
|
542
|
+
addresses = set()
|
|
543
|
+
for item in definition.lower().replace(',', ' ').split():
|
|
544
|
+
if item in self.aliases:
|
|
545
|
+
addresses |= self.aliases[item]
|
|
546
|
+
else:
|
|
547
|
+
addresses |= {item}
|
|
548
|
+
if add_domain:
|
|
549
|
+
return ' '.join(self._add_domain(addresses))
|
|
550
|
+
return ' '.join(addresses)
|
|
551
|
+
|
|
552
|
+
def __str__(self):
|
|
553
|
+
return '\n'.join(sorted(
|
|
554
|
+
['{}: {}'.format(k, ' '.join(sorted(v)))
|
|
555
|
+
for (k, v) in self.aliases.items()]
|
|
556
|
+
))
|
|
557
|
+
|
|
558
|
+
def _flatten(self):
|
|
559
|
+
"""Resolve recursive definitions from the dict of sets."""
|
|
560
|
+
changed = True
|
|
561
|
+
count = 0
|
|
562
|
+
while changed:
|
|
563
|
+
changed = False
|
|
564
|
+
count += 1
|
|
565
|
+
for kref, vref in self.aliases.items():
|
|
566
|
+
if kref in vref:
|
|
567
|
+
logger.error('Cycle detected in the aliases directory.\n'
|
|
568
|
+
'offending key: %s.\n'
|
|
569
|
+
'directory being flattened:\n%s',
|
|
570
|
+
str(kref), str(self))
|
|
571
|
+
raise ValueError('Cycle for key <{}> in directory definition'.format(kref))
|
|
572
|
+
for k, v in self.aliases.items():
|
|
573
|
+
if kref in v:
|
|
574
|
+
v -= {kref}
|
|
575
|
+
v |= vref
|
|
576
|
+
self.aliases[k] = v
|
|
577
|
+
changed = True
|
|
578
|
+
return count
|
|
579
|
+
|
|
580
|
+
def _add_domain(self, aset):
|
|
581
|
+
"""Add domain where missing in a set of addresses."""
|
|
582
|
+
return {
|
|
583
|
+
v if '@' in v
|
|
584
|
+
else v + '@' + self.domain
|
|
585
|
+
for v in aset
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class PromptService(Service):
|
|
590
|
+
"""
|
|
591
|
+
Class used to simulate a real Service: logs the argument it receives.
|
|
592
|
+
This class should not be called directly.
|
|
593
|
+
"""
|
|
594
|
+
|
|
595
|
+
_footprint = dict(
|
|
596
|
+
info = 'Simulate a call to a Service.',
|
|
597
|
+
attr = dict(
|
|
598
|
+
kind = dict(
|
|
599
|
+
values = ('prompt',),
|
|
600
|
+
),
|
|
601
|
+
comment = dict(
|
|
602
|
+
optional = True,
|
|
603
|
+
default = None,
|
|
604
|
+
),
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
def __call__(self, options):
|
|
609
|
+
"""Prints what arguments the action was called with."""
|
|
610
|
+
|
|
611
|
+
pf = EncodedPrettyPrinter().pformat
|
|
612
|
+
logger_action = getattr(logger, self.level, logger.warning)
|
|
613
|
+
msg = (self.comment or 'PromptService was called.') + '\noptions = {}'
|
|
614
|
+
logger_action(msg.format(pf(options)).replace('\n', '\n<prompt>'))
|
|
615
|
+
return True
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
class TemplatedMailService(MailService):
|
|
619
|
+
"""
|
|
620
|
+
Class responsible for sending templated mails.
|
|
621
|
+
This class should not be called directly.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
_footprint = dict(
|
|
625
|
+
info = 'Templated mail services class',
|
|
626
|
+
attr = dict(
|
|
627
|
+
kind = dict(
|
|
628
|
+
values = ['templatedmail'],
|
|
629
|
+
),
|
|
630
|
+
id = dict(
|
|
631
|
+
alias = ('template',),
|
|
632
|
+
),
|
|
633
|
+
subject = dict(
|
|
634
|
+
optional = True,
|
|
635
|
+
default = None,
|
|
636
|
+
access = 'rwx',
|
|
637
|
+
),
|
|
638
|
+
to = dict(
|
|
639
|
+
optional = True,
|
|
640
|
+
default = None,
|
|
641
|
+
access = 'rwx',
|
|
642
|
+
),
|
|
643
|
+
message = dict(
|
|
644
|
+
access = 'rwx',
|
|
645
|
+
),
|
|
646
|
+
directory = dict(
|
|
647
|
+
type = Directory,
|
|
648
|
+
optional = True,
|
|
649
|
+
default = None,
|
|
650
|
+
),
|
|
651
|
+
catalog = dict(
|
|
652
|
+
type = GenericConfigParser,
|
|
653
|
+
),
|
|
654
|
+
dryrun = dict(
|
|
655
|
+
info = "Do not actually send the email. Just render the template.",
|
|
656
|
+
type = bool,
|
|
657
|
+
optional = True,
|
|
658
|
+
default = False,
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
_TEMPLATES_SUBDIR = None
|
|
664
|
+
|
|
665
|
+
def __init__(self, *args, **kw):
|
|
666
|
+
ticket = kw.pop('ticket', sessions.get())
|
|
667
|
+
super().__init__(*args, **kw)
|
|
668
|
+
self._ticket = ticket
|
|
669
|
+
logger.debug('TemplatedMail init for id <%s>', self.id)
|
|
670
|
+
|
|
671
|
+
@property
|
|
672
|
+
def ticket(self):
|
|
673
|
+
return self._ticket
|
|
674
|
+
|
|
675
|
+
def header(self):
|
|
676
|
+
"""String prepended to the message body."""
|
|
677
|
+
return ''
|
|
678
|
+
|
|
679
|
+
def trailer(self):
|
|
680
|
+
"""String appended to the message body."""
|
|
681
|
+
return ''
|
|
682
|
+
|
|
683
|
+
def get_catalog_section(self):
|
|
684
|
+
"""Read section <id> (a dict-like) from the catalog."""
|
|
685
|
+
try:
|
|
686
|
+
section = dict(self.catalog.items(self.id))
|
|
687
|
+
except NoSectionError:
|
|
688
|
+
logger.error('Section <%s> is missing in catalog <%s>',
|
|
689
|
+
self.id, self.catalog.file)
|
|
690
|
+
section = None
|
|
691
|
+
return section
|
|
692
|
+
|
|
693
|
+
def substitution_dictionary(self, add_ons=None):
|
|
694
|
+
"""Dictionary used for template substitutions: env + add_ons."""
|
|
695
|
+
dico = UpperCaseDict(self.env)
|
|
696
|
+
if add_ons is not None:
|
|
697
|
+
dico.update(add_ons)
|
|
698
|
+
return dico
|
|
699
|
+
|
|
700
|
+
@staticmethod
|
|
701
|
+
def substitute(tpl, tpldict, depth=1):
|
|
702
|
+
"""Safely apply template substitution.
|
|
703
|
+
|
|
704
|
+
* Syntactic and missing keys errors are detected and logged.
|
|
705
|
+
* on error, a safe substitution is applied.
|
|
706
|
+
* The substitution is iterated ``depth`` times.
|
|
707
|
+
"""
|
|
708
|
+
if not isinstance(tpl, (Template, LegacyTemplatingAdapter)):
|
|
709
|
+
tpl = Template(tpl)
|
|
710
|
+
result = ''
|
|
711
|
+
for level in range(depth):
|
|
712
|
+
try:
|
|
713
|
+
result = tpl.substitute(tpldict)
|
|
714
|
+
except KeyError as exc:
|
|
715
|
+
logger.error('Undefined key <%s> in template substitution level %d',
|
|
716
|
+
str(exc), level + 1)
|
|
717
|
+
result = tpl.safe_substitute(tpldict)
|
|
718
|
+
except ValueError as exc:
|
|
719
|
+
logger.error('Illegal syntax in template: %s', exc)
|
|
720
|
+
result = tpl.safe_substitute(tpldict)
|
|
721
|
+
tpl = Template(result)
|
|
722
|
+
return result
|
|
723
|
+
|
|
724
|
+
def _template_name_rewrite(self, tplguess):
|
|
725
|
+
base = '@'
|
|
726
|
+
if self._TEMPLATES_SUBDIR is not None:
|
|
727
|
+
base = '@{!s}/'.format(self._TEMPLATES_SUBDIR)
|
|
728
|
+
if not tplguess.startswith(base):
|
|
729
|
+
tplguess = base + tplguess
|
|
730
|
+
if not tplguess.endswith('.tpl'):
|
|
731
|
+
tplguess += '.tpl'
|
|
732
|
+
return tplguess
|
|
733
|
+
|
|
734
|
+
def get_message(self, tpldict):
|
|
735
|
+
"""Contents:
|
|
736
|
+
|
|
737
|
+
* from the fp if given, else the catalog gives the template file name.
|
|
738
|
+
* template-substituted.
|
|
739
|
+
* header and trailer are added.
|
|
740
|
+
"""
|
|
741
|
+
tpl = self.message
|
|
742
|
+
if tpl == '':
|
|
743
|
+
tplfile = self.section.get('template', self.id)
|
|
744
|
+
tplfile = self._template_name_rewrite(tplfile)
|
|
745
|
+
try:
|
|
746
|
+
tpl = load_template(self.ticket, tplfile, encoding=self.inputs_charset)
|
|
747
|
+
except ValueError as exc:
|
|
748
|
+
logger.error('%s', exc.message)
|
|
749
|
+
return None
|
|
750
|
+
message = self.substitute(tpl, tpldict)
|
|
751
|
+
return self.header() + message + self.trailer()
|
|
752
|
+
|
|
753
|
+
def get_subject(self, tpldict):
|
|
754
|
+
"""Subject:
|
|
755
|
+
|
|
756
|
+
* from the fp if given, else from the catalog.
|
|
757
|
+
* template-substituted.
|
|
758
|
+
"""
|
|
759
|
+
tpl = self.subject
|
|
760
|
+
if tpl is None:
|
|
761
|
+
tpl = self.section.get('subject', None)
|
|
762
|
+
if tpl is None:
|
|
763
|
+
logger.error('Missing <subject> definition for id <%s>.', self.id)
|
|
764
|
+
return None
|
|
765
|
+
subject = self.substitute(tpl, tpldict)
|
|
766
|
+
return subject
|
|
767
|
+
|
|
768
|
+
def get_to(self, tpldict):
|
|
769
|
+
"""Recipients:
|
|
770
|
+
|
|
771
|
+
* from the fp if given, else from the catalog.
|
|
772
|
+
* template-substituted.
|
|
773
|
+
* expanded by the directory (if any).
|
|
774
|
+
* substituted again, to allow for $vars in the directory.
|
|
775
|
+
* directory-expanded again for domain completion and unicity.
|
|
776
|
+
"""
|
|
777
|
+
tpl = self.to
|
|
778
|
+
if tpl is None:
|
|
779
|
+
tpl = self.section.get('to', None)
|
|
780
|
+
if tpl is None:
|
|
781
|
+
logger.error('Missing <to> definition for id <%s>.', self.id)
|
|
782
|
+
return None
|
|
783
|
+
to = self.substitute(tpl, tpldict)
|
|
784
|
+
if self.directory:
|
|
785
|
+
to = self.directory.get_addresses(to, add_domain=False)
|
|
786
|
+
# substitute again for directory definitions
|
|
787
|
+
to = self.substitute(to, tpldict)
|
|
788
|
+
# last resolution, plus add domain and remove duplicates
|
|
789
|
+
if self.directory:
|
|
790
|
+
to = self.directory.get_addresses(to)
|
|
791
|
+
return to
|
|
792
|
+
|
|
793
|
+
def prepare(self, add_ons=None):
|
|
794
|
+
"""Prepare elements in turn, return True iff all succeeded."""
|
|
795
|
+
self.section = self.get_catalog_section()
|
|
796
|
+
if self.section is None:
|
|
797
|
+
return False
|
|
798
|
+
|
|
799
|
+
tpldict = self.substitution_dictionary(add_ons)
|
|
800
|
+
# Convert everything to unicode
|
|
801
|
+
for k in tpldict.keys():
|
|
802
|
+
tpldict[k] = str(tpldict[k])
|
|
803
|
+
|
|
804
|
+
self.message = self.get_message(tpldict)
|
|
805
|
+
if self.message is None:
|
|
806
|
+
return False
|
|
807
|
+
|
|
808
|
+
self.subject = self.get_subject(tpldict)
|
|
809
|
+
if self.subject is None:
|
|
810
|
+
return False
|
|
811
|
+
|
|
812
|
+
self.to = self.get_to(tpldict)
|
|
813
|
+
if self.to is None:
|
|
814
|
+
return False
|
|
815
|
+
|
|
816
|
+
return True
|
|
817
|
+
|
|
818
|
+
def __call__(self, *args):
|
|
819
|
+
"""Main action:
|
|
820
|
+
|
|
821
|
+
* substitute templates where needed.
|
|
822
|
+
* apply directory definitions to recipients.
|
|
823
|
+
* activation is checked before sending via the Mail Service.
|
|
824
|
+
|
|
825
|
+
Arguments are passed as add_ons to the substitution dictionary.
|
|
826
|
+
"""
|
|
827
|
+
add_ons = dict()
|
|
828
|
+
for arg in args:
|
|
829
|
+
add_ons.update(arg)
|
|
830
|
+
rc = False
|
|
831
|
+
if self.prepare(add_ons) and not self.dryrun:
|
|
832
|
+
rc = super().__call__()
|
|
833
|
+
return rc
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
class AbstractRdTemplatedMailService(TemplatedMailService):
|
|
837
|
+
|
|
838
|
+
_abstract = True
|
|
839
|
+
|
|
840
|
+
def header(self):
|
|
841
|
+
"""String prepended to the message body."""
|
|
842
|
+
now = date.now()
|
|
843
|
+
stamp1 = now.strftime('%A %d %B %Y')
|
|
844
|
+
stamp2 = now.strftime('%X')
|
|
845
|
+
return 'Email sent on {} at {} (from: {}).\n--\n\n'.format(stamp1, stamp2,
|
|
846
|
+
self.sh.default_target.hostname)
|
|
847
|
+
|
|
848
|
+
def substitution_dictionary(self, add_ons=None):
|
|
849
|
+
sdict = super().substitution_dictionary(add_ons=add_ons)
|
|
850
|
+
sdict['jobid'] = self.sh.guess_job_identifier()
|
|
851
|
+
# Try to detect MTOOL data (this may be empty if MTOOL is not used):
|
|
852
|
+
if self.env.MTOOL_STEP:
|
|
853
|
+
mt_stack = ['\nMTOOL details:', ]
|
|
854
|
+
mt_items = ['mtool_step_{:s}'.format(i)
|
|
855
|
+
for i in ('abort', 'depot', 'spool', 'idnum')
|
|
856
|
+
if 'mtool_step_{:s}'.format(i) in self.env]
|
|
857
|
+
print_tablelike('{:s} = {!s}', mt_items, [self.env[i] for i in mt_items],
|
|
858
|
+
output_callback=mt_stack.append)
|
|
859
|
+
sdict['mtool_info'] = '\n'.join(mt_stack) + '\n'
|
|
860
|
+
else:
|
|
861
|
+
sdict['mtool_info'] = ''
|
|
862
|
+
# The list of footprints' defaults
|
|
863
|
+
fpdefaults = footprints.setup.defaults
|
|
864
|
+
sdict['fpdefaults'] = pprint.pformat(fpdefaults, indent=2)
|
|
865
|
+
# A condensed indication on date/cutoff
|
|
866
|
+
sdict['timeid'] = fpdefaults.get('date', None)
|
|
867
|
+
if sdict['timeid']:
|
|
868
|
+
sdict['timeid'] = sdict['timeid'].vortex(cutoff=fpdefaults.get('cutoff', 'X'))
|
|
869
|
+
# The generic host/cluster name
|
|
870
|
+
sdict['host'] = self.sh.default_target.inetname
|
|
871
|
+
return sdict
|