vortex-nwp 2.0.0b1__py3-none-any.whl → 2.1.0__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 +75 -47
- vortex/algo/__init__.py +3 -2
- vortex/algo/components.py +944 -618
- vortex/algo/mpitools.py +802 -497
- vortex/algo/mpitools_templates/__init__.py +1 -0
- vortex/algo/serversynctools.py +34 -33
- vortex/config.py +19 -22
- vortex/data/__init__.py +9 -3
- vortex/data/abstractstores.py +593 -655
- vortex/data/containers.py +217 -162
- vortex/data/contents.py +65 -39
- vortex/data/executables.py +93 -102
- vortex/data/flow.py +40 -34
- vortex/data/geometries.py +228 -132
- vortex/data/handlers.py +436 -227
- vortex/data/outflow.py +15 -15
- vortex/data/providers.py +185 -163
- vortex/data/resources.py +48 -42
- vortex/data/stores.py +540 -417
- vortex/data/sync_templates/__init__.py +0 -0
- vortex/gloves.py +114 -87
- vortex/layout/__init__.py +1 -8
- vortex/layout/contexts.py +150 -84
- vortex/layout/dataflow.py +353 -202
- vortex/layout/monitor.py +264 -128
- vortex/nwp/__init__.py +5 -2
- vortex/nwp/algo/__init__.py +14 -5
- vortex/nwp/algo/assim.py +205 -151
- vortex/nwp/algo/clim.py +683 -517
- vortex/nwp/algo/coupling.py +447 -225
- vortex/nwp/algo/eda.py +437 -229
- vortex/nwp/algo/eps.py +403 -231
- vortex/nwp/algo/forecasts.py +416 -275
- vortex/nwp/algo/fpserver.py +683 -307
- vortex/nwp/algo/ifsnaming.py +205 -145
- vortex/nwp/algo/ifsroot.py +215 -122
- vortex/nwp/algo/monitoring.py +137 -76
- vortex/nwp/algo/mpitools.py +330 -190
- vortex/nwp/algo/odbtools.py +637 -353
- vortex/nwp/algo/oopsroot.py +454 -273
- vortex/nwp/algo/oopstests.py +90 -56
- vortex/nwp/algo/request.py +287 -206
- vortex/nwp/algo/stdpost.py +878 -522
- vortex/nwp/data/__init__.py +22 -4
- vortex/nwp/data/assim.py +125 -137
- vortex/nwp/data/boundaries.py +121 -68
- vortex/nwp/data/climfiles.py +193 -211
- vortex/nwp/data/configfiles.py +73 -69
- vortex/nwp/data/consts.py +426 -401
- vortex/nwp/data/ctpini.py +59 -43
- vortex/nwp/data/diagnostics.py +94 -66
- vortex/nwp/data/eda.py +50 -51
- vortex/nwp/data/eps.py +195 -146
- vortex/nwp/data/executables.py +440 -434
- vortex/nwp/data/fields.py +63 -48
- vortex/nwp/data/gridfiles.py +183 -111
- vortex/nwp/data/logs.py +250 -217
- vortex/nwp/data/modelstates.py +180 -151
- vortex/nwp/data/monitoring.py +72 -99
- vortex/nwp/data/namelists.py +254 -202
- vortex/nwp/data/obs.py +400 -308
- vortex/nwp/data/oopsexec.py +22 -20
- vortex/nwp/data/providers.py +90 -65
- vortex/nwp/data/query.py +71 -82
- vortex/nwp/data/stores.py +49 -36
- vortex/nwp/data/surfex.py +136 -137
- vortex/nwp/syntax/__init__.py +1 -1
- vortex/nwp/syntax/stdattrs.py +173 -111
- vortex/nwp/tools/__init__.py +2 -2
- vortex/nwp/tools/addons.py +22 -17
- vortex/nwp/tools/agt.py +24 -12
- vortex/nwp/tools/bdap.py +16 -5
- vortex/nwp/tools/bdcp.py +4 -1
- vortex/nwp/tools/bdm.py +3 -0
- vortex/nwp/tools/bdmp.py +14 -9
- vortex/nwp/tools/conftools.py +728 -378
- vortex/nwp/tools/drhook.py +12 -8
- vortex/nwp/tools/grib.py +65 -39
- vortex/nwp/tools/gribdiff.py +22 -17
- vortex/nwp/tools/ifstools.py +82 -42
- vortex/nwp/tools/igastuff.py +167 -143
- vortex/nwp/tools/mars.py +14 -2
- vortex/nwp/tools/odb.py +234 -125
- vortex/nwp/tools/partitioning.py +61 -37
- vortex/nwp/tools/satrad.py +27 -12
- vortex/nwp/util/async.py +83 -55
- vortex/nwp/util/beacon.py +10 -10
- vortex/nwp/util/diffpygram.py +174 -86
- vortex/nwp/util/ens.py +144 -63
- vortex/nwp/util/hooks.py +30 -19
- vortex/nwp/util/taskdeco.py +28 -24
- vortex/nwp/util/usepygram.py +278 -172
- vortex/nwp/util/usetnt.py +31 -17
- vortex/sessions.py +72 -39
- vortex/syntax/__init__.py +1 -1
- vortex/syntax/stdattrs.py +410 -171
- vortex/syntax/stddeco.py +31 -22
- vortex/toolbox.py +327 -192
- vortex/tools/__init__.py +11 -2
- vortex/tools/actions.py +110 -121
- vortex/tools/addons.py +111 -92
- vortex/tools/arm.py +42 -22
- vortex/tools/compression.py +72 -69
- vortex/tools/date.py +11 -4
- vortex/tools/delayedactions.py +242 -132
- vortex/tools/env.py +75 -47
- vortex/tools/folder.py +342 -171
- vortex/tools/grib.py +341 -162
- vortex/tools/lfi.py +423 -216
- vortex/tools/listings.py +109 -40
- vortex/tools/names.py +218 -156
- vortex/tools/net.py +655 -299
- vortex/tools/parallelism.py +93 -61
- vortex/tools/prestaging.py +55 -31
- vortex/tools/schedulers.py +172 -105
- vortex/tools/services.py +403 -334
- vortex/tools/storage.py +293 -358
- vortex/tools/surfex.py +24 -24
- vortex/tools/systems.py +1234 -643
- vortex/tools/targets.py +156 -100
- vortex/util/__init__.py +1 -1
- vortex/util/config.py +378 -327
- vortex/util/empty.py +2 -2
- vortex/util/helpers.py +56 -24
- vortex/util/introspection.py +18 -12
- vortex/util/iosponge.py +8 -4
- vortex/util/roles.py +4 -6
- vortex/util/storefunctions.py +39 -13
- vortex/util/structs.py +3 -3
- vortex/util/worker.py +29 -17
- vortex_nwp-2.1.0.dist-info/METADATA +67 -0
- vortex_nwp-2.1.0.dist-info/RECORD +144 -0
- {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info}/WHEEL +1 -1
- vortex/layout/appconf.py +0 -109
- vortex/layout/jobs.py +0 -1276
- vortex/layout/nodes.py +0 -1424
- vortex/layout/subjobs.py +0 -464
- vortex_nwp-2.0.0b1.dist-info/METADATA +0 -50
- vortex_nwp-2.0.0b1.dist-info/RECORD +0 -146
- {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info/licenses}/LICENSE +0 -0
- {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info}/top_level.txt +0 -0
vortex/tools/services.py
CHANGED
|
@@ -5,7 +5,7 @@ With the abstract class Service (inheritating from FootprintBase)
|
|
|
5
5
|
a default Mail Service is provided.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import configparser
|
|
9
9
|
import contextlib
|
|
10
10
|
import hashlib
|
|
11
11
|
import pprint
|
|
@@ -19,7 +19,10 @@ from bronx.stdtypes import date
|
|
|
19
19
|
from bronx.stdtypes.dictionaries import UpperCaseDict
|
|
20
20
|
from bronx.syntax.pretty import EncodedPrettyPrinter
|
|
21
21
|
from vortex import sessions
|
|
22
|
-
from vortex.util.config import
|
|
22
|
+
from vortex.util.config import (
|
|
23
|
+
load_template,
|
|
24
|
+
LegacyTemplatingAdapter,
|
|
25
|
+
)
|
|
23
26
|
|
|
24
27
|
#: No automatic export
|
|
25
28
|
__all__ = []
|
|
@@ -27,7 +30,7 @@ __all__ = []
|
|
|
27
30
|
logger = loggers.getLogger(__name__)
|
|
28
31
|
|
|
29
32
|
# See logging.handlers.SysLogHandler.priority_map
|
|
30
|
-
criticals = [
|
|
33
|
+
criticals = ["debug", "info", "error", "warning", "critical"]
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
class Service(footprints.FootprintBase):
|
|
@@ -36,31 +39,31 @@ class Service(footprints.FootprintBase):
|
|
|
36
39
|
"""
|
|
37
40
|
|
|
38
41
|
_abstract = True
|
|
39
|
-
_collector = (
|
|
42
|
+
_collector = ("service",)
|
|
40
43
|
_footprint = dict(
|
|
41
|
-
info
|
|
42
|
-
attr
|
|
43
|
-
kind
|
|
44
|
-
level
|
|
45
|
-
optional
|
|
46
|
-
default
|
|
47
|
-
values
|
|
48
|
-
),
|
|
49
|
-
)
|
|
44
|
+
info="Abstract services class",
|
|
45
|
+
attr=dict(
|
|
46
|
+
kind=dict(),
|
|
47
|
+
level=dict(
|
|
48
|
+
optional=True,
|
|
49
|
+
default="info",
|
|
50
|
+
values=criticals,
|
|
51
|
+
),
|
|
52
|
+
),
|
|
50
53
|
)
|
|
51
54
|
|
|
52
55
|
def __init__(self, *args, **kw):
|
|
53
|
-
logger.debug(
|
|
56
|
+
logger.debug("Abstract service init %s", self.__class__)
|
|
54
57
|
t = sessions.current()
|
|
55
|
-
glove = kw.pop(
|
|
56
|
-
sh = kw.pop(
|
|
58
|
+
glove = kw.pop("glove", t.glove)
|
|
59
|
+
sh = kw.pop("sh", t.system())
|
|
57
60
|
super().__init__(*args, **kw)
|
|
58
61
|
self._glove = glove
|
|
59
62
|
self._sh = sh
|
|
60
63
|
|
|
61
64
|
@property
|
|
62
65
|
def realkind(self):
|
|
63
|
-
return
|
|
66
|
+
return "service"
|
|
64
67
|
|
|
65
68
|
@property
|
|
66
69
|
def sh(self):
|
|
@@ -89,7 +92,7 @@ class Service(footprints.FootprintBase):
|
|
|
89
92
|
value = self.env.get(as_var, None)
|
|
90
93
|
if not value:
|
|
91
94
|
if as_conf is None:
|
|
92
|
-
as_conf =
|
|
95
|
+
as_conf = "services:" + key.lower()
|
|
93
96
|
value = self.sh.default_target.get(as_conf, default)
|
|
94
97
|
return value
|
|
95
98
|
|
|
@@ -104,69 +107,66 @@ class MailService(Service):
|
|
|
104
107
|
"""
|
|
105
108
|
|
|
106
109
|
_footprint = dict(
|
|
107
|
-
info
|
|
108
|
-
attr
|
|
109
|
-
kind
|
|
110
|
-
values
|
|
111
|
-
),
|
|
112
|
-
sender
|
|
113
|
-
optional
|
|
114
|
-
default
|
|
115
|
-
),
|
|
116
|
-
to
|
|
117
|
-
alias
|
|
118
|
-
),
|
|
119
|
-
replyto
|
|
120
|
-
optional
|
|
121
|
-
alias
|
|
122
|
-
default
|
|
123
|
-
),
|
|
124
|
-
message
|
|
125
|
-
optional
|
|
126
|
-
default
|
|
127
|
-
alias
|
|
128
|
-
type
|
|
129
|
-
),
|
|
130
|
-
filename
|
|
131
|
-
optional
|
|
132
|
-
default
|
|
133
|
-
),
|
|
134
|
-
attachments
|
|
135
|
-
type
|
|
136
|
-
optional
|
|
137
|
-
default
|
|
138
|
-
alias
|
|
139
|
-
),
|
|
140
|
-
subject
|
|
141
|
-
type
|
|
142
|
-
),
|
|
143
|
-
smtpserver
|
|
144
|
-
optional
|
|
145
|
-
),
|
|
146
|
-
smtpport
|
|
147
|
-
type
|
|
148
|
-
optional
|
|
149
|
-
),
|
|
150
|
-
smtpuser
|
|
151
|
-
optional
|
|
152
|
-
),
|
|
153
|
-
smtppass
|
|
154
|
-
optional
|
|
155
|
-
),
|
|
156
|
-
charset
|
|
157
|
-
info
|
|
158
|
-
optional
|
|
159
|
-
default
|
|
160
|
-
),
|
|
161
|
-
inputs_charset
|
|
162
|
-
info
|
|
163
|
-
optional
|
|
164
|
-
),
|
|
165
|
-
commaspace =
|
|
166
|
-
|
|
167
|
-
default = ', '
|
|
168
|
-
)
|
|
169
|
-
)
|
|
110
|
+
info="Mail services class",
|
|
111
|
+
attr=dict(
|
|
112
|
+
kind=dict(
|
|
113
|
+
values=["sendmail"],
|
|
114
|
+
),
|
|
115
|
+
sender=dict(
|
|
116
|
+
optional=True,
|
|
117
|
+
default="[glove::xmail]",
|
|
118
|
+
),
|
|
119
|
+
to=dict(
|
|
120
|
+
alias=("receiver", "recipients"),
|
|
121
|
+
),
|
|
122
|
+
replyto=dict(
|
|
123
|
+
optional=True,
|
|
124
|
+
alias=("reply", "reply_to"),
|
|
125
|
+
default=None,
|
|
126
|
+
),
|
|
127
|
+
message=dict(
|
|
128
|
+
optional=True,
|
|
129
|
+
default="",
|
|
130
|
+
alias=("contents", "body"),
|
|
131
|
+
type=str,
|
|
132
|
+
),
|
|
133
|
+
filename=dict(
|
|
134
|
+
optional=True,
|
|
135
|
+
default=None,
|
|
136
|
+
),
|
|
137
|
+
attachments=dict(
|
|
138
|
+
type=footprints.FPList,
|
|
139
|
+
optional=True,
|
|
140
|
+
default=footprints.FPList(),
|
|
141
|
+
alias=("files", "attach"),
|
|
142
|
+
),
|
|
143
|
+
subject=dict(
|
|
144
|
+
type=str,
|
|
145
|
+
),
|
|
146
|
+
smtpserver=dict(
|
|
147
|
+
optional=True,
|
|
148
|
+
),
|
|
149
|
+
smtpport=dict(
|
|
150
|
+
type=int,
|
|
151
|
+
optional=True,
|
|
152
|
+
),
|
|
153
|
+
smtpuser=dict(
|
|
154
|
+
optional=True,
|
|
155
|
+
),
|
|
156
|
+
smtppass=dict(
|
|
157
|
+
optional=True,
|
|
158
|
+
),
|
|
159
|
+
charset=dict(
|
|
160
|
+
info="The encoding that should be used when sending the email",
|
|
161
|
+
optional=True,
|
|
162
|
+
default="utf-8",
|
|
163
|
+
),
|
|
164
|
+
inputs_charset=dict(
|
|
165
|
+
info="The encoding that should be used when reading input files",
|
|
166
|
+
optional=True,
|
|
167
|
+
),
|
|
168
|
+
commaspace=dict(optional=True, default=", "),
|
|
169
|
+
),
|
|
170
170
|
)
|
|
171
171
|
|
|
172
172
|
def attach(self, *args):
|
|
@@ -186,28 +186,41 @@ class MailService(Service):
|
|
|
186
186
|
with open(self.filename, encoding=self.inputs_charset) as tmp:
|
|
187
187
|
body += tmp.read()
|
|
188
188
|
from email.message import EmailMessage
|
|
189
|
+
|
|
189
190
|
msg = EmailMessage()
|
|
190
|
-
msg.set_content(
|
|
191
|
-
|
|
191
|
+
msg.set_content(
|
|
192
|
+
body,
|
|
193
|
+
subtype="plain",
|
|
194
|
+
charset=(
|
|
195
|
+
self.charset if self.is_not_plain_ascii(body) else "us-ascii"
|
|
196
|
+
),
|
|
197
|
+
)
|
|
192
198
|
return msg
|
|
193
199
|
|
|
194
200
|
def as_multipart(self, msg):
|
|
195
201
|
"""Build a new multipart mail with default text contents and attachments."""
|
|
196
202
|
from email.message import MIMEPart
|
|
203
|
+
|
|
197
204
|
for xtra in self.attachments:
|
|
198
205
|
if isinstance(xtra, MIMEPart):
|
|
199
206
|
msg.add_attachment(xtra)
|
|
200
207
|
elif self.sh.path.isfile(xtra):
|
|
201
208
|
import mimetypes
|
|
209
|
+
|
|
202
210
|
ctype, encoding = mimetypes.guess_type(xtra)
|
|
203
211
|
if ctype is None or encoding is not None:
|
|
204
212
|
# No guess could be made, or the file is encoded
|
|
205
213
|
# (compressed), so use a generic bag-of-bits type.
|
|
206
|
-
ctype =
|
|
207
|
-
maintype, subtype = ctype.split(
|
|
208
|
-
with open(xtra,
|
|
209
|
-
msg.add_attachment(
|
|
210
|
-
|
|
214
|
+
ctype = "application/octet-stream"
|
|
215
|
+
maintype, subtype = ctype.split("/", 1)
|
|
216
|
+
with open(xtra, "rb") as fp:
|
|
217
|
+
msg.add_attachment(
|
|
218
|
+
fp.read(),
|
|
219
|
+
maintype,
|
|
220
|
+
subtype,
|
|
221
|
+
cte="base64",
|
|
222
|
+
filename=xtra,
|
|
223
|
+
)
|
|
211
224
|
return msg
|
|
212
225
|
|
|
213
226
|
def _set_header(self, msg, header, value):
|
|
@@ -215,25 +228,30 @@ class MailService(Service):
|
|
|
215
228
|
|
|
216
229
|
def set_headers(self, msg):
|
|
217
230
|
"""Put on the current message the header items associated to footprint attributes."""
|
|
218
|
-
self._set_header(msg,
|
|
219
|
-
self._set_header(msg,
|
|
220
|
-
self._set_header(msg,
|
|
231
|
+
self._set_header(msg, "From", self.sender)
|
|
232
|
+
self._set_header(msg, "To", self.commaspace.join(self.to.split()))
|
|
233
|
+
self._set_header(msg, "Subject", self.subject)
|
|
221
234
|
if self.replyto is not None:
|
|
222
|
-
self._set_header(
|
|
235
|
+
self._set_header(
|
|
236
|
+
msg, "Reply-To", self.commaspace.join(self.replyto.split())
|
|
237
|
+
)
|
|
223
238
|
|
|
224
239
|
@contextlib.contextmanager
|
|
225
240
|
def smtp_entrypoints(self):
|
|
226
241
|
import smtplib
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
242
|
+
|
|
243
|
+
my_smtpserver = self.actual_value(
|
|
244
|
+
"smtpserver", as_var="VORTEX_SMTPSERVER", default="localhost"
|
|
245
|
+
)
|
|
246
|
+
my_smtpport = self.actual_value(
|
|
247
|
+
"smtpport", as_var="VORTEX_SMTPPORT", default=smtplib.SMTP_PORT
|
|
248
|
+
)
|
|
233
249
|
if not self.sh.default_target.isnetworknode:
|
|
234
|
-
sshobj = self.sh.ssh(
|
|
250
|
+
sshobj = self.sh.ssh(
|
|
251
|
+
"network", virtualnode=True, mandatory_hostcheck=False
|
|
252
|
+
)
|
|
235
253
|
with sshobj.tunnel(my_smtpserver, my_smtpport) as tun:
|
|
236
|
-
yield
|
|
254
|
+
yield "localhost", tun.entranceport
|
|
237
255
|
else:
|
|
238
256
|
yield my_smtpserver, my_smtpport
|
|
239
257
|
|
|
@@ -246,9 +264,10 @@ class MailService(Service):
|
|
|
246
264
|
msgcorpus = msg.as_string()
|
|
247
265
|
with self.smtp_entrypoints() as (smtpserver, smtpport):
|
|
248
266
|
import smtplib
|
|
267
|
+
|
|
249
268
|
extras = dict()
|
|
250
269
|
if smtpport:
|
|
251
|
-
extras[
|
|
270
|
+
extras["port"] = smtpport
|
|
252
271
|
smtp = smtplib.SMTP(smtpserver, **extras)
|
|
253
272
|
if self.smtpuser and self.smtppass:
|
|
254
273
|
smtp.login(self.smtpuser, self.smtppass)
|
|
@@ -265,20 +284,15 @@ class ReportService(Service):
|
|
|
265
284
|
|
|
266
285
|
_abstract = True
|
|
267
286
|
_footprint = dict(
|
|
268
|
-
info
|
|
269
|
-
attr
|
|
270
|
-
kind
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
subject = dict(
|
|
278
|
-
optional = True,
|
|
279
|
-
default = 'Test'
|
|
280
|
-
),
|
|
281
|
-
)
|
|
287
|
+
info="Report services class",
|
|
288
|
+
attr=dict(
|
|
289
|
+
kind=dict(values=["sendreport"]),
|
|
290
|
+
sender=dict(
|
|
291
|
+
optional=True,
|
|
292
|
+
default="[glove::user]",
|
|
293
|
+
),
|
|
294
|
+
subject=dict(optional=True, default="Test"),
|
|
295
|
+
),
|
|
282
296
|
)
|
|
283
297
|
|
|
284
298
|
def __call__(self, *args):
|
|
@@ -291,14 +305,14 @@ class FileReportService(ReportService):
|
|
|
291
305
|
|
|
292
306
|
_abstract = True
|
|
293
307
|
_footprint = dict(
|
|
294
|
-
info
|
|
295
|
-
attr
|
|
296
|
-
kind
|
|
297
|
-
values
|
|
298
|
-
remap
|
|
299
|
-
),
|
|
300
|
-
filename
|
|
301
|
-
)
|
|
308
|
+
info="File Report services class",
|
|
309
|
+
attr=dict(
|
|
310
|
+
kind=dict(
|
|
311
|
+
values=["sendreport", "sendfilereport"],
|
|
312
|
+
remap=dict(sendfilereport="sendreport"),
|
|
313
|
+
),
|
|
314
|
+
filename=dict(),
|
|
315
|
+
),
|
|
302
316
|
)
|
|
303
317
|
|
|
304
318
|
|
|
@@ -318,57 +332,74 @@ class SSHProxy(Service):
|
|
|
318
332
|
"""
|
|
319
333
|
|
|
320
334
|
_footprint = dict(
|
|
321
|
-
info
|
|
322
|
-
attr
|
|
323
|
-
kind
|
|
324
|
-
values
|
|
325
|
-
remap
|
|
326
|
-
),
|
|
327
|
-
hostname
|
|
328
|
-
genericnode
|
|
329
|
-
optional
|
|
330
|
-
default
|
|
331
|
-
access
|
|
332
|
-
),
|
|
333
|
-
nodetype
|
|
334
|
-
optional
|
|
335
|
-
values
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
335
|
+
info="Remote command proxy",
|
|
336
|
+
attr=dict(
|
|
337
|
+
kind=dict(
|
|
338
|
+
values=["ssh", "ssh_proxy"],
|
|
339
|
+
remap=dict(autoremap="first"),
|
|
340
|
+
),
|
|
341
|
+
hostname=dict(),
|
|
342
|
+
genericnode=dict(
|
|
343
|
+
optional=True,
|
|
344
|
+
default=None,
|
|
345
|
+
access="rwx",
|
|
346
|
+
),
|
|
347
|
+
nodetype=dict(
|
|
348
|
+
optional=True,
|
|
349
|
+
values=[
|
|
350
|
+
"login",
|
|
351
|
+
"transfer",
|
|
352
|
+
"transfert",
|
|
353
|
+
"network",
|
|
354
|
+
"agt",
|
|
355
|
+
"syslog",
|
|
356
|
+
],
|
|
357
|
+
default="network",
|
|
358
|
+
remap=dict(transfer="transfert"),
|
|
359
|
+
),
|
|
360
|
+
permut=dict(
|
|
361
|
+
type=bool,
|
|
362
|
+
optional=True,
|
|
363
|
+
default=True,
|
|
364
|
+
),
|
|
365
|
+
maxtries=dict(
|
|
366
|
+
type=int,
|
|
367
|
+
optional=True,
|
|
368
|
+
default=2,
|
|
369
|
+
),
|
|
370
|
+
sshopts=dict(
|
|
371
|
+
optional=True,
|
|
372
|
+
type=footprints.FPList,
|
|
373
|
+
default=None,
|
|
374
|
+
),
|
|
375
|
+
),
|
|
355
376
|
)
|
|
356
377
|
|
|
357
378
|
def __init__(self, *args, **kw):
|
|
358
|
-
logger.debug(
|
|
379
|
+
logger.debug("Remote command proxy init %s", self.__class__)
|
|
359
380
|
super().__init__(*args, **kw)
|
|
360
381
|
hostname, virtualnode = self._actual_hostname()
|
|
361
|
-
extra_sshopts =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
382
|
+
extra_sshopts = (
|
|
383
|
+
None if self.sshopts is None else " ".join(self.sshopts)
|
|
384
|
+
)
|
|
385
|
+
self._sshobj = self.sh.ssh(
|
|
386
|
+
hostname,
|
|
387
|
+
sshopts=extra_sshopts,
|
|
388
|
+
maxtries=self.maxtries,
|
|
389
|
+
virtualnode=virtualnode,
|
|
390
|
+
permut=self.permut,
|
|
391
|
+
mandatory_hostcheck=False,
|
|
392
|
+
)
|
|
365
393
|
|
|
366
394
|
def _actual_hostname(self):
|
|
367
395
|
"""Build a list of candidate target hostnames."""
|
|
368
396
|
myhostname = self.hostname.strip().lower()
|
|
369
397
|
virtualnode = False
|
|
370
|
-
if myhostname ==
|
|
371
|
-
if
|
|
398
|
+
if myhostname == "node":
|
|
399
|
+
if (
|
|
400
|
+
self.genericnode is not None
|
|
401
|
+
and self.genericnode != "no_generic"
|
|
402
|
+
):
|
|
372
403
|
myhostname = self.genericnode
|
|
373
404
|
else:
|
|
374
405
|
myhostname = self.nodetype
|
|
@@ -381,7 +412,7 @@ class SSHProxy(Service):
|
|
|
381
412
|
|
|
382
413
|
def __call__(self, *args):
|
|
383
414
|
"""Remote execution."""
|
|
384
|
-
return self._sshobj.execute(
|
|
415
|
+
return self._sshobj.execute(" ".join(args))
|
|
385
416
|
|
|
386
417
|
|
|
387
418
|
class JeevesService(Service):
|
|
@@ -390,38 +421,39 @@ class JeevesService(Service):
|
|
|
390
421
|
"""
|
|
391
422
|
|
|
392
423
|
_footprint = dict(
|
|
393
|
-
info
|
|
394
|
-
attr
|
|
395
|
-
kind
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
),
|
|
416
|
-
)
|
|
424
|
+
info="Jeeves services class",
|
|
425
|
+
attr=dict(
|
|
426
|
+
kind=dict(values=["askjeeves"]),
|
|
427
|
+
todo=dict(),
|
|
428
|
+
jname=dict(
|
|
429
|
+
optional=True,
|
|
430
|
+
default="test",
|
|
431
|
+
),
|
|
432
|
+
juser=dict(
|
|
433
|
+
optional=True,
|
|
434
|
+
default="[glove::user]",
|
|
435
|
+
),
|
|
436
|
+
jpath=dict(
|
|
437
|
+
optional=True,
|
|
438
|
+
default=None,
|
|
439
|
+
access="rwx",
|
|
440
|
+
),
|
|
441
|
+
jfile=dict(
|
|
442
|
+
optional=True,
|
|
443
|
+
default="vortex",
|
|
444
|
+
),
|
|
445
|
+
),
|
|
417
446
|
)
|
|
418
447
|
|
|
419
448
|
def __call__(self, *args):
|
|
420
449
|
"""Main action: ..."""
|
|
421
450
|
if self.jpath is None:
|
|
422
|
-
self.jpath = self.sh.path.join(
|
|
451
|
+
self.jpath = self.sh.path.join(
|
|
452
|
+
self.env.HOME, "jeeves", self.jname, "depot"
|
|
453
|
+
)
|
|
423
454
|
if self.sh.path.isdir(self.jpath):
|
|
424
455
|
from jeeves import bertie
|
|
456
|
+
|
|
425
457
|
data = dict()
|
|
426
458
|
for arg in args:
|
|
427
459
|
data.update(arg)
|
|
@@ -429,10 +461,11 @@ class JeevesService(Service):
|
|
|
429
461
|
user=self.juser,
|
|
430
462
|
jtag=self.sh.path.join(self.jpath, self.jfile),
|
|
431
463
|
todo=self.todo,
|
|
432
|
-
mail=data.pop(
|
|
433
|
-
apps=data.pop(
|
|
434
|
-
conf=data.pop(
|
|
435
|
-
task=self.env.get(
|
|
464
|
+
mail=data.pop("mail", self.glove.xmail),
|
|
465
|
+
apps=data.pop("apps", (self.glove.vapp,)),
|
|
466
|
+
conf=data.pop("conf", (self.glove.vconf,)),
|
|
467
|
+
task=self.env.get("JOBNAME")
|
|
468
|
+
or self.env.get("SMSNAME", "interactif"),
|
|
436
469
|
)
|
|
437
470
|
fulltalk.update(
|
|
438
471
|
data=data,
|
|
@@ -440,7 +473,7 @@ class JeevesService(Service):
|
|
|
440
473
|
jr = bertie.ask(**fulltalk)
|
|
441
474
|
return jr.todo, jr.last
|
|
442
475
|
else:
|
|
443
|
-
logger.error(
|
|
476
|
+
logger.error("No valid path to jeeves <%s>", self.jpath)
|
|
444
477
|
return None
|
|
445
478
|
|
|
446
479
|
|
|
@@ -453,37 +486,41 @@ class HideService(Service):
|
|
|
453
486
|
"""
|
|
454
487
|
|
|
455
488
|
_footprint = dict(
|
|
456
|
-
info
|
|
457
|
-
attr
|
|
458
|
-
kind
|
|
459
|
-
values
|
|
460
|
-
remap
|
|
489
|
+
info="Hide a given object on current filesystem",
|
|
490
|
+
attr=dict(
|
|
491
|
+
kind=dict(
|
|
492
|
+
values=["hidden", "hide", "hiddencache"],
|
|
493
|
+
remap=dict(autoremap="first"),
|
|
461
494
|
),
|
|
462
|
-
rootdir
|
|
463
|
-
optional
|
|
464
|
-
default
|
|
495
|
+
rootdir=dict(
|
|
496
|
+
optional=True,
|
|
497
|
+
default=None,
|
|
465
498
|
),
|
|
466
|
-
headdir
|
|
467
|
-
optional
|
|
468
|
-
default
|
|
499
|
+
headdir=dict(
|
|
500
|
+
optional=True,
|
|
501
|
+
default="hidden",
|
|
469
502
|
),
|
|
470
|
-
asfmt
|
|
471
|
-
optional
|
|
472
|
-
default
|
|
503
|
+
asfmt=dict(
|
|
504
|
+
optional=True,
|
|
505
|
+
default=None,
|
|
473
506
|
),
|
|
474
|
-
)
|
|
507
|
+
),
|
|
475
508
|
)
|
|
476
509
|
|
|
477
510
|
def find_rootdir(self, filename):
|
|
478
511
|
"""Find a path for hiding files on the same filesystem."""
|
|
479
512
|
username = self.sh.getlogname()
|
|
480
|
-
work_dir = self.sh.path.join(
|
|
513
|
+
work_dir = self.sh.path.join(
|
|
514
|
+
self.sh.find_mount_point(filename), "work"
|
|
515
|
+
)
|
|
481
516
|
if not self.sh.path.exists(work_dir):
|
|
482
517
|
logger.warning("path <%s> doesn't exist", work_dir)
|
|
483
518
|
fullpath = self.sh.path.realpath(filename)
|
|
484
519
|
if username not in fullpath:
|
|
485
|
-
logger.error(
|
|
486
|
-
raise ValueError(
|
|
520
|
+
logger.error("No login <%s> in path <%s>", username, fullpath)
|
|
521
|
+
raise ValueError(
|
|
522
|
+
"Login name not in actual path for hiding data"
|
|
523
|
+
)
|
|
487
524
|
work_dir = fullpath.partition(username)[0]
|
|
488
525
|
logger.warning("using work_dir = <%s>", work_dir)
|
|
489
526
|
hidden_path = self.sh.path.join(work_dir, username, self.headdir)
|
|
@@ -494,21 +531,25 @@ class HideService(Service):
|
|
|
494
531
|
|
|
495
532
|
rootdir = self.rootdir
|
|
496
533
|
if rootdir is None:
|
|
497
|
-
rootdir = self.sh.default_target.get(
|
|
534
|
+
rootdir = self.sh.default_target.get("hidden_rootdir", None)
|
|
498
535
|
if rootdir is not None:
|
|
499
536
|
rootdir = self.sh.path.expanduser(rootdir)
|
|
500
537
|
|
|
501
538
|
actual_rootdir = rootdir or self.find_rootdir(filename)
|
|
502
539
|
destination = self.sh.path.join(
|
|
503
540
|
actual_rootdir,
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
541
|
+
".".join(
|
|
542
|
+
(
|
|
543
|
+
"HIDDEN",
|
|
544
|
+
date.now().strftime("%Y%m%d%H%M%S.%f"),
|
|
545
|
+
"P{:06d}".format(self.sh.getpid()),
|
|
546
|
+
hashlib.md5(
|
|
547
|
+
self.sh.path.abspath(filename).encode(encoding="utf-8")
|
|
548
|
+
).hexdigest(),
|
|
549
|
+
)
|
|
550
|
+
),
|
|
510
551
|
)
|
|
511
|
-
self.sh.cp(filename, destination, intent=
|
|
552
|
+
self.sh.cp(filename, destination, intent="in", fmt=self.asfmt)
|
|
512
553
|
return destination
|
|
513
554
|
|
|
514
555
|
|
|
@@ -519,20 +560,22 @@ class Directory:
|
|
|
519
560
|
Directory (en) means Annuaire (fr).
|
|
520
561
|
"""
|
|
521
562
|
|
|
522
|
-
def __init__(self, inifile, domain=
|
|
563
|
+
def __init__(self, inifile, domain="meteo.fr", encoding=None):
|
|
523
564
|
"""Keep aliases in memory, as a dict of sets."""
|
|
524
|
-
config =
|
|
565
|
+
config = configparser.ConfigParser()
|
|
566
|
+
config.read(inifile, encoding=encoding)
|
|
525
567
|
try:
|
|
526
|
-
self.domain = config.get(
|
|
527
|
-
except NoOptionError:
|
|
568
|
+
self.domain = config.get("general", "default_domain")
|
|
569
|
+
except configparser.NoOptionError:
|
|
528
570
|
self.domain = domain
|
|
529
571
|
self.aliases = {
|
|
530
|
-
k.lower(): set(v.lower().replace(
|
|
531
|
-
for (k, v) in config.items(
|
|
572
|
+
k.lower(): set(v.lower().replace(",", " ").split())
|
|
573
|
+
for (k, v) in config.items("aliases")
|
|
532
574
|
}
|
|
533
575
|
count = self._flatten()
|
|
534
|
-
logger.debug(
|
|
535
|
-
|
|
576
|
+
logger.debug(
|
|
577
|
+
"opmail aliases flattened in %d iterations:\n%s", count, str(self)
|
|
578
|
+
)
|
|
536
579
|
|
|
537
580
|
def get_addresses(self, definition, add_domain=True):
|
|
538
581
|
"""
|
|
@@ -540,20 +583,24 @@ class Directory:
|
|
|
540
583
|
may reference aliases.
|
|
541
584
|
"""
|
|
542
585
|
addresses = set()
|
|
543
|
-
for item in definition.lower().replace(
|
|
586
|
+
for item in definition.lower().replace(",", " ").split():
|
|
544
587
|
if item in self.aliases:
|
|
545
588
|
addresses |= self.aliases[item]
|
|
546
589
|
else:
|
|
547
590
|
addresses |= {item}
|
|
548
591
|
if add_domain:
|
|
549
|
-
return
|
|
550
|
-
return
|
|
592
|
+
return " ".join(self._add_domain(addresses))
|
|
593
|
+
return " ".join(addresses)
|
|
551
594
|
|
|
552
595
|
def __str__(self):
|
|
553
|
-
return
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
596
|
+
return "\n".join(
|
|
597
|
+
sorted(
|
|
598
|
+
[
|
|
599
|
+
"{}: {}".format(k, " ".join(sorted(v)))
|
|
600
|
+
for (k, v) in self.aliases.items()
|
|
601
|
+
]
|
|
602
|
+
)
|
|
603
|
+
)
|
|
557
604
|
|
|
558
605
|
def _flatten(self):
|
|
559
606
|
"""Resolve recursive definitions from the dict of sets."""
|
|
@@ -564,11 +611,18 @@ class Directory:
|
|
|
564
611
|
count += 1
|
|
565
612
|
for kref, vref in self.aliases.items():
|
|
566
613
|
if kref in vref:
|
|
567
|
-
logger.error(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
614
|
+
logger.error(
|
|
615
|
+
"Cycle detected in the aliases directory.\n"
|
|
616
|
+
"offending key: %s.\n"
|
|
617
|
+
"directory being flattened:\n%s",
|
|
618
|
+
str(kref),
|
|
619
|
+
str(self),
|
|
620
|
+
)
|
|
621
|
+
raise ValueError(
|
|
622
|
+
"Cycle for key <{}> in directory definition".format(
|
|
623
|
+
kref
|
|
624
|
+
)
|
|
625
|
+
)
|
|
572
626
|
for k, v in self.aliases.items():
|
|
573
627
|
if kref in v:
|
|
574
628
|
v -= {kref}
|
|
@@ -579,11 +633,7 @@ class Directory:
|
|
|
579
633
|
|
|
580
634
|
def _add_domain(self, aset):
|
|
581
635
|
"""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
|
-
}
|
|
636
|
+
return {v if "@" in v else v + "@" + self.domain for v in aset}
|
|
587
637
|
|
|
588
638
|
|
|
589
639
|
class PromptService(Service):
|
|
@@ -593,16 +643,16 @@ class PromptService(Service):
|
|
|
593
643
|
"""
|
|
594
644
|
|
|
595
645
|
_footprint = dict(
|
|
596
|
-
info
|
|
597
|
-
attr
|
|
598
|
-
kind
|
|
599
|
-
values
|
|
646
|
+
info="Simulate a call to a Service.",
|
|
647
|
+
attr=dict(
|
|
648
|
+
kind=dict(
|
|
649
|
+
values=("prompt",),
|
|
600
650
|
),
|
|
601
|
-
comment
|
|
602
|
-
optional
|
|
603
|
-
default
|
|
651
|
+
comment=dict(
|
|
652
|
+
optional=True,
|
|
653
|
+
default=None,
|
|
604
654
|
),
|
|
605
|
-
)
|
|
655
|
+
),
|
|
606
656
|
)
|
|
607
657
|
|
|
608
658
|
def __call__(self, options):
|
|
@@ -610,8 +660,8 @@ class PromptService(Service):
|
|
|
610
660
|
|
|
611
661
|
pf = EncodedPrettyPrinter().pformat
|
|
612
662
|
logger_action = getattr(logger, self.level, logger.warning)
|
|
613
|
-
msg = (self.comment or
|
|
614
|
-
logger_action(msg.format(pf(options)).replace(
|
|
663
|
+
msg = (self.comment or "PromptService was called.") + "\noptions = {}"
|
|
664
|
+
logger_action(msg.format(pf(options)).replace("\n", "\n<prompt>"))
|
|
615
665
|
return True
|
|
616
666
|
|
|
617
667
|
|
|
@@ -622,51 +672,51 @@ class TemplatedMailService(MailService):
|
|
|
622
672
|
"""
|
|
623
673
|
|
|
624
674
|
_footprint = dict(
|
|
625
|
-
info
|
|
626
|
-
attr
|
|
627
|
-
kind
|
|
628
|
-
values
|
|
629
|
-
),
|
|
630
|
-
id
|
|
631
|
-
alias
|
|
632
|
-
),
|
|
633
|
-
subject
|
|
634
|
-
optional
|
|
635
|
-
default
|
|
636
|
-
access
|
|
637
|
-
),
|
|
638
|
-
to
|
|
639
|
-
optional
|
|
640
|
-
default
|
|
641
|
-
access
|
|
642
|
-
),
|
|
643
|
-
message
|
|
644
|
-
access
|
|
645
|
-
),
|
|
646
|
-
directory
|
|
647
|
-
type
|
|
648
|
-
optional
|
|
649
|
-
default
|
|
650
|
-
),
|
|
651
|
-
catalog
|
|
652
|
-
type
|
|
653
|
-
),
|
|
654
|
-
dryrun
|
|
655
|
-
info
|
|
656
|
-
type
|
|
657
|
-
optional
|
|
658
|
-
default
|
|
659
|
-
),
|
|
660
|
-
)
|
|
675
|
+
info="Templated mail services class",
|
|
676
|
+
attr=dict(
|
|
677
|
+
kind=dict(
|
|
678
|
+
values=["templatedmail"],
|
|
679
|
+
),
|
|
680
|
+
id=dict(
|
|
681
|
+
alias=("template",),
|
|
682
|
+
),
|
|
683
|
+
subject=dict(
|
|
684
|
+
optional=True,
|
|
685
|
+
default=None,
|
|
686
|
+
access="rwx",
|
|
687
|
+
),
|
|
688
|
+
to=dict(
|
|
689
|
+
optional=True,
|
|
690
|
+
default=None,
|
|
691
|
+
access="rwx",
|
|
692
|
+
),
|
|
693
|
+
message=dict(
|
|
694
|
+
access="rwx",
|
|
695
|
+
),
|
|
696
|
+
directory=dict(
|
|
697
|
+
type=Directory,
|
|
698
|
+
optional=True,
|
|
699
|
+
default=None,
|
|
700
|
+
),
|
|
701
|
+
catalog=dict(
|
|
702
|
+
type=configparser.ConfigParser,
|
|
703
|
+
),
|
|
704
|
+
dryrun=dict(
|
|
705
|
+
info="Do not actually send the email. Just render the template.",
|
|
706
|
+
type=bool,
|
|
707
|
+
optional=True,
|
|
708
|
+
default=False,
|
|
709
|
+
),
|
|
710
|
+
),
|
|
661
711
|
)
|
|
662
712
|
|
|
663
713
|
_TEMPLATES_SUBDIR = None
|
|
664
714
|
|
|
665
715
|
def __init__(self, *args, **kw):
|
|
666
|
-
ticket = kw.pop(
|
|
716
|
+
ticket = kw.pop("ticket", sessions.get())
|
|
667
717
|
super().__init__(*args, **kw)
|
|
668
718
|
self._ticket = ticket
|
|
669
|
-
logger.debug(
|
|
719
|
+
logger.debug("TemplatedMail init for id <%s>", self.id)
|
|
670
720
|
|
|
671
721
|
@property
|
|
672
722
|
def ticket(self):
|
|
@@ -674,19 +724,21 @@ class TemplatedMailService(MailService):
|
|
|
674
724
|
|
|
675
725
|
def header(self):
|
|
676
726
|
"""String prepended to the message body."""
|
|
677
|
-
return
|
|
727
|
+
return ""
|
|
678
728
|
|
|
679
729
|
def trailer(self):
|
|
680
730
|
"""String appended to the message body."""
|
|
681
|
-
return
|
|
731
|
+
return ""
|
|
682
732
|
|
|
683
733
|
def get_catalog_section(self):
|
|
684
734
|
"""Read section <id> (a dict-like) from the catalog."""
|
|
685
735
|
try:
|
|
686
736
|
section = dict(self.catalog.items(self.id))
|
|
687
|
-
except NoSectionError:
|
|
688
|
-
logger.error(
|
|
689
|
-
|
|
737
|
+
except configparser.NoSectionError:
|
|
738
|
+
logger.error(
|
|
739
|
+
"Section <%s> is missing in catalog",
|
|
740
|
+
self.id,
|
|
741
|
+
)
|
|
690
742
|
section = None
|
|
691
743
|
return section
|
|
692
744
|
|
|
@@ -707,28 +759,31 @@ class TemplatedMailService(MailService):
|
|
|
707
759
|
"""
|
|
708
760
|
if not isinstance(tpl, (Template, LegacyTemplatingAdapter)):
|
|
709
761
|
tpl = Template(tpl)
|
|
710
|
-
result =
|
|
762
|
+
result = ""
|
|
711
763
|
for level in range(depth):
|
|
712
764
|
try:
|
|
713
765
|
result = tpl.substitute(tpldict)
|
|
714
766
|
except KeyError as exc:
|
|
715
|
-
logger.error(
|
|
716
|
-
|
|
767
|
+
logger.error(
|
|
768
|
+
"Undefined key <%s> in template substitution level %d",
|
|
769
|
+
str(exc),
|
|
770
|
+
level + 1,
|
|
771
|
+
)
|
|
717
772
|
result = tpl.safe_substitute(tpldict)
|
|
718
773
|
except ValueError as exc:
|
|
719
|
-
logger.error(
|
|
774
|
+
logger.error("Illegal syntax in template: %s", exc)
|
|
720
775
|
result = tpl.safe_substitute(tpldict)
|
|
721
776
|
tpl = Template(result)
|
|
722
777
|
return result
|
|
723
778
|
|
|
724
779
|
def _template_name_rewrite(self, tplguess):
|
|
725
|
-
base =
|
|
780
|
+
base = "@"
|
|
726
781
|
if self._TEMPLATES_SUBDIR is not None:
|
|
727
|
-
base =
|
|
782
|
+
base = "@{!s}/".format(self._TEMPLATES_SUBDIR)
|
|
728
783
|
if not tplguess.startswith(base):
|
|
729
784
|
tplguess = base + tplguess
|
|
730
|
-
if not tplguess.endswith(
|
|
731
|
-
tplguess +=
|
|
785
|
+
if not tplguess.endswith(".tpl"):
|
|
786
|
+
tplguess += ".tpl"
|
|
732
787
|
return tplguess
|
|
733
788
|
|
|
734
789
|
def get_message(self, tpldict):
|
|
@@ -739,13 +794,15 @@ class TemplatedMailService(MailService):
|
|
|
739
794
|
* header and trailer are added.
|
|
740
795
|
"""
|
|
741
796
|
tpl = self.message
|
|
742
|
-
if tpl ==
|
|
743
|
-
tplfile = self.section.get(
|
|
797
|
+
if tpl == "":
|
|
798
|
+
tplfile = self.section.get("template", self.id)
|
|
744
799
|
tplfile = self._template_name_rewrite(tplfile)
|
|
745
800
|
try:
|
|
746
|
-
tpl = load_template(
|
|
801
|
+
tpl = load_template(
|
|
802
|
+
self.ticket, tplfile, encoding=self.inputs_charset
|
|
803
|
+
)
|
|
747
804
|
except ValueError as exc:
|
|
748
|
-
logger.error(
|
|
805
|
+
logger.error("%s", exc.message)
|
|
749
806
|
return None
|
|
750
807
|
message = self.substitute(tpl, tpldict)
|
|
751
808
|
return self.header() + message + self.trailer()
|
|
@@ -758,9 +815,11 @@ class TemplatedMailService(MailService):
|
|
|
758
815
|
"""
|
|
759
816
|
tpl = self.subject
|
|
760
817
|
if tpl is None:
|
|
761
|
-
tpl = self.section.get(
|
|
818
|
+
tpl = self.section.get("subject", None)
|
|
762
819
|
if tpl is None:
|
|
763
|
-
logger.error(
|
|
820
|
+
logger.error(
|
|
821
|
+
"Missing <subject> definition for id <%s>.", self.id
|
|
822
|
+
)
|
|
764
823
|
return None
|
|
765
824
|
subject = self.substitute(tpl, tpldict)
|
|
766
825
|
return subject
|
|
@@ -776,9 +835,9 @@ class TemplatedMailService(MailService):
|
|
|
776
835
|
"""
|
|
777
836
|
tpl = self.to
|
|
778
837
|
if tpl is None:
|
|
779
|
-
tpl = self.section.get(
|
|
838
|
+
tpl = self.section.get("to", None)
|
|
780
839
|
if tpl is None:
|
|
781
|
-
logger.error(
|
|
840
|
+
logger.error("Missing <to> definition for id <%s>.", self.id)
|
|
782
841
|
return None
|
|
783
842
|
to = self.substitute(tpl, tpldict)
|
|
784
843
|
if self.directory:
|
|
@@ -834,38 +893,48 @@ class TemplatedMailService(MailService):
|
|
|
834
893
|
|
|
835
894
|
|
|
836
895
|
class AbstractRdTemplatedMailService(TemplatedMailService):
|
|
837
|
-
|
|
838
896
|
_abstract = True
|
|
839
897
|
|
|
840
898
|
def header(self):
|
|
841
899
|
"""String prepended to the message body."""
|
|
842
900
|
now = date.now()
|
|
843
|
-
stamp1 = now.strftime(
|
|
844
|
-
stamp2 = now.strftime(
|
|
845
|
-
return
|
|
846
|
-
|
|
901
|
+
stamp1 = now.strftime("%A %d %B %Y")
|
|
902
|
+
stamp2 = now.strftime("%X")
|
|
903
|
+
return "Email sent on {} at {} (from: {}).\n--\n\n".format(
|
|
904
|
+
stamp1, stamp2, self.sh.default_target.hostname
|
|
905
|
+
)
|
|
847
906
|
|
|
848
907
|
def substitution_dictionary(self, add_ons=None):
|
|
849
908
|
sdict = super().substitution_dictionary(add_ons=add_ons)
|
|
850
|
-
sdict[
|
|
909
|
+
sdict["jobid"] = self.sh.guess_job_identifier()
|
|
851
910
|
# Try to detect MTOOL data (this may be empty if MTOOL is not used):
|
|
852
911
|
if self.env.MTOOL_STEP:
|
|
853
|
-
mt_stack = [
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
912
|
+
mt_stack = [
|
|
913
|
+
"\nMTOOL details:",
|
|
914
|
+
]
|
|
915
|
+
mt_items = [
|
|
916
|
+
"mtool_step_{:s}".format(i)
|
|
917
|
+
for i in ("abort", "depot", "spool", "idnum")
|
|
918
|
+
if "mtool_step_{:s}".format(i) in self.env
|
|
919
|
+
]
|
|
920
|
+
print_tablelike(
|
|
921
|
+
"{:s} = {!s}",
|
|
922
|
+
mt_items,
|
|
923
|
+
[self.env[i] for i in mt_items],
|
|
924
|
+
output_callback=mt_stack.append,
|
|
925
|
+
)
|
|
926
|
+
sdict["mtool_info"] = "\n".join(mt_stack) + "\n"
|
|
860
927
|
else:
|
|
861
|
-
sdict[
|
|
928
|
+
sdict["mtool_info"] = ""
|
|
862
929
|
# The list of footprints' defaults
|
|
863
930
|
fpdefaults = footprints.setup.defaults
|
|
864
|
-
sdict[
|
|
931
|
+
sdict["fpdefaults"] = pprint.pformat(fpdefaults, indent=2)
|
|
865
932
|
# A condensed indication on date/cutoff
|
|
866
|
-
sdict[
|
|
867
|
-
if sdict[
|
|
868
|
-
sdict[
|
|
933
|
+
sdict["timeid"] = fpdefaults.get("date", None)
|
|
934
|
+
if sdict["timeid"]:
|
|
935
|
+
sdict["timeid"] = sdict["timeid"].vortex(
|
|
936
|
+
cutoff=fpdefaults.get("cutoff", "X")
|
|
937
|
+
)
|
|
869
938
|
# The generic host/cluster name
|
|
870
|
-
sdict[
|
|
939
|
+
sdict["host"] = self.sh.default_target.inetname
|
|
871
940
|
return sdict
|