# Copyright (C) 2008-2011 Dejan Muhamedagic # Copyright (C) 2013-2016 Kristoffer Gronlund # See COPYING for license information. import shlex import re import inspect from lxml import etree from . import ra from . import constants from .ra import disambiguate_ra_type, ra_type_validate from . import schema from .utils import keyword_cmp, verify_boolean, lines2cli from .utils import get_boolean, olist, canonical_boolean from .utils import handle_role_for_ocf_1_1, compatible_role from . import xmlutil from . import log logger = log.setup_logger(__name__) logger_utils = log.LoggerUtils(logger) _NVPAIR_RE = re.compile(r'([^=@$][^=]*)=(.*)$') _NVPAIR_ID_RE = re.compile(r'\$([^:=]+)(?::(.+))?=(.*)$') _NVPAIR_REF_RE = re.compile(r'@([^:]+)(?::(.+))?$') _NVPAIR_KEY_RE = re.compile(r'([^:=]+)$', re.IGNORECASE) _IDENT_RE = re.compile(r'([a-z0-9_#$-][^=]*)$', re.IGNORECASE) _DISPATCH_RE = re.compile(r'[a-z0-9_]+$', re.IGNORECASE) _DESC_RE = re.compile(r'description=(.+)$', re.IGNORECASE) _ATTR_RE = re.compile(r'\$?([^=]+)=(.*)$') _ALERT_PATH_RE = re.compile(r'(.*)$') _RESOURCE_RE = re.compile(r'([a-z_#$][^=]*)$', re.IGNORECASE) _IDSPEC_RE = re.compile(r'(\$id-ref|\$id)=(.*)$', re.IGNORECASE) _ID_RE = re.compile(r'\$id=(.*)$', re.IGNORECASE) _ID_NEW_RE = re.compile(r'([\w-]+):$', re.IGNORECASE) _SCORE_RE = re.compile(r"([^:]+):$") _ROLE_RE = re.compile(r"\$?role=(.+)$", re.IGNORECASE) _BOOLOP_RE = re.compile(r'(%s)$' % ('|'.join(constants.boolean_ops)), re.IGNORECASE) _UNARYOP_RE = re.compile(r'(%s)$' % ('|'.join(constants.unary_ops)), re.IGNORECASE) _ACL_RIGHT_RE = re.compile(r'(%s)$' % ('|'.join(constants.acl_rule_names)), re.IGNORECASE) _ROLE_REF_RE = re.compile(r'role:(.+)$', re.IGNORECASE) _PERM_RE = re.compile(r"([^:]+)(?::(.+))?$", re.I) _UNAME_RE = re.compile(r'([^:]+)(:(normal|member|ping|remote))?$', re.IGNORECASE) _TEMPLATE_RE = re.compile(r'@(.+)$') _RA_TYPE_RE = re.compile(r'[a-z0-9_:-]+$', re.IGNORECASE) _TAG_RE = re.compile(r"([a-zA-Z_][^\s:]*):?$") _ROLE2_RE = re.compile(r"role=(.+)$", re.IGNORECASE) _TARGET_RE = re.compile(r'([^:]+):$') _TARGET_ATTR_RE = re.compile(r'attr:([\w-]+)=([\w-]+)$', re.IGNORECASE) _TARGET_PATTERN_RE = re.compile(r'pattern:(.+)$', re.IGNORECASE) TERMINATORS = ('params', 'meta', 'utilization', 'operations', 'op', 'op_params', 'op_meta', 'rule', 'attributes') class ParseError(Exception): ''' Raised by parsers when parsing fails. No error message, parsers should write error messages before raising the exception. ''' class Validation(object): def resource_roles(self): 'returns list of valid resource roles' return schema.rng_attr_values('resource_set', 'role') def resource_actions(self): 'returns list of valid resource actions' return schema.rng_attr_values('resource_set', 'action') def date_ops(self): 'returns list of valid date operations' return schema.rng_attr_values_l('date_expression', 'operation') def expression_types(self): 'returns list of valid expression types' return schema.rng_attr_values_l('expression', 'type') def rsc_order_kinds(self): return schema.rng_attr_values('rsc_order', 'kind') def class_provider_type(self, value): """ Unravel [class:[provider:]]type returns: (class, provider, type) """ c_p_t = disambiguate_ra_type(value) if not ra_type_validate(value, *c_p_t): return None return c_p_t def canonize(self, value, lst): 'case-normalizes value to what is in lst' value = value.lower() for x in lst: if value == x.lower(): return x return None def classify_role(self, role): if not role: return role, None elif role in olist(self.resource_roles()): return self.canonize(role, self.resource_roles()), 'role' elif role.isdigit(): return role, 'instance' return role, None def classify_action(self, action): if not action: return action, None elif action in olist(self.resource_actions()): return self.canonize(action, self.resource_actions()), 'action' elif action.isdigit(): return action, 'instance' return action, None def op_attributes(self): return olist(schema.get('attr', 'op', 'a')) def acl_2_0(self): vname = schema.validate_name() sp = vname.split('-') try: return sp[0] == 'pacemaker' and sp[1] == 'next' or float(sp[1]) >= 2.0 except Exception: return False def node_type_optional(self): ns = {'t': 'http://relaxng.org/ns/structure/1.0'} path = '//t:element[@name="nodes"]' path = path + '//t:element[@name="node"]/t:optional/t:attribute[@name="type"]' has_optional = schema.rng_xpath(path, namespaces=ns) return len(has_optional) > 0 validator = Validation() class BaseParser(object): _BINOP_RE = None _VALUE_SOURCE_RE = None def parse(self, cmd): "Called by do_parse(). Raises ParseError if parsing fails." raise NotImplementedError def err(self, msg, context=None, token=None): "Report a parse error and abort." if token is None and self.has_tokens(): token = self._cmd[self._currtok] if context is None: context = self._cmd[0] logger_utils.syntax_err(self._cmd, context=context, token=token, msg=msg) raise ParseError def begin(self, cmd, min_args=-1): self._cmd = cmd self._currtok = 0 self._lastmatch = None if min_args > -1 and len(cmd) < min_args + 1: self.err("Expected at least %d arguments" % (min_args)) def begin_dispatch(self, cmd, min_args=-1): """ Begin parsing cmd. Dispatches to parse_ based on the first token. """ self.begin(cmd, min_args=min_args) return self.match_dispatch(errmsg="Unknown command") def do_parse(self, cmd, ignore_empty, complete_advised): """ Called by CliParser. Calls parse() Parsers should pass their return value through this method. """ self.ignore_empty = ignore_empty self.complete_advised = complete_advised out = self.parse(cmd) if self.has_tokens(): self.err("Unknown arguments: " + ' '.join(self._cmd[self._currtok:])) return out def try_match(self, rx): """ Try to match the given regex with the curren token. rx: compiled regex or string returns: the match object, if the match is successful """ tok = self.current_token() if not tok: return None if isinstance(rx, str): if not rx.endswith('$'): rx = rx + '$' self._lastmatch = re.match(rx, tok, re.IGNORECASE) else: self._lastmatch = rx.match(tok) if self._lastmatch is not None: if not self.has_tokens(): self.err("Unexpected end of line") self._currtok += 1 return self._lastmatch def match(self, rx, errmsg=None): """ Match the given regex with the current token. If match fails, parse is aborted and an error reported. rx: compiled regex or string. errmsg: optional error message if match fails. Returns: The matched token. """ if not self.try_match(rx): if errmsg: self.err(errmsg) elif isinstance(rx, str): self.err("Expected " + rx) else: self.err("Expected " + rx.pattern.rstrip('$')) return self.matched(0) def matched(self, idx=0): """ After a successful match, returns the groups generated by the match. """ if hasattr(self._lastmatch, "group"): return self._lastmatch.group(idx) return None def lastmatch(self): return self._lastmatch def rewind(self): "useful for when validation fails, to undo the match" if self._currtok > 0: self._currtok -= 1 def current_token(self): if self.has_tokens(): return self._cmd[self._currtok] return None def has_tokens(self): return self._currtok < len(self._cmd) def match_rest(self): ''' matches and returns the rest of the tokens in a list ''' ret = self._cmd[self._currtok:] self._currtok = len(self._cmd) return ret def match_any(self): if not self.has_tokens(): self.err("Unexpected end of line") tok = self.current_token() self._currtok += 1 self._lastmatch = tok return tok def match_nvpairs_bykey(self, valid_keys, minpairs=1): """ matches string of p=v | p tokens, but only if p is in valid_keys Returns list of tags """ _KEY_RE = re.compile(r'(%s)=(.+)$' % '|'.join(valid_keys)) _NOVAL_RE = re.compile(r'(%s)$' % '|'.join(valid_keys)) ret = [] while True: if self.try_match(_KEY_RE): ret.append(xmlutil.nvpair(self.matched(1), self.matched(2))) elif self.try_match(_NOVAL_RE): ret.append(xmlutil.nvpair(self.matched(1), "")) else: break if len(ret) < minpairs: if minpairs == 1: self.err("Expected at least one name-value pair") else: self.err("Expected at least %d name-value pairs" % (minpairs)) return ret def match_nvpairs(self, terminator=None, minpairs=1, allow_empty=True): """ Matches string of p=v tokens Returns list of tags p tokens are also accepted and an nvpair tag with no value attribute is created, as long as they are not in the terminator list """ ret = [] if terminator is None: terminator = TERMINATORS while True: tok = self.current_token() if tok is not None and tok.lower() in terminator: break elif self.try_match(_NVPAIR_REF_RE): ret.append(xmlutil.nvpair_ref(self.matched(1), self.matched(2))) elif self.try_match(_NVPAIR_ID_RE): ret.append(xmlutil.nvpair_id(self.matched(1), self.matched(2), self.matched(3))) elif self.try_match(_NVPAIR_RE): if not allow_empty and not self.matched(2): self.err("Empty value for {} is not allowed".format(self.matched(1))) ret.append(xmlutil.nvpair(self.matched(1), self.matched(2))) elif len(terminator) and self.try_match(_NVPAIR_KEY_RE): ret.append(xmlutil.new("nvpair", name=self.matched(1))) else: break if len(ret) < minpairs: if minpairs == 1: self.err("Expected at least one name-value pair") else: self.err("Expected at least %d name-value pairs" % (minpairs)) return ret def try_match_nvpairs(self, name, terminator=None): """ Matches sequence of [= [= ...] ...] """ if self.try_match(name): self._lastmatch = self.match_nvpairs(terminator=terminator, minpairs=1) else: self._lastmatch = [] return self._lastmatch def match_identifier(self): return self.match(_IDENT_RE, errmsg="Expected identifier") def match_resource(self): return self.match(_RESOURCE_RE, errmsg="Expected resource") def match_idspec(self): """ matches $id= | $id-ref= matched(1) = $id|$id-ref matched(2) = """ return self.match(_IDSPEC_RE, errmsg="Expected $id-ref= or $id=") def try_match_idspec(self): """ matches $id= | $id-ref= matched(1) = $id|$id-ref matched(2) = """ return self.try_match(_IDSPEC_RE) def try_match_initial_id(self): """ Used as the first match on certain commands like node and property, to match either node $id= or node : """ m = self.try_match(_ID_RE) if m: return m return self.try_match(_ID_NEW_RE) def match_split(self): """ matches value[:value] """ if not self.current_token(): self.err("Expected value[:value]") sp = self.current_token().split(':') if len(sp) > 2: self.err("Expected value[:value]") while len(sp) < 2: sp.append(None) self.match_any() return sp def match_dispatch(self, errmsg=None): """ Match on the next token. Looks for a method named parse_. If found, the named function is called. Else, an error is reported. """ t = self.match(_DISPATCH_RE, errmsg=errmsg) t = 'parse_' + t.lower() if hasattr(self, t) and callable(getattr(self, t)): return getattr(self, t)() self.rewind() # rewind for more accurate error message self.err(errmsg) def try_match_description(self): """ reads a description=? token if one is next """ if self.try_match(_DESC_RE): return self.matched(1) return None def match_until(self, end_token): tokens = [] while self.current_token() is not None and self.current_token() != end_token: tokens.append(self.match_any()) return tokens def match_attr_list(self, name, tag, allow_empty=True, terminator=None): """ matches [$id=] [:] = = ... | $id-ref= if matchname is False, matches: = = ... """ from .cibconfig import cib_factory xmlid = None if self.try_match_idspec(): if self.matched(1) == '$id-ref': r = xmlutil.new(tag) ref = cib_factory.resolve_id_ref(name, self.matched(2)) r.set('id-ref', ref) return r else: xmlid = self.matched(2) score = None if self.try_match(_SCORE_RE): score = self.matched(1) rules = self.match_rules() values = self.match_nvpairs(minpairs=0, terminator=terminator) if (allow_empty, xmlid, score, len(rules), len(values)) == (False, None, None, 0, 0): return None return xmlutil.attributes(tag, rules, values, xmlid=xmlid, score=score) def match_attr_lists(self, name_map, implicit_initial=None, terminator=None): """ generator which matches attr_lists name_map: maps CLI name to XML name """ to_match = '|'.join(list(name_map.keys())) if self.try_match(to_match): name = self.matched(0).lower() yield self.match_attr_list(name, name_map[name], terminator=terminator) elif implicit_initial is not None: attrs = self.match_attr_list(implicit_initial, name_map[implicit_initial], allow_empty=False, terminator=terminator) if attrs is not None: yield attrs while self.try_match(to_match): name = self.matched(0).lower() yield self.match_attr_list(name, name_map[name], terminator=terminator) def match_rules(self): '''parse rule definitions''' from .cibconfig import cib_factory rules = [] while self.try_match('rule'): rule = xmlutil.new('rule') rules.append(rule) idref = False if self.try_match_idspec(): idtyp, idval = self.matched(1)[1:], self.matched(2) if idtyp == 'id-ref': idval = cib_factory.resolve_id_ref('rule', idval) idref = True rule.set(idtyp, idval) if self.try_match(_ROLE_RE): rule.set('role', handle_role_for_ocf_1_1(self.matched(1))) if idref: continue if self.try_match(_SCORE_RE): rule.set(*self.validate_score(self.matched(1))) else: rule.set('score', 'INFINITY') boolop, exprs = self.match_rule_expression() if boolop and not keyword_cmp(boolop, 'and'): rule.set('boolean-op', boolop) for expr in exprs: rule.append(expr) return rules def match_rule_expression(self): """ expression :: [bool_op ...] bool_op :: or | and simple_exp :: [type:] | | date type :: string | version | number binary_op :: lt | gt | lte | gte | eq | ne unary_op :: defined | not_defined date_expr :: lt | gt | in_range start= end= | in_range start= | date_spec duration|date_spec :: hours= | monthdays= | weekdays= | yearsdays= | months= | weeks= | years= | weekyears= | moon= """ boolop = None exprs = [self._match_simple_exp()] while self.try_match(_BOOLOP_RE): if boolop and self.matched(1) != boolop: self.err("Mixing bool ops not allowed: %s != %s" % (boolop, self.matched(1))) else: boolop = self.matched(1) exprs.append(self._match_simple_exp()) return boolop, exprs def _match_simple_exp(self): if self.try_match('date'): return self.match_date() elif self.try_match(_UNARYOP_RE): unary_op = self.matched(1) attr = self.match_identifier() return xmlutil.new('expression', operation=unary_op, attribute=attr) else: attr = self.match_identifier() if not self._BINOP_RE: self._BINOP_RE = re.compile(r'((%s):)?(%s)$' % ( '|'.join(validator.expression_types()), '|'.join(constants.binary_ops)), re.IGNORECASE) self.match(self._BINOP_RE) optype = self.matched(2) binop = self.matched(3) node = xmlutil.new('expression', operation=binop, attribute=attr) xmlutil.maybe_set(node, 'type', optype) val = self.match_any() if not self._VALUE_SOURCE_RE: self._VALUE_SOURCE_RE = re.compile(r"^(?P[^\s{}]+)({(?P\S+)})?$") val_src_match = re.match(self._VALUE_SOURCE_RE, val) if val_src_match.group('val') is None: node.set('value', val) else: node.set('value', val_src_match.group('val')) node.set('value-source', val_src_match.group('val_src')) return node def match_date(self): """ returns for example: """ node = xmlutil.new('date_expression') date_ops = validator.date_ops() # spec -> date_spec if 'date_spec' in date_ops: date_ops.append('spec') # in -> in_range if 'in_range' in date_ops: date_ops.append('in') self.match('(%s)$' % ('|'.join(date_ops))) op = self.matched(1) opmap = {'in': 'in_range', 'spec': 'date_spec'} node.set('operation', opmap.get(op, op)) if op in olist(constants.simple_date_ops): # lt|gt val = self.match_any() if keyword_cmp(op, 'lt'): node.set('end', val) else: node.set('start', val) return node elif op in ('in_range', 'in'): # date in start= end= # date in start= valid_keys = list(constants.in_range_attrs) + constants.date_spec_names vals = self.match_nvpairs_bykey(valid_keys, minpairs=2) return xmlutil.set_date_expression(node, 'duration', vals) elif op in ('date_spec', 'spec'): valid_keys = constants.date_spec_names vals = self.match_nvpairs_bykey(valid_keys, minpairs=1) return xmlutil.set_date_expression(node, 'date_spec', vals) else: self.err("Unknown date operation '%s', please upgrade crmsh" % (op)) def validate_score(self, score, noattr=False, to_kind=False): if not noattr and score in olist(constants.score_types): return ["score", constants.score_types[score.lower()]] elif re.match("^[+-]?(inf(inity)?|INF(INITY)?|[0-9]+)$", score): score = re.sub("inf(inity)?|INF(INITY)?", "INFINITY", score) if to_kind: return ["kind", score_to_kind(score)] else: return ["score", score] if noattr: # orders have the special kind attribute kind = validator.canonize(score, validator.rsc_order_kinds()) if not kind: self.err("Invalid kind: " + score) return ['kind', kind] else: return ['score-attribute', score] def match_arguments(self, out, name_map, implicit_initial=None, terminator=None): """ [ attr_list] [operations id_spec] [op op_type [= ...] ...] attr_list :: [$id=] = [=...] | $id-ref= id_spec :: $id= | $id-ref= op_type :: start | stop | monitor implicit_initial: when matching attr lists, if none match at first parse an implicit initial token and then continue. This is so for example: primitive foo Dummy state=1 is accepted when params is the implicit initial. """ names = olist(list(name_map.keys())) oplist = olist([op for op in name_map if op.lower() in ('operations', 'op')]) for op in oplist: del name_map[op] bundle_list = olist([op for op in name_map if op.lower() in ('docker', 'rkt', 'network', 'port-mapping', 'storage', 'primitive')]) for bl in bundle_list: del name_map[bl] initial = True while self.has_tokens(): t = self.current_token().lower() if t in names: initial = False if t in oplist: self.match_operations(out, t == 'operations') if t in bundle_list: self.match_container(out, t) else: if bundle_list: terminator = ['network', 'storage', 'primitive'] for attr_list in self.match_attr_lists(name_map, terminator=terminator): out.append(attr_list) elif initial: initial = False for attr_list in self.match_attr_lists(name_map, implicit_initial=implicit_initial, terminator=terminator): out.append(attr_list) else: break self.complete_advised_ops(out) def complete_advised_ops(self, out): """ Complete operation actions advised values """ if not self.complete_advised or out.tag != "primitive": return ra_inst = ra.RAInfo(out.get('class'), out.get('type'), out.get('provider')) ra_actions_dict = ra_inst.actions() if not ra_actions_dict: return def extract_advised_value(advised_dict, action, attr, role=None): adv_attr_value = None try: if action == "monitor": if role: for monitor_item in advised_dict[action]: if compatible_role(role, monitor_item['role']): adv_attr_value = monitor_item[attr] else: adv_attr_value = advised_dict[action][0][attr] else: adv_attr_value = advised_dict[action][attr] except KeyError: pass return adv_attr_value action_advised_attr_dict = {k:v for k, v in ra_actions_dict.items() if k in constants.ADVISED_ACTION_LIST} operations_node = out.find("operations") configured_action_list = [] # no operations configured if operations_node is None: operations_node = xmlutil.child(out, 'operations') # has operations configured else: op_nodes_list = operations_node.findall("op") for op_node in op_nodes_list: action = op_node.get('name') # complete advised value if interval or timeout not configured adv_interval = extract_advised_value(action_advised_attr_dict, action, 'interval', op_node.get('role')) or \ constants.DEFAULT_INTERVAL_IN_ACTION adv_timeout = extract_advised_value(action_advised_attr_dict, action, 'timeout', op_node.get('role')) if op_node.get('interval') is None: op_node.set('interval', adv_interval) if op_node.get('timeout') is None and adv_timeout: op_node.set('timeout', adv_timeout) configured_action_list.append(action) for action in action_advised_attr_dict: if action in configured_action_list: continue # complete advised value if the operation not configured value = action_advised_attr_dict[action] # for multi actions, like multi monitor if isinstance(value, list): for v_dict in value: op_node = xmlutil.new('op', name=action) for k, v in v_dict.items(): # set normal attributes if k in constants.ADVISED_KEY_LIST: op_node.set(k, handle_role_for_ocf_1_1(v)) operations_node.append(op_node) else: op_node = xmlutil.new('op', name=action, **value) operations_node.append(op_node) out.append(operations_node) def match_container(self, out, _type): container_node = None self.match(_type) all_attrs = self.match_nvpairs(minpairs=0, terminator=['network', 'storage', 'meta', 'primitive']) if _type != "primitive": exist_node = out.find(_type) if exist_node is None: container_node = xmlutil.new(_type) else: container_node = exist_node child_flag = False for nvp in all_attrs: if nvp.get('name') in ['port-mapping', 'storage-mapping']: inst_attrs = xmlutil.child(container_node, nvp.get('name')) child_flag = True continue if child_flag: inst_attrs.set(nvp.get('name'), nvp.get('value')) else: container_node.set(nvp.get('name'), nvp.get('value')) out.append(container_node) else: if len(all_attrs) != 1 or all_attrs[0].get('value'): self.err("Expected primitive reference, got {}".format(", ".join("{}={}".format(nvp.get('name'), nvp.get('value') or "") for nvp in all_attrs))) xmlutil.child(out, 'crmsh-ref', id=all_attrs[0].get('name')) def match_op(self, out, pfx='op'): """ op [= ...] to: """ self.match('op') op_type = self.match_identifier() all_attrs = self.match_nvpairs(minpairs=0) node = xmlutil.new('op', name=op_type) if not any(nvp.get('name') == 'interval' for nvp in all_attrs) and op_type != "monitor": all_attrs.append(xmlutil.nvpair('interval', '0s')) valid_attrs = validator.op_attributes() inst_attrs = None for nvp in all_attrs: if nvp.get('name') in valid_attrs: if inst_attrs is not None: self.err("Attribute order error: {} must appear before any instance attribute".format(nvp.get('name'))) node.set(nvp.get('name'), nvp.get('value')) else: if inst_attrs is None: inst_attrs = xmlutil.child(node, 'instance_attributes') inst_attrs.append(nvp) if inst_attrs is not None: node.append(inst_attrs) for attr_list in self.match_attr_lists({'op_params': 'instance_attributes', 'op_meta': 'meta_attributes'}, implicit_initial='op_params'): node.append(attr_list) out.append(node) def match_operations(self, out, match_id): from .cibconfig import cib_factory def is_op(): return self.has_tokens() and self.current_token().lower() == 'op' if match_id: self.match('operations') node = xmlutil.child(out, 'operations') if match_id: self.match_idspec() match_id = self.matched(1)[1:].lower() idval = self.matched(2) if match_id == 'id-ref': idval = cib_factory.resolve_id_ref('operations', idval) node.set(match_id, idval) # The ID assignment skips the operations node if possible, # so we need to pass the prefix (id of the owner node) # to match_op pfx = out.get('id') or 'op' while is_op(): self.match_op(node, pfx=pfx) _parsers = {} def parser_for(*lst): def decorator(thing): if inspect.isfunction(thing): def parse(self, cmd): return thing(self, cmd) ret = type("Parser-" + '-'.join(lst), (BaseParser,), {'parse': parse}) else: ret = thing ret.can_parse = lst for x in lst: _parsers[x] = ret() return ret return decorator @parser_for('node') def parse_node(self, cmd): """ node [:|$id=] [:] [description=] [attributes = [=...]] [utilization = [=...]] type :: normal | member | ping | remote """ self.begin(cmd, min_args=1) self.match('node') out = xmlutil.new('node') xmlutil.maybe_set(out, "id", self.try_match_initial_id() and self.matched(1)) self.match(_UNAME_RE, errmsg="Expected uname[:type]") out.set("uname", self.matched(1)) if validator.node_type_optional(): xmlutil.maybe_set(out, "type", self.matched(3)) else: out.set("type", self.matched(3) or constants.node_default_type) xmlutil.maybe_set(out, "description", self.try_match_description()) self.match_arguments(out, {'attributes': 'instance_attributes', 'utilization': 'utilization'}, implicit_initial='attributes') return out @parser_for('primitive', 'group', 'clone', 'ms', 'master', 'rsc_template', 'bundle') class ResourceParser(BaseParser): def match_ra_type(self, out): "[:[:]]" if not self.current_token(): self.err("Expected resource type") cpt = validator.class_provider_type(self.current_token()) if not cpt: self.err("Unknown resource type") self.match_any() xmlutil.maybe_set(out, 'class', cpt[0]) xmlutil.maybe_set(out, 'provider', cpt[1]) xmlutil.maybe_set(out, 'type', cpt[2]) def parse(self, cmd): return self.begin_dispatch(cmd, min_args=2) def _primitive_or_template(self): """ primitive {[:[:]]|@