1# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic@suse.de> 2# See COPYING for license information. 3 4import copy 5import os 6import sys 7import re 8import fnmatch 9import time 10import collections 11from lxml import etree 12from . import config 13from . import options 14from . import constants 15from . import tmpfiles 16from . import clidisplay 17from . import idmgmt 18from . import schema 19from . import utils 20from . import cibverify 21from . import parse 22from . import ordereddict 23from . import orderedset 24from . import cibstatus 25from . import crm_gv 26from . import ui_utils 27from . import userdir 28from .ra import get_ra, get_properties_list, get_pe_meta, get_properties_meta 29from .msg import common_warn, common_err, common_debug, common_info, err_buf 30from .msg import common_error, constraint_norefobj_err, cib_parse_err, no_object_err 31from .msg import missing_obj_err, common_warning, update_err, unsupported_err, empty_cib_err 32from .msg import invalid_id_err, cib_ver_unsupported_err 33from .utils import ext_cmd, safe_open_w, pipe_string, safe_close_w, crm_msec 34from .utils import ask, lines2cli, olist 35from .utils import page_string, cibadmin_can_patch, str2tmp, ensure_sudo_readable 36from .utils import run_ptest, is_id_valid, edit_file, get_boolean, filter_string 37from .xmlutil import is_child_rsc, rsc_constraint, sanitize_cib, rename_id, get_interesting_nodes 38from .xmlutil import is_pref_location, get_topnode, new_cib, get_rscop_defaults_meta_node 39from .xmlutil import rename_rscref, is_ms, silly_constraint, is_container, fix_comments 40from .xmlutil import sanity_check_nvpairs, merge_nodes, op2list, mk_rsc_type, is_resource 41from .xmlutil import stuff_comments, is_comment, is_constraint, read_cib, processing_sort_cli 42from .xmlutil import find_operation, get_rsc_children_ids, is_primitive, referenced_resources 43from .xmlutil import cibdump2elem, processing_sort, get_rsc_ref_ids, merge_tmpl_into_prim 44from .xmlutil import remove_id_used_attributes, get_top_cib_nodes 45from .xmlutil import merge_attributes, is_cib_element, sanity_check_meta 46from .xmlutil import is_simpleconstraint, is_template, rmnode, is_defaults, is_live_cib 47from .xmlutil import get_rsc_operations, delete_rscref, xml_equals, lookup_node, RscState 48from .xmlutil import cibtext2elem, is_related, check_id_ref, xml_tostring 49from .xmlutil import sanitize_cib_for_patching 50from .cliformat import get_score, nvpairs2list, abs_pos_score, cli_acl_roleref, nvpair_format 51from .cliformat import cli_nvpair, cli_acl_rule, rsc_set_constraint, get_kind, head_id_format 52from .cliformat import simple_rsc_constraint, cli_rule, cli_format 53from .cliformat import cli_acl_role, cli_acl_permission, cli_path 54 55 56def show_unrecognized_elems(cib_elem): 57 try: 58 conf = cib_elem.findall("configuration")[0] 59 except IndexError: 60 common_warn("CIB has no configuration element") 61 return False 62 rc = True 63 for topnode in conf.iterchildren(): 64 if is_defaults(topnode) or topnode.tag == "fencing-topology": 65 continue 66 for c in topnode.iterchildren(): 67 if c.tag not in cib_object_map: 68 common_warn("unrecognized CIB element %s" % c.tag) 69 rc = False 70 return rc 71 72 73# 74# object sets (enables operations on sets of elements) 75# 76def mkset_obj(*args): 77 if not cib_factory.is_cib_sane(): 78 raise ValueError("CIB is not valid") 79 if args and args[0] == "xml": 80 return CibObjectSetRaw(*args[1:]) 81 return CibObjectSetCli(*args) 82 83 84def set_graph_attrs(gv_obj, obj_type): 85 try: 86 for attr, attr_v in constants.graph['*'].items(): 87 gv_obj.new_graph_attr(attr, attr_v) 88 except KeyError: 89 pass 90 try: 91 for attr, attr_v in constants.graph[obj_type].items(): 92 gv_obj.new_graph_attr(attr, attr_v) 93 except KeyError: 94 pass 95 96 97def set_obj_attrs(gv_obj, obj_id, obj_type): 98 try: 99 for attr, attr_v in constants.graph['*'].items(): 100 gv_obj.new_attr(obj_id, attr, attr_v) 101 except KeyError: 102 pass 103 try: 104 for attr, attr_v in constants.graph[obj_type].items(): 105 gv_obj.new_attr(obj_id, attr, attr_v) 106 except KeyError: 107 pass 108 109 110def set_edge_attrs(gv_obj, edge_id, obj_type): 111 try: 112 for attr, attr_v in constants.graph[obj_type].items(): 113 gv_obj.new_edge_attr(edge_id, attr, attr_v) 114 except KeyError: 115 pass 116 117 118def fill_nvpairs(name, node, attrs, id_hint): 119 ''' 120 Fill the container node with attrs: 121 name: name of container 122 node: container Element 123 attrs: dict containing values 124 id_hint: used to generate unique ids for nvpairs 125 ''' 126 subpfx = constants.subpfx_list.get(name, '') 127 subpfx = "%s_%s" % (id_hint, subpfx) if subpfx else id_hint 128 nvpair_pfx = node.get("id") or subpfx 129 for n, v in attrs.items(): 130 nvpair = etree.SubElement(node, "nvpair", name=n) 131 if v is not None: 132 nvpair.set("value", v) 133 idmgmt.set_id(nvpair, None, nvpair_pfx) 134 return node 135 136 137def mkxmlnvpairs(name, attrs, id_hint): 138 ''' 139 name: Name of the element. 140 attrs: dict containing a set of nvpairs. 141 hint: Used to generate ids. 142 143 Example: instance_attributes, {name: value...}, <hint> 144 145 Notes: 146 147 Other tags not containing nvpairs are fine if the dict is empty. 148 149 cluster_property_set and defaults have nvpairs as direct children. 150 In that case, use the id_hint directly as id. 151 This is important in case there are multiple sets. 152 153 ''' 154 xml_node_type = "meta_attributes" if name in constants.defaults_tags else name 155 node = etree.Element(xml_node_type) 156 notops = name != "operations" 157 158 if (name == "cluster_property_set" or name in constants.defaults_tags) and id_hint: 159 node.set("id", id_hint) 160 id_ref = attrs.get("$id-ref") 161 if id_ref: 162 id_ref_2 = cib_factory.resolve_id_ref(name, id_ref) 163 node.set("id-ref", id_ref_2) 164 if notops: 165 return node # id_ref is the only attribute (if not operations) 166 if '$id-ref' in attrs: 167 del attrs['$id-ref'] 168 v = attrs.get('$id') 169 if v: 170 node.set("id", v) 171 del attrs['$id'] 172 elif name in constants.nvset_cli_names: 173 node.set("id", id_hint) 174 else: 175 # operations don't need no id 176 idmgmt.set_id(node, None, id_hint, id_required=notops) 177 return fill_nvpairs(name, node, attrs, id_hint) 178 179 180def copy_nvpair(nvpairs, nvp, id_hint=None): 181 """ 182 Copies the given nvpair into the given tag containing nvpairs 183 """ 184 common_debug("copy_nvpair: %s" % (xml_tostring(nvp))) 185 if 'value' not in nvp.attrib: 186 nvpairs.append(copy.deepcopy(nvp)) 187 return 188 n = nvp.get('name') 189 if id_hint is None: 190 id_hint = n 191 for nvp2 in nvpairs: 192 if nvp2.get('name') == n: 193 nvp2.set('value', nvp.get('value')) 194 break 195 else: 196 m = copy.deepcopy(nvp) 197 nvpairs.append(m) 198 if 'id' not in m.attrib: 199 m.set('id', idmgmt.new(m, id_hint)) 200 201 202def copy_nvpairs(tonode, fromnode): 203 """ 204 copy nvpairs from fromnode to tonode. 205 things to copy can be nvpairs, comments or rules. 206 """ 207 def copy_comment(cnode): 208 for nvp2 in tonode: 209 if is_comment(nvp2) and nvp2.text == cnode.text: 210 break # no need to copy 211 else: 212 tonode.append(copy.deepcopy(cnode)) 213 214 def copy_id(node): 215 nid = node.get('id') 216 for nvp2 in tonode: 217 if nvp2.get('id') == nid: 218 tonode.replace(nvp2, copy.deepcopy(node)) 219 break 220 else: 221 tonode.append(copy.deepcopy(node)) 222 223 common_debug("copy_nvpairs: %s -> %s" % (xml_tostring(fromnode), xml_tostring(tonode))) 224 id_hint = tonode.get('id') 225 for c in fromnode: 226 if is_comment(c): 227 copy_comment(c) 228 elif c.tag == "nvpair": 229 copy_nvpair(tonode, c, id_hint=id_hint) 230 elif 'id' in c.attrib: # ok, it has an id, we can work with this 231 copy_id(c) 232 else: # no idea what this is, just copy it 233 tonode.append(copy.deepcopy(c)) 234 235 236class CibObjectSet(object): 237 ''' 238 Edit or display a set of cib objects. 239 repr() for objects representation and 240 save() used to store objects into internal structures 241 are defined in subclasses. 242 ''' 243 def __init__(self, *args): 244 self.args = args 245 self._initialize() 246 247 def _initialize(self): 248 rc, self.obj_set = cib_factory.mkobj_set(*self.args) 249 self.search_rc = rc 250 self.all_set = cib_factory.get_all_obj_set() 251 self.obj_ids = orderedset.oset([o.obj_id for o in self.obj_set]) 252 self.all_ids = orderedset.oset([o.obj_id for o in self.all_set]) 253 self.locked_ids = self.all_ids - self.obj_ids 254 255 def _open_url(self, src): 256 if src == "-": 257 return sys.stdin 258 import urllib.request 259 import urllib.error 260 import urllib.parse 261 try: 262 ret = urllib.request.urlopen(src) 263 return ret 264 except (urllib.error.URLError, ValueError): 265 pass 266 try: 267 ret = open(src) 268 return ret 269 except IOError as e: 270 common_err("could not open %s: %s" % (src, e)) 271 return False 272 273 def _pre_edit(self, s): 274 '''Extra processing of the string to be editted''' 275 return s 276 277 def _post_edit(self, s): 278 '''Extra processing after editing''' 279 return s 280 281 def _edit_save(self, s): 282 ''' 283 Save string s to a tmp file. Invoke editor to edit it. 284 Parse/save the resulting file. In case of syntax error, 285 allow user to reedit. 286 If no changes are done, return silently. 287 ''' 288 rc = False 289 try: 290 s = self._pre_edit(s) 291 filehash = hash(s) 292 tmp = str2tmp(s) 293 if not tmp: 294 return False 295 while not rc: 296 if edit_file(tmp) != 0: 297 break 298 s = open(tmp).read() 299 if hash(s) != filehash: 300 ok = self.save(self._post_edit(s)) 301 if not ok and config.core.force: 302 common_err("Save failed and --force is set, " + 303 "aborting edit to avoid infinite loop") 304 elif not ok and ask("Edit or discard changes (yes to edit, no to discard)?"): 305 continue 306 rc = True 307 os.unlink(tmp) 308 except OSError as e: 309 common_debug("unlink(%s) failure: %s" % (tmp, e)) 310 except IOError as msg: 311 common_err(msg) 312 return rc 313 314 def edit(self): 315 if options.batch: 316 common_info("edit not allowed in batch mode") 317 return False 318 with clidisplay.nopretty(): 319 s = self.repr() 320 # don't allow edit if one or more elements were not 321 # found 322 if not self.search_rc: 323 return self.search_rc 324 return self._edit_save(s) 325 326 def _filter_save(self, fltr, s): 327 ''' 328 Pipe string s through a filter. Parse/save the output. 329 If no changes are done, return silently. 330 ''' 331 rc, outp = filter_string(fltr, s) 332 if rc != 0: 333 return False 334 if hash(outp) == hash(s): 335 return True 336 return self.save(outp) 337 338 def filter(self, fltr): 339 with clidisplay.nopretty(): 340 s = self.repr(format_mode=-1) 341 # don't allow filter if one or more elements were not 342 # found 343 if not self.search_rc: 344 return self.search_rc 345 return self._filter_save(fltr, s) 346 347 def save_to_file(self, fname): 348 f = safe_open_w(fname) 349 if not f: 350 return False 351 rc = True 352 with clidisplay.nopretty(): 353 s = self.repr() 354 if s: 355 f.write(s) 356 f.write('\n') 357 elif self.obj_set: 358 rc = False 359 safe_close_w(f) 360 return rc 361 362 def _get_gv_obj(self, gtype): 363 if not self.obj_set: 364 return True, None 365 if gtype not in crm_gv.gv_types: 366 common_err("graphviz type %s is not supported" % gtype) 367 return False, None 368 gv_obj = crm_gv.gv_types[gtype]() 369 set_graph_attrs(gv_obj, ".") 370 return True, gv_obj 371 372 def _graph_repr(self, gv_obj): 373 '''Let CIB elements produce graph elements. 374 ''' 375 for obj in processing_sort_cli(list(self.obj_set)): 376 obj.repr_gv(gv_obj, from_grp=False) 377 378 def query_graph(self, *args): 379 "usage: graph <pe> [<gtype> [<file> [<img_format>]]]" 380 rc, gtype, outf, ftype = ui_utils.graph_args(args) 381 if not rc: 382 return None 383 rc, d = utils.load_graphviz_file(userdir.GRAPHVIZ_USER_FILE) 384 if rc and d: 385 constants.graph = d 386 if outf is None: 387 return self.show_graph(gtype) 388 elif gtype == ftype: 389 rc = self.save_graph(gtype, outf) 390 else: 391 rc = self.graph_img(gtype, outf, ftype) 392 return rc 393 394 def show_graph(self, gtype): 395 '''Display graph using dotty''' 396 rc, gv_obj = self._get_gv_obj(gtype) 397 if not rc or not gv_obj: 398 return rc 399 self._graph_repr(gv_obj) 400 return gv_obj.display() 401 402 def graph_img(self, gtype, outf, img_type): 403 '''Render graph to image and save it to a file (done by 404 dot(1))''' 405 rc, gv_obj = self._get_gv_obj(gtype) 406 if not rc or not gv_obj: 407 return rc 408 self._graph_repr(gv_obj) 409 return gv_obj.image(img_type, outf) 410 411 def save_graph(self, gtype, outf): 412 '''Save graph to a file''' 413 rc, gv_obj = self._get_gv_obj(gtype) 414 if not rc or not gv_obj: 415 return rc 416 self._graph_repr(gv_obj) 417 return gv_obj.save(outf) 418 419 def show(self): 420 s = self.repr() 421 if s: 422 page_string(s) 423 return self.search_rc 424 425 def import_file(self, method, fname): 426 ''' 427 method: update or replace or push 428 ''' 429 if not cib_factory.is_cib_sane(): 430 return False 431 f = self._open_url(fname) 432 if not f: 433 return False 434 s = f.read() 435 if f != sys.stdin: 436 f.close() 437 if method == 'push': 438 return self.save(s, remove=True, method='update') 439 else: 440 return self.save(s, remove=False, method=method) 441 442 def repr(self, format_mode=0): 443 ''' 444 Return a string with objects's representations (either 445 CLI or XML). 446 ''' 447 return '' 448 449 def save(self, s, remove=True, method='replace'): 450 ''' 451 For each object: 452 - try to find a corresponding object in obj_set 453 - if (update and not found) or found: 454 replace the object in the obj_set with 455 the new object 456 - if not found: create new 457 See below for specific implementations. 458 ''' 459 pass 460 461 def _check_unique_clash(self, set_obj_all): 462 'Check whether resource parameters with attribute "unique" clash' 463 def process_primitive(prim, clash_dict): 464 ''' 465 Update dict clash_dict with 466 (ra_class, ra_provider, ra_type, name, value) -> [ resourcename ] 467 if parameter "name" should be unique 468 ''' 469 ra_id = prim.get("id") 470 r_node = reduce_primitive(prim) 471 if r_node is None: 472 return # template not defined yet 473 ra_type = node.get("type") 474 ra_class = node.get("class") 475 ra_provider = node.get("provider") 476 ra = get_ra(r_node) 477 if ra.mk_ra_node() is None: # no RA found? 478 return 479 ra_params = ra.params() 480 for p in r_node.xpath("./instance_attributes/nvpair"): 481 name, value = p.get("name"), p.get("value") 482 if value is None: 483 continue 484 # don't fail if the meta-data doesn't contain the 485 # expected attributes 486 if name in ra_params and ra_params[name].get("unique") == "1": 487 clash_dict[(ra_class, ra_provider, ra_type, name, value)].append(ra_id) 488 return 489 # we check the whole CIB for clashes as a clash may originate between 490 # an object already committed and a new one 491 check_set = set([o.obj_id 492 for o in self.obj_set 493 if o.obj_type == "primitive"]) 494 if not check_set: 495 return 0 496 clash_dict = collections.defaultdict(list) 497 for obj in set_obj_all.obj_set: 498 node = obj.node 499 if is_primitive(node): 500 process_primitive(node, clash_dict) 501 # but we only warn if a 'new' object is involved 502 rc = 0 503 for param, resources in list(clash_dict.items()): 504 # at least one new object must be involved 505 if len(resources) > 1 and len(set(resources) & check_set) > 0: 506 rc = 2 507 msg = 'Resources %s violate uniqueness for parameter "%s": "%s"' % ( 508 ",".join(sorted(resources)), param[3], param[4]) 509 common_warning(msg) 510 return rc 511 512 def semantic_check(self, set_obj_all): 513 ''' 514 Test objects for sanity. This is about semantics. 515 ''' 516 rc = self._check_unique_clash(set_obj_all) 517 for obj in sorted(self.obj_set, key=lambda x: x.obj_id): 518 rc |= obj.check_sanity() 519 return rc 520 521 522class CibObjectSetCli(CibObjectSet): 523 ''' 524 Edit or display a set of cib objects (using cli notation). 525 ''' 526 vim_stx_str = "# vim: set filetype=pcmk:\n" 527 528 def __init__(self, *args): 529 CibObjectSet.__init__(self, *args) 530 531 def repr_nopretty(self, format_mode=1): 532 with clidisplay.nopretty(): 533 return self.repr(format_mode=format_mode) 534 535 def repr(self, format_mode=1): 536 "Return a string containing cli format of all objects." 537 if not self.obj_set: 538 return '' 539 return '\n'.join(obj.repr_cli(format_mode=format_mode) 540 for obj in processing_sort_cli(list(self.obj_set))) 541 542 def _pre_edit(self, s): 543 '''Extra processing of the string to be edited''' 544 if config.core.editor.startswith("vi"): 545 return "%s\n%s" % (s, self.vim_stx_str) 546 return s 547 548 def _post_edit(self, s): 549 if config.core.editor.startswith("vi"): 550 return s.replace(self.vim_stx_str, "") 551 return s 552 553 def _get_id(self, node): 554 ''' 555 Get the id from a CLI representation. Normally, it should 556 be value of the id attribute, but sometimes the 557 attribute is missing. 558 ''' 559 if node.tag == 'fencing-topology': 560 return 'fencing_topology' 561 if node.tag in constants.defaults_tags: 562 return node[0].get('id') 563 return node.get('id') 564 565 def save(self, s, remove=True, method='replace'): 566 ''' 567 Save a user supplied cli format configuration. 568 On errors user is typically asked to review the 569 configuration (for instance on editting). 570 571 On errors, the user is asked to edit again (if we're 572 coming from edit). The original CIB is preserved and no 573 changes are made. 574 ''' 575 diff = CibDiff(self) 576 rc = True 577 err_buf.start_tmp_lineno() 578 comments = [] 579 for cli_text in lines2cli(s): 580 err_buf.incr_lineno() 581 node = parse.parse(cli_text, comments=comments) 582 if node not in (False, None): 583 rc = rc and diff.add(node) 584 elif node is False: 585 rc = False 586 err_buf.stop_tmp_lineno() 587 588 # we can't proceed if there was a syntax error, but we 589 # can ask the user to fix problems 590 if not rc: 591 return rc 592 593 rc = diff.apply(cib_factory, mode='cli', remove=remove, method=method) 594 if not rc: 595 self._initialize() 596 return rc 597 598 599class CibObjectSetRaw(CibObjectSet): 600 ''' 601 Edit or display one or more CIB objects (XML). 602 ''' 603 def __init__(self, *args): 604 CibObjectSet.__init__(self, *args) 605 606 def repr(self, format_mode="ignored"): 607 "Return a string containing xml of all objects." 608 cib_elem = cib_factory.obj_set2cib(self.obj_set) 609 610 from .utils import obscured 611 for nvp in cib_elem.xpath('//nvpair'): 612 if 'value' in nvp.attrib: 613 nvp.set('value', obscured(nvp.get('name'), nvp.get('value'))) 614 615 s = xml_tostring(cib_elem, pretty_print=True) 616 return '<?xml version="1.0" ?>\n' + s 617 618 def _get_id(self, node): 619 if node.tag == "fencing-topology": 620 return "fencing_topology" 621 return node.get("id") 622 623 def save(self, s, remove=True, method='replace'): 624 try: 625 cib_elem = etree.fromstring(s) 626 except etree.ParseError as msg: 627 cib_parse_err(msg, s) 628 return False 629 sanitize_cib(cib_elem) 630 if not show_unrecognized_elems(cib_elem): 631 return False 632 rc = True 633 diff = CibDiff(self) 634 for node in get_top_cib_nodes(cib_elem, []): 635 rc = diff.add(node) 636 if not rc: 637 return rc 638 rc = diff.apply(cib_factory, mode='xml', remove=remove, method=method) 639 if not rc: 640 self._initialize() 641 return rc 642 643 def verify(self): 644 if not self.obj_set: 645 return True 646 with clidisplay.nopretty(): 647 cib = self.repr(format_mode=-1) 648 rc = cibverify.verify(cib) 649 650 if rc not in (0, 1): 651 common_debug("verify (rc=%s): %s" % (rc, cib)) 652 return rc in (0, 1) 653 654 def ptest(self, nograph, scores, utilization, actions, verbosity): 655 if not cib_factory.is_cib_sane(): 656 return False 657 cib_elem = cib_factory.obj_set2cib(self.obj_set) 658 status = cibstatus.cib_status.get_status() 659 if status is None: 660 common_err("no status section found") 661 return False 662 cib_elem.append(copy.deepcopy(status)) 663 graph_s = etree.tostring(cib_elem) 664 return run_ptest(graph_s, nograph, scores, utilization, actions, verbosity) 665 666 667def find_comment_nodes(node): 668 return [c for c in node.iterchildren() if is_comment(c)] 669 670 671def fix_node_ids(node, oldnode): 672 """ 673 Fills in missing ids, getting ids from oldnode 674 as much as possible. Tries to generate reasonable 675 ids as well. 676 """ 677 hint_map = { 678 'node': 'node', 679 'primitive': 'rsc', 680 'template': 'rsc', 681 'master': 'grp', 682 'group': 'grp', 683 'clone': 'grp', 684 'rsc_location': 'location', 685 'fencing-topology': 'fencing', 686 'tags': 'tag', 687 'alerts': 'alert', 688 } 689 690 idless = set([ 691 'operations', 'fencing-topology', 'network', 'docker', 'rkt', 692 'storage', 'select', 'select_attributes', 'select_fencing', 693 'select_nodes', 'select_resources' 694 ]) 695 isref = set(['resource_ref', 'obj_ref', 'crmsh-ref']) 696 697 def needs_id(node): 698 a = node.attrib 699 if node.tag in isref: 700 return False 701 return 'id-ref' not in a and node.tag not in idless 702 703 def next_prefix(node, refnode, prefix): 704 if node.tag == 'node' and 'uname' in node.attrib: 705 return node.get('uname') 706 if 'id' in node.attrib: 707 return node.get('id') 708 return prefix 709 710 def recurse(node, oldnode, prefix): 711 refnode = lookup_node(node, oldnode) 712 if needs_id(node): 713 idmgmt.set_id(node, refnode, prefix, id_required=(node.tag not in idless)) 714 prefix = next_prefix(node, refnode, prefix) 715 for c in node.iterchildren(): 716 if not is_comment(c): 717 recurse(c, refnode if refnode is not None else oldnode, prefix) 718 719 recurse(node, oldnode, hint_map.get(node.tag, '')) 720 721 722def resolve_idref(node): 723 """ 724 resolve id-ref references that refer 725 to object ids, not attribute lists 726 """ 727 id_ref = node.get('id-ref') 728 attr_list_type = node.tag 729 obj = cib_factory.find_object(id_ref) 730 if obj: 731 nodes = obj.node.xpath(".//%s" % attr_list_type) 732 if len(nodes) > 1: 733 common_warn("%s contains more than one %s, using first" % 734 (obj.obj_id, attr_list_type)) 735 if len(nodes) > 0: 736 node_id = nodes[0].get("id") 737 if node_id: 738 return node_id 739 check_id_ref(cib_factory.get_cib(), id_ref) 740 return id_ref 741 742 743def resolve_references(node): 744 """ 745 In the output from parse(), there are 746 possible references to other nodes in 747 the CIB. This resolves those references. 748 """ 749 idrefnodes = node.xpath('.//*[@id-ref]') 750 if 'id-ref' in node.attrib: 751 idrefnodes += [node] 752 for ref in idrefnodes: 753 ref.set('id-ref', resolve_idref(ref)) 754 for ref in node.iterchildren('crmsh-ref'): 755 child_id = ref.get('id') 756 # TODO: This always refers to a resource ATM. 757 # Handle case where it may refer to a node name? 758 obj = cib_factory.find_resource(child_id) 759 common_debug("resolve_references: %s -> %s" % (child_id, obj)) 760 if obj is not None: 761 newnode = copy.deepcopy(obj.node) 762 node.replace(ref, newnode) 763 else: 764 node.remove(ref) 765 common_err("%s refers to missing object %s" % (node.get('id'), 766 child_id)) 767 768 769def id_for_node(node, id_hint=None): 770 "find id for unprocessed node" 771 root = node 772 if node.tag in constants.defaults_tags: 773 node = node[0] 774 if node.tag == 'fencing-topology': 775 obj_id = 'fencing_topology' 776 else: 777 obj_id = node.get('id') or node.get('uname') 778 if obj_id is None: 779 if node.tag == 'op': 780 if id_hint is None: 781 id_hint = node.get("rsc") 782 idmgmt.set_id(node, None, id_hint) 783 obj_id = node.get('id') 784 else: 785 defid = default_id_for_tag(root.tag) 786 if defid is not None: 787 try: 788 node.set('id', defid) 789 except TypeError as e: 790 raise ValueError('Internal error: %s (%s)' % (e, xml_tostring(node))) 791 obj_id = node.get('id') 792 idmgmt.save(obj_id) 793 if root.tag != "node" and obj_id and not is_id_valid(obj_id): 794 invalid_id_err(obj_id) 795 return None 796 return obj_id 797 798 799def postprocess_cli(node, oldnode=None, id_hint=None): 800 """ 801 input: unprocessed but parsed XML 802 output: XML, obj_type, obj_id 803 """ 804 if node.tag == 'op': 805 obj_type = 'op' 806 else: 807 obj_type = cib_object_map[node.tag][0] 808 obj_id = id_for_node(node, id_hint=id_hint) 809 if obj_id is None: 810 if obj_type == 'op': 811 # In this case, we need to delay postprocessing 812 # until we know where to insert the op 813 return node, obj_type, None 814 common_err("No ID found for %s: %s" % (obj_type, xml_tostring(node))) 815 return None, None, None 816 if node.tag in constants.defaults_tags: 817 node = node[0] 818 fix_node_ids(node, oldnode) 819 resolve_references(node) 820 if oldnode is not None: 821 remove_id_used_attributes(oldnode) 822 return node, obj_type, obj_id 823 824 825def parse_cli_to_xml(cli, oldnode=None): 826 """ 827 input: CLI text 828 output: XML, obj_type, obj_id 829 """ 830 node = None 831 comments = [] 832 if isinstance(cli, str): 833 for s in lines2cli(cli): 834 node = parse.parse(s, comments=comments) 835 else: # should be a pre-tokenized list 836 node = parse.parse(cli, comments=comments) 837 if node is False: 838 return None, None, None 839 elif node is None: 840 return None, None, None 841 return postprocess_cli(node, oldnode) 842 843 844# 845# cib element classes (CibObject the parent class) 846# 847class CibObject(object): 848 ''' 849 The top level object of the CIB. Resources and constraints. 850 ''' 851 state_fmt = "%16s %-8s%-8s%-8s%-4s" 852 set_names = {} 853 854 def __init__(self, xml_obj_type): 855 if xml_obj_type not in cib_object_map: 856 unsupported_err(xml_obj_type) 857 return 858 self.obj_type = cib_object_map[xml_obj_type][0] 859 self.parent_type = cib_object_map[xml_obj_type][2] 860 self.xml_obj_type = xml_obj_type 861 self.origin = "" # where did it originally come from? 862 self.nocli = False # we don't support this one 863 self.nocli_warn = True # don't issue warnings all the time 864 self.updated = False # was the object updated 865 self.parent = None # object superior (group/clone/ms) 866 self.children = [] # objects inferior 867 self.obj_id = None 868 self.node = None 869 870 def __str__(self): 871 return "%s:%s" % (self.obj_type, self.obj_id) 872 873 def set_updated(self): 874 self.updated = True 875 self.propagate_updated() 876 877 def dump_state(self): 878 'Print object status' 879 print(self.state_fmt % (self.obj_id, 880 self.origin, 881 self.updated, 882 self.parent and self.parent.obj_id or "", 883 len(self.children))) 884 885 def _repr_cli_xml(self, format_mode): 886 with clidisplay.nopretty(format_mode < 0): 887 h = clidisplay.keyword("xml") 888 l = xml_tostring(self.node, pretty_print=True).split('\n') 889 l = [x for x in l if x] # drop empty lines 890 return "%s %s" % (h, cli_format(l, break_lines=(format_mode > 0), xml=True)) 891 892 def _gv_rsc_id(self): 893 if self.parent and self.parent.obj_type in constants.clonems_tags: 894 return "%s:%s" % (self.parent.obj_type, self.obj_id) 895 return self.obj_id 896 897 def _set_gv_attrs(self, gv_obj, obj_type=None): 898 if not obj_type: 899 obj_type = self.obj_type 900 obj_id = self.node.get("uname") or self.obj_id 901 set_obj_attrs(gv_obj, obj_id, obj_type) 902 903 def _set_sg_attrs(self, sg_obj, obj_type=None): 904 if not obj_type: 905 obj_type = self.obj_type 906 set_graph_attrs(sg_obj, obj_type) 907 908 def _set_edge_attrs(self, gv_obj, e_id, obj_type=None): 909 if not obj_type: 910 obj_type = self.obj_type 911 set_edge_attrs(gv_obj, e_id, obj_type) 912 913 def repr_gv(self, gv_obj, from_grp=False): 914 ''' 915 Add some graphviz elements to gv_obj. 916 ''' 917 pass 918 919 def normalize_parameters(self): 920 pass 921 922 def _repr_cli_head(self, format_mode): 923 'implemented in subclasses' 924 pass 925 926 def repr_cli(self, format_mode=1): 927 ''' 928 CLI representation for the node. 929 _repr_cli_head and _repr_cli_child in subclasess. 930 ''' 931 if self.nocli: 932 return self._repr_cli_xml(format_mode) 933 l = [] 934 with clidisplay.nopretty(format_mode < 0): 935 head_s = self._repr_cli_head(format_mode) 936 # everybody must have a head 937 if not head_s: 938 return None 939 comments = [] 940 l.append(head_s) 941 desc = self.node.get("description") 942 if desc: 943 l.append(nvpair_format("description", desc)) 944 for c in self.node.iterchildren(): 945 if is_comment(c): 946 comments.append(c.text) 947 continue 948 s = self._repr_cli_child(c, format_mode) 949 if s: 950 l.append(s) 951 return self._cli_format_and_comment(l, comments, format_mode=format_mode) 952 953 def _attr_set_str(self, node): 954 ''' 955 Add $id=<id> if the set id is referenced by another 956 element. 957 958 also show rule expressions if found 959 ''' 960 961 # has_nvpairs = len(node.xpath('.//nvpair')) > 0 962 idref = node.get('id-ref') 963 964 # don't skip empty sets: skipping these breaks 965 # patching 966 # empty set 967 # if not (has_nvpairs or idref is not None): 968 # return '' 969 970 ret = "%s " % (clidisplay.keyword(self.set_names[node.tag])) 971 node_id = node.get("id") 972 if node_id is not None and cib_factory.is_id_refd(node.tag, node_id): 973 ret += "%s " % (nvpair_format("$id", node_id)) 974 elif idref is not None: 975 ret += "%s " % (nvpair_format("$id-ref", idref)) 976 977 if node.tag in ["docker", "network"]: 978 for item in node.keys(): 979 ret += "%s " % nvpair_format(item, node.get(item)) 980 if node.tag == "primitive": 981 ret += node.get('id') 982 for _type in ["port-mapping", "storage-mapping"]: 983 for c in node.iterchildren(_type): 984 ret += "%s " % _type 985 for item in c.keys(): 986 ret += "%s " % nvpair_format(item, c.get(item)) 987 988 score = node.get("score") 989 if score: 990 ret += "%s: " % (clidisplay.score(score)) 991 992 for c in node.iterchildren(): 993 if c.tag == "rule": 994 ret += "%s %s " % (clidisplay.keyword("rule"), cli_rule(c)) 995 for c in node.iterchildren(): 996 if c.tag == "nvpair": 997 ret += "%s " % (cli_nvpair(c)) 998 if ret[-1] == ' ': 999 ret = ret[:-1] 1000 return ret 1001 1002 def _repr_cli_child(self, c, format_mode): 1003 if c.tag in self.set_names: 1004 return self._attr_set_str(c) 1005 1006 def _get_oldnode(self): 1007 ''' 1008 Used to retrieve sub id's. 1009 ''' 1010 if self.obj_type == "property": 1011 return get_topnode(cib_factory.get_cib(), self.parent_type) 1012 elif self.obj_type in constants.defaults_tags: 1013 return self.node.getparent() 1014 return self.node 1015 1016 def set_id(self, obj_id=None): 1017 if obj_id is None and self.node is not None: 1018 obj_id = self.node.get("id") or self.node.get('uname') 1019 if obj_id is None: 1020 m = cib_object_map.get(self.node.tag) 1021 if m and len(m) > 3: 1022 obj_id = m[3] 1023 self.obj_id = obj_id 1024 1025 def set_nodeid(self): 1026 if self.node is not None and self.obj_id: 1027 self.node.set("id", self.obj_id) 1028 1029 def cli2node(self, cli): 1030 ''' 1031 Convert CLI representation to a DOM node. 1032 ''' 1033 oldnode = self._get_oldnode() 1034 node, obj_type, obj_id = parse_cli_to_xml(cli, oldnode) 1035 return node 1036 1037 def set_node(self, node, oldnode=None): 1038 self.node = node 1039 self.set_id() 1040 return self.node 1041 1042 def _cli_format_and_comment(self, l, comments, format_mode): 1043 ''' 1044 Format and add comment (if any). 1045 ''' 1046 s = cli_format(l, break_lines=(format_mode > 0)) 1047 cs = '\n'.join(comments) 1048 if len(comments) and format_mode >= 0: 1049 return '\n'.join([cs, s]) 1050 return s 1051 1052 def move_comments(self): 1053 ''' 1054 Move comments to the top of the node. 1055 ''' 1056 l = [] 1057 firstelem = None 1058 for n in self.node.iterchildren(): 1059 if is_comment(n): 1060 if firstelem: 1061 l.append(n) 1062 else: 1063 if not firstelem: 1064 firstelem = self.node.index(n) 1065 for comm_node in l: 1066 self.node.remove(comm_node) 1067 self.node.insert(firstelem, comm_node) 1068 firstelem += 1 1069 1070 def mknode(self, obj_id): 1071 if self.xml_obj_type in constants.defaults_tags: 1072 tag = "meta_attributes" 1073 else: 1074 tag = self.xml_obj_type 1075 self.node = etree.Element(tag) 1076 self.set_id(obj_id) 1077 self.set_nodeid() 1078 self.origin = "user" 1079 return True 1080 1081 def can_be_renamed(self): 1082 ''' 1083 Return False if this object can't be renamed. 1084 ''' 1085 if self.obj_id is None: 1086 return False 1087 rscstat = RscState() 1088 if not rscstat.can_delete(self.obj_id): 1089 common_err("cannot rename a running resource (%s)" % self.obj_id) 1090 return False 1091 if not is_live_cib() and self.node.tag == "node": 1092 common_err("cannot rename nodes") 1093 return False 1094 return True 1095 1096 def cli_use_validate(self): 1097 ''' 1098 Check validity of the object, as we know it. It may 1099 happen that we don't recognize a construct, but that the 1100 object is still valid for the CRM. In that case, the 1101 object is marked as "CLI read only", i.e. we will neither 1102 convert it to CLI nor try to edit it in that format. 1103 1104 The validation procedure: 1105 we convert xml to cli and then back to xml. If the two 1106 xml representations match then we can understand the xml. 1107 1108 Complication: 1109 There are valid variations of the XML where the CLI syntax 1110 cannot express the difference. For example, sub-tags in a 1111 <primitive> are not ordered, but the CLI syntax can only express 1112 one specific ordering. 1113 1114 This is usually not a problem unless mixing pcs and crmsh. 1115 ''' 1116 if self.node is None: 1117 return True 1118 with clidisplay.nopretty(): 1119 cli_text = self.repr_cli(format_mode=0) 1120 if not cli_text: 1121 common_debug("validation failed: %s" % (xml_tostring(self.node))) 1122 return False 1123 xml2 = self.cli2node(cli_text) 1124 if xml2 is None: 1125 common_debug("validation failed: %s -> %s" % ( 1126 xml_tostring(self.node), 1127 cli_text)) 1128 return False 1129 if not xml_equals(self.node, xml2, show=True): 1130 common_debug("validation failed: %s -> %s -> %s" % ( 1131 xml_tostring(self.node), 1132 cli_text, 1133 xml_tostring(xml2))) 1134 return False 1135 return True 1136 1137 def _verify_op_attributes(self, op_node): 1138 ''' 1139 Check if all operation attributes are supported by the 1140 schema. 1141 ''' 1142 rc = 0 1143 op_id = op_node.get("name") 1144 for name in list(op_node.keys()): 1145 vals = schema.rng_attr_values(op_node.tag, name) 1146 if not vals: 1147 continue 1148 v = op_node.get(name) 1149 if v not in vals: 1150 common_warn("%s: op '%s' attribute '%s' value '%s' not recognized" % 1151 (self.obj_id, op_id, name, v)) 1152 rc = 1 1153 return rc 1154 1155 def _check_ops_attributes(self): 1156 ''' 1157 Check if operation attributes settings are valid. 1158 ''' 1159 rc = 0 1160 if self.node is None: 1161 return rc 1162 for op_node in self.node.xpath("operations/op"): 1163 rc |= self._verify_op_attributes(op_node) 1164 return rc 1165 1166 def check_sanity(self): 1167 ''' 1168 Right now, this is only for primitives. 1169 And groups/clones/ms and cluster properties. 1170 ''' 1171 return 0 1172 1173 def reset_updated(self): 1174 self.updated = False 1175 for child in self.children: 1176 child.reset_updated() 1177 1178 def propagate_updated(self): 1179 if self.parent: 1180 self.parent.updated = self.updated 1181 self.parent.propagate_updated() 1182 1183 def top_parent(self): 1184 '''Return the top parent or self''' 1185 if self.parent: 1186 return self.parent.top_parent() 1187 else: 1188 return self 1189 1190 def meta_attributes(self, name): 1191 "Returns all meta attribute values with the given name" 1192 v = self.node.xpath('./meta_attributes/nvpair[@name="%s"]/@value' % (name)) 1193 return v 1194 1195 def find_child_in_node(self, child): 1196 for c in self.node.iterchildren(): 1197 if c.tag == child.node.tag and \ 1198 c.get("id") == child.obj_id: 1199 return c 1200 return None 1201 1202 1203def gv_first_prim(node): 1204 if node.tag != "primitive": 1205 for c in node.iterchildren(): 1206 if is_child_rsc(c): 1207 return gv_first_prim(c) 1208 return node.get("id") 1209 1210 1211def gv_first_rsc(rsc_id): 1212 rsc_obj = cib_factory.find_object(rsc_id) 1213 if not rsc_obj: 1214 return rsc_id 1215 return gv_first_prim(rsc_obj.node) 1216 1217 1218def gv_last_prim(node): 1219 if node.tag != "primitive": 1220 for c in node.iterchildren(reversed=True): 1221 if is_child_rsc(c): 1222 return gv_last_prim(c) 1223 return node.get("id") 1224 1225 1226def gv_last_rsc(rsc_id): 1227 rsc_obj = cib_factory.find_object(rsc_id) 1228 if not rsc_obj: 1229 return rsc_id 1230 return gv_last_prim(rsc_obj.node) 1231 1232 1233def gv_edge_score_label(gv_obj, e_id, node): 1234 score = get_score(node) or get_kind(node) 1235 if abs_pos_score(score): 1236 gv_obj.new_edge_attr(e_id, 'style', 'solid') 1237 return 1238 elif re.match("-?([0-9]+|inf)$", score): 1239 lbl = score 1240 elif score in schema.rng_attr_values('rsc_order', 'kind'): 1241 lbl = score 1242 elif not score: 1243 lbl = 'Adv' 1244 else: 1245 lbl = "attr:%s" % score 1246 gv_obj.new_edge_attr(e_id, 'label', lbl) 1247 1248 1249class CibNode(CibObject): 1250 ''' 1251 Node and node's attributes. 1252 ''' 1253 set_names = { 1254 "instance_attributes": "attributes", 1255 "utilization": "utilization", 1256 } 1257 1258 def _repr_cli_head(self, format_mode): 1259 uname = self.node.get("uname") 1260 s = clidisplay.keyword(self.obj_type) 1261 if self.obj_id != uname: 1262 if utils.noquotes(self.obj_id): 1263 s = "%s %s:" % (s, self.obj_id) 1264 else: 1265 s = '%s $id="%s"' % (s, self.obj_id) 1266 s = '%s %s' % (s, clidisplay.ident(uname)) 1267 node_type = self.node.get("type") 1268 if node_type and node_type != constants.node_default_type: 1269 s = '%s:%s' % (s, node_type) 1270 return s 1271 1272 def repr_gv(self, gv_obj, from_grp=False): 1273 ''' 1274 Create a gv node. The label consists of the ID. 1275 Nodes are square. 1276 ''' 1277 uname = self.node.get("uname") 1278 if not uname: 1279 uname = self.obj_id 1280 gv_obj.new_node(uname, top_node=True) 1281 gv_obj.new_attr(uname, 'label', uname) 1282 self._set_gv_attrs(gv_obj) 1283 1284 1285def reduce_primitive(node): 1286 ''' 1287 A primitive may reference template. If so, put the two 1288 together. 1289 Returns: 1290 - if no template reference, node itself 1291 - if template reference, but no template found, None 1292 - return merged primitive node into template node 1293 ''' 1294 template = node.get("template") 1295 if not template: 1296 return node 1297 template_obj = cib_factory.find_object(template) 1298 if not template_obj: 1299 return None 1300 return merge_tmpl_into_prim(node, template_obj.node) 1301 1302 1303class Op(object): 1304 ''' 1305 Operations. 1306 ''' 1307 elem_type = "op" 1308 1309 def __init__(self, op_name, prim, node=None): 1310 self.prim = prim 1311 self.node = node 1312 self.attr_d = ordereddict.odict() 1313 self.attr_d["name"] = op_name 1314 if self.node is not None: 1315 self.xml2dict() 1316 1317 def set_attr(self, n, v): 1318 self.attr_d[n] = v 1319 1320 def get_attr(self, n): 1321 try: 1322 return self.attr_d[n] 1323 except KeyError: 1324 return None 1325 1326 def del_attr(self, n): 1327 try: 1328 del self.attr_d[n] 1329 except KeyError: 1330 pass 1331 1332 def xml2dict(self): 1333 for name in list(self.node.keys()): 1334 if name != "id": # skip the id 1335 self.set_attr(name, self.node.get(name)) 1336 for p in self.node.xpath("instance_attributes/nvpair"): 1337 n = p.get("name") 1338 v = p.get("value") 1339 if n is not None and v is not None: 1340 self.set_attr(n, v) 1341 1342 def mkxml(self): 1343 # create an xml node 1344 if self.node is not None: 1345 if self.node.getparent() is not None: 1346 self.node.getparent().remove(self.node) 1347 idmgmt.remove_xml(self.node) 1348 self.node = etree.Element(self.elem_type) 1349 inst_attr = {} 1350 valid_attrs = olist(schema.get('attr', 'op', 'a')) 1351 for n, v in self.attr_d.items(): 1352 if n in valid_attrs: 1353 self.node.set(n, v) 1354 else: 1355 inst_attr[n] = v 1356 idmgmt.set_id(self.node, None, self.prim) 1357 if inst_attr: 1358 nia = mkxmlnvpairs("instance_attributes", inst_attr, self.node.get("id")) 1359 self.node.append(nia) 1360 return self.node 1361 1362 1363class CibOp(CibObject): 1364 ''' 1365 Operations 1366 ''' 1367 1368 set_names = { 1369 "instance_attributes": "op_params", 1370 "meta_attributes": "op_meta" 1371 } 1372 1373 def _repr_cli_head(self, format_mode): 1374 action, pl = op2list(self.node) 1375 if not action: 1376 return "" 1377 ret = ["%s %s" % (clidisplay.keyword("op"), action)] 1378 ret += [nvpair_format(n, v) for n, v in pl] 1379 return ' '.join(ret) 1380 1381 1382class CibPrimitive(CibObject): 1383 ''' 1384 Primitives. 1385 ''' 1386 1387 set_names = { 1388 "instance_attributes": "params", 1389 "meta_attributes": "meta", 1390 "utilization": "utilization", 1391 } 1392 1393 def _repr_cli_head(self, format_mode): 1394 if self.obj_type == "primitive": 1395 template_ref = self.node.get("template") 1396 else: 1397 template_ref = None 1398 if template_ref: 1399 rsc_spec = "@%s" % clidisplay.idref(template_ref) 1400 else: 1401 rsc_spec = mk_rsc_type(self.node) 1402 s = clidisplay.keyword(self.obj_type) 1403 ident = clidisplay.ident(self.obj_id) 1404 return "%s %s %s" % (s, ident, rsc_spec) 1405 1406 def _repr_cli_child(self, c, format_mode): 1407 if c.tag in self.set_names: 1408 return self._attr_set_str(c) 1409 elif c.tag == "operations": 1410 l = [] 1411 s = '' 1412 c_id = c.get("id") 1413 if c_id: 1414 s = nvpair_format('$id', c_id) 1415 idref = c.get("id-ref") 1416 if idref: 1417 s = '%s %s' % (s, nvpair_format('$id-ref', idref)) 1418 if s: 1419 l.append("%s %s" % (clidisplay.keyword("operations"), s)) 1420 for op_node in c.iterchildren(): 1421 op_obj = cib_object_map[op_node.tag][1](op_node.tag) 1422 op_obj.set_node(op_node) 1423 l.append(op_obj.repr_cli(format_mode > 0)) 1424 return cli_format(l, break_lines=(format_mode > 0)) 1425 1426 def _append_op(self, op_node): 1427 try: 1428 ops_node = self.node.findall("operations")[0] 1429 except IndexError: 1430 ops_node = etree.SubElement(self.node, "operations") 1431 ops_node.append(op_node) 1432 1433 def add_operation(self, node): 1434 # check if there is already an op with the same interval 1435 name = node.get("name") 1436 interval = node.get("interval") 1437 if find_operation(self.node, name, interval) is not None: 1438 common_err("%s already has a %s op with interval %s" % 1439 (self.obj_id, name, interval)) 1440 return None 1441 # create an xml node 1442 if 'id' not in node.attrib: 1443 idmgmt.set_id(node, None, self.obj_id) 1444 valid_attrs = olist(schema.get('attr', 'op', 'a')) 1445 inst_attr = {} 1446 for attr in list(node.attrib.keys()): 1447 if attr not in valid_attrs: 1448 inst_attr[attr] = node.attrib[attr] 1449 del node.attrib[attr] 1450 if inst_attr: 1451 attr_nodes = node.xpath('./instance_attributes') 1452 if len(attr_nodes) == 1: 1453 fill_nvpairs("instance_attributes", attr_nodes[0], inst_attr, node.get("id")) 1454 else: 1455 nia = mkxmlnvpairs("instance_attributes", inst_attr, node.get("id")) 1456 node.append(nia) 1457 1458 self._append_op(node) 1459 comments = find_comment_nodes(node) 1460 for comment in comments: 1461 node.remove(comment) 1462 if comments and self.node is not None: 1463 stuff_comments(self.node, [c.text for c in comments]) 1464 self.set_updated() 1465 return self 1466 1467 def del_operation(self, op_node): 1468 if op_node.getparent() is None: 1469 return 1470 ops_node = op_node.getparent() 1471 op_node.getparent().remove(op_node) 1472 idmgmt.remove_xml(op_node) 1473 if len(ops_node) == 0: 1474 rmnode(ops_node) 1475 self.set_updated() 1476 1477 def is_dummy_operation(self, op_node): 1478 '''If the op has just name, id, and interval=0, then it's 1479 not of much use.''' 1480 interval = op_node.get("interval") 1481 if len(op_node) == 0 and crm_msec(interval) == 0: 1482 attr_names = set(op_node.keys()) 1483 basic_attr_names = set(["id", "name", "interval"]) 1484 if len(attr_names ^ basic_attr_names) == 0: 1485 return True 1486 return False 1487 1488 def set_op_attr(self, op_node, attr_n, attr_v): 1489 name = op_node.get("name") 1490 op_obj = Op(name, self.obj_id, op_node) 1491 op_obj.set_attr(attr_n, attr_v) 1492 new_op_node = op_obj.mkxml() 1493 self._append_op(new_op_node) 1494 # the resource is updated 1495 self.set_updated() 1496 return new_op_node 1497 1498 def del_op_attr(self, op_node, attr_n): 1499 name = op_node.get("name") 1500 op_obj = Op(name, self.obj_id, op_node) 1501 op_obj.del_attr(attr_n) 1502 new_op_node = op_obj.mkxml() 1503 self._append_op(new_op_node) 1504 self.set_updated() 1505 return new_op_node 1506 1507 def normalize_parameters(self): 1508 """ 1509 Normalize parameter names: 1510 If a parameter "foo-bar" is set but the 1511 agent doesn't have a parameter "foo-bar", 1512 and instead has a parameter "foo_bar", then 1513 change the name to set the value of "foo_bar" 1514 instead. 1515 """ 1516 r_node = self.node 1517 if self.obj_type == "primitive": 1518 r_node = reduce_primitive(self.node) 1519 if r_node is None: 1520 return 1521 ra = get_ra(r_node) 1522 ra.normalize_parameters(r_node) 1523 1524 def check_sanity(self): 1525 ''' 1526 Check operation timeouts and if all required parameters 1527 are defined. 1528 ''' 1529 if self.node is None: # eh? 1530 common_err("%s: no xml (strange)" % self.obj_id) 1531 return utils.get_check_rc() 1532 rc3 = sanity_check_meta(self.obj_id, self.node, constants.rsc_meta_attributes) 1533 if self.obj_type == "primitive": 1534 r_node = reduce_primitive(self.node) 1535 if r_node is None: 1536 common_err("%s: no such resource template" % self.node.get("template")) 1537 return utils.get_check_rc() 1538 else: 1539 r_node = self.node 1540 ra = get_ra(r_node) 1541 if ra.mk_ra_node() is None: # no RA found? 1542 if cib_factory.is_asymm_cluster(): 1543 return rc3 1544 if config.core.ignore_missing_metadata: 1545 return rc3 1546 ra.error("no such resource agent") 1547 return utils.get_check_rc() 1548 actions = get_rsc_operations(r_node) 1549 default_timeout = get_default_timeout() 1550 rc2 = ra.sanity_check_ops(self.obj_id, actions, default_timeout) 1551 rc4 = self._check_ops_attributes() 1552 params = [] 1553 for c in r_node.iterchildren("instance_attributes"): 1554 params += nvpairs2list(c) 1555 rc1 = ra.sanity_check_params(self.obj_id, 1556 params, 1557 existence_only=(self.obj_type != "primitive")) 1558 return rc1 | rc2 | rc3 | rc4 1559 1560 def repr_gv(self, gv_obj, from_grp=False): 1561 ''' 1562 Create a gv node. The label consists of the ID and the 1563 RA type. 1564 ''' 1565 if self.obj_type == "primitive": 1566 # if we belong to a group, but were not called with 1567 # from_grp=True, then skip 1568 if not from_grp and self.parent and self.parent.obj_type == "group": 1569 return 1570 n = reduce_primitive(self.node) 1571 if n is None: 1572 raise ValueError("Referenced template not found") 1573 ra_class = n.get("class") 1574 ra_type = n.get("type") 1575 lbl_top = self._gv_rsc_id() 1576 if ra_class in ("ocf", "stonith"): 1577 lbl_bottom = ra_type 1578 else: 1579 lbl_bottom = "%s:%s" % (ra_class, ra_type) 1580 gv_obj.new_node(self.obj_id, norank=(ra_class == "stonith")) 1581 gv_obj.new_attr(self.obj_id, 'label', '%s\\n%s' % (lbl_top, lbl_bottom)) 1582 self._set_gv_attrs(gv_obj) 1583 self._set_gv_attrs(gv_obj, "class:%s" % ra_class) 1584 # if it's clone/ms, then get parent graph attributes 1585 if self.parent and self.parent.obj_type in constants.clonems_tags: 1586 self._set_gv_attrs(gv_obj, self.parent.obj_type) 1587 1588 template_ref = self.node.get("template") 1589 if template_ref: 1590 e = [template_ref, self.obj_id] 1591 e_id = gv_obj.new_edge(e) 1592 self._set_edge_attrs(gv_obj, e_id, 'template:edge') 1593 1594 elif self.obj_type == "rsc_template": 1595 n = reduce_primitive(self.node) 1596 if n is None: 1597 raise ValueError("Referenced template not found") 1598 ra_class = n.get("class") 1599 ra_type = n.get("type") 1600 lbl_top = self._gv_rsc_id() 1601 if ra_class in ("ocf", "stonith"): 1602 lbl_bottom = ra_type 1603 else: 1604 lbl_bottom = "%s:%s" % (ra_class, ra_type) 1605 gv_obj.new_node(self.obj_id, norank=(ra_class == "stonith")) 1606 gv_obj.new_attr(self.obj_id, 'label', '%s\\n%s' % (lbl_top, lbl_bottom)) 1607 self._set_gv_attrs(gv_obj) 1608 self._set_gv_attrs(gv_obj, "class:%s" % ra_class) 1609 # if it's clone/ms, then get parent graph attributes 1610 if self.parent and self.parent.obj_type in constants.clonems_tags: 1611 self._set_gv_attrs(gv_obj, self.parent.obj_type) 1612 1613 1614class CibContainer(CibObject): 1615 ''' 1616 Groups and clones and ms. 1617 ''' 1618 set_names = { 1619 "instance_attributes": "params", 1620 "meta_attributes": "meta", 1621 } 1622 1623 def _repr_cli_head(self, format_mode): 1624 children = [] 1625 for c in self.node.iterchildren(): 1626 if (self.obj_type == "group" and is_primitive(c)) or \ 1627 is_child_rsc(c): 1628 children.append(clidisplay.rscref(c.get("id"))) 1629 elif self.obj_type in constants.clonems_tags and is_child_rsc(c): 1630 children.append(clidisplay.rscref(c.get("id"))) 1631 s = clidisplay.keyword(self.obj_type) 1632 ident = clidisplay.ident(self.obj_id) 1633 return "%s %s %s" % (s, ident, ' '.join(children)) 1634 1635 def check_sanity(self): 1636 ''' 1637 Check meta attributes. 1638 ''' 1639 if self.node is None: # eh? 1640 common_err("%s: no xml (strange)" % self.obj_id) 1641 return utils.get_check_rc() 1642 l = constants.rsc_meta_attributes 1643 if self.obj_type == "clone": 1644 l += constants.clone_meta_attributes 1645 elif self.obj_type == "ms": 1646 l += constants.clone_meta_attributes + constants.ms_meta_attributes 1647 elif self.obj_type == "group": 1648 l += constants.group_meta_attributes 1649 rc = sanity_check_meta(self.obj_id, self.node, l) 1650 return rc 1651 1652 def repr_gv(self, gv_obj, from_grp=False): 1653 ''' 1654 A group is a subgraph. 1655 Clones and ms just get different attributes. 1656 ''' 1657 if self.obj_type != "group": 1658 return 1659 sg_obj = gv_obj.group([x.obj_id for x in self.children], 1660 "cluster_%s" % self.obj_id) 1661 sg_obj.new_graph_attr('label', self._gv_rsc_id()) 1662 self._set_sg_attrs(sg_obj, self.obj_type) 1663 if self.parent and self.parent.obj_type in constants.clonems_tags: 1664 self._set_sg_attrs(sg_obj, self.parent.obj_type) 1665 for child_rsc in self.children: 1666 child_rsc.repr_gv(sg_obj, from_grp=True) 1667 1668 1669class CibBundle(CibObject): 1670 ''' 1671 bundle type resource 1672 ''' 1673 set_names = { 1674 "instance_attributes": "params", 1675 "meta_attributes": "meta", 1676 "docker": "docker", 1677 "network": "network", 1678 "storage": "storage", 1679 "primitive": "primitive", 1680 "meta": "meta" 1681 } 1682 1683 def _repr_cli_head(self, format_mode): 1684 s = clidisplay.keyword(self.obj_type) 1685 ident = clidisplay.ident(self.obj_id) 1686 return "%s %s" % (s, ident) 1687 1688 def _repr_cli_child(self, c, format_mode): 1689 return self._attr_set_str(c) 1690 1691 1692def _check_if_constraint_ref_is_child(obj): 1693 """ 1694 Used by check_sanity for constraints to verify 1695 that referenced resources are not children in 1696 a container. 1697 """ 1698 rc = 0 1699 for rscid in obj.referenced_resources(): 1700 tgt = cib_factory.find_object(rscid) 1701 if not tgt: 1702 common_warn("%s: resource %s does not exist" % (obj.obj_id, rscid)) 1703 rc = 1 1704 elif tgt.parent and tgt.parent.obj_type == "group": 1705 if obj.obj_type == "colocation": 1706 common_warn("%s: resource %s is grouped, constraints should apply to the group" % (obj.obj_id, rscid)) 1707 rc = 1 1708 elif tgt.parent and tgt.parent.obj_type in constants.container_tags: 1709 common_warn("%s: resource %s ambiguous, apply constraints to container" % (obj.obj_id, rscid)) 1710 rc = 1 1711 return rc 1712 1713 1714class CibLocation(CibObject): 1715 ''' 1716 Location constraint. 1717 ''' 1718 1719 def _repr_cli_head(self, format_mode): 1720 rsc = None 1721 if "rsc" in list(self.node.keys()): 1722 rsc = self.node.get("rsc") 1723 elif "rsc-pattern" in list(self.node.keys()): 1724 rsc = '/%s/' % (self.node.get("rsc-pattern")) 1725 if rsc is not None: 1726 rsc = clidisplay.rscref(rsc) 1727 elif self.node.find("resource_set") is not None: 1728 rsc = '{ %s }' % (' '.join(rsc_set_constraint(self.node, self.obj_type))) 1729 else: 1730 common_err("%s: unknown rsc_location format" % self.obj_id) 1731 return None 1732 s = clidisplay.keyword(self.obj_type) 1733 ident = clidisplay.ident(self.obj_id) 1734 s = "%s %s %s" % (s, ident, rsc) 1735 1736 known_attrs = ['role', 'resource-discovery'] 1737 for attr in known_attrs: 1738 val = self.node.get(attr) 1739 if val is not None: 1740 s += " %s=%s" % (attr, val) 1741 1742 pref_node = self.node.get("node") 1743 score = clidisplay.score(get_score(self.node)) 1744 if pref_node is not None: 1745 s = "%s %s: %s" % (s, score, pref_node) 1746 return s 1747 1748 def _repr_cli_child(self, c, format_mode): 1749 if c.tag == "rule": 1750 return "%s %s" % \ 1751 (clidisplay.keyword("rule"), cli_rule(c)) 1752 1753 def check_sanity(self): 1754 ''' 1755 Check if node references match existing nodes. 1756 ''' 1757 if self.node is None: # eh? 1758 common_err("%s: no xml (strange)" % self.obj_id) 1759 return utils.get_check_rc() 1760 rc = 0 1761 uname = self.node.get("node") 1762 if uname and uname.lower() not in [ident.lower() for ident in cib_factory.node_id_list()]: 1763 common_warn("%s: referenced node %s does not exist" % (self.obj_id, uname)) 1764 rc = 1 1765 pattern = self.node.get("rsc-pattern") 1766 if pattern: 1767 try: 1768 re.compile(pattern) 1769 except IndexError as e: 1770 common_warn("%s: '%s' may not be a valid regular expression (%s)" % 1771 (self.obj_id, pattern, e)) 1772 rc = 1 1773 except re.error as e: 1774 common_warn("%s: '%s' may not be a valid regular expression (%s)" % 1775 (self.obj_id, pattern, e)) 1776 rc = 1 1777 for enode in self.node.xpath("rule/expression"): 1778 if enode.get("attribute") == "#uname": 1779 uname = enode.get("value") 1780 ids = [i.lower() for i in cib_factory.node_id_list()] 1781 if uname and uname.lower() not in ids: 1782 common_warn("%s: referenced node %s does not exist" % (self.obj_id, uname)) 1783 rc = 1 1784 rc2 = _check_if_constraint_ref_is_child(self) 1785 if rc2 > rc: 1786 rc = rc2 1787 return rc 1788 1789 def referenced_resources(self): 1790 ret = self.node.xpath('.//resource_set/resource_ref/@id') 1791 return ret or [self.node.get("rsc")] 1792 1793 def repr_gv(self, gv_obj, from_grp=False): 1794 ''' 1795 What to do with the location constraint? 1796 ''' 1797 pref_node = self.node.get("node") 1798 if pref_node is not None: 1799 score_n = self.node 1800 # otherwise, it's too complex to render 1801 elif is_pref_location(self.node): 1802 score_n = self.node.findall("rule")[0] 1803 exp = self.node.xpath("rule/expression")[0] 1804 pref_node = exp.get("value") 1805 if pref_node is None: 1806 return 1807 rsc_id = gv_first_rsc(self.node.get("rsc")) 1808 if rsc_id is not None: 1809 e = [pref_node, rsc_id] 1810 e_id = gv_obj.new_edge(e) 1811 self._set_edge_attrs(gv_obj, e_id) 1812 gv_edge_score_label(gv_obj, e_id, score_n) 1813 1814 1815def _opt_set_name(n): 1816 return "cluster%s" % n.get("id") 1817 1818 1819def rsc_set_gv_edges(node, gv_obj): 1820 def traverse_set(cum, st): 1821 e = [] 1822 for i, elem in enumerate(cum): 1823 if isinstance(elem, list): 1824 for rsc in elem: 1825 cum2 = copy.copy(cum) 1826 cum2[i] = rsc 1827 traverse_set(cum2, st) 1828 return 1829 else: 1830 e.append(elem) 1831 st.append(e) 1832 1833 cum = [] 1834 for n in node.iterchildren("resource_set"): 1835 sequential = get_boolean(n.get("sequential"), True) 1836 require_all = get_boolean(n.get("require-all"), True) 1837 l = get_rsc_ref_ids(n) 1838 if not require_all and len(l) > 1: 1839 sg_name = _opt_set_name(n) 1840 cum.append('[%s]%s' % (sg_name, l[0])) 1841 elif not sequential and len(l) > 1: 1842 cum.append(l) 1843 else: 1844 cum += l 1845 st = [] 1846 # deliver only 2-edges 1847 for i, lvl in enumerate(cum): 1848 if i == len(cum)-1: 1849 break 1850 traverse_set([cum[i], cum[i+1]], st) 1851 return st 1852 1853 1854class CibSimpleConstraint(CibObject): 1855 ''' 1856 Colocation and order constraints. 1857 ''' 1858 1859 def _repr_cli_head(self, format_mode): 1860 s = clidisplay.keyword(self.obj_type) 1861 ident = clidisplay.ident(self.obj_id) 1862 score = get_score(self.node) or get_kind(self.node) 1863 if self.node.find("resource_set") is not None: 1864 col = rsc_set_constraint(self.node, self.obj_type) 1865 else: 1866 col = simple_rsc_constraint(self.node, self.obj_type) 1867 if not col: 1868 return None 1869 if self.obj_type == "order": 1870 symm = self.node.get("symmetrical") 1871 if symm: 1872 col.append("symmetrical=%s" % symm) 1873 elif self.obj_type == "colocation": 1874 node_attr = self.node.get("node-attribute") 1875 if node_attr: 1876 col.append("node-attribute=%s" % node_attr) 1877 s = "%s %s " % (s, ident) 1878 if score != '': 1879 s += "%s: " % (clidisplay.score(score)) 1880 return s + ' '.join(col) 1881 1882 def _mk_optional_set(self, gv_obj, n): 1883 ''' 1884 Put optional resource set in a box. 1885 ''' 1886 members = get_rsc_ref_ids(n) 1887 sg_name = _opt_set_name(n) 1888 sg_obj = gv_obj.optional_set(members, sg_name) 1889 self._set_sg_attrs(sg_obj, "optional_set") 1890 1891 def _mk_one_edge(self, gv_obj, e): 1892 ''' 1893 Create an edge between two resources (used for resource 1894 sets). If the first resource name starts with '[', it's 1895 an optional resource set which is later put into a subgraph. 1896 The edge then goes from the subgraph to the resource 1897 which follows. An expensive exception. 1898 ''' 1899 optional_rsc = False 1900 r = re.match(r'\[(.*)\]', e[0]) 1901 if r: 1902 optional_rsc = True 1903 sg_name = r.group(1) 1904 e = [re.sub(r'\[(.*)\]', '', x) for x in e] 1905 e = [gv_last_rsc(e[0]), gv_first_rsc(e[1])] 1906 e_id = gv_obj.new_edge(e) 1907 gv_edge_score_label(gv_obj, e_id, self.node) 1908 if optional_rsc: 1909 self._set_edge_attrs(gv_obj, e_id, 'optional_set') 1910 gv_obj.new_edge_attr(e_id, 'ltail', gv_obj.gv_id(sg_name)) 1911 1912 def repr_gv(self, gv_obj, from_grp=False): 1913 ''' 1914 What to do with the collocation constraint? 1915 ''' 1916 if self.obj_type != "order": 1917 return 1918 if self.node.find("resource_set") is not None: 1919 for e in rsc_set_gv_edges(self.node, gv_obj): 1920 self._mk_one_edge(gv_obj, e) 1921 for n in self.node.iterchildren("resource_set"): 1922 if not get_boolean(n.get("require-all"), True): 1923 self._mk_optional_set(gv_obj, n) 1924 else: 1925 self._mk_one_edge(gv_obj, [ 1926 self.node.get("first"), 1927 self.node.get("then")]) 1928 1929 def referenced_resources(self): 1930 ret = self.node.xpath('.//resource_set/resource_ref/@id') 1931 if ret: 1932 return ret 1933 if self.obj_type == "order": 1934 return [self.node.get("first"), self.node.get("then")] 1935 elif self.obj_type == "colocation": 1936 return [self.node.get("rsc"), self.node.get("with-rsc")] 1937 elif self.node.get("rsc"): 1938 return [self.node.get("rsc")] 1939 1940 def check_sanity(self): 1941 if self.node is None: 1942 common_err("%s: no xml (strange)" % self.obj_id) 1943 return utils.get_check_rc() 1944 return _check_if_constraint_ref_is_child(self) 1945 1946 1947class CibRscTicket(CibSimpleConstraint): 1948 ''' 1949 rsc_ticket constraint. 1950 ''' 1951 1952 def _repr_cli_head(self, format_mode): 1953 s = clidisplay.keyword(self.obj_type) 1954 ident = clidisplay.ident(self.obj_id) 1955 ticket = clidisplay.ticket(self.node.get("ticket")) 1956 if self.node.find("resource_set") is not None: 1957 col = rsc_set_constraint(self.node, self.obj_type) 1958 else: 1959 col = simple_rsc_constraint(self.node, self.obj_type) 1960 if not col: 1961 return None 1962 a = self.node.get("loss-policy") 1963 if a: 1964 col.append("loss-policy=%s" % a) 1965 return "%s %s %s: %s" % (s, ident, ticket, ' '.join(col)) 1966 1967 1968class CibProperty(CibObject): 1969 ''' 1970 Cluster properties. 1971 ''' 1972 1973 def _repr_cli_head(self, format_mode): 1974 return "%s %s" % (clidisplay.keyword(self.obj_type), 1975 head_id_format(self.obj_id)) 1976 1977 def _repr_cli_child(self, c, format_mode): 1978 if c.tag == "rule": 1979 return ' '.join((clidisplay.keyword("rule"), 1980 cli_rule(c))) 1981 elif c.tag == "nvpair": 1982 return cli_nvpair(c) 1983 else: 1984 return '' 1985 1986 def check_sanity(self): 1987 ''' 1988 Match properties with PE metadata. 1989 ''' 1990 if self.node is None: # eh? 1991 common_err("%s: no xml (strange)" % self.obj_id) 1992 return utils.get_check_rc() 1993 l = [] 1994 if self.obj_type == "property": 1995 # don't check property sets which are not 1996 # "cib-bootstrap-options", they are probably used by 1997 # some resource agents such as mysql to store RA 1998 # specific state 1999 if self.obj_id != cib_object_map[self.xml_obj_type][3]: 2000 return 0 2001 l = get_properties_list() 2002 l += constants.extra_cluster_properties 2003 elif self.obj_type == "op_defaults": 2004 l = schema.get('attr', 'op', 'a') 2005 elif self.obj_type == "rsc_defaults": 2006 l = constants.rsc_meta_attributes 2007 rc = sanity_check_nvpairs(self.obj_id, self.node, l) 2008 return rc 2009 2010 2011def is_stonith_rsc(xmlnode): 2012 ''' 2013 True if resource is stonith or derived from stonith template. 2014 ''' 2015 xmlnode = reduce_primitive(xmlnode) 2016 if xmlnode is None: 2017 return False 2018 return xmlnode.get('class') == 'stonith' 2019 2020 2021class CibFencingOrder(CibObject): 2022 ''' 2023 Fencing order (fencing-topology). 2024 ''' 2025 2026 def set_id(self, obj_id=None): 2027 self.obj_id = "fencing_topology" 2028 2029 def set_nodeid(self): 2030 '''This id is not part of attributes''' 2031 pass 2032 2033 def __str__(self): 2034 return self.obj_id 2035 2036 def can_be_renamed(self): 2037 ''' Cannot rename this one. ''' 2038 return False 2039 2040 def _repr_cli_head(self, format_mode): 2041 s = clidisplay.keyword(self.obj_type) 2042 d = ordereddict.odict() 2043 for c in self.node.iterchildren("fencing-level"): 2044 if "target-pattern" in c.attrib: 2045 target = (None, c.get("target-pattern")) 2046 elif "target-attribute" in c.attrib: 2047 target = (c.get("target-attribute"), c.get("target-value")) 2048 else: 2049 target = c.get("target") 2050 if target not in d: 2051 d[target] = {} 2052 d[target][c.get("index")] = c.get("devices") 2053 dd = ordereddict.odict() 2054 for target in list(d.keys()): 2055 sorted_keys = sorted([int(i) for i in list(d[target].keys())]) 2056 dd[target] = [d[target][str(x)] for x in sorted_keys] 2057 d2 = {} 2058 for target in list(dd.keys()): 2059 devs_s = ' '.join(dd[target]) 2060 d2[devs_s] = 1 2061 if len(d2) == 1 and len(d) == len(cib_factory.node_id_list()): 2062 return "%s %s" % (s, devs_s) 2063 2064 def fmt_target(tgt): 2065 if isinstance(tgt, tuple): 2066 if tgt[0] is None: 2067 return "pattern:%s" % (tgt[1]) 2068 return "attr:%s=%s" % tgt 2069 return tgt + ":" 2070 return cli_format([s] + ["%s %s" % (fmt_target(x), ' '.join(dd[x])) 2071 for x in list(dd.keys())], 2072 break_lines=(format_mode > 0)) 2073 2074 def _repr_cli_child(self, c, format_mode): 2075 pass # no children here 2076 2077 def check_sanity(self): 2078 ''' 2079 Targets are nodes and resource are stonith resources. 2080 ''' 2081 if self.node is None: # eh? 2082 common_err("%s: no xml (strange)" % self.obj_id) 2083 return utils.get_check_rc() 2084 rc = 0 2085 nl = self.node.findall("fencing-level") 2086 for target in [x.get("target") for x in nl if x.get("target") is not None]: 2087 if target.lower() not in [ident.lower() for ident in cib_factory.node_id_list()]: 2088 common_warn("%s: target %s not a node" % (self.obj_id, target)) 2089 rc = 1 2090 stonith_rsc_l = [x.obj_id for x in 2091 cib_factory.get_elems_on_type("type:primitive") 2092 if is_stonith_rsc(x.node)] 2093 for devices in [x.get("devices") for x in nl]: 2094 for dev in devices.split(","): 2095 if not cib_factory.find_object(dev): 2096 common_warn("%s: resource %s does not exist" % (self.obj_id, dev)) 2097 rc = 1 2098 elif dev not in stonith_rsc_l: 2099 common_warn("%s: %s not a stonith resource" % (self.obj_id, dev)) 2100 rc = 1 2101 return rc 2102 2103 2104class CibAcl(CibObject): 2105 ''' 2106 User and role ACL. 2107 2108 Now with support for 1.1.12 style ACL rules. 2109 2110 ''' 2111 2112 def _repr_cli_head(self, format_mode): 2113 s = clidisplay.keyword(self.obj_type) 2114 ident = clidisplay.ident(self.obj_id) 2115 return "%s %s" % (s, ident) 2116 2117 def _repr_cli_child(self, c, format_mode): 2118 if c.tag in constants.acl_rule_names: 2119 return cli_acl_rule(c, format_mode) 2120 elif c.tag == "role_ref": 2121 return cli_acl_roleref(c, format_mode) 2122 elif c.tag == "role": 2123 return cli_acl_role(c) 2124 elif c.tag == "acl_permission": 2125 return cli_acl_permission(c) 2126 2127 2128class CibTag(CibObject): 2129 ''' 2130 Tag objects 2131 2132 TODO: check_sanity, repr_gv 2133 2134 ''' 2135 2136 def _repr_cli_head(self, fmt): 2137 return ' '.join([clidisplay.keyword(self.obj_type), 2138 clidisplay.ident(self.obj_id)] + 2139 [clidisplay.rscref(c.get('id')) 2140 for c in self.node.iterchildren() if not is_comment(c)]) 2141 2142 2143class CibAlert(CibObject): 2144 ''' 2145 Alert objects 2146 2147 TODO: check_sanity, repr_gv 2148 2149 FIXME: display instance / meta attributes, description 2150 2151 ''' 2152 set_names = { 2153 "instance_attributes": "attributes", 2154 "meta_attributes": "meta", 2155 } 2156 2157 def _repr_cli_head(self, fmt): 2158 ret = [clidisplay.keyword(self.obj_type), 2159 clidisplay.ident(self.obj_id), 2160 cli_path(self.node.get('path'))] 2161 return ' '.join(ret) 2162 2163 def _repr_cli_child(self, c, format_mode): 2164 if c.tag in self.set_names: 2165 return self._attr_set_str(c) 2166 elif c.tag == "select": 2167 r = ["select"] 2168 for sel in c.iterchildren(): 2169 if not sel.tag.startswith('select_'): 2170 continue 2171 r.append(sel.tag.lstrip('select_')) 2172 if sel.tag == 'select_attributes': 2173 r.append('{') 2174 r.extend(sel.xpath('attribute/@name')) 2175 r.append('}') 2176 return ' '.join(r) 2177 elif c.tag == "recipient": 2178 r = ["to"] 2179 is_complex = self._is_complex() 2180 if is_complex: 2181 r.append('{') 2182 r.append(cli_path(c.get('value'))) 2183 for subset in c.xpath('instance_attributes|meta_attributes'): 2184 r.append(self._attr_set_str(subset)) 2185 if is_complex: 2186 r.append('}') 2187 return ' '.join(r) 2188 2189 def _is_complex(self): 2190 ''' 2191 True if this alert is ambiguous wrt meta attributes in recipient tags 2192 ''' 2193 children = [c.tag for c in self.node.xpath('recipient|instance_attributes|meta_attributes')] 2194 ri = children.index('recipient') 2195 if ri < 0: 2196 return False 2197 children = children[ri+1:] 2198 return 'instance_attributes' in children or 'meta_attributes' in children 2199 2200 2201# 2202################################################################ 2203 2204 2205# 2206# cib factory 2207# 2208cib_piped = "cibadmin -p" 2209 2210 2211def get_default_timeout(): 2212 t = cib_factory.get_op_default("timeout") 2213 if t is not None: 2214 return t 2215 2216 2217# xml -> cli translations (and classes) 2218cib_object_map = { 2219 # xml_tag: ( cli_name, element class, parent element tag, id hint ) 2220 "node": ("node", CibNode, "nodes"), 2221 "op": ("op", CibOp, "operations"), 2222 "primitive": ("primitive", CibPrimitive, "resources"), 2223 "group": ("group", CibContainer, "resources"), 2224 "clone": ("clone", CibContainer, "resources"), 2225 "master": ("ms", CibContainer, "resources"), 2226 "template": ("rsc_template", CibPrimitive, "resources"), 2227 "bundle": ("bundle", CibBundle, "resources"), 2228 "rsc_location": ("location", CibLocation, "constraints"), 2229 "rsc_colocation": ("colocation", CibSimpleConstraint, "constraints"), 2230 "rsc_order": ("order", CibSimpleConstraint, "constraints"), 2231 "rsc_ticket": ("rsc_ticket", CibRscTicket, "constraints"), 2232 "cluster_property_set": ("property", CibProperty, "crm_config", "cib-bootstrap-options"), 2233 "rsc_defaults": ("rsc_defaults", CibProperty, "rsc_defaults", "rsc-options"), 2234 "op_defaults": ("op_defaults", CibProperty, "op_defaults", "op-options"), 2235 "fencing-topology": ("fencing_topology", CibFencingOrder, "configuration"), 2236 "acl_role": ("role", CibAcl, "acls"), 2237 "acl_user": ("user", CibAcl, "acls"), 2238 "acl_target": ("acl_target", CibAcl, "acls"), 2239 "acl_group": ("acl_group", CibAcl, "acls"), 2240 "tag": ("tag", CibTag, "tags"), 2241 "alert": ("alert", CibAlert, "alerts"), 2242} 2243 2244 2245# generate a translation cli -> tag 2246backtrans = ordereddict.odict((item[0], key) for key, item in cib_object_map.items()) 2247 2248 2249def default_id_for_tag(tag): 2250 "Get default id for XML tag" 2251 m = cib_object_map.get(tag, tuple()) 2252 return m[3] if len(m) > 3 else None 2253 2254 2255def default_id_for_obj(obj_type): 2256 "Get default id for object type" 2257 return default_id_for_tag(backtrans.get(obj_type)) 2258 2259 2260def can_migrate(node): 2261 return 'true' in node.xpath('.//nvpair[@name="allow-migrate"]/@value') 2262 2263 2264class CibDiff(object): 2265 ''' 2266 Represents a cib edit order. 2267 Is complicated by the fact that 2268 nodes and resources can have 2269 colliding ids. 2270 2271 Can carry changes either as CLI objects 2272 or as XML statements. 2273 ''' 2274 def __init__(self, objset): 2275 self.objset = objset 2276 self._node_set = orderedset.oset() 2277 self._nodes = {} 2278 self._rsc_set = orderedset.oset() 2279 self._resources = {} 2280 2281 def add(self, item): 2282 obj_id = id_for_node(item) 2283 is_node = item.tag == 'node' 2284 if obj_id is None: 2285 common_err("element %s has no id!" % 2286 xml_tostring(item, pretty_print=True)) 2287 return False 2288 elif is_node and obj_id in self._node_set: 2289 common_err("Duplicate node: %s" % (obj_id)) 2290 return False 2291 elif not is_node and obj_id in self._rsc_set: 2292 common_err("Duplicate resource: %s" % (obj_id)) 2293 return False 2294 elif is_node: 2295 self._node_set.add(obj_id) 2296 self._nodes[obj_id] = item 2297 else: 2298 self._rsc_set.add(obj_id) 2299 self._resources[obj_id] = item 2300 return True 2301 2302 def _obj_type(self, nid): 2303 for obj in self.objset.all_set: 2304 if obj.obj_id == nid: 2305 return obj.obj_type 2306 return None 2307 2308 def _is_node(self, nid): 2309 for obj in self.objset.all_set: 2310 if obj.obj_id == nid and obj.obj_type == 'node': 2311 return True 2312 return False 2313 2314 def _is_resource(self, nid): 2315 for obj in self.objset.all_set: 2316 if obj.obj_id == nid and obj.obj_type != 'node': 2317 return True 2318 return False 2319 2320 def _obj_nodes(self): 2321 return orderedset.oset([n for n in self.objset.obj_ids 2322 if self._is_node(n)]) 2323 2324 def _obj_resources(self): 2325 return orderedset.oset([n for n in self.objset.obj_ids 2326 if self._is_resource(n)]) 2327 2328 def _is_edit_valid(self, id_set, existing): 2329 ''' 2330 1. Cannot name any elements as those which exist but 2331 were not picked for editing. 2332 2. Cannot remove running resources. 2333 ''' 2334 rc = True 2335 not_allowed = id_set & self.objset.locked_ids 2336 rscstat = RscState() 2337 if not_allowed: 2338 common_err("Elements %s already exist" % 2339 ', '.join(list(not_allowed))) 2340 rc = False 2341 delete_set = existing - id_set 2342 cannot_delete = [x for x in delete_set 2343 if not rscstat.can_delete(x)] 2344 if cannot_delete: 2345 common_err("Cannot delete running resources: %s" % 2346 ', '.join(cannot_delete)) 2347 rc = False 2348 return rc 2349 2350 def apply(self, factory, mode='cli', remove=True, method='replace'): 2351 rc = True 2352 2353 edited_nodes = self._nodes.copy() 2354 edited_resources = self._resources.copy() 2355 2356 def calc_sets(input_set, existing): 2357 rc = True 2358 if remove: 2359 rc = self._is_edit_valid(input_set, existing) 2360 del_set = existing - (input_set) 2361 else: 2362 del_set = orderedset.oset() 2363 mk_set = (input_set) - existing 2364 upd_set = (input_set) & existing 2365 return rc, mk_set, upd_set, del_set 2366 2367 if not rc: 2368 return rc 2369 2370 for e, s, existing in ((edited_nodes, self._node_set, self._obj_nodes()), 2371 (edited_resources, self._rsc_set, self._obj_resources())): 2372 rc, mk, upd, rm = calc_sets(s, existing) 2373 if not rc: 2374 return rc 2375 rc = cib_factory.set_update(e, mk, upd, rm, upd_type=mode, method=method) 2376 if not rc: 2377 return rc 2378 return rc 2379 2380 2381class CibFactory(object): 2382 ''' 2383 Juggle with CIB objects. 2384 See check_structure below for details on the internal cib 2385 representation. 2386 ''' 2387 2388 def __init__(self): 2389 self._init_vars() 2390 self.regtest = options.regression_tests 2391 self.last_commit_time = 0 2392 # internal (just not to produce silly messages) 2393 self._no_constraint_rm_msg = False 2394 self._crm_diff_cmd = "crm_diff --no-version" 2395 2396 def is_cib_sane(self): 2397 # try to initialize 2398 if self.cib_elem is None: 2399 self.initialize() 2400 if self.cib_elem is None: 2401 empty_cib_err() 2402 return False 2403 return True 2404 2405 def get_cib(self): 2406 if not self.is_cib_sane(): 2407 return None 2408 return self.cib_elem 2409 # 2410 # check internal structures 2411 # 2412 2413 def _check_parent(self, obj, parent): 2414 if obj not in parent.children: 2415 common_err("object %s does not reference its child %s" % 2416 (parent.obj_id, obj.obj_id)) 2417 return False 2418 if parent.node != obj.node.getparent(): 2419 if obj.node.getparent() is None: 2420 common_err("object %s node is not a child of its parent %s" % 2421 (obj.obj_id, parent.obj_id)) 2422 else: 2423 common_err("object %s node is not a child of its parent %s, but %s:%s" % 2424 (obj.obj_id, 2425 parent.obj_id, 2426 obj.node.getparent().tag, 2427 obj.node.getparent().get("id"))) 2428 return False 2429 return True 2430 2431 def check_structure(self): 2432 if not self.is_cib_sane(): 2433 return False 2434 rc = True 2435 for obj in self.cib_objects: 2436 if obj.parent: 2437 if not self._check_parent(obj, obj.parent): 2438 common_debug("check_parent failed: %s %s" % (obj.obj_id, obj.parent)) 2439 rc = False 2440 for child in obj.children: 2441 if not child.parent: 2442 common_err("child %s does not reference its parent %s" % 2443 (child.obj_id, obj.obj_id)) 2444 rc = False 2445 return rc 2446 2447 def regression_testing(self, param): 2448 # provide some help for regression testing 2449 # in particular by trying to provide output which is 2450 # easier to predict 2451 if param == "off": 2452 self.regtest = False 2453 elif param == "on": 2454 self.regtest = True 2455 else: 2456 common_warn("bad parameter for regtest: %s" % param) 2457 2458 def get_schema(self): 2459 return self.cib_attrs["validate-with"] 2460 2461 def change_schema(self, schema_st): 2462 'Use another schema' 2463 if schema_st == self.get_schema(): 2464 common_info("already using schema %s" % schema_st) 2465 return True 2466 if not schema.is_supported(schema_st): 2467 common_warn("schema %s is not supported by the shell" % schema_st) 2468 self.cib_elem.set("validate-with", schema_st) 2469 if not schema.test_schema(self.cib_elem): 2470 self.cib_elem.set("validate-with", self.get_schema()) 2471 common_err("schema %s does not exist" % schema_st) 2472 return False 2473 schema.init_schema(self.cib_elem) 2474 rc = True 2475 for obj in self.cib_objects: 2476 if schema.get('sub', obj.node.tag, 'a') is None: 2477 common_err("Element '%s' is not supported by the RNG schema %s" % 2478 (obj.node.tag, schema_st)) 2479 common_debug("Offending object: %s" % (xml_tostring(obj.node))) 2480 rc = False 2481 if not rc: 2482 # revert, as some elements won't validate 2483 self.cib_elem.set("validate-with", self.get_schema()) 2484 schema.init_schema(self.cib_elem) 2485 common_err("Schema %s conflicts with current configuration" % schema_st) 2486 return 4 2487 self.cib_attrs["validate-with"] = schema_st 2488 self.new_schema = True 2489 return 0 2490 2491 def is_elem_supported(self, obj_type): 2492 'Do we support this element?' 2493 try: 2494 if schema.get('sub', backtrans[obj_type], 'a') is None: 2495 return False 2496 except KeyError: 2497 pass 2498 return True 2499 2500 def is_cib_supported(self): 2501 'Do we support this CIB?' 2502 req = self.cib_elem.get("crm_feature_set") 2503 validator = self.cib_elem.get("validate-with") 2504 # if no schema is configured, just assume that it validates 2505 if not validator or schema.is_supported(validator): 2506 return True 2507 cib_ver_unsupported_err(validator, req) 2508 return False 2509 2510 def upgrade_validate_with(self, force=False): 2511 """Upgrade the CIB. 2512 2513 Requires the force argument to be set if 2514 validate-with is configured to anything other than 2515 0.6. 2516 """ 2517 if not self.is_cib_sane(): 2518 return False 2519 validator = self.cib_elem.get("validate-with") 2520 if force or not validator or re.match("0[.]6", validator): 2521 return ext_cmd("cibadmin --upgrade --force") == 0 2522 2523 def _import_cib(self, cib_elem): 2524 'Parse the current CIB (from cibadmin -Q).' 2525 self.cib_elem = cib_elem 2526 if self.cib_elem is None: 2527 return False 2528 if not self.is_cib_supported(): 2529 common_warn("CIB schema is not supported by the shell") 2530 self._get_cib_attributes(self.cib_elem) 2531 schema.init_schema(self.cib_elem) 2532 return True 2533 2534 def _get_cib_attributes(self, cib): 2535 for attr in list(cib.keys()): 2536 self.cib_attrs[attr] = cib.get(attr) 2537 2538 def _set_cib_attributes(self, cib): 2539 for attr in self.cib_attrs: 2540 cib.set(attr, self.cib_attrs[attr]) 2541 2542 def _copy_cib_attributes(self, src_cib, cib): 2543 """ 2544 Copy CIB attributes from src_cib to cib. 2545 Also updates self.cib_attrs. 2546 Preserves attributes that may be modified by 2547 the user (for example validate-with). 2548 """ 2549 attrs = ((attr, src_cib.get(attr)) 2550 for attr in self.cib_attrs 2551 if attr not in constants.cib_user_attrs) 2552 for attr, value in attrs: 2553 self.cib_attrs[attr] = value 2554 cib.set(attr, value) 2555 2556 def obj_set2cib(self, obj_set, obj_filter=None): 2557 ''' 2558 Return document containing objects in obj_set. 2559 Must remove all children from the object list, because 2560 printing xml of parents will include them. 2561 Optional filter to sieve objects. 2562 ''' 2563 cib_elem = new_cib() 2564 # get only top parents for the objects in the list 2565 # e.g. if we get a primitive which is part of a clone, 2566 # then the clone gets in, not the primitive 2567 # dict will weed out duplicates 2568 d = {} 2569 for obj in obj_set: 2570 if obj_filter and not obj_filter(obj): 2571 continue 2572 d[obj.top_parent()] = 1 2573 for obj in d: 2574 get_topnode(cib_elem, obj.parent_type).append(copy.deepcopy(obj.node)) 2575 self._set_cib_attributes(cib_elem) 2576 return cib_elem 2577 2578 # 2579 # commit changed objects to the CIB 2580 # 2581 def _attr_match(self, c, a): 2582 'Does attribute match?' 2583 return c.get(a) == self.cib_attrs.get(a) 2584 2585 def is_current_cib_equal(self, silent=False): 2586 cib_elem = read_cib(cibdump2elem) 2587 if cib_elem is None: 2588 return False 2589 rc = self._attr_match(cib_elem, 'epoch') and \ 2590 self._attr_match(cib_elem, 'admin_epoch') 2591 if not silent and not rc: 2592 common_warn("CIB changed in the meantime: won't touch it!") 2593 return rc 2594 2595 def _state_header(self): 2596 'Print object status header' 2597 print(CibObject.state_fmt % \ 2598 ("", "origin", "updated", "parent", "children")) 2599 2600 def showobjects(self): 2601 self._state_header() 2602 for obj in self.cib_objects: 2603 obj.dump_state() 2604 if self.remove_queue: 2605 print("Remove queue:") 2606 for obj in self.remove_queue: 2607 obj.dump_state() 2608 2609 def commit(self, force=False, replace=False): 2610 'Commit the configuration to the CIB.' 2611 if not self.is_cib_sane(): 2612 return False 2613 if not replace and cibadmin_can_patch(): 2614 rc = self._patch_cib(force) 2615 else: 2616 rc = self._replace_cib(force) 2617 if rc: 2618 # reload the cib! 2619 t = time.time() 2620 common_debug("CIB commit successful at %s" % (t)) 2621 if is_live_cib(): 2622 self.last_commit_time = t 2623 self.refresh() 2624 return rc 2625 2626 def _update_schema(self): 2627 ''' 2628 Set the validate-with, if the schema changed. 2629 ''' 2630 s = '<cib validate-with="%s"/>' % self.cib_attrs["validate-with"] 2631 rc = pipe_string("%s -U" % cib_piped, s) 2632 if rc != 0: 2633 update_err("cib", "-U", s, rc) 2634 return False 2635 self.new_schema = False 2636 return True 2637 2638 def _replace_cib(self, force): 2639 try: 2640 conf_el = self.cib_elem.findall("configuration")[0] 2641 except IndexError: 2642 common_error("cannot find the configuration element") 2643 return False 2644 if self.new_schema and not self._update_schema(): 2645 return False 2646 cibadmin_opts = force and "-R --force" or "-R" 2647 rc = pipe_string("%s %s" % (cib_piped, cibadmin_opts), etree.tostring(conf_el)) 2648 if rc != 0: 2649 update_err("cib", cibadmin_opts, xml_tostring(conf_el), rc) 2650 return False 2651 return True 2652 2653 def _patch_cib(self, force): 2654 # copy the epoch from the current cib to both the target 2655 # cib and the original one (otherwise cibadmin won't want 2656 # to apply the patch) 2657 current_cib = read_cib(cibdump2elem) 2658 if current_cib is None: 2659 return False 2660 2661 self._copy_cib_attributes(current_cib, self.cib_orig) 2662 current_cib = None # don't need that anymore 2663 self._set_cib_attributes(self.cib_elem) 2664 cib_s = xml_tostring(self.cib_orig, pretty_print=True) 2665 tmpf = str2tmp(cib_s, suffix=".xml") 2666 if not tmpf or not ensure_sudo_readable(tmpf): 2667 return False 2668 tmpfiles.add(tmpf) 2669 cibadmin_opts = force and "-P --force" or "-P" 2670 2671 # produce a diff: 2672 # dump_new_conf | crm_diff -o self.cib_orig -n - 2673 2674 common_debug("Basis: %s" % (open(tmpf).read())) 2675 common_debug("Input: %s" % (xml_tostring(self.cib_elem))) 2676 rc, cib_diff = filter_string("%s -o %s -n -" % 2677 (self._crm_diff_cmd, tmpf), 2678 etree.tostring(self.cib_elem)) 2679 if not cib_diff and (rc == 0): 2680 # no diff = no action 2681 return True 2682 elif not cib_diff: 2683 common_err("crm_diff apparently failed to produce the diff (rc=%d)" % rc) 2684 return False 2685 2686 # for v1 diffs, fall back to non-patching if 2687 # any containers are modified, else strip the digest 2688 if "<diff" in cib_diff and "digest=" in cib_diff: 2689 if not self.can_patch_v1(): 2690 return self._replace_cib(force) 2691 e = etree.fromstring(cib_diff) 2692 for tag in e.xpath("/diff"): 2693 if "digest" in tag.attrib: 2694 del tag.attrib["digest"] 2695 cib_diff = xml_tostring(e) 2696 common_debug("Diff: %s" % (cib_diff)) 2697 rc = pipe_string("%s %s" % (cib_piped, cibadmin_opts), 2698 cib_diff.encode('utf-8')) 2699 if rc != 0: 2700 update_err("cib", cibadmin_opts, cib_diff, rc) 2701 return False 2702 return True 2703 2704 def can_patch_v1(self): 2705 """ 2706 The v1 patch format cannot handle reordering, 2707 so if there are any changes to any containers 2708 or acl tags, don't patch. 2709 """ 2710 def group_changed(): 2711 for obj in self.cib_objects: 2712 if not obj.updated: 2713 continue 2714 if obj.obj_type in constants.container_tags: 2715 return True 2716 if obj.obj_type in ('user', 'role', 'acl_target', 'acl_group'): 2717 return True 2718 return False 2719 return not group_changed() 2720 2721 # 2722 # initialize cib_objects from CIB 2723 # 2724 def _create_object_from_cib(self, node, pnode=None): 2725 ''' 2726 Need pnode (parent node) acrobacy because cluster 2727 properties and rsc/op_defaults hold stuff in a 2728 meta_attributes child. 2729 ''' 2730 assert node is not None 2731 if pnode is None: 2732 pnode = node 2733 obj = cib_object_map[pnode.tag][1](pnode.tag) 2734 obj.origin = "cib" 2735 obj.node = node 2736 obj.set_id() 2737 self.cib_objects.append(obj) 2738 return obj 2739 2740 def _populate(self): 2741 "Walk the cib and collect cib objects." 2742 all_nodes = get_interesting_nodes(self.cib_elem, []) 2743 if not all_nodes: 2744 return 2745 for node in processing_sort(all_nodes): 2746 if is_defaults(node): 2747 for c in node.xpath("./meta_attributes"): 2748 self._create_object_from_cib(c, node) 2749 else: 2750 self._create_object_from_cib(node) 2751 for obj in self.cib_objects: 2752 obj.move_comments() 2753 fix_comments(obj.node) 2754 self.cli_use_validate_all() 2755 for obj in self.cib_objects: 2756 self._update_links(obj) 2757 2758 def cli_use_validate_all(self): 2759 for obj in self.cib_objects: 2760 if not obj.cli_use_validate(): 2761 obj.nocli = True 2762 obj.nocli_warn = False 2763 # no need to warn, user can see the object displayed as XML 2764 common_debug("object %s cannot be represented in the CLI notation" % (obj.obj_id)) 2765 2766 def initialize(self, cib=None): 2767 if self.cib_elem is not None: 2768 return True 2769 if cib is None: 2770 cib = read_cib(cibdump2elem) 2771 elif isinstance(cib, str): 2772 cib = cibtext2elem(cib) 2773 if not self._import_cib(cib): 2774 return False 2775 if cibadmin_can_patch(): 2776 self.cib_orig = copy.deepcopy(self.cib_elem) 2777 sanitize_cib_for_patching(self.cib_orig) 2778 sanitize_cib(self.cib_elem) 2779 show_unrecognized_elems(self.cib_elem) 2780 self._populate() 2781 return self.check_structure() 2782 2783 def _init_vars(self): 2784 self.cib_elem = None # the cib 2785 self.cib_orig = None # the CIB which we loaded 2786 self.cib_attrs = {} # cib version dictionary 2787 self.cib_objects = [] # a list of cib objects 2788 self.remove_queue = [] # a list of cib objects to be removed 2789 self.id_refs = {} # dict of id-refs 2790 self.new_schema = False # schema changed 2791 self._state = [] 2792 2793 def _push_state(self): 2794 ''' 2795 A rudimentary instance state backup. Just make copies of 2796 all important variables. 2797 idmgmt has to be backed up too. 2798 ''' 2799 self._state.append([copy.deepcopy(x) 2800 for x in (self.cib_elem, 2801 self.cib_attrs, 2802 self.cib_objects, 2803 self.remove_queue, 2804 self.id_refs)]) 2805 idmgmt.push_state() 2806 2807 def _pop_state(self): 2808 try: 2809 common_debug("performing rollback from %s" % (self.cib_objects)) 2810 self.cib_elem, \ 2811 self.cib_attrs, self.cib_objects, \ 2812 self.remove_queue, self.id_refs = self._state.pop() 2813 except KeyError: 2814 return False 2815 # need to get addresses of all new objects created by 2816 # deepcopy 2817 for obj in self.cib_objects: 2818 obj.node = self.find_xml_node(obj.xml_obj_type, obj.obj_id) 2819 self._update_links(obj) 2820 idmgmt.pop_state() 2821 return self.check_structure() 2822 2823 def _drop_state(self): 2824 try: 2825 self._state.pop() 2826 except KeyError: 2827 pass 2828 idmgmt.drop_state() 2829 2830 def _clean_state(self): 2831 self._state = [] 2832 idmgmt.clean_state() 2833 2834 def reset(self): 2835 if self.cib_elem is None: 2836 return 2837 self.cib_elem = None 2838 self.cib_orig = None 2839 self._init_vars() 2840 self._clean_state() 2841 idmgmt.clear() 2842 2843 def find_objects(self, obj_id): 2844 "Find objects for id (can be a wildcard-glob)." 2845 def matchfn(x): 2846 return x and fnmatch.fnmatch(x, obj_id) 2847 if not self.is_cib_sane() or obj_id is None: 2848 return None 2849 objs = [] 2850 for obj in self.cib_objects: 2851 if matchfn(obj.obj_id): 2852 objs.append(obj) 2853 # special case for Heartbeat nodes which have id 2854 # different from uname 2855 elif obj.obj_type == "node" and matchfn(obj.node.get("uname")): 2856 objs.append(obj) 2857 return objs 2858 2859 def find_object(self, obj_id): 2860 if not self.is_cib_sane(): 2861 return None 2862 objs = self.find_objects(obj_id) 2863 if objs is None: 2864 return None 2865 if objs: 2866 for obj in objs: 2867 if obj.obj_type != 'node': 2868 return obj 2869 return objs[0] 2870 return None 2871 2872 def find_resource(self, obj_id): 2873 if not self.is_cib_sane(): 2874 return None 2875 objs = self.find_objects(obj_id) 2876 if objs is None: 2877 return None 2878 for obj in objs: 2879 if obj.obj_type != 'node': 2880 return obj 2881 return None 2882 2883 def find_node(self, obj_id): 2884 if not self.is_cib_sane(): 2885 return None 2886 objs = self.find_objects(obj_id) 2887 if objs is None: 2888 return None 2889 for obj in objs: 2890 if obj.obj_type == 'node': 2891 return obj 2892 return None 2893 2894 # 2895 # tab completion functions 2896 # 2897 def id_list(self): 2898 "List of ids (for completion)." 2899 return [x.obj_id for x in self.cib_objects] 2900 2901 def type_list(self): 2902 "List of object types (for completion)" 2903 return list(set([x.obj_type for x in self.cib_objects])) 2904 2905 def tag_list(self): 2906 "List of tags (for completion)" 2907 return list(set([x.obj_id for x in self.cib_objects if x.obj_type == "tag"])) 2908 2909 def prim_id_list(self): 2910 "List of primitives ids (for group completion)." 2911 return [x.obj_id for x in self.cib_objects if x.obj_type == "primitive"] 2912 2913 def children_id_list(self): 2914 "List of child ids (for clone/master completion)." 2915 return [x.obj_id for x in self.cib_objects if x.obj_type in constants.children_tags] 2916 2917 def rsc_id_list(self): 2918 "List of all resource ids." 2919 return [x.obj_id for x in self.cib_objects 2920 if x.obj_type in constants.resource_tags] 2921 2922 def top_rsc_id_list(self): 2923 "List of top resource ids (for constraint completion)." 2924 return [x.obj_id for x in self.cib_objects 2925 if x.obj_type in constants.resource_tags and not x.parent] 2926 2927 def node_id_list(self): 2928 "List of node ids." 2929 return sorted([x.node.get("uname") for x in self.cib_objects 2930 if x.obj_type == "node"]) 2931 2932 def f_prim_free_id_list(self): 2933 "List of possible primitives ids (for group completion)." 2934 return [x.obj_id for x in self.cib_objects 2935 if x.obj_type == "primitive" and not x.parent] 2936 2937 def f_prim_list_in_group(self, gname): 2938 "List resources in a group" 2939 return [x.obj_id for x in self.cib_objects 2940 if x.obj_type == "primitive" and x.parent and \ 2941 x.parent.obj_id == gname] 2942 2943 def f_group_id_list(self): 2944 "List of group ids." 2945 return [x.obj_id for x in self.cib_objects 2946 if x.obj_type == "group"] 2947 2948 def rsc_template_list(self): 2949 "List of templates." 2950 return [x.obj_id for x in self.cib_objects 2951 if x.obj_type == "rsc_template"] 2952 2953 def f_children_id_list(self): 2954 "List of possible child ids (for clone/master completion)." 2955 return [x.obj_id for x in self.cib_objects 2956 if x.obj_type in constants.children_tags and not x.parent] 2957 2958 # 2959 # a few helper functions 2960 # 2961 def find_container_child(self, node): 2962 "Find an object which may be the child in a container." 2963 for obj in reversed(self.cib_objects): 2964 if node.tag == "fencing-topology" and obj.xml_obj_type == "fencing-topology": 2965 return obj 2966 if node.tag == obj.node.tag and node.get("id") == obj.obj_id: 2967 return obj 2968 return None 2969 2970 def find_xml_node(self, tag, ident, strict=True): 2971 "Find a xml node of this type with this id." 2972 try: 2973 if tag in constants.defaults_tags: 2974 expr = '//%s/meta_attributes[@id="%s"]' % (tag, ident) 2975 elif tag == 'fencing-topology': 2976 expr = '//fencing-topology' 2977 else: 2978 expr = '//%s[@id="%s"]' % (tag, ident) 2979 return self.cib_elem.xpath(expr)[0] 2980 except IndexError: 2981 if strict: 2982 common_warn("strange, %s element %s not found" % (tag, ident)) 2983 return None 2984 2985 # 2986 # Element editing stuff. 2987 # 2988 def default_timeouts(self, *args): 2989 ''' 2990 Set timeouts for operations from the defaults provided in 2991 the meta-data. 2992 ''' 2993 implied_actions = ["start", "stop"] 2994 implied_ms_actions = ["promote", "demote"] 2995 implied_migrate_actions = ["migrate_to", "migrate_from"] 2996 other_actions = ("monitor",) 2997 if not self.is_cib_sane(): 2998 return False 2999 rc = True 3000 for obj_id in args: 3001 obj = self.find_resource(obj_id) 3002 if not obj: 3003 no_object_err(obj_id) 3004 rc = False 3005 continue 3006 if obj.obj_type != "primitive": 3007 common_warn("element %s is not a primitive" % obj_id) 3008 rc = False 3009 continue 3010 r_node = reduce_primitive(obj.node) 3011 if r_node is None: 3012 # cannot do anything without template defined 3013 common_warn("template for %s not defined" % obj_id) 3014 rc = False 3015 continue 3016 ra = get_ra(r_node) 3017 if not ra.mk_ra_node(): # no RA found? 3018 if not self.is_asymm_cluster(): 3019 ra.error("no resource agent found for %s" % obj_id) 3020 continue 3021 obj_modified = False 3022 for c in r_node.iterchildren(): 3023 if c.tag == "operations": 3024 for c2 in c.iterchildren(): 3025 if not c2.tag == "op": 3026 continue 3027 op, pl = op2list(c2) 3028 if not op: 3029 continue 3030 if op in implied_actions: 3031 implied_actions.remove(op) 3032 elif can_migrate(r_node) and op in implied_migrate_actions: 3033 implied_migrate_actions.remove(op) 3034 elif is_ms(obj.node.getparent()) and op in implied_ms_actions: 3035 implied_ms_actions.remove(op) 3036 elif op not in other_actions: 3037 continue 3038 adv_timeout = ra.get_adv_timeout(op, c2) 3039 if adv_timeout: 3040 c2.set("timeout", adv_timeout) 3041 obj_modified = True 3042 l = implied_actions 3043 if can_migrate(r_node): 3044 l += implied_migrate_actions 3045 if is_ms(obj.node.getparent()): 3046 l += implied_ms_actions 3047 for op in l: 3048 adv_timeout = ra.get_adv_timeout(op) 3049 if not adv_timeout: 3050 continue 3051 n = etree.Element('op') 3052 n.set('name', op) 3053 n.set('timeout', adv_timeout) 3054 n.set('interval', '0') 3055 if not obj.add_operation(n): 3056 rc = False 3057 else: 3058 obj_modified = True 3059 if obj_modified: 3060 obj.set_updated() 3061 return rc 3062 3063 def is_id_refd(self, attr_list_type, ident): 3064 ''' 3065 Is this ID referenced anywhere? 3066 Used from cliformat 3067 ''' 3068 try: 3069 return self.id_refs[ident] == attr_list_type 3070 except KeyError: 3071 return False 3072 3073 def resolve_id_ref(self, attr_list_type, id_ref): 3074 ''' 3075 User is allowed to specify id_ref either as a an object 3076 id or as attributes id. Here we try to figure out which 3077 one, i.e. if the former is the case to find the right 3078 id to reference. 3079 ''' 3080 self.id_refs[id_ref] = attr_list_type 3081 obj = self.find_resource(id_ref) 3082 if obj: 3083 nodes = obj.node.xpath(".//%s" % attr_list_type) 3084 numnodes = len(nodes) 3085 if numnodes > 1: 3086 common_warn("%s contains more than one %s, using first" % 3087 (obj.obj_id, attr_list_type)) 3088 if numnodes > 0: 3089 node_id = nodes[0].get("id") 3090 if node_id: 3091 return node_id 3092 check_id_ref(self.cib_elem, id_ref) 3093 return id_ref 3094 3095 def _get_attr_value(self, obj_type, attr): 3096 if not self.is_cib_sane(): 3097 return None 3098 for obj in self.cib_objects: 3099 if obj.obj_type == obj_type and obj.node is not None: 3100 for n in nvpairs2list(obj.node): 3101 if n.get('name') == attr: 3102 return n.get('value') 3103 return None 3104 3105 def get_property(self, prop): 3106 ''' 3107 Get the value of the given cluster property. 3108 ''' 3109 return self._get_attr_value("property", prop) 3110 3111 def get_property_w_default(self, prop): 3112 ''' 3113 Get the value of the given property. If it is 3114 not set, return the default value. 3115 ''' 3116 v = self.get_property(prop) 3117 if v is None: 3118 try: 3119 v = get_properties_meta().param_default(prop) 3120 except: 3121 pass 3122 return v 3123 3124 def get_op_default(self, attr): 3125 ''' 3126 Get the value of the attribute from op_defaults. 3127 ''' 3128 return self._get_attr_value("op_defaults", attr) 3129 3130 def is_asymm_cluster(self): 3131 symm = self.get_property("symmetric-cluster") 3132 return symm and symm != "true" 3133 3134 def new_object(self, obj_type, obj_id): 3135 "Create a new object of type obj_type." 3136 common_debug("new_object: %s:%s" % (obj_type, obj_id)) 3137 existing = self.find_object(obj_id) 3138 if existing and [obj_type, existing.obj_type].count("node") != 1: 3139 common_error("Cannot create %s with ID '%s': Found existing %s with same ID." % (obj_type, obj_id, existing.obj_type)) 3140 return None 3141 xml_obj_type = backtrans.get(obj_type) 3142 v = cib_object_map.get(xml_obj_type) 3143 if v is None: 3144 return None 3145 obj = v[1](xml_obj_type) 3146 obj.obj_type = obj_type 3147 obj.set_id(obj_id) 3148 obj.node = None 3149 obj.origin = "user" 3150 return obj 3151 3152 def modified_elems(self): 3153 return [x for x in self.cib_objects 3154 if x.updated or x.origin == "user"] 3155 3156 def get_elems_on_type(self, spec): 3157 if not spec.startswith("type:"): 3158 return [] 3159 t = spec[5:] 3160 return [x for x in self.cib_objects if x.obj_type == t] 3161 3162 def get_elems_on_tag(self, spec): 3163 if not spec.startswith("tag:"): 3164 return [] 3165 t = spec[4:] 3166 matching_tags = [x for x in self.cib_objects if x.obj_type == 'tag' and x.obj_id == t] 3167 ret = [] 3168 for mt in matching_tags: 3169 matches = [cib_factory.find_resource(o) for o in mt.node.xpath('./obj_ref/@id')] 3170 ret += [m for m in matches if m is not None] 3171 return ret 3172 3173 def filter_objects(self, filters): 3174 """ 3175 Filter out a set of objects given a list of filters. 3176 3177 Complication: We want to refine selections, for example 3178 type:primitive tag:foo should give all primitives tagged foo, 3179 or type:node boo should give the node boo, but not the primitive boo. 3180 3181 Add keywords and|or to influence selection? 3182 Default to "or" between matches (like now) 3183 3184 type:primitive or type:group = all primitives and groups 3185 type:primitive and foo = primitives with id foo 3186 type:primitive and foo* = primitives that start with id foo 3187 type:primitive or foo* = all that start with id foo plus all primitives 3188 type:primitive and tag:foo 3189 3190 Returns: 3191 True, set() on success 3192 false, err on failure 3193 """ 3194 if not filters: 3195 return True, copy.copy(self.cib_objects) 3196 if filters[0] == 'NOOBJ': 3197 return True, orderedset.oset([]) 3198 obj_set = orderedset.oset([]) 3199 and_filter, and_set = False, None 3200 for spec in filters: 3201 if spec == "or": 3202 continue 3203 elif spec == "and": 3204 and_filter, and_set = True, obj_set 3205 obj_set = orderedset.oset([]) 3206 continue 3207 if spec == "changed": 3208 obj_set |= orderedset.oset(self.modified_elems()) 3209 elif spec.startswith("type:"): 3210 obj_set |= orderedset.oset(self.get_elems_on_type(spec)) 3211 elif spec.startswith("tag:"): 3212 obj_set |= orderedset.oset(self.get_elems_on_tag(spec)) 3213 elif spec.startswith("related:"): 3214 name = spec[len("related:"):] 3215 obj_set |= orderedset.oset(self.find_objects(name) or []) 3216 obj = self.find_object(name) 3217 if obj is not None: 3218 obj_set |= orderedset.oset(self.related_elements(obj)) 3219 else: 3220 objs = self.find_objects(spec) or [] 3221 for obj in objs: 3222 obj_set.add(obj) 3223 if not objs: 3224 return False, spec 3225 if and_filter is True: 3226 and_filter, obj_set = False, obj_set.intersection(and_set) 3227 if and_filter is True: 3228 and_filter, obj_set = False, and_set 3229 return True, obj_set 3230 3231 def mkobj_set(self, *args): 3232 rc, obj_set = self.filter_objects(args) 3233 if rc is False: 3234 no_object_err(obj_set) 3235 return False, orderedset.oset([]) 3236 return rc, obj_set 3237 3238 def get_all_obj_set(self): 3239 return set(self.cib_objects) 3240 3241 def has_no_primitives(self): 3242 return not self.get_elems_on_type("type:primitive") 3243 3244 def has_cib_changed(self): 3245 if not self.is_cib_sane(): 3246 return False 3247 return self.modified_elems() or self.remove_queue 3248 3249 def _verify_constraints(self, node): 3250 ''' 3251 Check if all resources referenced in a constraint exist 3252 ''' 3253 rc = True 3254 constraint_id = node.get("id") 3255 for obj_id in referenced_resources(node): 3256 if not self.find_resource(obj_id): 3257 constraint_norefobj_err(constraint_id, obj_id) 3258 rc = False 3259 return rc 3260 3261 def _verify_rsc_children(self, obj): 3262 ''' 3263 Check prerequisites: 3264 a) all children must exist 3265 b) no child may have more than one parent 3266 c) there may not be duplicate children 3267 ''' 3268 obj_id = obj.obj_id 3269 rc = True 3270 c_dict = {} 3271 for c in obj.node.iterchildren(): 3272 if not is_cib_element(c): 3273 continue 3274 child_id = c.get("id") 3275 if not self._verify_child(child_id, obj.node.tag, obj_id): 3276 rc = False 3277 if child_id in c_dict: 3278 common_err("in group %s child %s listed more than once" % 3279 (obj_id, child_id)) 3280 rc = False 3281 c_dict[child_id] = 1 3282 for other in [x for x in self.cib_objects 3283 if x != obj and is_container(x.node)]: 3284 shared_obj = set(obj.children) & set(other.children) 3285 if shared_obj: 3286 common_err("%s contained in both %s and %s" % 3287 (','.join([x.obj_id for x in shared_obj]), 3288 obj_id, other.obj_id)) 3289 rc = False 3290 return rc 3291 3292 def _verify_child(self, child_id, parent_tag, obj_id): 3293 'Check if child exists and obj_id is (or may become) its parent.' 3294 child = self.find_resource(child_id) 3295 if not child: 3296 no_object_err(child_id) 3297 return False 3298 if parent_tag == "group" and child.obj_type != "primitive": 3299 common_err("a group may contain only primitives; %s is %s" % 3300 (child_id, child.obj_type)) 3301 return False 3302 if child.parent and child.parent.obj_id != obj_id: 3303 common_err("%s already in use at %s" % (child_id, child.parent.obj_id)) 3304 return False 3305 if child.node.tag not in constants.children_tags: 3306 common_err("%s may contain a primitive or a group; %s is %s" % 3307 (parent_tag, child_id, child.obj_type)) 3308 return False 3309 return True 3310 3311 def _verify_element(self, obj): 3312 ''' 3313 Can we create this object given its CLI representation. 3314 This is not about syntax, we're past that, but about 3315 semantics. 3316 Right now we check if the children, if any, are fit for 3317 the parent. And if this is a constraint, if all 3318 referenced resources are present. 3319 ''' 3320 rc = True 3321 node = obj.node 3322 obj_id = obj.obj_id 3323 try: 3324 cib_object_map[node.tag][0] 3325 except KeyError: 3326 common_err("element %s (%s) not recognized" % (node.tag, obj_id)) 3327 return False 3328 if is_container(node): 3329 rc &= self._verify_rsc_children(obj) 3330 elif is_constraint(node): 3331 rc &= self._verify_constraints(node) 3332 return rc 3333 3334 def create_object(self, *args): 3335 if not self.is_cib_sane(): 3336 return False 3337 return self.create_from_cli(list(args)) is not None 3338 3339 def set_property_cli(self, obj_type, node): 3340 pset_id = node.get('id') or default_id_for_obj(obj_type) 3341 obj = self.find_object(pset_id) 3342 if not obj: 3343 if not is_id_valid(pset_id): 3344 invalid_id_err(pset_id) 3345 return None 3346 obj = self.new_object(obj_type, pset_id) 3347 if not obj: 3348 return None 3349 topnode = get_topnode(self.cib_elem, obj.parent_type) 3350 obj.node = etree.SubElement(topnode, node.tag) 3351 obj.origin = "user" 3352 obj.node.set('id', pset_id) 3353 topnode.append(obj.node) 3354 self.cib_objects.append(obj) 3355 copy_nvpairs(obj.node, node) 3356 obj.normalize_parameters() 3357 obj.set_updated() 3358 return obj 3359 3360 def add_op(self, node): 3361 '''Add an op to a primitive.''' 3362 # does the referenced primitive exist 3363 rsc_id = node.get('rsc') 3364 rsc_obj = self.find_resource(rsc_id) 3365 if not rsc_obj: 3366 no_object_err(rsc_id) 3367 return None 3368 if rsc_obj.obj_type != "primitive": 3369 common_err("%s is not a primitive" % rsc_id) 3370 return None 3371 3372 # the given node is not postprocessed 3373 node, obj_type, obj_id = postprocess_cli(node, id_hint=rsc_obj.obj_id) 3374 3375 del node.attrib['rsc'] 3376 return rsc_obj.add_operation(node) 3377 3378 def create_from_cli(self, cli): 3379 'Create a new cib object from the cli representation.' 3380 if not self.is_cib_sane(): 3381 common_debug("create_from_cli (%s): is_cib_sane() failed" % (cli)) 3382 return None 3383 if isinstance(cli, (list, str)): 3384 elem, obj_type, obj_id = parse_cli_to_xml(cli) 3385 else: 3386 elem, obj_type, obj_id = postprocess_cli(cli) 3387 if elem is None: 3388 # FIXME: raise error? 3389 common_debug("create_from_cli (%s): failed" % (cli)) 3390 return None 3391 common_debug("create_from_cli: %s, %s, %s" % (xml_tostring(elem), obj_type, obj_id)) 3392 if obj_type in olist(constants.nvset_cli_names): 3393 return self.set_property_cli(obj_type, elem) 3394 if obj_type == "op": 3395 return self.add_op(elem) 3396 if obj_type == "node": 3397 obj = self.find_node(obj_id) 3398 # make an exception and allow updating nodes 3399 if obj: 3400 self.merge_from_cli(obj, elem) 3401 return obj 3402 obj = self.new_object(obj_type, obj_id) 3403 if not obj: 3404 return None 3405 return self._add_element(obj, elem) 3406 3407 def update_from_cli(self, obj, node, method): 3408 ''' 3409 Replace element from the cli intermediate. 3410 If this is an update and the element is properties, then 3411 the new properties should be merged with the old. 3412 Otherwise, users may be surprised. 3413 ''' 3414 if method == 'update' and obj.obj_type in constants.nvset_cli_names: 3415 return self.merge_from_cli(obj, node) 3416 return self.update_element(obj, node) 3417 3418 def update_from_node(self, obj, node): 3419 'Update element from a doc node.' 3420 idmgmt.replace_xml(obj.node, node) 3421 return self.update_element(obj, node) 3422 3423 def update_element(self, obj, newnode): 3424 'Update element from a doc node.' 3425 if newnode is None: 3426 return False 3427 if not self.is_cib_sane(): 3428 idmgmt.replace_xml(newnode, obj.node) 3429 return False 3430 oldnode = obj.node 3431 if xml_equals(oldnode, newnode): 3432 if newnode.getparent() is not None: 3433 newnode.getparent().remove(newnode) 3434 return True # the new and the old versions are equal 3435 obj.node = newnode 3436 common_debug("update CIB element: %s" % str(obj)) 3437 if oldnode.getparent() is not None: 3438 oldnode.getparent().replace(oldnode, newnode) 3439 obj.nocli = False # try again after update 3440 if not self._adjust_children(obj): 3441 return False 3442 if not obj.cli_use_validate(): 3443 common_debug("update_element: validation failed (%s, %s)" % (obj, xml_tostring(newnode))) 3444 obj.nocli_warn = True 3445 obj.nocli = True 3446 obj.set_updated() 3447 return True 3448 3449 def merge_from_cli(self, obj, node): 3450 common_debug("merge_from_cli: %s %s" % (obj.obj_type, xml_tostring(node))) 3451 if obj.obj_type in constants.nvset_cli_names: 3452 rc = merge_attributes(obj.node, node, "nvpair") 3453 else: 3454 rc = merge_nodes(obj.node, node) 3455 if rc: 3456 obj.set_updated() 3457 return True 3458 3459 def _cli_set_update(self, edit_d, mk_set, upd_set, del_set, method): 3460 ''' 3461 Create/update/remove elements. 3462 edit_d is a dict with id keys and parsed xml values. 3463 mk_set is a set of ids to be created. 3464 upd_set is a set of ids to be updated (replaced). 3465 del_set is a set to be removed. 3466 method is either replace or update. 3467 ''' 3468 common_debug("_cli_set_update: mk=%s, upd=%s, del=%s" % (mk_set, upd_set, del_set)) 3469 test_l = [] 3470 3471 def obj_is_container(x): 3472 obj = self.find_resource(x) 3473 return obj and is_container(obj.node) 3474 3475 def obj_is_constraint(x): 3476 obj = self.find_resource(x) 3477 return obj and is_constraint(obj.node) 3478 3479 del_constraints = [] 3480 del_containers = [] 3481 del_objs = [] 3482 for x in del_set: 3483 if obj_is_constraint(x): 3484 del_constraints.append(x) 3485 elif obj_is_container(x): 3486 del_containers.append(x) 3487 else: 3488 del_objs.append(x) 3489 3490 # delete constraints and containers first in case objects are moved elsewhere 3491 if not self.delete(*del_constraints): 3492 common_debug("delete %s failed" % (list(del_set))) 3493 return False 3494 if not self.delete(*del_containers): 3495 common_debug("delete %s failed" % (list(del_set))) 3496 return False 3497 3498 for cli in processing_sort([edit_d[x] for x in mk_set]): 3499 obj = self.create_from_cli(cli) 3500 if not obj: 3501 common_debug("create_from_cli '%s' failed" % 3502 (xml_tostring(cli, pretty_print=True))) 3503 return False 3504 test_l.append(obj) 3505 3506 for ident in upd_set: 3507 if edit_d[ident].tag == 'node': 3508 obj = self.find_node(ident) 3509 else: 3510 obj = self.find_resource(ident) 3511 if not obj: 3512 common_debug("%s not found!" % (ident)) 3513 return False 3514 node, _, _ = postprocess_cli(edit_d[ident], oldnode=obj.node) 3515 if node is None: 3516 common_debug("postprocess_cli failed: %s" % (ident)) 3517 return False 3518 if not self.update_from_cli(obj, node, method): 3519 common_debug("update_from_cli failed: %s, %s, %s" % 3520 (obj, xml_tostring(node), method)) 3521 return False 3522 test_l.append(obj) 3523 3524 if not self.delete(*reversed(del_objs)): 3525 common_debug("delete %s failed" % (list(del_set))) 3526 return False 3527 rc = True 3528 for obj in test_l: 3529 if not self.test_element(obj): 3530 common_debug("test_element failed for %s" % (obj)) 3531 rc = False 3532 return rc & self.check_structure() 3533 3534 def _xml_set_update(self, edit_d, mk_set, upd_set, del_set): 3535 ''' 3536 Create/update/remove elements. 3537 node_l is a list of elementtree elements. 3538 mk_set is a set of ids to be created. 3539 upd_set is a set of ids to be updated (replaced). 3540 del_set is a set to be removed. 3541 ''' 3542 common_debug("_xml_set_update: %s, %s, %s" % (mk_set, upd_set, del_set)) 3543 test_l = [] 3544 for el in processing_sort([edit_d[x] for x in mk_set]): 3545 obj = self.create_from_node(el) 3546 if not obj: 3547 return False 3548 test_l.append(obj) 3549 for ident in upd_set: 3550 if edit_d[ident].tag == 'node': 3551 obj = self.find_node(ident) 3552 else: 3553 obj = self.find_resource(ident) 3554 if not obj: 3555 return False 3556 if not self.update_from_node(obj, edit_d[ident]): 3557 return False 3558 test_l.append(obj) 3559 if not self.delete(*list(del_set)): 3560 return False 3561 rc = True 3562 for obj in test_l: 3563 if not self.test_element(obj): 3564 rc = False 3565 return rc & self.check_structure() 3566 3567 def _set_update(self, edit_d, mk_set, upd_set, del_set, upd_type, method): 3568 if upd_type == "xml": 3569 return self._xml_set_update(edit_d, mk_set, upd_set, del_set) 3570 return self._cli_set_update(edit_d, mk_set, upd_set, del_set, method) 3571 3572 def set_update(self, edit_d, mk_set, upd_set, del_set, upd_type="cli", method='replace'): 3573 ''' 3574 Just a wrapper for _set_update() to allow for a 3575 rollback. 3576 ''' 3577 self._push_state() 3578 if not self._set_update(edit_d, mk_set, upd_set, del_set, upd_type, method): 3579 if not self._pop_state(): 3580 raise RuntimeError("this should never happen!") 3581 return False 3582 self._drop_state() 3583 return True 3584 3585 def _adjust_children(self, obj): 3586 ''' 3587 All stuff children related: manage the nodes of children, 3588 update the list of children for the parent, update 3589 parents in the children. 3590 ''' 3591 new_children_ids = get_rsc_children_ids(obj.node) 3592 if not new_children_ids: 3593 return True 3594 old_children = [x for x in obj.children if x.parent == obj] 3595 new_children = [self.find_resource(x) for x in new_children_ids] 3596 new_children = [c for c in new_children if c is not None] 3597 obj.children = new_children 3598 # relink orphans to top 3599 for child in set(old_children) - set(obj.children): 3600 common_debug("relink child %s to top" % str(child)) 3601 self._relink_child_to_top(child) 3602 if not self._are_children_orphans(obj): 3603 return False 3604 return self._update_children(obj) 3605 3606 def _relink_child_to_top(self, obj): 3607 'Relink a child to the top node.' 3608 get_topnode(self.cib_elem, obj.parent_type).append(obj.node) 3609 obj.parent = None 3610 3611 def _are_children_orphans(self, obj): 3612 """ 3613 Check if we're adding a container containing objects 3614 we've already added to a different container 3615 """ 3616 for child in obj.children: 3617 if not child.parent: 3618 continue 3619 if child.parent == obj or child.parent.obj_id == obj.obj_id: 3620 continue 3621 if child.parent.obj_type in constants.container_tags: 3622 common_err("Cannot create %s: Child %s already in %s" % (obj, child, child.parent)) 3623 return False 3624 return True 3625 3626 def _update_children(self, obj): 3627 '''For composite objects: update all children nodes. 3628 ''' 3629 # unlink all and find them in the new node 3630 for child in obj.children: 3631 oldnode = child.node 3632 newnode = obj.find_child_in_node(child) 3633 if newnode is None: 3634 common_err("Child found in children list but not in node: %s, %s" % (obj, child)) 3635 return False 3636 child.node = newnode 3637 if child.children: # and children of children 3638 if not self._update_children(child): 3639 return False 3640 rmnode(oldnode) 3641 if child.parent: 3642 child.parent.updated = True 3643 child.parent = obj 3644 return True 3645 3646 def test_element(self, obj): 3647 if obj.xml_obj_type not in constants.defaults_tags: 3648 if not self._verify_element(obj): 3649 return False 3650 if utils.is_check_always() and obj.check_sanity() > 1: 3651 return False 3652 return True 3653 3654 def _update_links(self, obj): 3655 ''' 3656 Update the structure links for the object (obj.children, 3657 obj.parent). Update also the XML, if necessary. 3658 ''' 3659 obj.children = [] 3660 if obj.obj_type not in constants.container_tags: 3661 return 3662 for c in obj.node.iterchildren(): 3663 if is_child_rsc(c): 3664 child = self.find_container_child(c) 3665 if not child: 3666 missing_obj_err(c) 3667 continue 3668 child.parent = obj 3669 obj.children.append(child) 3670 if c != child.node: 3671 rmnode(child.node) 3672 child.node = c 3673 3674 def _add_element(self, obj, node): 3675 assert node is not None 3676 obj.node = node 3677 obj.set_id() 3678 pnode = get_topnode(self.cib_elem, obj.parent_type) 3679 common_debug("_add_element: append child %s to %s" % (obj.obj_id, pnode.tag)) 3680 if not self._adjust_children(obj): 3681 return None 3682 pnode.append(node) 3683 self._redirect_children_constraints(obj) 3684 obj.normalize_parameters() 3685 if not obj.cli_use_validate(): 3686 self.nocli_warn = True 3687 obj.nocli = True 3688 self._update_links(obj) 3689 obj.origin = "user" 3690 self.cib_objects.append(obj) 3691 return obj 3692 3693 def _add_children(self, obj_type, node): 3694 """ 3695 Called from create_from_node 3696 In case this is a clone/group/master create from XML, 3697 and the child node(s) haven't been added as a separate objects. 3698 """ 3699 if obj_type not in constants.container_tags: 3700 return True 3701 3702 # bsc#959895: also process cloned groups 3703 for c in node.iterchildren(): 3704 if c.tag not in ('primitive', 'group'): 3705 continue 3706 pid = c.get('id') 3707 child_obj = self.find_resource(pid) 3708 if child_obj is None: 3709 child_obj = self.create_from_node(copy.deepcopy(c)) 3710 if not child_obj: 3711 return False 3712 return True 3713 3714 def create_from_node(self, node): 3715 'Create a new cib object from a document node.' 3716 if node is None: 3717 common_debug("create_from_node: got None") 3718 return None 3719 try: 3720 obj_type = cib_object_map[node.tag][0] 3721 except KeyError: 3722 common_debug("create_from_node: keyerror (%s)" % (node.tag)) 3723 return None 3724 if is_defaults(node): 3725 node = get_rscop_defaults_meta_node(node) 3726 if node is None: 3727 common_debug("create_from_node: get_rscop_defaults_meta_node failed") 3728 return None 3729 3730 if not self._add_children(obj_type, node): 3731 return None 3732 3733 obj = self.new_object(obj_type, node.get("id")) 3734 if not obj: 3735 return None 3736 return self._add_element(obj, node) 3737 3738 def _remove_obj(self, obj): 3739 "Remove a cib object." 3740 common_debug("remove object %s" % str(obj)) 3741 for child in obj.children: 3742 # just relink, don't remove children 3743 self._relink_child_to_top(child) 3744 if obj.parent: # remove obj from its parent, if any 3745 obj.parent.children.remove(obj) 3746 idmgmt.remove_xml(obj.node) 3747 rmnode(obj.node) 3748 self._add_to_remove_queue(obj) 3749 self.cib_objects.remove(obj) 3750 for tag in self.related_tags(obj): 3751 # remove self from tag 3752 # remove tag if self is last tagged object in tag 3753 selfies = [x for x in tag.node.iterchildren() if x.get('id') == obj.obj_id] 3754 for c in selfies: 3755 rmnode(c) 3756 if not tag.node.xpath('./obj_ref'): 3757 self._remove_obj(tag) 3758 if not self._no_constraint_rm_msg: 3759 err_buf.info("hanging %s deleted" % str(tag)) 3760 for c_obj in self.related_constraints(obj): 3761 if is_simpleconstraint(c_obj.node) and obj.children: 3762 # the first child inherits constraints 3763 rename_rscref(c_obj, obj.obj_id, obj.children[0].obj_id) 3764 deleted = False 3765 if delete_rscref(c_obj, obj.obj_id): 3766 deleted = True 3767 if silly_constraint(c_obj.node, obj.obj_id): 3768 # remove invalid constraints 3769 self._remove_obj(c_obj) 3770 if not self._no_constraint_rm_msg: 3771 err_buf.info("hanging %s deleted" % str(c_obj)) 3772 elif deleted: 3773 err_buf.info("constraint %s updated" % str(c_obj)) 3774 3775 def related_tags(self, obj): 3776 def related_tag(tobj): 3777 if tobj.obj_type != 'tag': 3778 return False 3779 for c in tobj.node.iterchildren(): 3780 if c.get('id') == obj.obj_id: 3781 return True 3782 return False 3783 return [x for x in self.cib_objects if related_tag(x)] 3784 3785 def related_constraints(self, obj): 3786 def related_constraint(obj2): 3787 return is_constraint(obj2.node) and rsc_constraint(obj.obj_id, obj2.node) 3788 if not is_resource(obj.node): 3789 return [] 3790 return [x for x in self.cib_objects if related_constraint(x)] 3791 3792 def related_elements(self, obj): 3793 "Both constraints, groups, tags, ..." 3794 if not is_resource(obj.node): 3795 return [] 3796 return [x for x in self.cib_objects if is_related(obj.obj_id, x.node)] 3797 3798 def _redirect_children_constraints(self, obj): 3799 ''' 3800 Redirect constraints to the new parent 3801 ''' 3802 for child in obj.children: 3803 for c_obj in self.related_constraints(child): 3804 rename_rscref(c_obj, child.obj_id, obj.obj_id) 3805 # drop useless constraints which may have been created above 3806 for c_obj in self.related_constraints(obj): 3807 if silly_constraint(c_obj.node, obj.obj_id): 3808 self._no_constraint_rm_msg = True 3809 self._remove_obj(c_obj) 3810 self._no_constraint_rm_msg = False 3811 3812 def template_primitives(self, obj): 3813 if not is_template(obj.node): 3814 return [] 3815 c_list = [] 3816 for obj2 in self.cib_objects: 3817 if not is_primitive(obj2.node): 3818 continue 3819 if obj2.node.get("template") == obj.obj_id: 3820 c_list.append(obj2) 3821 return c_list 3822 3823 def _check_running_primitives(self, prim_l): 3824 rscstat = RscState() 3825 for prim in prim_l: 3826 if not rscstat.can_delete(prim.obj_id): 3827 common_err("resource %s is running, can't delete it" % prim.obj_id) 3828 return False 3829 return True 3830 3831 def _add_to_remove_queue(self, obj): 3832 if obj.origin == "cib": 3833 self.remove_queue.append(obj) 3834 3835 def _delete_1(self, obj): 3836 ''' 3837 Remove an object and its parent in case the object is the 3838 only child. 3839 ''' 3840 if obj.parent and len(obj.parent.children) == 1: 3841 self._delete_1(obj.parent) 3842 if obj in self.cib_objects: # don't remove parents twice 3843 self._remove_obj(obj) 3844 3845 def delete(self, *args): 3846 'Delete a cib object.' 3847 if not self.is_cib_sane(): 3848 return False 3849 rc = True 3850 l = [] 3851 rscstat = RscState() 3852 for obj_id in args: 3853 obj = self.find_object(obj_id) 3854 if not obj: 3855 # If --force is set: 3856 # Unless something more serious goes wrong here, 3857 # don't return an error code if the object 3858 # to remove doesn't exist. This should help scripted 3859 # workflows without compromising an interactive 3860 # use. 3861 if not config.core.force: 3862 no_object_err(obj_id) 3863 rc = False 3864 continue 3865 if not rscstat.can_delete(obj_id): 3866 common_err("resource %s is running, can't delete it" % obj_id) 3867 rc = False 3868 continue 3869 if is_template(obj.node): 3870 prim_l = self.template_primitives(obj) 3871 prim_l = [x for x in prim_l 3872 if x not in l and x.obj_id not in args] 3873 if not self._check_running_primitives(prim_l): 3874 rc = False 3875 continue 3876 for prim in prim_l: 3877 common_info("hanging %s deleted" % str(prim)) 3878 l.append(prim) 3879 l.append(obj) 3880 if l: 3881 l = processing_sort_cli(l) 3882 for obj in reversed(l): 3883 self._delete_1(obj) 3884 return rc 3885 3886 def rename(self, old_id, new_id): 3887 ''' 3888 Rename a cib object. 3889 - check if the resource (if it's a resource) is stopped 3890 - check if the new id is not taken 3891 - find the object with old id 3892 - rename old id to new id in all related objects 3893 (constraints) 3894 - if the object came from the CIB, then it must be 3895 deleted and the one with the new name created 3896 - rename old id to new id in the object 3897 ''' 3898 if not self.is_cib_sane() or not new_id: 3899 return False 3900 if idmgmt.id_in_use(new_id): 3901 return False 3902 obj = self.find_object(old_id) 3903 if not obj: 3904 no_object_err(old_id) 3905 return False 3906 if not obj.can_be_renamed(): 3907 return False 3908 for c_obj in self.related_constraints(obj): 3909 rename_rscref(c_obj, old_id, new_id) 3910 rename_id(obj.node, old_id, new_id) 3911 obj.obj_id = new_id 3912 idmgmt.rename(old_id, new_id) 3913 # FIXME: (bnc#901543) 3914 # for each child node; if id starts with "%(old_id)s-" and 3915 # is not referenced by anything, change that id as well? 3916 # otherwise inner ids will resemble old name, not new 3917 obj.set_updated() 3918 3919 def erase(self): 3920 "Remove all cib objects." 3921 # remove only bottom objects and no constraints 3922 # the rest will automatically follow 3923 if not self.is_cib_sane(): 3924 return False 3925 erase_ok = True 3926 l = [] 3927 rscstat = RscState() 3928 for obj in [obj for obj in self.cib_objects if not obj.children and not is_constraint(obj.node) and obj.obj_type != "node"]: 3929 if not rscstat.can_delete(obj.obj_id): 3930 common_warn("resource %s is running, can't delete it" % obj.obj_id) 3931 erase_ok = False 3932 else: 3933 l.append(obj) 3934 if not erase_ok: 3935 common_err("CIB erase aborted (nothing was deleted)") 3936 return False 3937 self._no_constraint_rm_msg = True 3938 for obj in l: 3939 self.delete(obj.obj_id) 3940 self._no_constraint_rm_msg = False 3941 remaining = 0 3942 for obj in self.cib_objects: 3943 if obj.obj_type != "node": 3944 remaining += 1 3945 if remaining > 0: 3946 common_err("strange, but these objects remained:") 3947 for obj in self.cib_objects: 3948 if obj.obj_type != "node": 3949 print(str(obj), file=sys.stderr) 3950 self.cib_objects = [] 3951 return True 3952 3953 def erase_nodes(self): 3954 "Remove nodes only." 3955 if not self.is_cib_sane(): 3956 return False 3957 l = [obj for obj in self.cib_objects if obj.obj_type == "node"] 3958 for obj in l: 3959 self.delete(obj.obj_id) 3960 3961 def refresh(self): 3962 "Refresh from the CIB." 3963 self.reset() 3964 self.initialize() 3965 return self.is_cib_sane() 3966 3967 3968cib_factory = CibFactory() 3969 3970# vim:ts=4:sw=4:et: 3971