1 #!/usr/bin/python2.6 |
|
2 # |
|
3 # CDDL HEADER START |
|
4 # |
|
5 # The contents of this file are subject to the terms of the |
|
6 # Common Development and Distribution License (the "License"). |
|
7 # You may not use this file except in compliance with the License. |
|
8 # |
|
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE |
|
10 # or http://www.opensolaris.org/os/licensing. |
|
11 # See the License for the specific language governing permissions |
|
12 # and limitations under the License. |
|
13 # |
|
14 # When distributing Covered Code, include this CDDL HEADER in each |
|
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE. |
|
16 # If applicable, add the following below this CDDL HEADER, with the |
|
17 # fields enclosed by brackets "[]" replaced with your own identifying |
|
18 # information: Portions Copyright [yyyy] [name of copyright owner] |
|
19 # |
|
20 # CDDL HEADER END |
|
21 # |
|
22 # Copyright 2010 Sun Microsystems, Inc. All rights reserved. |
|
23 # Use is subject to license terms. |
|
24 """ |
|
25 |
|
26 A/I Publish_Manifest |
|
27 |
|
28 """ |
|
29 |
|
30 import os.path |
|
31 import sys |
|
32 import StringIO |
|
33 import gettext |
|
34 import lxml.etree |
|
35 import hashlib |
|
36 from optparse import OptionParser |
|
37 |
|
38 import osol_install.auto_install.AI_database as AIdb |
|
39 import osol_install.auto_install.verifyXML as verifyXML |
|
40 import osol_install.libaiscf as smf |
|
41 |
|
42 INFINITY = str(0xFFFFFFFFFFFFFFFF) |
|
43 IMG_AI_MANIFEST_SCHEMA = "auto_install/ai_manifest.rng" |
|
44 SYS_AI_MANIFEST_SCHEMA = "/usr/share/auto_install/ai_manifest.rng" |
|
45 |
|
46 def parse_options(): |
|
47 """ |
|
48 Parse and validate options |
|
49 Args: None |
|
50 Returns: the DataFiles object populated and initialized |
|
51 Raises: The DataFiles initialization of manifest(s) A/I, SC, SMF looks for |
|
52 many error conditions and, when caught, are flagged to the user |
|
53 via raising SystemExit exceptions. |
|
54 """ |
|
55 |
|
56 usage = _("usage: %prog service_name criteria_manifest") |
|
57 parser = OptionParser(usage=usage) |
|
58 # since no options are specified simply retrieve the args list |
|
59 # args should be a list with indices: |
|
60 # 0 - service name |
|
61 # 1 - manifest path to work with |
|
62 args = parser.parse_args()[1] |
|
63 |
|
64 # check that we got the install service's name and |
|
65 # a criteria manifest |
|
66 if len(args) != 2: |
|
67 parser.print_help() |
|
68 sys.exit(1) |
|
69 |
|
70 # get an AIservice object for requested service |
|
71 try: |
|
72 svc = smf.AIservice(smf.AISCF(FMRI="system/install/server"), args[0]) |
|
73 except KeyError: |
|
74 raise SystemExit(_("Error:\tFailed to find service %s") % args[0]) |
|
75 |
|
76 # argument two is the criteria manifest |
|
77 crit_manifest = args[1] |
|
78 |
|
79 # get the service's data directory path and imagepath |
|
80 try: |
|
81 image_path = svc['image_path'] |
|
82 # txt_record is of the form "aiwebserver=jumprope:46503" so split on ":" |
|
83 # and take the trailing portion for the port number |
|
84 port = svc['txt_record'].rsplit(':')[-1] |
|
85 except KeyError: |
|
86 raise SystemExit(_("SMF data for service %s is corrupt.\n") % |
|
87 args[0]) |
|
88 service_dir = os.path.abspath("/var/ai/" + port) |
|
89 |
|
90 # check that the service and imagepath directories exist, |
|
91 # and the AI.db, criteria_schema.rng and ai_manifest.rng files |
|
92 # are present otherwise the service is misconfigured |
|
93 if not (os.path.isdir(service_dir) and |
|
94 os.path.exists(os.path.join(service_dir, "AI.db"))): |
|
95 raise SystemExit("Error:\tNeed a valid A/I service directory") |
|
96 |
|
97 files = DataFiles(service_dir = service_dir, image_path = image_path, |
|
98 database_path = os.path.join(service_dir, "AI.db"), |
|
99 criteria_path = crit_manifest) |
|
100 |
|
101 return(files) |
|
102 |
|
103 def find_colliding_criteria(files): |
|
104 """ |
|
105 Returns: A dictionary of colliding criteria with keys being manifest name |
|
106 and instance tuples and values being the DB column names which |
|
107 collided |
|
108 Args: DataFiles object with a valid _criteria_root and database object |
|
109 Raises: SystemExit if: criteria is not found in database |
|
110 value is not valid for type (integer and hexadecimal |
|
111 checks) |
|
112 range is improper |
|
113 """ |
|
114 # define convenience strings: |
|
115 class fields(object): |
|
116 # manifest name is row index 0 |
|
117 MANNAME = 0 |
|
118 # manifest instance is row index 1 |
|
119 MANINST = 1 |
|
120 # criteria is row index 2 (when a single valued criteria) |
|
121 CRIT = 2 |
|
122 # minimum criteria is row index 2 (when a range valued criteria) |
|
123 MINCRIT = 2 |
|
124 # maximum criteria is row index 3 (when a range valued criteria) |
|
125 MAXCRIT = 3 |
|
126 |
|
127 # collisions is a dictionary to hold keys of the form (manifest name, |
|
128 # instance) which will point to a comma-separated string of colliding |
|
129 # criteria |
|
130 collisions = dict() |
|
131 |
|
132 # verify each range criteria in the manifest is well formed and collect |
|
133 # collisions with database entries |
|
134 for crit in files.criteria: |
|
135 # gather this criteria's values from the manifest |
|
136 man_criterion = files.criteria[crit] |
|
137 |
|
138 # check "value" criteria here (check the criteria exists in DB, and |
|
139 # then find collisions) |
|
140 if not isinstance(man_criterion, list): |
|
141 # only check criteria in use in the DB |
|
142 if crit not in AIdb.getCriteria(files.database.getQueue(), |
|
143 onlyUsed=False, strip=False): |
|
144 raise SystemExit(_("Error:\tCriteria %s is not a " + |
|
145 "valid criteria!") % crit) |
|
146 |
|
147 # get all values in the database for this criteria (and |
|
148 # manifest/instance paris for each value) |
|
149 db_criteria = AIdb.getSpecificCriteria( |
|
150 files.database.getQueue(), crit, None, |
|
151 provideManNameAndInstance=True) |
|
152 |
|
153 # will iterate over a list of the form [manName, manInst, crit, |
|
154 # None] |
|
155 for row in db_criteria: |
|
156 # check if the database and manifest values differ |
|
157 if(str(row[fields.CRIT]).lower() == str(man_criterion).lower()): |
|
158 # record manifest name, instance and criteria name |
|
159 try: |
|
160 collisions[row[fields.MANNAME], |
|
161 row[fields.MANINST]] += crit + "," |
|
162 except KeyError: |
|
163 collisions[row[fields.MANNAME], |
|
164 row[fields.MANINST]] = crit + "," |
|
165 |
|
166 # This is a range criteria. (Check that ranges are valid, that |
|
167 # "unbounded" gets set to 0/+inf, ensure the criteria exists |
|
168 # in the DB, then look for collisions.) |
|
169 else: |
|
170 # check for a properly ordered range (with unbounded being 0 or |
|
171 # Inf.) but ensure both are not unbounded |
|
172 if( |
|
173 # Check for a range of -inf to inf -- not a valid range |
|
174 (man_criterion[0] == "unbounded" and |
|
175 man_criterion[1] == "unbounded" |
|
176 ) or |
|
177 # Check min > max -- range order reversed |
|
178 ( |
|
179 (man_criterion[0] != "unbounded" and |
|
180 man_criterion[1] != "unbounded" |
|
181 ) and |
|
182 (man_criterion[0] > man_criterion[1]) |
|
183 ) |
|
184 ): |
|
185 raise SystemExit(_("Error:\tCriteria %s " |
|
186 "is not a valid range (MIN > MAX) or " |
|
187 "(MIN and MAX unbounded).") % crit) |
|
188 |
|
189 # Clean-up NULL's and changed "unbounded"s to 0 and |
|
190 # really large numbers in case this Python does |
|
191 # not support IEEE754. Note "unbounded"s are already |
|
192 # converted to lower case during manifest processing. |
|
193 if man_criterion[0] == "unbounded": |
|
194 man_criterion[0] = "0" |
|
195 if man_criterion[1] == "unbounded": |
|
196 man_criterion[1] = INFINITY |
|
197 if crit == "mac": |
|
198 # convert hex mac address (w/o colons) to a number |
|
199 try: |
|
200 man_criterion[0] = long(str(man_criterion[0]).upper(), 16) |
|
201 man_criterion[1] = long(str(man_criterion[1]).upper(), 16) |
|
202 except ValueError: |
|
203 raise SystemExit(_("Error:\tCriteria %s " |
|
204 "is not a valid hexadecimal value") % |
|
205 crit) |
|
206 |
|
207 else: |
|
208 # this is a decimal value |
|
209 try: |
|
210 man_criterion = [long(str(man_criterion[0]).upper()), |
|
211 long(str(man_criterion[1]).upper())] |
|
212 except ValueError: |
|
213 raise SystemExit(_("Error:\tCriteria %s " |
|
214 "is not a valid integer value") % crit) |
|
215 |
|
216 # check to see that this criteria exists in the database columns |
|
217 if ('MIN' + crit not in AIdb.getCriteria( |
|
218 files.database.getQueue(), onlyUsed=False, strip=False))\ |
|
219 and ('MAX' + crit not in AIdb.getCriteria( |
|
220 files.database.getQueue(), onlyUsed=False, strip=False)): |
|
221 raise SystemExit(_("Error:\tCriteria %s is not a " + |
|
222 "valid criteria!") % crit) |
|
223 db_criteria = AIdb.getSpecificCriteria( |
|
224 files.database.getQueue(), 'MIN' + crit, 'MAX' + crit, |
|
225 provideManNameAndInstance=True) |
|
226 |
|
227 # will iterate over a list of the form [manName, manInst, mincrit, |
|
228 # maxcrit] |
|
229 for row in db_criteria: |
|
230 # arbitrarily large number in case this Python does |
|
231 # not support IEEE754 |
|
232 db_criterion = ["0", INFINITY] |
|
233 |
|
234 # now populate in valid database values (i.e. non-NULL values) |
|
235 if row[fields.MINCRIT]: |
|
236 db_criterion[0] = row[fields.MINCRIT] |
|
237 if row[fields.MAXCRIT]: |
|
238 db_criterion[1] = row[fields.MAXCRIT] |
|
239 if crit == "mac": |
|
240 # use a hexadecimal conversion |
|
241 db_criterion = [long(str(db_criterion[0]), 16), |
|
242 long(str(db_criterion[1]), 16)] |
|
243 else: |
|
244 # these are decimal numbers |
|
245 db_criterion = [long(str(db_criterion[0])), |
|
246 long(str(db_criterion[1]))] |
|
247 |
|
248 # these three criteria can determine if there's a range overlap |
|
249 if((man_criterion[1] >= db_criterion[0] and |
|
250 db_criterion[1] >= man_criterion[0]) or |
|
251 man_criterion[0] == db_criterion[1]): |
|
252 # range overlap so record the collision |
|
253 try: |
|
254 collisions[row[fields.MANNAME], |
|
255 row[fields.MANINST]] += "MIN" + crit + "," |
|
256 collisions[row[fields.MANNAME], |
|
257 row[fields.MANINST]] += "MAX" + crit + "," |
|
258 except KeyError: |
|
259 collisions[row[fields.MANNAME], |
|
260 row[fields.MANINST]] = "MIN" + crit + "," |
|
261 collisions[row[fields.MANNAME], |
|
262 row[fields.MANINST]] += "MAX" + crit + "," |
|
263 return collisions |
|
264 |
|
265 def find_colliding_manifests(files, collisions): |
|
266 """ |
|
267 For each manifest/instance pair in collisions check that the manifest |
|
268 criteria diverge (i.e. are not exactly the same) and that the ranges do not |
|
269 collide for ranges. |
|
270 Raises if: a range collides, or if the manifest has the same criteria as a |
|
271 manifest already in the database (SystemExit raised) |
|
272 Returns: Nothing |
|
273 Args: files - DataFiles object with vaild _criteria_root and database |
|
274 object |
|
275 collisions - a dictionary with collisions, as produced by |
|
276 find_colliding_criteria() |
|
277 """ |
|
278 # check every manifest in collisions to see if manifest collides (either |
|
279 # identical criteria, or overlaping ranges) |
|
280 for man_inst in collisions: |
|
281 # get all criteria from this manifest/instance pair |
|
282 db_criteria = AIdb.getManifestCriteria(man_inst[0], |
|
283 man_inst[1], |
|
284 files.database.getQueue(), |
|
285 humanOutput=True, |
|
286 onlyUsed=False) |
|
287 |
|
288 # iterate over every criteria in the database |
|
289 for crit in AIdb.getCriteria(files.database.getQueue(), |
|
290 onlyUsed=False, strip=False): |
|
291 |
|
292 # Get the criteria name (i.e. no MIN or MAX) |
|
293 crit_name = crit.replace('MIN', '', 1).replace('MAX', '', 1) |
|
294 # Set man_criterion to the key of the DB criteria or None |
|
295 man_criterion = files.criteria[crit_name] |
|
296 if man_criterion and crit.startswith('MIN'): |
|
297 man_criterion = man_criterion[0] |
|
298 elif man_criterion and crit.startswith('MAX'): |
|
299 man_criterion = man_criterion[1] |
|
300 |
|
301 # set the database criteria |
|
302 if db_criteria[str(crit)] == '': |
|
303 # replace database NULL's with a Python None |
|
304 db_criterion = None |
|
305 else: |
|
306 db_criterion = db_criteria[str(crit)] |
|
307 |
|
308 # Replace unbounded's in the criteria (i.e. 0/+inf) |
|
309 # with a Python None. |
|
310 if isinstance(man_criterion, basestring) and \ |
|
311 man_criterion == "unbounded": |
|
312 man_criterion = None |
|
313 |
|
314 # check to determine if this is a range collision by using |
|
315 # collisions and if not are the manifests divergent |
|
316 |
|
317 if((crit.startswith('MIN') and |
|
318 collisions[man_inst].find(crit + ",") != -1) or |
|
319 (crit.startswith('MAX') and |
|
320 collisions[man_inst].find(crit + ",") != -1) |
|
321 ): |
|
322 if (str(db_criterion).lower() != str(man_criterion).lower()): |
|
323 raise SystemExit(_("Error:\tManifest has a range " |
|
324 "collision with manifest:%s/%i" |
|
325 "\n\tin criteria: %s!") % |
|
326 (man_inst[0], man_inst[1], |
|
327 crit.replace('MIN', '', 1). |
|
328 replace('MAX', '', 1))) |
|
329 |
|
330 # the range did not collide or this is a single value (if we |
|
331 # differ we can break out knowing we diverge for this |
|
332 # manifest/instance) |
|
333 elif(str(db_criterion).lower() != str(man_criterion).lower()): |
|
334 # manifests diverge (they don't collide) |
|
335 break |
|
336 |
|
337 # end of for loop and we never broke out (diverged) |
|
338 else: |
|
339 raise SystemExit(_("Error:\tManifest has same criteria as " + |
|
340 "manifest:%s/%i!") % |
|
341 (man_inst[0], man_inst[1])) |
|
342 |
|
343 def insert_SQL(files): |
|
344 """ |
|
345 Ensures all data is properly sanitized and formatted, then inserts it into |
|
346 the database |
|
347 Args: None |
|
348 Returns: None |
|
349 """ |
|
350 query = "INSERT INTO manifests VALUES(" |
|
351 |
|
352 # add the manifest name to the query string |
|
353 query += "'" + AIdb.sanitizeSQL(files.manifest_name) + "'," |
|
354 # check to see if manifest name is alreay in database (affects instance |
|
355 # number) |
|
356 if AIdb.sanitizeSQL(files.manifest_name) in \ |
|
357 AIdb.getManNames(files.database.getQueue()): |
|
358 # database already has this manifest name get the number of |
|
359 # instances |
|
360 instance = AIdb.numInstances(AIdb.sanitizeSQL(files.manifest_name), |
|
361 files.database.getQueue()) |
|
362 |
|
363 # this a new manifest |
|
364 else: |
|
365 instance = 0 |
|
366 |
|
367 # actually add the instance to the query string |
|
368 query += str(instance) + "," |
|
369 |
|
370 # we need to fill in the criteria or NULLs for each criteria the database |
|
371 # supports (so iterate over each criteria) |
|
372 for crit in AIdb.getCriteria(files.database.getQueue(), |
|
373 onlyUsed=False, strip=False): |
|
374 # for range values trigger on the MAX criteria (skip the MIN's |
|
375 # arbitrary as we handle rows in one pass) |
|
376 if crit.startswith('MIN'): |
|
377 continue |
|
378 |
|
379 # get the values from the manifest |
|
380 values = files.criteria[crit.replace('MAX', '', 1)] |
|
381 |
|
382 # if the values are a list this is a range |
|
383 if isinstance(values, list): |
|
384 for value in values: |
|
385 # translate "unbounded" to a database NULL |
|
386 if value == "unbounded": |
|
387 query += "NULL," |
|
388 # we need to deal with mac addresses specially being |
|
389 # hexadecimal |
|
390 elif crit.endswith("mac"): |
|
391 # need to insert with hex operand x'<val>' |
|
392 # use an upper case string for hex values |
|
393 query += "x'" + AIdb.sanitizeSQL(str(value).upper())+"'," |
|
394 else: |
|
395 query += AIdb.sanitizeSQL(str(value).upper()) + "," |
|
396 |
|
397 # this is a single criteria (not a range) |
|
398 elif isinstance(values, basestring): |
|
399 # translate "unbounded" to a database NULL |
|
400 if values == "unbounded": |
|
401 query += "NULL," |
|
402 else: |
|
403 # use lower case for text strings |
|
404 query += "'" + AIdb.sanitizeSQL(str(values).lower()) + "'," |
|
405 |
|
406 # the critera manifest didn't specify this criteria so fill in NULLs |
|
407 else: |
|
408 # use the criteria name to determine if this is a range |
|
409 if crit.startswith('MAX'): |
|
410 query += "NULL,NULL," |
|
411 # this is a single value |
|
412 else: |
|
413 query += "NULL," |
|
414 |
|
415 # strip trailing comma and close parentheses |
|
416 query = query[:-1] + ")" |
|
417 |
|
418 # update the database |
|
419 query = AIdb.DBrequest(query, commit=True) |
|
420 files.database.getQueue().put(query) |
|
421 query.waitAns() |
|
422 # in case there's an error call the response function (which will print the |
|
423 # error) |
|
424 query.getResponse() |
|
425 |
|
426 def do_default(files): |
|
427 """ |
|
428 Removes old default.xml after ensuring proper format of new manifest |
|
429 (does not copy new manifest over -- see place_manifest) |
|
430 Args: None |
|
431 Returns: None |
|
432 Raises if: Manifest has criteria, old manifest can not be removed (exits |
|
433 with SystemExit) |
|
434 """ |
|
435 # check to see if any criteria is present -- if so, it can not be a default |
|
436 # manifest (as they do not have criteria) |
|
437 if files.criteria: |
|
438 raise SystemExit(_("Error:\tCan not use AI criteria in a default " + |
|
439 "manifest")) |
|
440 # remove old manifest |
|
441 try: |
|
442 os.remove(os.path.join(files.get_service(), 'AI_data', 'default.xml')) |
|
443 except IOError, e: |
|
444 raise SystemExit(_("Error:\tUnable to remove default.xml:\n\t%s") % e) |
|
445 |
|
446 def place_manifest(files): |
|
447 """ |
|
448 Compares src and dst manifests to ensure they are the same; if manifest |
|
449 does not yet exist, copies new manifest into place and sets correct |
|
450 permissions and ownership |
|
451 Args: None |
|
452 Returns: None |
|
453 Raises if: src and dst manifests differ (in MD5 sum), unable to write dst |
|
454 manifest (raises SystemExit -- no clean up of database performed) |
|
455 """ |
|
456 manifest_path = os.path.join(files.get_service(), "AI_data", |
|
457 files.manifest_name) |
|
458 |
|
459 # if the manifest already exists see if it is different from what was |
|
460 # passed in. If so, warn the user that we're using the existing manifest |
|
461 if os.path.exists(manifest_path): |
|
462 old_manifest = open(manifest_path, "r") |
|
463 existing_MD5 = hashlib.md5("".join(old_manifest.readlines())).digest() |
|
464 old_manifest.close() |
|
465 current_MD5 = hashlib.md5(lxml.etree.tostring(files._AI_root, |
|
466 pretty_print=True, encoding=unicode)).digest() |
|
467 if existing_MD5 != current_MD5: |
|
468 raise SystemExit(_("Error:\tNot copying manifest, source and " + |
|
469 "current versions differ -- criteria in place.")) |
|
470 |
|
471 # the manifest does not yet exist so write it out |
|
472 else: |
|
473 try: |
|
474 new_man = open(manifest_path, "w") |
|
475 new_man.writelines('<ai_criteria_manifest>\n') |
|
476 new_man.writelines('\t<ai_embedded_manifest>\n') |
|
477 new_man.writelines(lxml.etree.tostring( |
|
478 files._AI_root, pretty_print=True, |
|
479 encoding=unicode)) |
|
480 new_man.writelines('\t</ai_embedded_manifest>\n') |
|
481 # write out each SMF SC manifest |
|
482 for key in files._smfDict: |
|
483 new_man.writelines('\t<sc_embedded_manifest name = "%s">\n'% |
|
484 key) |
|
485 new_man.writelines("\t\t<!-- ") |
|
486 new_man.writelines("<?xml version='1.0'?>\n") |
|
487 new_man.writelines(lxml.etree.tostring(files._smfDict[key], |
|
488 pretty_print=True, encoding=unicode)) |
|
489 new_man.writelines('\t -->\n') |
|
490 new_man.writelines('\t</sc_embedded_manifest>\n') |
|
491 new_man.writelines('\t</ai_criteria_manifest>\n') |
|
492 new_man.close() |
|
493 except IOError, e: |
|
494 raise SystemExit(_("Error:\tUnable to write to dest. " |
|
495 "manifest:\n\t%s") % e) |
|
496 |
|
497 # change read for all and write for owner |
|
498 os.chmod(manifest_path, 0644) |
|
499 # change to user/group root (uid/gid 0) |
|
500 os.chown(manifest_path, 0, 0) |
|
501 |
|
502 class DataFiles(object): |
|
503 """ |
|
504 Class to contain and work with data files necessary for program |
|
505 """ |
|
506 # schema for validating an AI criteria manifest |
|
507 criteriaSchema = "/usr/share/auto_install/criteria_schema.rng" |
|
508 # DTD for validating an SMF SC manifest |
|
509 smfDtd = "/usr/share/lib/xml/dtd/service_bundle.dtd.1" |
|
510 |
|
511 |
|
512 def __init__(self, service_dir = None, image_path = None, |
|
513 database_path = None, criteria_path = None): |
|
514 """ |
|
515 Initialize DataFiles instance. All parameters optional, however, proper |
|
516 setup order asurred, if all data provided upon instantiation. |
|
517 """ |
|
518 |
|
519 # |
|
520 # State variables |
|
521 ################# |
|
522 # |
|
523 |
|
524 # Variable to cache criteria class for criteria property |
|
525 self._criteria_cache = None |
|
526 |
|
527 # |
|
528 # File system path variables |
|
529 ############################ |
|
530 # |
|
531 |
|
532 # Check AI Criteria Schema exists |
|
533 if not os.path.exists(self.criteriaSchema): |
|
534 raise SystemExit(_("Error:\tUnable to find criteria_schema: " + |
|
535 "%s") % self.criteriaSchema) |
|
536 |
|
537 # Check SC manifest SMF DTD exists |
|
538 if not os.path.exists(self.smfDtd): |
|
539 raise SystemExit(_("Error:\tUnable to find SMF system " + |
|
540 "configuration DTD: %s") % self.smfDtd) |
|
541 |
|
542 # A/I Manifest Schema |
|
543 self._AIschema = None |
|
544 |
|
545 # Holds path to service directory (i.e. /var/ai/46501) |
|
546 self._service = None |
|
547 if service_dir: |
|
548 self.service = service_dir |
|
549 |
|
550 # Holds path to AI image |
|
551 self._imagepath = None |
|
552 if image_path: |
|
553 self.image_path = image_path |
|
554 # set the AI schema once image_path is set |
|
555 self.set_AI_schema() |
|
556 |
|
557 # Holds database object for criteria database |
|
558 self._db = None |
|
559 if database_path: |
|
560 # Set Database Path and Open SQLite3 Object |
|
561 self.database = database_path |
|
562 # verify the database's table/column structure (or exit if errors) |
|
563 self.database.verifyDBStructure() |
|
564 |
|
565 # |
|
566 # XML DOM variables |
|
567 ################### |
|
568 # |
|
569 |
|
570 # |
|
571 # Criteria manifest setup |
|
572 # |
|
573 |
|
574 # Holds DOM for criteria manifest |
|
575 self._criteria_root = None |
|
576 |
|
577 # Holds path for criteria manifest |
|
578 self.criteria_path = criteria_path |
|
579 # find SC manifests from the criteria manifest and validate according |
|
580 # to the SMF DTD (exit if errors) |
|
581 if criteria_path: |
|
582 # sets _criteria_root DOM |
|
583 self.verifyCriteria() |
|
584 |
|
585 # |
|
586 # SC manifest setup |
|
587 # |
|
588 |
|
589 # Holds DOMs for SC manifests |
|
590 self._smfDict = dict() |
|
591 |
|
592 # if we were provided a criteria manifest, look for a SC manifests |
|
593 # specified by the criteria manifest |
|
594 if self._criteria_root: |
|
595 # sets _smfDict DOMs |
|
596 self.find_SC_from_crit_man() |
|
597 |
|
598 # |
|
599 # AI manifest setup |
|
600 # |
|
601 |
|
602 # Holds DOM for AI manifest |
|
603 self._AI_root = None |
|
604 |
|
605 # Holds path to AI manifest being published (may not be set if an |
|
606 # embedded manifest) |
|
607 self._manifest = None |
|
608 |
|
609 # if we were provided a criteria manifest, look for an A/I manifest |
|
610 # specified by the criteria manifest |
|
611 if self._criteria_root: |
|
612 # this will set _manifest to be the AI manifest path (if a file), |
|
613 # set _AI_root to the correct location in the criteria DOM (if |
|
614 # embedded), or exit (if unable to find an AI manifest) |
|
615 self.find_AI_from_criteria() |
|
616 # this will verify the _AI_root DOM and exit on error |
|
617 self.verify_AI_manifest() |
|
618 |
|
619 # overload the _criteria class to be a list with a special get_item to act |
|
620 # like a dictionary |
|
621 class _criteria(list): |
|
622 """ |
|
623 Wrap list class to provide lookups in the criteria file when |
|
624 requested |
|
625 """ |
|
626 def __init__(self, criteria_root): |
|
627 # store the criteria manifest DOM root |
|
628 self._criteria_root = criteria_root |
|
629 # call the _init_() for the list class with a generator provided by |
|
630 # find_criteria() to populate this _criteria() instance. |
|
631 super(DataFiles._criteria, self).__init__(self.find_criteria()) |
|
632 |
|
633 def __getitem__(self, key): |
|
634 """ |
|
635 Look up a requested criteria (akin to dictionary access) but for an |
|
636 uninitialized key will not raise an exception but return None) |
|
637 """ |
|
638 return self.get_criterion(key) |
|
639 |
|
640 def find_criteria(self): |
|
641 """ |
|
642 Find criteria from the criteria manifest |
|
643 Returns: A generator providing all criteria name attributes from |
|
644 <ai_criteria> tags |
|
645 """ |
|
646 root = self._criteria_root.findall(".//ai_criteria") |
|
647 |
|
648 # actually find criteria |
|
649 for tag in root: |
|
650 for child in tag.getchildren(): |
|
651 if (child.tag == "range" or child.tag == "value") and \ |
|
652 child.text is not None: |
|
653 # criteria names are lower case |
|
654 yield tag.attrib['name'].lower() |
|
655 # should not happen according to schema |
|
656 else: |
|
657 raise AssertionError(_( |
|
658 "Criteria contains no values")) |
|
659 |
|
660 def get_criterion(self, criterion): |
|
661 """ |
|
662 Return criterion out of the criteria manifest |
|
663 Returns: A list for range criterion with a min and max entry |
|
664 A string for value criterion |
|
665 """ |
|
666 source = self._criteria_root |
|
667 for tag in source.getiterator('ai_criteria'): |
|
668 crit = tag.get('name') |
|
669 # compare criteria name case-insensitive |
|
670 if crit.lower() == criterion.lower(): |
|
671 for child in tag.getchildren(): |
|
672 if child.tag == "range": |
|
673 # this is a range response (split on white space) |
|
674 return child.text.split() |
|
675 elif child.tag == "value": |
|
676 # this is a value response (strip white space) |
|
677 return child.text.strip() |
|
678 # should not happen according to schema |
|
679 elif child.text is None: |
|
680 raise AssertionError(_( |
|
681 "Criteria contains no values")) |
|
682 return None |
|
683 |
|
684 # disable trying to update criteria |
|
685 __setitem__ = None |
|
686 __delitem__ = None |
|
687 |
|
688 @property |
|
689 def criteria(self): |
|
690 """ |
|
691 Function to provide access to criteria class (and provide caching of |
|
692 class created) |
|
693 Returns: A criteria instance |
|
694 """ |
|
695 # if we don't have a cached _criteria class, create one and update the |
|
696 # cache |
|
697 if not self._criteria_cache: |
|
698 self._criteria_cache = self._criteria(self._criteria_root) |
|
699 # now return cached _criteria class |
|
700 return self._criteria_cache |
|
701 |
|
702 def open_database(self, db_file): |
|
703 """ |
|
704 Sets self._db (opens database object) and errors if already set or file |
|
705 does not yet exist |
|
706 Args: A file path to an SQLite3 database |
|
707 Raises: SystemExit if path does not exist, |
|
708 AssertionError if self._db is already set |
|
709 Returns: Nothing |
|
710 """ |
|
711 if not os.path.exists(db_file): |
|
712 raise SystemExit(_("Error:\tFile %s is not a valid database " |
|
713 "file") % db_file) |
|
714 elif self._db is None: |
|
715 self._db = AIdb.DB(db_file, commit=True) |
|
716 else: |
|
717 raise AssertionError('Opening database when already open!') |
|
718 |
|
719 def get_database(self): |
|
720 """ |
|
721 Returns self._db (database object) and errors if not set |
|
722 Raises: AssertionError if self._db is not yet set |
|
723 Returns: SQLite3 database object |
|
724 """ |
|
725 if isinstance(self._db, AIdb.DB): |
|
726 return(self._db) |
|
727 else: |
|
728 raise AssertionError('Database not yet open!') |
|
729 |
|
730 database = property(get_database, open_database, None, |
|
731 "Holds database object for criteria database") |
|
732 |
|
733 def get_service(self): |
|
734 """ |
|
735 Returns self._service and errors if not yet set |
|
736 Raises: AssertionError if self._service is not yet set |
|
737 Returns: String object |
|
738 """ |
|
739 if self._service is not None: |
|
740 return(self._service) |
|
741 else: |
|
742 raise AssertionError('Service not yet set!') |
|
743 |
|
744 def set_service(self, serv=None): |
|
745 """ |
|
746 Sets self._service and errors if already set |
|
747 Args: A string path to an AI service directory |
|
748 Raises: SystemExit if path does not exist, |
|
749 AssertionError if self._service is already set |
|
750 Returns: Nothing |
|
751 """ |
|
752 if not os.path.isdir(serv): |
|
753 raise SystemExit(_("Error:\tDirectory %s is not a valid AI " |
|
754 "directory") % db_file) |
|
755 elif self._service is None: |
|
756 self._service = os.path.abspath(serv) |
|
757 else: |
|
758 raise AssertionError('Setting service when already set!') |
|
759 |
|
760 service = property(get_service, set_service, None, |
|
761 "Holds path to service directory (i.e. /var/ai/46501)") |
|
762 |
|
763 def find_SC_from_crit_man(self): |
|
764 """ |
|
765 Find SC manifests as referenced in the criteria manifest |
|
766 Preconditions: self._criteria_root is a valid XML DOM |
|
767 Postconditions: self._smfDict will be a dictionary containing all |
|
768 SC manifest DOMs |
|
769 Raises: SystemExit for XML processing errors |
|
770 for two SC manifests named the same |
|
771 AssertionError if _critteria_root not set |
|
772 Args: None |
|
773 Returns: None |
|
774 """ |
|
775 if self._criteria_root is None: |
|
776 raise AssertionError(_("Error:\t _criteria_root not set!")) |
|
777 try: |
|
778 root = self._criteria_root.iterfind(".//sc_manifest_file") |
|
779 except lxml.etree.LxmlError, e: |
|
780 raise SystemExit(_("Error:\tCriteria manifest error:%s") % e) |
|
781 # for each SC manifest file: get the URI and verify it, adding it to the |
|
782 # dictionary of SMF SC manifests (this means we can support a criteria |
|
783 # manifest with multiple SC manifests embedded or referenced) |
|
784 for SC_man in root: |
|
785 if SC_man.attrib['name'] in self._smfDict: |
|
786 raise SystemExit(_("Error:\tTwo SC manfiests with name %s") % |
|
787 SC_man.attrib['name']) |
|
788 # if this is an absolute path just hand it off |
|
789 if os.path.isabs(str(SC_man.attrib['URI'])): |
|
790 self._smfDict[SC_man.attrib['name']] = \ |
|
791 self.verify_SC_manifest(SC_man.attrib['URI']) |
|
792 # this is not an absolute path - make it one |
|
793 else: |
|
794 self._smfDict[SC_man.attrib['name']] = \ |
|
795 self.verify_SC_manifest(os.path.join(os.path.dirname( |
|
796 self.criteria_path), |
|
797 SC_man.attrib['URI'])) |
|
798 try: |
|
799 root = self._criteria_root.iterfind(".//sc_embedded_manifest") |
|
800 except lxml.etree.LxmlError, e: |
|
801 raise SystemExit(_("Error:\tCriteria manifest error:%s") % e) |
|
802 # for each SC manifest embedded: verify it, adding it to the |
|
803 # dictionary of SMF SC manifests |
|
804 for SC_man in root: |
|
805 # strip the comments off the SC manifest |
|
806 xml_data = lxml.etree.tostring(SC_man.getchildren()[0]) |
|
807 xml_data = xml_data.replace("<!-- ", "").replace(" -->", "") |
|
808 xml_data = StringIO.StringIO(xml_data) |
|
809 # parse and read in the SC manifest |
|
810 self._smfDict[SC_man.attrib['name']] = \ |
|
811 self.verify_SC_manifest(xml_data, name=SC_man.attrib['name']) |
|
812 |
|
813 def find_AI_from_criteria(self): |
|
814 """ |
|
815 Find A/I manifest as referenced or embedded in criteria manifest |
|
816 Preconditions: self._criteria_root is a valid XML DOM |
|
817 Postconditions: self.manifest_path will be set if using a free-standing |
|
818 AI manifest otherwise self._AI_root will eb set to a |
|
819 valid XML DOM for the AI manifest |
|
820 Raises: SystemExit for XML processing errors |
|
821 for no ai_manifest_file specification |
|
822 AssertionError if _critteria_root not set |
|
823 """ |
|
824 if self._criteria_root is None: |
|
825 raise AssertionError(_("Error:\t_criteria_root not set!")) |
|
826 try: |
|
827 root = self._criteria_root.find(".//ai_manifest_file") |
|
828 except lxml.etree.LxmlError, e: |
|
829 raise SystemExit(_("Error:\tCriteria manifest error:%s") % e) |
|
830 if not isinstance(root, lxml.etree._Element): |
|
831 try: |
|
832 root = self._criteria_root.find(".//ai_embedded_manifest") |
|
833 except lxml.etree.LxmlError, e: |
|
834 raise SystemExit(_("Error:\tCriteria manifest error:%s") % e) |
|
835 if not isinstance(root, lxml.etree._Element): |
|
836 raise SystemExit(_("Error:\tNo <ai_manifest_file> or " + |
|
837 "<ai_embedded_manifest> element in " |
|
838 "criteria manifest.")) |
|
839 try: |
|
840 root.attrib['URI'] |
|
841 except KeyError: |
|
842 self._AI_root = \ |
|
843 lxml.etree.tostring(root.find(".//ai_manifest")) |
|
844 return |
|
845 if os.path.isabs(root.attrib['URI']): |
|
846 self.manifest_path = root.attrib['URI'] |
|
847 else: |
|
848 # if we do not have an absolute path try using the criteria |
|
849 # manifest's location for a base |
|
850 self.manifest_path = \ |
|
851 os.path.join(os.path.dirname(self.criteria_path), |
|
852 root.attrib['URI']) |
|
853 @property |
|
854 def AI_schema(self): |
|
855 """ |
|
856 Returns self._AIschema and errors if not yet set |
|
857 Args: None |
|
858 Raises: AssertionError if self._AIschema is not yet set |
|
859 Returns: String object |
|
860 """ |
|
861 if self._AIschema is not None: |
|
862 return (self._AIschema) |
|
863 else: |
|
864 raise AssertionError('AIschema not set') |
|
865 |
|
866 def set_AI_schema(self): |
|
867 """ |
|
868 Sets self._AIschema and errors if imagepath not yet set. |
|
869 Args: None |
|
870 Raises: SystemExit if unable to find a valid AI schema |
|
871 Returns: None |
|
872 """ |
|
873 if os.path.exists(os.path.join(self.image_path, |
|
874 IMG_AI_MANIFEST_SCHEMA)): |
|
875 self._AIschema = os.path.join(self.image_path, |
|
876 IMG_AI_MANIFEST_SCHEMA) |
|
877 else: |
|
878 if os.path.exists(SYS_AI_MANIFEST_SCHEMA): |
|
879 self._AIschema = SYS_AI_MANIFEST_SCHEMA |
|
880 print (_("Warning: Using A/I manifest schema <%s>\n") % |
|
881 self._AIschema) |
|
882 else: |
|
883 raise SystemExit(_("Error:\tUnable to find an A/I schema!")) |
|
884 |
|
885 def get_image_path(self): |
|
886 """ |
|
887 Returns self._imagepath and errors if not set |
|
888 Raises: AssertionError if self._imagepath is not yet set |
|
889 Returns: String object |
|
890 """ |
|
891 if self._imagepath is not None: |
|
892 return (self._imagepath) |
|
893 else: |
|
894 raise AssertionError('Imagepath not set') |
|
895 |
|
896 def set_image_path(self, imagepath): |
|
897 """ |
|
898 Sets self._imagepath but exits if already set or not a directory |
|
899 Args: image path to a valid AI image |
|
900 Raises: SystemExit if image path provided is not a directory |
|
901 AssertionError if image path is already set |
|
902 Returns: None |
|
903 """ |
|
904 if not os.path.isdir(imagepath): |
|
905 raise SystemExit(_("Error:\tInvalid imagepath " + |
|
906 "directory: %s") % imagepath) |
|
907 if self._imagepath is None: |
|
908 self._imagepath = os.path.abspath(imagepath) |
|
909 else: |
|
910 raise AssertionError('imagepath already set') |
|
911 |
|
912 image_path = property(get_image_path, set_image_path, None, |
|
913 "Holds path to service's AI image") |
|
914 |
|
915 def get_manifest_path(self): |
|
916 """ |
|
917 Returns self._manifest and errors if not set |
|
918 Raises: AssertionError if self._manifest is not yet set |
|
919 Returns: String object path to AI manifest |
|
920 """ |
|
921 if self._manifest is not None: |
|
922 return(self._manifest) |
|
923 else: |
|
924 raise AssertionError('Manifest path not yet set!') |
|
925 |
|
926 def set_manifest_path(self, mani=None): |
|
927 """ |
|
928 Sets self._manifest and errors if already set |
|
929 Args: path to an AI manifest |
|
930 Raises: AssertionError if manifest is already set |
|
931 Returns: None |
|
932 """ |
|
933 if self._manifest is None: |
|
934 self._manifest = os.path.abspath(mani) |
|
935 else: |
|
936 raise AssertionError('Setting manifest when already set!') |
|
937 |
|
938 manifest_path = property(get_manifest_path, set_manifest_path, None, |
|
939 "Holds path to AI manifest being published") |
|
940 @property |
|
941 def manifest_name(self): |
|
942 """ |
|
943 Returns: manifest name as defined in the A/I manifest (ensuring .xml is |
|
944 applied to the string) |
|
945 Raises: SystemExit if <ai_manifest> tag can not be found |
|
946 """ |
|
947 if self._AI_root.getroot().tag == "ai_manifest": |
|
948 name = self._AI_root.getroot().attrib['name'] |
|
949 else: |
|
950 raise SystemExit(_("Error:\tCan not find <ai_manifest> tag!")) |
|
951 # everywhere we expect manifest names to be file names so ensure |
|
952 # the name matches |
|
953 if not name.endswith('.xml'): |
|
954 name += ".xml" |
|
955 return name |
|
956 |
|
957 def verify_AI_manifest(self): |
|
958 """ |
|
959 Used for verifying and loading AI manifest as defined by |
|
960 DataFiles._AIschema. |
|
961 Args: None. |
|
962 Postconditions: Sets DataFiles._AI_root on success to a XML DOM |
|
963 Raises: SystemExit on file open error or validation error. |
|
964 """ |
|
965 try: |
|
966 schema = file(self.AI_schema, 'r') |
|
967 except IOError: |
|
968 raise SystemExit(_("Error:\tCan not open: %s ") % |
|
969 self.AI_schema) |
|
970 try: |
|
971 xml_data = file(self.manifest_path, 'r') |
|
972 except IOError: |
|
973 raise SystemExit(_("Error:\tCan not open: %s ") % |
|
974 self.manifest_path) |
|
975 except AssertionError: |
|
976 # manifest path will be unset if we're not using a separate file for |
|
977 # A/I manifest so we must emulate a file |
|
978 xml_data = StringIO.StringIO(self._AI_root) |
|
979 self._AI_root = verifyXML.verifyRelaxNGManifest(schema, xml_data) |
|
980 if isinstance(self._AI_root, lxml.etree._LogEntry): |
|
981 # catch if we area not using a manifest we can name with |
|
982 # manifest_path |
|
983 try: |
|
984 raise SystemExit(_("Error:\tFile %s failed validation:\n\t%s") % |
|
985 (os.path.basename(self.manifest_path), |
|
986 self._AI_root.message)) |
|
987 # manifest_path will throw an AssertionError if it does not have |
|
988 # a path use a different error message |
|
989 except AssertionError: |
|
990 raise SystemExit(_("Error:\tA/I manifest failed validation:" |
|
991 "\n\t%s") % self._AI_root.message) |
|
992 |
|
993 |
|
994 def verify_SC_manifest(self, data, name=None): |
|
995 """ |
|
996 Used for verifying and loading SC manifest |
|
997 Args: data - file path, or StringIO object. |
|
998 name - Optionally, takes a name to provide error output, |
|
999 as a StringIO object will not have a file path to |
|
1000 provide. |
|
1001 Returns: Provide an XML DOM for the SC manifest |
|
1002 Raises: SystemExit on validation or file open error. |
|
1003 """ |
|
1004 if not isinstance(data, StringIO.StringIO): |
|
1005 try: |
|
1006 data = file(data, 'r') |
|
1007 except IOError: |
|
1008 if name is None: |
|
1009 raise SystemExit(_("Error:\tCan not open: %s") % data) |
|
1010 else: |
|
1011 raise SystemExit(_("Error:\tCan not open: %s") % name) |
|
1012 xml_root = verifyXML.verifyDTDManifest(data, self.smfDtd) |
|
1013 if isinstance(xml_root, list): |
|
1014 if not isinstance(data, StringIO.StringIO): |
|
1015 print >> sys.stderr, (_("Error:\tFile %s failed validation:") % |
|
1016 data.name) |
|
1017 else: |
|
1018 print >> sys.stderr, (_("Error:\tSC Manifest %s failed " |
|
1019 "validation:") % name) |
|
1020 for err in xml_root: |
|
1021 print >> sys.stderr, err |
|
1022 raise SystemExit() |
|
1023 return(xml_root) |
|
1024 |
|
1025 def verifyCriteria(self): |
|
1026 """ |
|
1027 Used for verifying and loading criteria XML |
|
1028 Raises SystemExit: |
|
1029 *if the schema does not open |
|
1030 *if the XML file does not open |
|
1031 *if the XML is invalid according to the schema |
|
1032 Postconditions: self._criteria_root is a valid XML DOM of the criteria |
|
1033 manifest and all MAC and IPv4 values are formatted |
|
1034 according to verifyXML.prepValuesAndRanges() |
|
1035 """ |
|
1036 try: |
|
1037 schema = file(self.criteriaSchema, 'r') |
|
1038 except IOError: |
|
1039 raise SystemExit(_("Error:\tCan not open: %s") % |
|
1040 self.criteriaSchema) |
|
1041 try: |
|
1042 file(self.criteria_path, 'r') |
|
1043 except IOError: |
|
1044 raise SystemExit(_("Error:\tCan not open: %s") % self.criteria_path) |
|
1045 self._criteria_root = (verifyXML.verifyRelaxNGManifest(schema, |
|
1046 self.criteria_path)) |
|
1047 if isinstance(self._criteria_root, lxml.etree._LogEntry): |
|
1048 raise SystemExit(_("Error:\tFile %s failed validation:\n\t%s") % |
|
1049 (self.criteria_path, self._criteria_root.message)) |
|
1050 try: |
|
1051 verifyXML.prepValuesAndRanges(self._criteria_root, |
|
1052 self.database) |
|
1053 except ValueError, e: |
|
1054 raise SystemExit(_("Error:\tCriteria manifest error: %s") % e) |
|
1055 |
|
1056 |
|
1057 if __name__ == '__main__': |
|
1058 gettext.install("ai", "/usr/lib/locale") |
|
1059 |
|
1060 # check that we are root |
|
1061 if os.geteuid() != 0: |
|
1062 raise SystemExit(_("Error:\tNeed root privileges to execute")) |
|
1063 |
|
1064 # load in all the options and file data |
|
1065 data = parse_options() |
|
1066 |
|
1067 # if we have a default manifest do default manifest handling |
|
1068 if data.manifest_name == "default.xml": |
|
1069 do_default(data) |
|
1070 |
|
1071 # if we have a non-default manifest first ensure it is a unique criteria |
|
1072 # set and then, if unique, add the manifest to the criteria database |
|
1073 else: |
|
1074 # if we have a None criteria from the criteria list then the manifest |
|
1075 # has no criteria which is illegal for a non-default manifest |
|
1076 if not data.criteria: |
|
1077 raise SystemExit(_("Error:\tNo criteria found " + |
|
1078 "in non-default manifest -- " |
|
1079 "at least one criterion needed!")) |
|
1080 find_colliding_manifests(data, find_colliding_criteria(data)) |
|
1081 insert_SQL(data) |
|
1082 |
|
1083 # move the manifest into place |
|
1084 place_manifest(data) |
|