vortex-nwp 2.0.0b1__py3-none-any.whl → 2.0.0b2__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 +59 -45
- vortex/algo/__init__.py +3 -2
- vortex/algo/components.py +940 -614
- vortex/algo/mpitools.py +802 -497
- 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 +428 -225
- vortex/data/outflow.py +15 -15
- vortex/data/providers.py +185 -163
- vortex/data/resources.py +48 -42
- vortex/data/stores.py +544 -413
- 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 +420 -271
- vortex/nwp/algo/fpserver.py +683 -307
- vortex/nwp/algo/ifsnaming.py +205 -145
- vortex/nwp/algo/ifsroot.py +210 -122
- vortex/nwp/algo/monitoring.py +132 -76
- vortex/nwp/algo/mpitools.py +321 -191
- vortex/nwp/algo/odbtools.py +617 -353
- vortex/nwp/algo/oopsroot.py +449 -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 +125 -59
- 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 +311 -149
- vortex/tools/lfi.py +423 -216
- vortex/tools/listings.py +109 -40
- vortex/tools/names.py +218 -156
- vortex/tools/net.py +632 -298
- vortex/tools/parallelism.py +93 -61
- vortex/tools/prestaging.py +55 -31
- vortex/tools/schedulers.py +172 -105
- vortex/tools/services.py +402 -333
- vortex/tools/storage.py +293 -358
- vortex/tools/surfex.py +24 -24
- vortex/tools/systems.py +1211 -631
- vortex/tools/targets.py +156 -100
- vortex/util/__init__.py +1 -1
- vortex/util/config.py +377 -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.0.0b2.dist-info/METADATA +66 -0
- vortex_nwp-2.0.0b2.dist-info/RECORD +142 -0
- {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.0.0b2.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.0.0b2.dist-info}/LICENSE +0 -0
- {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.0.0b2.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,11 @@ 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
|
+
GenericConfigParser,
|
|
24
|
+
load_template,
|
|
25
|
+
LegacyTemplatingAdapter,
|
|
26
|
+
)
|
|
23
27
|
|
|
24
28
|
#: No automatic export
|
|
25
29
|
__all__ = []
|
|
@@ -27,7 +31,7 @@ __all__ = []
|
|
|
27
31
|
logger = loggers.getLogger(__name__)
|
|
28
32
|
|
|
29
33
|
# See logging.handlers.SysLogHandler.priority_map
|
|
30
|
-
criticals = [
|
|
34
|
+
criticals = ["debug", "info", "error", "warning", "critical"]
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class Service(footprints.FootprintBase):
|
|
@@ -36,31 +40,31 @@ class Service(footprints.FootprintBase):
|
|
|
36
40
|
"""
|
|
37
41
|
|
|
38
42
|
_abstract = True
|
|
39
|
-
_collector = (
|
|
43
|
+
_collector = ("service",)
|
|
40
44
|
_footprint = dict(
|
|
41
|
-
info
|
|
42
|
-
attr
|
|
43
|
-
kind
|
|
44
|
-
level
|
|
45
|
-
optional
|
|
46
|
-
default
|
|
47
|
-
values
|
|
48
|
-
),
|
|
49
|
-
)
|
|
45
|
+
info="Abstract services class",
|
|
46
|
+
attr=dict(
|
|
47
|
+
kind=dict(),
|
|
48
|
+
level=dict(
|
|
49
|
+
optional=True,
|
|
50
|
+
default="info",
|
|
51
|
+
values=criticals,
|
|
52
|
+
),
|
|
53
|
+
),
|
|
50
54
|
)
|
|
51
55
|
|
|
52
56
|
def __init__(self, *args, **kw):
|
|
53
|
-
logger.debug(
|
|
57
|
+
logger.debug("Abstract service init %s", self.__class__)
|
|
54
58
|
t = sessions.current()
|
|
55
|
-
glove = kw.pop(
|
|
56
|
-
sh = kw.pop(
|
|
59
|
+
glove = kw.pop("glove", t.glove)
|
|
60
|
+
sh = kw.pop("sh", t.system())
|
|
57
61
|
super().__init__(*args, **kw)
|
|
58
62
|
self._glove = glove
|
|
59
63
|
self._sh = sh
|
|
60
64
|
|
|
61
65
|
@property
|
|
62
66
|
def realkind(self):
|
|
63
|
-
return
|
|
67
|
+
return "service"
|
|
64
68
|
|
|
65
69
|
@property
|
|
66
70
|
def sh(self):
|
|
@@ -89,7 +93,7 @@ class Service(footprints.FootprintBase):
|
|
|
89
93
|
value = self.env.get(as_var, None)
|
|
90
94
|
if not value:
|
|
91
95
|
if as_conf is None:
|
|
92
|
-
as_conf =
|
|
96
|
+
as_conf = "services:" + key.lower()
|
|
93
97
|
value = self.sh.default_target.get(as_conf, default)
|
|
94
98
|
return value
|
|
95
99
|
|
|
@@ -104,69 +108,66 @@ class MailService(Service):
|
|
|
104
108
|
"""
|
|
105
109
|
|
|
106
110
|
_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
|
-
)
|
|
111
|
+
info="Mail services class",
|
|
112
|
+
attr=dict(
|
|
113
|
+
kind=dict(
|
|
114
|
+
values=["sendmail"],
|
|
115
|
+
),
|
|
116
|
+
sender=dict(
|
|
117
|
+
optional=True,
|
|
118
|
+
default="[glove::xmail]",
|
|
119
|
+
),
|
|
120
|
+
to=dict(
|
|
121
|
+
alias=("receiver", "recipients"),
|
|
122
|
+
),
|
|
123
|
+
replyto=dict(
|
|
124
|
+
optional=True,
|
|
125
|
+
alias=("reply", "reply_to"),
|
|
126
|
+
default=None,
|
|
127
|
+
),
|
|
128
|
+
message=dict(
|
|
129
|
+
optional=True,
|
|
130
|
+
default="",
|
|
131
|
+
alias=("contents", "body"),
|
|
132
|
+
type=str,
|
|
133
|
+
),
|
|
134
|
+
filename=dict(
|
|
135
|
+
optional=True,
|
|
136
|
+
default=None,
|
|
137
|
+
),
|
|
138
|
+
attachments=dict(
|
|
139
|
+
type=footprints.FPList,
|
|
140
|
+
optional=True,
|
|
141
|
+
default=footprints.FPList(),
|
|
142
|
+
alias=("files", "attach"),
|
|
143
|
+
),
|
|
144
|
+
subject=dict(
|
|
145
|
+
type=str,
|
|
146
|
+
),
|
|
147
|
+
smtpserver=dict(
|
|
148
|
+
optional=True,
|
|
149
|
+
),
|
|
150
|
+
smtpport=dict(
|
|
151
|
+
type=int,
|
|
152
|
+
optional=True,
|
|
153
|
+
),
|
|
154
|
+
smtpuser=dict(
|
|
155
|
+
optional=True,
|
|
156
|
+
),
|
|
157
|
+
smtppass=dict(
|
|
158
|
+
optional=True,
|
|
159
|
+
),
|
|
160
|
+
charset=dict(
|
|
161
|
+
info="The encoding that should be used when sending the email",
|
|
162
|
+
optional=True,
|
|
163
|
+
default="utf-8",
|
|
164
|
+
),
|
|
165
|
+
inputs_charset=dict(
|
|
166
|
+
info="The encoding that should be used when reading input files",
|
|
167
|
+
optional=True,
|
|
168
|
+
),
|
|
169
|
+
commaspace=dict(optional=True, default=", "),
|
|
170
|
+
),
|
|
170
171
|
)
|
|
171
172
|
|
|
172
173
|
def attach(self, *args):
|
|
@@ -186,28 +187,41 @@ class MailService(Service):
|
|
|
186
187
|
with open(self.filename, encoding=self.inputs_charset) as tmp:
|
|
187
188
|
body += tmp.read()
|
|
188
189
|
from email.message import EmailMessage
|
|
190
|
+
|
|
189
191
|
msg = EmailMessage()
|
|
190
|
-
msg.set_content(
|
|
191
|
-
|
|
192
|
+
msg.set_content(
|
|
193
|
+
body,
|
|
194
|
+
subtype="plain",
|
|
195
|
+
charset=(
|
|
196
|
+
self.charset if self.is_not_plain_ascii(body) else "us-ascii"
|
|
197
|
+
),
|
|
198
|
+
)
|
|
192
199
|
return msg
|
|
193
200
|
|
|
194
201
|
def as_multipart(self, msg):
|
|
195
202
|
"""Build a new multipart mail with default text contents and attachments."""
|
|
196
203
|
from email.message import MIMEPart
|
|
204
|
+
|
|
197
205
|
for xtra in self.attachments:
|
|
198
206
|
if isinstance(xtra, MIMEPart):
|
|
199
207
|
msg.add_attachment(xtra)
|
|
200
208
|
elif self.sh.path.isfile(xtra):
|
|
201
209
|
import mimetypes
|
|
210
|
+
|
|
202
211
|
ctype, encoding = mimetypes.guess_type(xtra)
|
|
203
212
|
if ctype is None or encoding is not None:
|
|
204
213
|
# No guess could be made, or the file is encoded
|
|
205
214
|
# (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
|
-
|
|
215
|
+
ctype = "application/octet-stream"
|
|
216
|
+
maintype, subtype = ctype.split("/", 1)
|
|
217
|
+
with open(xtra, "rb") as fp:
|
|
218
|
+
msg.add_attachment(
|
|
219
|
+
fp.read(),
|
|
220
|
+
maintype,
|
|
221
|
+
subtype,
|
|
222
|
+
cte="base64",
|
|
223
|
+
filename=xtra,
|
|
224
|
+
)
|
|
211
225
|
return msg
|
|
212
226
|
|
|
213
227
|
def _set_header(self, msg, header, value):
|
|
@@ -215,25 +229,30 @@ class MailService(Service):
|
|
|
215
229
|
|
|
216
230
|
def set_headers(self, msg):
|
|
217
231
|
"""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,
|
|
232
|
+
self._set_header(msg, "From", self.sender)
|
|
233
|
+
self._set_header(msg, "To", self.commaspace.join(self.to.split()))
|
|
234
|
+
self._set_header(msg, "Subject", self.subject)
|
|
221
235
|
if self.replyto is not None:
|
|
222
|
-
self._set_header(
|
|
236
|
+
self._set_header(
|
|
237
|
+
msg, "Reply-To", self.commaspace.join(self.replyto.split())
|
|
238
|
+
)
|
|
223
239
|
|
|
224
240
|
@contextlib.contextmanager
|
|
225
241
|
def smtp_entrypoints(self):
|
|
226
242
|
import smtplib
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
243
|
+
|
|
244
|
+
my_smtpserver = self.actual_value(
|
|
245
|
+
"smtpserver", as_var="VORTEX_SMTPSERVER", default="localhost"
|
|
246
|
+
)
|
|
247
|
+
my_smtpport = self.actual_value(
|
|
248
|
+
"smtpport", as_var="VORTEX_SMTPPORT", default=smtplib.SMTP_PORT
|
|
249
|
+
)
|
|
233
250
|
if not self.sh.default_target.isnetworknode:
|
|
234
|
-
sshobj = self.sh.ssh(
|
|
251
|
+
sshobj = self.sh.ssh(
|
|
252
|
+
"network", virtualnode=True, mandatory_hostcheck=False
|
|
253
|
+
)
|
|
235
254
|
with sshobj.tunnel(my_smtpserver, my_smtpport) as tun:
|
|
236
|
-
yield
|
|
255
|
+
yield "localhost", tun.entranceport
|
|
237
256
|
else:
|
|
238
257
|
yield my_smtpserver, my_smtpport
|
|
239
258
|
|
|
@@ -246,9 +265,10 @@ class MailService(Service):
|
|
|
246
265
|
msgcorpus = msg.as_string()
|
|
247
266
|
with self.smtp_entrypoints() as (smtpserver, smtpport):
|
|
248
267
|
import smtplib
|
|
268
|
+
|
|
249
269
|
extras = dict()
|
|
250
270
|
if smtpport:
|
|
251
|
-
extras[
|
|
271
|
+
extras["port"] = smtpport
|
|
252
272
|
smtp = smtplib.SMTP(smtpserver, **extras)
|
|
253
273
|
if self.smtpuser and self.smtppass:
|
|
254
274
|
smtp.login(self.smtpuser, self.smtppass)
|
|
@@ -265,20 +285,15 @@ class ReportService(Service):
|
|
|
265
285
|
|
|
266
286
|
_abstract = True
|
|
267
287
|
_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
|
-
)
|
|
288
|
+
info="Report services class",
|
|
289
|
+
attr=dict(
|
|
290
|
+
kind=dict(values=["sendreport"]),
|
|
291
|
+
sender=dict(
|
|
292
|
+
optional=True,
|
|
293
|
+
default="[glove::user]",
|
|
294
|
+
),
|
|
295
|
+
subject=dict(optional=True, default="Test"),
|
|
296
|
+
),
|
|
282
297
|
)
|
|
283
298
|
|
|
284
299
|
def __call__(self, *args):
|
|
@@ -291,14 +306,14 @@ class FileReportService(ReportService):
|
|
|
291
306
|
|
|
292
307
|
_abstract = True
|
|
293
308
|
_footprint = dict(
|
|
294
|
-
info
|
|
295
|
-
attr
|
|
296
|
-
kind
|
|
297
|
-
values
|
|
298
|
-
remap
|
|
299
|
-
),
|
|
300
|
-
filename
|
|
301
|
-
)
|
|
309
|
+
info="File Report services class",
|
|
310
|
+
attr=dict(
|
|
311
|
+
kind=dict(
|
|
312
|
+
values=["sendreport", "sendfilereport"],
|
|
313
|
+
remap=dict(sendfilereport="sendreport"),
|
|
314
|
+
),
|
|
315
|
+
filename=dict(),
|
|
316
|
+
),
|
|
302
317
|
)
|
|
303
318
|
|
|
304
319
|
|
|
@@ -318,57 +333,74 @@ class SSHProxy(Service):
|
|
|
318
333
|
"""
|
|
319
334
|
|
|
320
335
|
_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
|
-
|
|
336
|
+
info="Remote command proxy",
|
|
337
|
+
attr=dict(
|
|
338
|
+
kind=dict(
|
|
339
|
+
values=["ssh", "ssh_proxy"],
|
|
340
|
+
remap=dict(autoremap="first"),
|
|
341
|
+
),
|
|
342
|
+
hostname=dict(),
|
|
343
|
+
genericnode=dict(
|
|
344
|
+
optional=True,
|
|
345
|
+
default=None,
|
|
346
|
+
access="rwx",
|
|
347
|
+
),
|
|
348
|
+
nodetype=dict(
|
|
349
|
+
optional=True,
|
|
350
|
+
values=[
|
|
351
|
+
"login",
|
|
352
|
+
"transfer",
|
|
353
|
+
"transfert",
|
|
354
|
+
"network",
|
|
355
|
+
"agt",
|
|
356
|
+
"syslog",
|
|
357
|
+
],
|
|
358
|
+
default="network",
|
|
359
|
+
remap=dict(transfer="transfert"),
|
|
360
|
+
),
|
|
361
|
+
permut=dict(
|
|
362
|
+
type=bool,
|
|
363
|
+
optional=True,
|
|
364
|
+
default=True,
|
|
365
|
+
),
|
|
366
|
+
maxtries=dict(
|
|
367
|
+
type=int,
|
|
368
|
+
optional=True,
|
|
369
|
+
default=2,
|
|
370
|
+
),
|
|
371
|
+
sshopts=dict(
|
|
372
|
+
optional=True,
|
|
373
|
+
type=footprints.FPList,
|
|
374
|
+
default=None,
|
|
375
|
+
),
|
|
376
|
+
),
|
|
355
377
|
)
|
|
356
378
|
|
|
357
379
|
def __init__(self, *args, **kw):
|
|
358
|
-
logger.debug(
|
|
380
|
+
logger.debug("Remote command proxy init %s", self.__class__)
|
|
359
381
|
super().__init__(*args, **kw)
|
|
360
382
|
hostname, virtualnode = self._actual_hostname()
|
|
361
|
-
extra_sshopts =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
383
|
+
extra_sshopts = (
|
|
384
|
+
None if self.sshopts is None else " ".join(self.sshopts)
|
|
385
|
+
)
|
|
386
|
+
self._sshobj = self.sh.ssh(
|
|
387
|
+
hostname,
|
|
388
|
+
sshopts=extra_sshopts,
|
|
389
|
+
maxtries=self.maxtries,
|
|
390
|
+
virtualnode=virtualnode,
|
|
391
|
+
permut=self.permut,
|
|
392
|
+
mandatory_hostcheck=False,
|
|
393
|
+
)
|
|
365
394
|
|
|
366
395
|
def _actual_hostname(self):
|
|
367
396
|
"""Build a list of candidate target hostnames."""
|
|
368
397
|
myhostname = self.hostname.strip().lower()
|
|
369
398
|
virtualnode = False
|
|
370
|
-
if myhostname ==
|
|
371
|
-
if
|
|
399
|
+
if myhostname == "node":
|
|
400
|
+
if (
|
|
401
|
+
self.genericnode is not None
|
|
402
|
+
and self.genericnode != "no_generic"
|
|
403
|
+
):
|
|
372
404
|
myhostname = self.genericnode
|
|
373
405
|
else:
|
|
374
406
|
myhostname = self.nodetype
|
|
@@ -381,7 +413,7 @@ class SSHProxy(Service):
|
|
|
381
413
|
|
|
382
414
|
def __call__(self, *args):
|
|
383
415
|
"""Remote execution."""
|
|
384
|
-
return self._sshobj.execute(
|
|
416
|
+
return self._sshobj.execute(" ".join(args))
|
|
385
417
|
|
|
386
418
|
|
|
387
419
|
class JeevesService(Service):
|
|
@@ -390,38 +422,39 @@ class JeevesService(Service):
|
|
|
390
422
|
"""
|
|
391
423
|
|
|
392
424
|
_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
|
-
)
|
|
425
|
+
info="Jeeves services class",
|
|
426
|
+
attr=dict(
|
|
427
|
+
kind=dict(values=["askjeeves"]),
|
|
428
|
+
todo=dict(),
|
|
429
|
+
jname=dict(
|
|
430
|
+
optional=True,
|
|
431
|
+
default="test",
|
|
432
|
+
),
|
|
433
|
+
juser=dict(
|
|
434
|
+
optional=True,
|
|
435
|
+
default="[glove::user]",
|
|
436
|
+
),
|
|
437
|
+
jpath=dict(
|
|
438
|
+
optional=True,
|
|
439
|
+
default=None,
|
|
440
|
+
access="rwx",
|
|
441
|
+
),
|
|
442
|
+
jfile=dict(
|
|
443
|
+
optional=True,
|
|
444
|
+
default="vortex",
|
|
445
|
+
),
|
|
446
|
+
),
|
|
417
447
|
)
|
|
418
448
|
|
|
419
449
|
def __call__(self, *args):
|
|
420
450
|
"""Main action: ..."""
|
|
421
451
|
if self.jpath is None:
|
|
422
|
-
self.jpath = self.sh.path.join(
|
|
452
|
+
self.jpath = self.sh.path.join(
|
|
453
|
+
self.env.HOME, "jeeves", self.jname, "depot"
|
|
454
|
+
)
|
|
423
455
|
if self.sh.path.isdir(self.jpath):
|
|
424
456
|
from jeeves import bertie
|
|
457
|
+
|
|
425
458
|
data = dict()
|
|
426
459
|
for arg in args:
|
|
427
460
|
data.update(arg)
|
|
@@ -429,10 +462,11 @@ class JeevesService(Service):
|
|
|
429
462
|
user=self.juser,
|
|
430
463
|
jtag=self.sh.path.join(self.jpath, self.jfile),
|
|
431
464
|
todo=self.todo,
|
|
432
|
-
mail=data.pop(
|
|
433
|
-
apps=data.pop(
|
|
434
|
-
conf=data.pop(
|
|
435
|
-
task=self.env.get(
|
|
465
|
+
mail=data.pop("mail", self.glove.xmail),
|
|
466
|
+
apps=data.pop("apps", (self.glove.vapp,)),
|
|
467
|
+
conf=data.pop("conf", (self.glove.vconf,)),
|
|
468
|
+
task=self.env.get("JOBNAME")
|
|
469
|
+
or self.env.get("SMSNAME", "interactif"),
|
|
436
470
|
)
|
|
437
471
|
fulltalk.update(
|
|
438
472
|
data=data,
|
|
@@ -440,7 +474,7 @@ class JeevesService(Service):
|
|
|
440
474
|
jr = bertie.ask(**fulltalk)
|
|
441
475
|
return jr.todo, jr.last
|
|
442
476
|
else:
|
|
443
|
-
logger.error(
|
|
477
|
+
logger.error("No valid path to jeeves <%s>", self.jpath)
|
|
444
478
|
return None
|
|
445
479
|
|
|
446
480
|
|
|
@@ -453,37 +487,41 @@ class HideService(Service):
|
|
|
453
487
|
"""
|
|
454
488
|
|
|
455
489
|
_footprint = dict(
|
|
456
|
-
info
|
|
457
|
-
attr
|
|
458
|
-
kind
|
|
459
|
-
values
|
|
460
|
-
remap
|
|
490
|
+
info="Hide a given object on current filesystem",
|
|
491
|
+
attr=dict(
|
|
492
|
+
kind=dict(
|
|
493
|
+
values=["hidden", "hide", "hiddencache"],
|
|
494
|
+
remap=dict(autoremap="first"),
|
|
461
495
|
),
|
|
462
|
-
rootdir
|
|
463
|
-
optional
|
|
464
|
-
default
|
|
496
|
+
rootdir=dict(
|
|
497
|
+
optional=True,
|
|
498
|
+
default=None,
|
|
465
499
|
),
|
|
466
|
-
headdir
|
|
467
|
-
optional
|
|
468
|
-
default
|
|
500
|
+
headdir=dict(
|
|
501
|
+
optional=True,
|
|
502
|
+
default="hidden",
|
|
469
503
|
),
|
|
470
|
-
asfmt
|
|
471
|
-
optional
|
|
472
|
-
default
|
|
504
|
+
asfmt=dict(
|
|
505
|
+
optional=True,
|
|
506
|
+
default=None,
|
|
473
507
|
),
|
|
474
|
-
)
|
|
508
|
+
),
|
|
475
509
|
)
|
|
476
510
|
|
|
477
511
|
def find_rootdir(self, filename):
|
|
478
512
|
"""Find a path for hiding files on the same filesystem."""
|
|
479
513
|
username = self.sh.getlogname()
|
|
480
|
-
work_dir = self.sh.path.join(
|
|
514
|
+
work_dir = self.sh.path.join(
|
|
515
|
+
self.sh.find_mount_point(filename), "work"
|
|
516
|
+
)
|
|
481
517
|
if not self.sh.path.exists(work_dir):
|
|
482
518
|
logger.warning("path <%s> doesn't exist", work_dir)
|
|
483
519
|
fullpath = self.sh.path.realpath(filename)
|
|
484
520
|
if username not in fullpath:
|
|
485
|
-
logger.error(
|
|
486
|
-
raise ValueError(
|
|
521
|
+
logger.error("No login <%s> in path <%s>", username, fullpath)
|
|
522
|
+
raise ValueError(
|
|
523
|
+
"Login name not in actual path for hiding data"
|
|
524
|
+
)
|
|
487
525
|
work_dir = fullpath.partition(username)[0]
|
|
488
526
|
logger.warning("using work_dir = <%s>", work_dir)
|
|
489
527
|
hidden_path = self.sh.path.join(work_dir, username, self.headdir)
|
|
@@ -494,21 +532,25 @@ class HideService(Service):
|
|
|
494
532
|
|
|
495
533
|
rootdir = self.rootdir
|
|
496
534
|
if rootdir is None:
|
|
497
|
-
rootdir = self.sh.default_target.get(
|
|
535
|
+
rootdir = self.sh.default_target.get("hidden_rootdir", None)
|
|
498
536
|
if rootdir is not None:
|
|
499
537
|
rootdir = self.sh.path.expanduser(rootdir)
|
|
500
538
|
|
|
501
539
|
actual_rootdir = rootdir or self.find_rootdir(filename)
|
|
502
540
|
destination = self.sh.path.join(
|
|
503
541
|
actual_rootdir,
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
542
|
+
".".join(
|
|
543
|
+
(
|
|
544
|
+
"HIDDEN",
|
|
545
|
+
date.now().strftime("%Y%m%d%H%M%S.%f"),
|
|
546
|
+
"P{:06d}".format(self.sh.getpid()),
|
|
547
|
+
hashlib.md5(
|
|
548
|
+
self.sh.path.abspath(filename).encode(encoding="utf-8")
|
|
549
|
+
).hexdigest(),
|
|
550
|
+
)
|
|
551
|
+
),
|
|
510
552
|
)
|
|
511
|
-
self.sh.cp(filename, destination, intent=
|
|
553
|
+
self.sh.cp(filename, destination, intent="in", fmt=self.asfmt)
|
|
512
554
|
return destination
|
|
513
555
|
|
|
514
556
|
|
|
@@ -519,20 +561,21 @@ class Directory:
|
|
|
519
561
|
Directory (en) means Annuaire (fr).
|
|
520
562
|
"""
|
|
521
563
|
|
|
522
|
-
def __init__(self, inifile, domain=
|
|
564
|
+
def __init__(self, inifile, domain="meteo.fr", encoding=None):
|
|
523
565
|
"""Keep aliases in memory, as a dict of sets."""
|
|
524
566
|
config = GenericConfigParser(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
|