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