-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathautoyast-xml-validate.py
More file actions
executable file
·334 lines (293 loc) · 9.87 KB
/
autoyast-xml-validate.py
File metadata and controls
executable file
·334 lines (293 loc) · 9.87 KB
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
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Copyright SUSE LCC 2021
# Author: Martin Rey <mrey@suse.de> 2021
# Author: Thomas Renninger <trenn@suse.de> 2021
import argparse
import logging
import sys
import os
import glob
import types
import tempfile
from logging.config import dictConfig
from socket import getfqdn
from subprocess import PIPE, Popen
from urllib.request import urlopen
# Global
COBBLER_XML_S_URL = 'http://{0}/cblr/svc/op/autoinstall/system/{1}'
COBBLER_XML_P_URL = 'http://{0}/cblr/svc/op/autoinstall/profile/{1}'
PROFILE_GLOB = '/usr/share/YaST2/schema/autoyast/products/*'
PROFILE_LOCATION = '/usr/share/YaST2/schema/autoyast/products/{0}/{1}/profile.rng'
#: The dictionary, used by :class:`logging.config.dictConfig`
#: use it to setup your logging formatters, handlers, and loggers
#: For details, see
# https://docs.python.org/3.4/library/logging.config.html#configuration-dictionary-schema
DEFAULT_LOGGING_DICT = types.MappingProxyType(
{
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {'format': '[%(levelname)s]: %(message)s'},
},
'handlers': {
'default': {
'level': 'NOTSET',
'formatter': 'standard',
'class': 'logging.StreamHandler',
},
},
'loggers': {
__name__: {
'handlers': ['default'],
'level': 'INFO',
'propagate': True,
},
},
}
)
#: Map verbosity level (int) to log level
LOGLEVELS = types.MappingProxyType({
None: logging.WARNING, # 0
0: logging.WARNING,
1: logging.INFO,
2: logging.DEBUG,
})
#: Instantiate our logger
log = logging.getLogger(__name__)
def parse(cliargs=None):
"""Parse the command line and return parsed results.
:param cliargs: Arguments to parse or None (=use sys.argv) (Default value = None)
:returns: parsed CLI result
:rtype: class:`argparse.Namespace`
"""
parser = argparse.ArgumentParser(description='Validate autoyast XML', formatter_class=argparse.RawTextHelpFormatter)
parsergroup = parser.add_mutually_exclusive_group(required=True)
parsergroup.add_argument(
'-u',
'--url',
action='store',
default=False,
help='Use autoyast XML from URL'
)
parsergroup.add_argument(
'-f',
'--file',
action='store',
default=False,
help='Use autoyast XML from file'
)
parsergroup.add_argument(
'-s',
'--cobbler_system',
action='store',
default=False,
help='Use autoyast XML from cobbler system (cobbler system list)'
)
parsergroup.add_argument(
'-p',
'--cobbler_profile',
action='store',
default=False,
help='Use autoyast XML from cobbler profile (cobbler profile list)\n\n'
)
parsergroup.add_argument(
'-l', '--list', action='store_true', default=False,
help='Show products and archs (to be used with -P [ -a ] param)\nof installed and available XML syntax definitions\n\n'
)
parser.add_argument(
'-r',
'--profile-rng',
action='store',
default='/usr/share/YaST2/schema/autoyast/rng/profile.rng',
help='Path to RELAX NG schema to use to validate XML\n(default: %(default)s)\n\n',
required=False
)
parser.add_argument(
'-c',
'--cobbler',
action='store',
default='localhost',
help='Cobbler hostname or IP address (default: %(default)s)',
required=False,
)
parser.add_argument(
'-P',
'--product',
action='store',
default='',
help='Product to check/validate against, needs yast2-schemas package (see --list option)',
required=False
)
parser.add_argument(
'-a',
'--arch',
action='store',
default='',
help='Architecture to check/validate against, needs yast2-schemas package (default: x86_64)\n\n',
required=False
)
parser.add_argument(
'--save', action='store_true', default=False,
help='Always store retrieved XML file, not only in error case'
)
parser.add_argument(
'-v', '--verbose', action='count', help='Raise verbosity level \n-v info, -vv debug e.g. to see URL from retrieved autoyast file'
)
args = parser.parse_args(cliargs)
# Setup logging and the log level according to the '-v' option
dictConfig(DEFAULT_LOGGING_DICT)
log.setLevel(LOGLEVELS.get(args.verbose, logging.DEBUG))
log.debug('CLI args: %s', args)
if args.arch and not args.product:
log.error("-a/--arch option needs a -P/--product option")
sys.exit(2)
if not args.arch:
# Need to set default here to check empty value above
args.arch = 'x86_64'
return args
def get_content_from_url(url):
"""Get content of a website and return it.
:param url: URL to get content from
:returns: content of URL
:rtype: bool
"""
with urlopen(url) as xml_site:
site_content = xml_site.read().decode('utf-8')
return site_content
def list_products():
product = {}
product_folders = glob.glob(PROFILE_GLOB)
print("\nList of supported distributions and architectures:\n")
print("Product")
print("[ Architectures, ]\n")
for f in product_folders:
archs = []
arch_folders = glob.glob("%s/*" % f)
for a in arch_folders:
archs.append(os.path.basename(a))
if archs:
print(os.path.basename(f))
print(archs)
print()
product[os.path.basename(f)] = archs
def get_rng(args):
"""Get profile.rng file location depending on CLI args.
:param args: arguments passed to CLI
:returns: autoyast XML
:rtype: str
"""
profile_rng = None
if args.product:
profile_rng = PROFILE_LOCATION.format(args.product, args.arch)
else:
profile_rng = args.profile_rng
if not os.path.isfile(profile_rng):
raise IOError("Cannot locate %s" % profile_rng)
return profile_rng
def get_xml(args):
"""Get XML string from different sources, depending on CLI args.
:param args: arguments passed to CLI
:returns: autoyast XML
:rtype: str
"""
xml = None
if args.url:
xml = get_content_from_url(args.url)
elif args.cobbler_system:
url = COBBLER_XML_S_URL.format(args.cobbler, args.cobbler_system)
log.info('Try to download XML content located at URL: %s', url)
xml = get_content_from_url(url)
elif args.cobbler_profile:
url = COBBLER_XML_P_URL.format(args.cobbler, args.cobbler_profile)
log.info('Try to download XML content located at URL: %s', url)
xml = get_content_from_url(url)
elif args.file:
with open(args.file) as xml_file:
xml = xml_file.read()
else:
log.error(
'No autoyast.xml source specified. Use either -u, -f or -p and -s options'
)
return xml.strip()
def validate_xml(args, xml, profile_rng):
"""Check xml for errors with xmllint.
:param args: arguments passed to CLI
:param xml: autoyast XML as string
:returns: returns: returns: return if autoyast XML validates
:rtype: bool
"""
# check xml syntax with xmllint
success = True
log_delim = "---------------------------------------------------------"
command = ['xmllint', '--noout', '--relaxng', profile_rng, '/dev/stdin']
process = Popen(
command,
stdout=PIPE,
stderr=PIPE,
stdin=PIPE
)
stdout, stderr = process.communicate(input=xml.encode())
log.debug('xmllint return code: %d', process.returncode)
if process.returncode:
log.warning("xmllint output %s", log_delim)
log.warning('stderr: %s', str(stderr, encoding='utf-8'))
log.warning('stdout: %s', str(stdout, encoding='utf-8'))
log.warning("xmllint output %s", log_delim)
success = False
command = ['jing', profile_rng, '/dev/stdin']
# check RELAX NG schema with jing
process = Popen(
command,
stdout=PIPE,
stderr=PIPE,
stdin=PIPE,
)
stdout, stderr = process.communicate(input=xml.encode())
log.debug('jing return code: %d', process.returncode)
if process.returncode:
log.warning("jing output %s", log_delim)
log.warning('stderr: %s', str(stderr, encoding='utf-8'))
log.warning('stdout: %s', str(stdout, encoding='utf-8'))
log.warning("jing output %s", log_delim)
success = False
if success:
print("XML file successfully parsed - No errors found")
if not success or args.save:
temp = tempfile.NamedTemporaryFile(prefix="autoyast_xml_validator_", delete=False)
temp.write(xml.encode())
temp.close()
if not success:
raise SyntaxError('XML has wrong Syntax. XML file store here: %s' % temp.name)
else:
print("XML file stored: %s" % temp.name)
"""
A returncode of 0 means "good". But the function should return True
if the XML is valid. As 0 is falsy in Python, bool(0) would return
False. Thus it is needed to invert the bool with a 'not' to adapt it
to our needs.
return True if both checks have a returncode of 0
"""
return success
if __name__ == '__main__':
RETURN_CODE = 0
args = parse()
try:
if args.list:
list_products()
else:
profile_rng = get_rng(args)
xml = get_xml(args)
if not validate_xml(args, xml, profile_rng):
RETURN_CODE = 99
except IOError as err:
log.error('IOError: %s', err)
RETURN_CODE = 3
except SyntaxError as err:
log.error('SyntaxError: %s', err)
RETURN_CODE = 2
except Exception:
log.exception("Unknown Error")
RETURN_CODE = 1
sys.exit(RETURN_CODE)
# EOF