-
Notifications
You must be signed in to change notification settings - Fork 0
/
install-docset.py
355 lines (291 loc) · 11.1 KB
/
install-docset.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
#!/usr/bin/env python
"""
Deploy a fresh set of CDR schema or filter documents.
Used as part of a CDR release deployment, typically following an
invocation of `deploy-cdr.py`. This script is separated out from
that one because `install-docset.py` needs to import things that
`deploy-cdr.py` will probably need to install. That would work on
*nix, but Windows doesn't always play nice with file locking. We
could work around that problem with deferred local imports, but
it's cleaner to separate out this task to a separate script.
JIRA::OCECDR-4300
"""
import argparse
import os
import re
import subprocess
import sys
import cdr
import cdrapi.db as cdrdb
class DocumentSet:
"""
Base class for master driver with runtime configuration settings.
Class values:
POPEN_OPTS - options for launching a sub process
Attributes:
logger - object for recording what we do
opts - runtime control settings
session - authority to add/update documents being installed
cursor - for running guest CDR database queries
"""
DOCTYPE = ACCOUNT = None # Overridden in the derived classes
POPEN_OPTS = dict(
shell=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
def __init__(self, opts):
"""
Collect and validate runtime settings and set up logging.
"""
self.logger = cdr.Logging.get_logger("deploy", console=True)
self.opts = opts
self.session = self.login()
self.cursor = cdrdb.connect(name="CdrGuest").cursor()
def login(self):
"""
Create a CDR login session for adding/updating the documents.
"""
if self.opts.test:
return None
password = cdr.getpw(self.ACCOUNT)
if not password:
self.logger.error("account password not found")
sys.exit(1)
session = cdr.login(self.ACCOUNT, password)
error = cdr.checkErr(session)
if error:
self.logger.error(error)
sys.exit(1)
return session
def run(self):
"""
Install the documents and perform any necessary postprocessing.
"""
if not os.path.isdir(opts.source):
self.logger.error("%s not found", opts.source)
sys.exit(1)
action = "comparing" if opts.test else "installing"
doctype = self.DOCTYPE.lower()
self.logger.info("%s %ss", action, doctype)
self.logger.info("from %s", opts.source)
changes = 0
for name in os.listdir(self.opts.source):
if name.endswith(".xml"):
xml = open(os.path.join(self.opts.source, name), "rb").read()
doc = self.Document(self, name, xml)
if doc.install():
changes += 1
if changes:
if not self.opts.test:
self.post_process()
else:
self.logger.info("%ss already up to date", doctype)
if not self.opts.test:
cdr.logout(self.session)
self.logger.info("%s installation complete", doctype)
def post_process(self):
"""
Override in the derived class as appropriate.
"""
@staticmethod
def fetch_options():
"""
Parse and validate the command-line arguments.
"""
desc = "Install a fresh set of CDR filters or schemas"
doctypes = "schema", "filter"
parser = argparse.ArgumentParser(description=desc)
parser.add_argument("source", help="path location of document set")
parser.add_argument("doctype", choices=doctypes)
parser.add_argument("--test", "-t", action="store_true",
help="don't store, just compare and report")
opts = parser.parse_args()
return opts
@classmethod
def execute(cls, args):
"""
Run an external program and return the results.
"""
p = subprocess.Popen(args, **cls.POPEN_OPTS)
output, error = p.communicate()
class Result:
def __init__(self, code, output):
self.code = code
self.output = output
return Result(p.returncode, output)
class Document:
"""
A CDR document to be installed if new or changed.
"""
def __init__(self, control, name, xml):
"""
Store the properties not dependent on document type.
"""
self.name = self.title = name
self.xml = xml
self.control = control
self.doctype = control.DOCTYPE
self.id = self.old = None
def fetch_doc(self):
"""
Retrieve the document ID and stored XML if not new.
"""
query = cdrdb.Query("document d", "d.id", "d.xml")
query.join("doc_type t", "t.id = d.doc_type")
query.where(query.Condition("t.name", self.doctype))
query.where(query.Condition("d.title", self.title))
rows = query.execute(self.control.cursor).fetchall()
if not rows:
return
if len(rows) > 1:
self.control.logger.warning("multiple %r docs", self.title)
else:
self.id, self.old = rows[0]
def install(self):
"""
Install CDR document if it is new or changed.
Return False if document unchanged; otherwise True
"""
if not self.id:
return self.add()
elif self.changed():
return self.replace()
return False
def changed(self):
"""
Compare the old and new docs.
Ignore leading and trailing whitespace differences.
"""
old = self.old.strip().replace("\\r", "").encode("utf-8")
return self.xml.strip() != old
def add(self):
"""
Add the document to the CDR repository (if not testing).
Return True, which is bubbled up to the main loop in `run()`.
"""
if self.control.opts.test:
self.control.logger.info("%s is new", self.name)
return True
comment = "Added by install-docset.py"
ctrl = {"DocTitle": self.title}
opts = {"type": self.doctype, "encoding": "utf-8", "ctrl": ctrl}
cdr_doc = str(cdr.Doc(self.xml, **opts))
opts = dict(doc=cdr_doc, checkIn="Y", ver="Y", comment=comment)
opts["publishable"] = self.control.PUBLISHABLE
cdr_id = cdr.addDoc(self.control.session, **opts)
error = cdr.checkErr(cdr_id)
if error:
self.control.logger.error(error)
sys.exit(1)
self.control.logger.info("added %s as %s", self.name, cdr_id)
return True
def replace(self):
"""
Update an existing CDR document (if not testing).
Return True, which is bubbled up to the main loop in `run()`.
"""
if self.control.opts.test:
self.control.logger.info("%s is changed", self.name)
return True
cdr.checkOutDoc(self.control.session, self.id, force="Y")
comment = "Updated by install-docset.py"
ctrl = {"DocTitle": self.title}
opts = {"type": self.doctype, "encoding": "utf-8", "ctrl": ctrl}
opts["id"] = cdr.normalize(self.id)
cdr_doc = str(cdr.Doc(self.xml, **opts))
opts = dict(doc=cdr_doc, checkIn="Y", ver="Y", comment=comment)
opts["publishable"] = self.control.PUBLISHABLE
cdr_id = cdr.repDoc(self.control.session, **opts)
error = cdr.checkErr(cdr_id)
if error:
self.control.logger.error(error)
sys.exit(1)
self.control.logger.info("replaced %s (%s)", self.name, cdr_id)
return True
class SchemaSet(DocumentSet):
"""
Processing control for installing a CDR schema document set.
The custom part for schemas is the postprocessing.
Class values:
CHECK_DTDS - path to script to rebuild the client DTD files
REFRESH_MANIFEST - path to script to rebuild the client manifest
ACCOUNT - name of CDR account for installing schemas
"""
CHECK_DTDS = f"{cdr.BASEDIR}/Build/CheckDtds.py"
REFRESH_MANIFEST = f"{cdr.BASEDIR}/Build/RefreshManifest.py"
ACCOUNT = "SchemaUpdater"
DOCTYPE = "schema"
PUBLISHABLE = "N"
def post_process(self):
"""
Rebuild fresh DTDs and the client manifest.
"""
self.rebuild_dtds()
self.refresh_manifest()
def rebuild_dtds(self):
"""
Reflect changes to the schemas in regenerated DTDs for XMetaL.
"""
args = cdr.PYTHON, self.CHECK_DTDS
result = self.execute(args)
if result.code:
self.logger.error(f"failure rebuilding DTDs: {result.output}")
sys.exit(1)
self.logger.info("rebuild client dtds")
def refresh_manifest(self):
"""
Make sure the client manifest refrects changes to the DTDs
"""
args = cdr.PYTHON, self.REFRESH_MANIFEST
result = self.execute(args)
if result.code:
self.logger.error("failure refreshing manifest: %s", result.output)
sys.exit(1)
self.logger.info("refreshed client manifest")
class Document(DocumentSet.Document):
"""
A CDR schema document to be installed if new or changed.
"""
def __init__(self, control, name, xml):
"""
Store the properties, encoding the xml using utf-8.
"""
DocumentSet.Document.__init__(self, control, name, xml)
self.fetch_doc()
class FilterSet(DocumentSet):
"""
Processing control for installing a CDR filter document set.
The tricky bit for filters is finding the document title.
Class values:
ACCOUNT - name of CDR account for installing filters
DOCTYPE - CDR name for document type
"""
DOCTYPE = "Filter"
ACCOUNT = "ReleaseInstaller"
PUBLISHABLE = "Y"
class Document(DocumentSet.Document):
"""
A CDR filter document to be installed if new or changed.
"""
def __init__(self, control, name, xml):
"""
Store the properties, encoding the xml using utf-8.
"""
DocumentSet.Document.__init__(self, control, name, xml)
xml = xml.decode("utf-8")
match = re.search("<!--\\s*filter title:(.*?)-->", xml, re.I)
if not match:
self.control.logger.warning("title not found in %s", name)
self.title = ""
else:
self.title = match.group(1).strip()
if not self.title:
self.control.logger.warning("empty title for %s", name)
else:
self.fetch_doc()
if __name__ == "__main__":
"Top-level entry point."
opts = DocumentSet.fetch_options()
dict(schema=SchemaSet, filter=FilterSet)[opts.doctype](opts).run()