Hello community, here is the log from the commit of package crmsh for openSUSE:Factory checked in at 2015-09-13 09:45:11 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/crmsh (Old) and /work/SRC/openSUSE:Factory/.crmsh.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "crmsh" Changes: -------- --- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes 2015-09-08 17:44:35.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.crmsh.new/crmsh.changes 2015-09-13 09:45:18.000000000 +0200 @@ -1,0 +2,14 @@ +Fri Sep 11 10:21:55 UTC 2015 - [email protected] + +- Update to version 2.2.0~rc3+git.1441965248.a9a616d: + + low: scripts: Fix typo in email type verifier + + low: scripts: [MailTo] install mailx package + + high: scripts: Add enum type to script values + + medium: parse: Add support for node attribute as fencing topology target + + doc: Improve documentation for the history level + + low: ui_history: Swap from and to times if to < from + + low: ui_history: Better error handling and documentation for the detail command + + medium: report: Add transition tags command (bsc#943470) + + medium: report: Mark transitions with errors with a star in info output (bsc#943470) + +------------------------------------------------------------------- Old: ---- crmsh-2.2.0~rc3+git.1441319359.d823416.tar.bz2 New: ---- crmsh-2.2.0~rc3+git.1441965248.a9a616d.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ crmsh.spec ++++++ --- /var/tmp/diff_new_pack.cU0IDa/_old 2015-09-13 09:45:19.000000000 +0200 +++ /var/tmp/diff_new_pack.cU0IDa/_new 2015-09-13 09:45:19.000000000 +0200 @@ -36,7 +36,7 @@ Summary: High Availability cluster command-line interface License: GPL-2.0+ Group: %{pkg_group} -Version: 2.2.0~rc3+git.1441319359.d823416 +Version: 2.2.0~rc3+git.1441965248.a9a616d Release: 0 Url: http://crmsh.github.io Source0: %{name}-%{version}.tar.bz2 ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.cU0IDa/_old 2015-09-13 09:45:19.000000000 +0200 +++ /var/tmp/diff_new_pack.cU0IDa/_new 2015-09-13 09:45:19.000000000 +0200 @@ -1,4 +1,4 @@ <servicedata> <service name="tar_scm"> <param name="url">git://github.com/ClusterLabs/crmsh.git</param> - <param name="changesrevision">d823416786d3e5c66d27d6de2fe43228df22fdac</param></service></servicedata> \ No newline at end of file + <param name="changesrevision">a9a616da75689af2b80f39d530e99e8aa855da64</param></service></servicedata> \ No newline at end of file ++++++ crmsh-2.2.0~rc3+git.1441319359.d823416.tar.bz2 -> crmsh-2.2.0~rc3+git.1441965248.a9a616d.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/data-manifest new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/data-manifest --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/data-manifest 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/data-manifest 2015-09-11 12:21:55.000000000 +0200 @@ -30,6 +30,7 @@ scripts/init/verify.py scripts/libvirt/main.yml scripts/lvm/main.yml +scripts/mailto/main.yml scripts/nfsserver/main.yml scripts/ocfs2/main.yml scripts/oracle/main.yml diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/doc/crm.8.adoc new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/doc/crm.8.adoc --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/doc/crm.8.adoc 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/doc/crm.8.adoc 2015-09-11 12:21:55.000000000 +0200 @@ -2755,14 +2755,19 @@ If the node is left out, the order is used for all nodes. That should reduce the configuration size in some stonith setups. +From Pacemaker version 1.1.14, it is possible to use a node attribute +as the +target+ in a fencing topology. The syntax for this usage is +described below. + Usage: ............... fencing_topology stonith_resources [stonith_resources ...] fencing_topology fencing_order [fencing_order ...] -fencing_order :: <node>: stonith_resources [stonith_resources ...] +fencing_order :: target stonith_resources [stonith_resources ...] stonith_resources :: <rsc>[,<rsc>...] +target :: <node>: | attr:<node-attribute>=<value> ............... Example: ............... @@ -2770,6 +2775,7 @@ fencing_topology \ node-a: poison-pill power node-b: ipmi serial +fencing_topology attr:rack=1 apc01,apc02 ............... [[cmdhelp_configure_filter,filter CIB objects]] @@ -4220,34 +4226,42 @@ [[cmdhelp_history,Cluster history]] === `history` - Cluster history -Examining Pacemaker's history is a particularly involved task. -The number of subsystems to be considered, the complexity of the -configuration, and the set of various information sources, most -of which are not exactly human readable, keep analyzing resource -or node problems accessible to only the most knowledgeable. Or, -depending on the point of view, to the most persistent. The -following set of commands has been devised in hope to make -cluster history more accessible. - -Of course, looking at _all_ history could be time consuming -regardless of how good tools at hand are. Therefore, one should -first say which period he or she wants to analyze. If not -otherwise specified, the last hour is considered. Logs and other -relevant information is collected using `hb_report`. Since this -process takes some time and we always need fresh logs, -information is refreshed in a much faster way using the python parallax module. If -+python-parallax+ is not found on the system, examining live cluster -is still possible though not as comfortable. - -Apart from examining live cluster, events may be retrieved from a -report generated by `hb_report` (see also the +-H+ option). In -that case we assume that the period stretching the whole report -needs to be investigated. Of course, it is still possible to -further reduce the time range. - -If you think you may have found a bug or just need clarification -from developers or your support, the `session pack` command can -help create a report. +Examining Pacemaker's history is a particularly involved task. The +number of subsystems to be considered, the complexity of the +configuration, and the set of various information sources, most of +which are not exactly human readable, keep analyzing resource or node +problems accessible to only the most knowledgeable. Or, depending on +the point of view, to the most persistent. The following set of +commands has been devised in hope to make cluster history more +accessible. + +Of course, looking at _all_ history could be time consuming regardless +of how good the tools at hand are. Therefore, one should first say +which period he or she wants to analyze. If not otherwise specified, +the last hour is considered. Logs and other relevant information is +collected using `crm report`. Since this process takes some time and +we always need fresh logs, information is refreshed in a much faster +way using the python parallax module. If +python-parallax+ is not +found on the system, examining a live cluster is still possible -- +though not as comfortable. + +Apart from examining a live cluster, events may be retrieved from a +report generated by `crm report` (see also the +-H+ option). In that +case we assume that the period stretching the whole report needs to be +investigated. Of course, it is still possible to further reduce the +time range. + +If you have discovered an issue that you want to show someone else, +you can use the `session pack` command to save the current session as +a tarball, similar to those generated by `crm report`. + +In order to minimize the size of the tarball, and to make it easier +for others to find the interesting events, it is recommended to limit +the time frame which the saved session covers. This can be done using +the `timeframe` command (example below). + +It is also possible to name the saved session using the `session save` +command. Example: ............... @@ -4258,14 +4272,12 @@ crm(live)history# ............... -In order to reduce report size and allow developers to -concentrate on the issue, you should beforehand limit the time -frame. Giving a meaningful session name helps too. - [[cmdhelp_history_detail,set the level of detail shown]] ==== `detail` -How much detail to show from the logs. +How much detail to show from the logs. Valid detail levels are either +`0` or `1`, where `1` is the highest detail level. The default detail +level is `0`. Usage: ............... @@ -4383,30 +4395,33 @@ [[cmdhelp_history_limit,limit timeframe to be examined]] ==== `limit` (`timeframe`) -All history commands look at events within certain period. It -defaults to the last hour for the live cluster source. There is -no limit for the `hb_report` source. Use this command to set the -timeframe. +This command can be used to modify the time span to examine. All +history commands look at events within a certain time span. + +For the `live` source, the default time span is the _last hour_. -The time period is parsed by the dateutil python module. It -covers wide range of date formats. For instance: +There is no time span limit for the `hb_report` source. -- 3:00 (today at 3am) -- 15:00 (today at 3pm) +The time period is parsed by the `dateutil` python module. It +covers a wide range of date formats. For instance: + +- 3:00 (today at 3am) +- 15:00 (today at 3pm) - 2010/9/1 2pm (September 1st 2010 at 2pm) -We won't bother to give definition of the time specification in -usage below. Either use common sense or read the -http://labix.org/python-dateutil[dateutil] documentation. +For more examples of valid time/date statements, please refer to the +`python-dateutil` documentation: + +- https://dateutil.readthedocs.org/[dateutil.readthedocs.org] -If dateutil is not available, then the time is parsed using +If the dateutil module is not available, then the time is parsed using strptime and only the kind as printed by `date(1)` is allowed: - Tue Sep 15 20:46:27 CEST 2010 Usage: ............... -limit [<from_time> [<to_time>]] +limit [<from_time>] [<to_time>] ............... Examples: ............... @@ -4634,12 +4649,18 @@ After the `ptest` output, logs about events that happened during the transition are printed. +The `tags` subcommand scans the logs for the transition and return a +list of key events during that transition. For example, the tag ++error+ will be returned if there are any errors logged during the +transition. + Usage: ............... transition [<number>|<index>|<file>] [nograph] [v...] [scores] [actions] [utilization] transition showdot [<number>|<index>|<file>] transition log [<number>|<index>|<file>] transition save [<number>|<index>|<file> [name]] +transition tags [<number>|<index>|<file>] ............... Examples: ............... diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/cibconfig.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/cibconfig.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/cibconfig.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/cibconfig.py 2015-09-11 12:21:55.000000000 +0200 @@ -1928,7 +1928,10 @@ s = clidisplay.keyword(self.obj_type) d = odict() for c in self.node.iterchildren("fencing-level"): - target = c.get("target") + if "target-attribute" in c.attrib: + target = (c.get("target-attribute"), c.get("target-value")) + else: + target = c.get("target") if target not in d: d[target] = {} d[target][c.get("index")] = c.get("devices") @@ -1942,7 +1945,13 @@ d2[devs_s] = 1 if len(d2) == 1 and len(d) == len(cib_factory.node_id_list()): return "%s %s" % (s, devs_s) - return cli_format([s] + ["%s: %s" % (x, ' '.join(dd[x])) + + def fmt_target(tgt): + if isinstance(tgt, tuple): + return "attr:%s=%s" % tgt + else: + return tgt + ":" + return cli_format([s] + ["%s %s" % (fmt_target(x), ' '.join(dd[x])) for x in dd.keys()], break_lines=(format > 0)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/parse.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/parse.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/parse.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/parse.py 2015-09-11 12:21:55.000000000 +0200 @@ -961,9 +961,22 @@ <fencing-topology> <fencing-level id=<id> target=<text> index=<+int> devices="\w,\w..."/> </fencing-topology> + + new: + + from 1.1.14 on, target can be a node attribute value mapping: + + attr:<name>=<value> maps to XML: + + <fencing-topology> + <fencing-level id=<id> target-attribute=<text> target-value=<text> + index=<+int> devices="\w,\w..."/> + </fencing-topology> + """ - _TARGET_RE = re.compile(r'([^:]+):$') + _TARGET_RE = re.compile(r'([\w=-]+):$') + _TARGET_ATTR_RE = re.compile(r'attr:([\w-]+)=([\w-]+)$') def can_parse(self): return ('fencing-topology', 'fencing_topology') @@ -976,7 +989,9 @@ # (target, devices) raw_levels = [] while self.has_tokens(): - if self.try_match(self._TARGET_RE): + if self.try_match(self._TARGET_ATTR_RE): + target = (self.matched(1), self.matched(2)) + elif self.try_match(self._TARGET_RE): target = self.matched(1) else: raw_levels.append((target, self.match_any())) @@ -995,16 +1010,26 @@ yield node, devices lvl_generator = node_levels else: - lvl_generator = lambda: raw_levels + def wrap_levels(): + return raw_levels + lvl_generator = wrap_levels out = xmlbuilder.new('fencing-topology') targets = defaultdict(repeat(1).next) for target, devices in lvl_generator(): - xmlbuilder.child(out, 'fencing-level', - target=target, - index=str(targets[target]), - devices=devices) - targets[target] += 1 + if isinstance(target, tuple): + c = xmlbuilder.child(out, 'fencing-level', + index=str(targets[target[0]]), + devices=devices) + c.set('target-attribute', target[0]) + c.set('target-value', target[1]) + targets[target[0]] += 1 + else: + xmlbuilder.child(out, 'fencing-level', + target=target, + index=str(targets[target]), + devices=devices) + targets[target] += 1 return out diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/report.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/report.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/report.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/report.py 2015-09-11 12:21:55.000000000 +0200 @@ -17,12 +17,12 @@ from .utils import page_string, release_lock, rmdir_r, parse_time, get_cib_attributes from .utils import is_pcmk_118, pipe_cmd_nosudo, file_find_by_name -_NO_PARALLAX = False - +_HAS_PARALLAX = False try: from .crm_pssh import next_loglines, next_peinputs + _HAS_PARALLAX = True except: - _NO_PARALLAX = True + pass YEAR = None @@ -497,13 +497,17 @@ s = s[r.end():] -def extract_pe_file(msg): +def get_pe_file_num_from_msg(msg): + """ + Get PE file name and number from log message + Returns: (file, num) + """ msg_a = msg.split() if len(msg_a) < 5: # this looks too short common_warn("log message <%s> unexpected format, please report a bug" % msg) - return "" - return msg_a[-1] + return ("", "-1") + return (msg_a[-1], get_pe_num(msg_a[-1])) def transition_start_re(number_re): @@ -552,13 +556,12 @@ return None -def get_matching_run_msg(te_invoke_msg, trans_msg_l): +def find_transition_end_msg(transition_start_msg, trans_msg_l): """ Given the start of a transition log message, find and return the end of the transition log messages. """ - pe_file = extract_pe_file(te_invoke_msg) - pe_num = get_pe_num(pe_file) + pe_file, pe_num = get_pe_file_num_from_msg(transition_start_msg) if pe_num == "-1": common_warn("%s: strange, transition number not found" % pe_file) return "" @@ -566,7 +569,7 @@ def trans_str(node, pe_file): - '''Convert node,pe_file to transition sting.''' + '''Convert node,pe_file to transition string.''' return "%s:%s" % (node, os.path.basename(pe_file).replace(".bz2", "")) @@ -581,29 +584,28 @@ Capture transition related information. ''' - def __init__(self, te_invoke_msg, run_msg): - self.te_invoke_msg = te_invoke_msg - self.run_msg = run_msg - self.parse_msgs() + def __init__(self, start_msg, end_msg): + self.start_msg = start_msg + self.end_msg = end_msg + self.tags = set() + self.pe_file, self.pe_num = get_pe_file_num_from_msg(start_msg) + self.dc = syslog2node(start_msg) + self.start_ts = syslog_ts(start_msg) + if end_msg: + self.end_ts = syslog_ts(end_msg) + else: + common_warn("end of transition %s not found in logs (transition not complete yet?)" % self) + self.end_ts = self.start_ts def __str__(self): - return trans_str(self.dc, self.pe_file) + return self.get_node_file() - def parse_msgs(self): - self.pe_file = extract_pe_file(self.te_invoke_msg) - self.pe_num = get_pe_num(self.pe_file) - self.dc = syslog2node(self.te_invoke_msg) - self.start_ts = syslog_ts(self.te_invoke_msg) - if self.run_msg: - self.end_ts = syslog_ts(self.run_msg) - else: - common_warn("end of transition %s not found in logs (transition not complete yet?)" % - self) - self.end_ts = self.start_ts + def get_node_file(self): + return trans_str(self.dc, self.pe_file) def actions_count(self): - if self.run_msg: - act_d = run_graph_msg_actions(self.run_msg) + if self.end_msg: + act_d = run_graph_msg_actions(self.end_msg) return sum(act_d.values()) else: return -1 @@ -613,9 +615,9 @@ def transition_info(self): print "Transition %s (%s -" % (self, shorttime(self.start_ts)), - if self.run_msg: + if self.end_msg: print "%s):" % shorttime(self.end_ts) - act_d = run_graph_msg_actions(self.run_msg) + act_d = run_graph_msg_actions(self.end_msg) total = sum(act_d.values()) s = ", ".join(["%d %s" % (act_d[x], x) for x in act_d if act_d[x]]) print "\ttotal %d actions: %s" % (total, s) @@ -677,7 +679,7 @@ self.nodecolor = {} self.logobj = None self.desc = None - self.peinputs_l = [] + self._transitions = [] self.cibgrp_d = {} self.cibcln_d = {} self.cibrsc_l = [] @@ -688,7 +690,7 @@ self.detail = 0 self.log_filter_out = [] self.log_filter_out_re = [] - # change_origin may be CH_SRC, CH_TIME, CH_UPD + # change_origin may be 0, CH_SRC, CH_TIME, CH_UPD # depending on the change_origin, we update our attributes self.change_origin = CH_SRC set_year() @@ -706,7 +708,7 @@ return self.node_l def peinputs_list(self): - return [x.pe_num for x in self.peinputs_l] + return [x.pe_num for x in self._transitions] def session_subcmd_list(self): return ["save", "load", "pack", "delete", "list", "update"] @@ -934,7 +936,7 @@ continue pe_l = [] for new_t_obj in self.list_transitions(log_l, future_pe=True): - self.new_peinput(new_t_obj) + self._new_transition(new_t_obj) pe_l.append(new_t_obj.pe_file) if pe_l: node_pe_l.append([node, pe_l]) @@ -973,7 +975,7 @@ # try just to refresh the live report if self.to_dt or self.is_live_very_recent() or no_live_update: return self._live_loc() - if not _NO_PARALLAX: + if _HAS_PARALLAX: if not acquire_lock(self.report_cache_dir): return None rc = self.update_live_report() @@ -989,6 +991,13 @@ ''' Run hb_report to get logs now. ''' + from . import ui_report + + extcmd = ui_report.report_tool() + if extcmd is None: + self.error("No reporting tool found") + return None + d = self._live_loc() rmdir_r(d) tarball = "%s.tar.bz2" % d @@ -1000,12 +1009,7 @@ nodes_option = "'-n %s'" % ' '.join(self.setnodes) if pipe_cmd_nosudo("mkdir -p %s" % os.path.dirname(d)) != 0: return None - common_info("retrieving information from cluster nodes, please wait ...") - from . import ui_report - extcmd = ui_report.report_tool() - if extcmd is None: - self.error("No reporting tool found") - return None + common_info("Retrieving information from cluster nodes, please wait...") rc = pipe_cmd_nosudo("%s -Z -Q -f '%s' %s %s %s %s" % (extcmd, self.from_dt.ctime(), @@ -1046,7 +1050,7 @@ refresh = from_dt and top_dt > from_dt if refresh: self.set_change_origin(CH_UPD) - self.refresh_source(force=True) + return self.refresh_source(force=True) else: self.set_change_origin(CH_TIME) self.report_setup() @@ -1102,13 +1106,13 @@ pass self.cibnotcloned_l = [x for x in self.cibrsc_l if x not in self.cibcloned_l] - def new_peinput(self, new_pe): - t_obj = self.find_peinput(str(new_pe)) + def _new_transition(self, transition): + t_obj = self.find_transition(transition.get_node_file()) if t_obj: - common_debug("duplicate %s, replacing older PE file" % t_obj) - self.peinputs_l.remove(t_obj) - common_debug("appending new PE %s" % new_pe) - self.peinputs_l.append(new_pe) + common_debug("duplicate %s, replacing older PE file" % transition) + self._transitions.remove(t_obj) + common_debug("appending new PE %s" % transition) + self._transitions.append(transition) def set_node_colors(self): i = 0 @@ -1164,11 +1168,12 @@ trans_start_msg_l = self.get_invoke_trans_msgs(trans_msg_l) prev_transition = None for msg in trans_start_msg_l: - run_msg = get_matching_run_msg(msg, trans_msg_l) - t_obj = Transition(msg, run_msg) + transition_end_msg = find_transition_end_msg(msg, trans_msg_l) + t_obj = Transition(msg, transition_end_msg) if self.is_empty_transition(prev_transition, t_obj): common_debug("skipping empty transition (%s)" % t_obj) continue + self._set_transition_tags(t_obj) if not future_pe: pe_l_file = self.pe_report_path(t_obj) if not os.path.isfile(pe_l_file): @@ -1178,52 +1183,62 @@ prev_transition = t_obj yield t_obj - def report_setup(self): - if not self.change_origin: - return - if not self.loc: + def _report_setup_source(self): + constants.pcmk_version = None + # is this an hb_report or a crm_report? + for descname in ("description.txt", "report.summary"): + self.desc = os.path.join(self.loc, descname) + if os.path.isfile(self.desc): + yr = os.stat(self.desc).st_mtime + common_debug("Found %s, created %s" % (descname, yr)) + self._creation_time = time.strftime("%a %d %b %H:%M:%S %Z %Y", + time.localtime(yr)) + if descname == 'report.summary': + self._creator = "crm_report" + else: + self._creator = 'unknown' + set_year(yr) + break + else: + self.error("Invalid report: No description found") return - if self.change_origin == CH_SRC: - constants.pcmk_version = None - # is this an hb_report or a crm_report? - for descname in ("description.txt", "report.summary"): - self.desc = os.path.join(self.loc, descname) - if os.path.isfile(self.desc): - yr = os.stat(self.desc).st_mtime - common_debug("Found %s, created %s" % (descname, yr)) - self._creation_time = time.strftime("%a %d %b %H:%M:%S %Z %Y", - time.localtime(yr)) - if descname == 'report.summary': - self._creator = "crm_report" - else: - self._creator = 'unknown' - set_year(yr) - break - else: - self.error("Invalid report: No description found") - return - self.node_l = self.get_nodes() + self.node_l = self.get_nodes() + self.set_node_colors() + self.log_l = self.find_logs() + self.find_central_log() + self.read_cib() + + def _report_setup_update(self): + l = self.get_nodes() + if self.node_l != l: + self.node_l = l self.set_node_colors() self.log_l = self.find_logs() - self.find_central_log() self.read_cib() + + def report_setup(self): + if self.change_origin == 0: + return False + if not self.loc: + return False + + if self.change_origin == CH_SRC: + self._report_setup_source() elif self.change_origin == CH_UPD: - l = self.get_nodes() - if self.node_l != l: - self.node_l = l - self.set_node_colors() - self.log_l = self.find_logs() - self.read_cib() + self._report_setup_update() + self.logobj = LogSyslog(self.central_log, self.log_l, self.from_dt, self.to_dt) + if self.change_origin != CH_UPD: common_debug("getting transitions from logs") - self.peinputs_l = [] + self._transitions = [] for new_t_obj in self.list_transitions(): - self.new_peinput(new_t_obj) + self._new_transition(new_t_obj) + self.ready = self.check_report() self.set_change_origin(0) @@ -1356,11 +1371,15 @@ output''' max_output = 20 s = "" - if len(self.peinputs_l) > max_output: + if len(self._transitions) > max_output: s = "... " - return "%s%s" % (s, - ' '.join([self._str_nodecolor(x.dc, x.pe_num) - for x in self.peinputs_l[-max_output:]])) + + def fmt(t): + if 'error' in t.tags: + return self._str_nodecolor(t.dc, t.pe_num) + "*" + return self._str_nodecolor(t.dc, t.pe_num) + + return "%s%s" % (s, ' '.join([fmt(x) for x in self._transitions[-max_output:]])) def get_rpt_dt(self, dt, whence): ''' @@ -1422,9 +1441,9 @@ return False self.show_logs(re_l=all_re_l) - def find_peinput(self, t_str): - for t_obj in self.peinputs_l: - if str(t_obj) == t_str: + def find_transition(self, t_str): + for t_obj in self._transitions: + if t_obj.get_node_file() == t_str: return t_obj return None @@ -1434,7 +1453,7 @@ ''' if not self.prepare_source(no_live_update=self.prevent_live_update()): return False - t_obj = self.find_peinput(rpt_pe2t_str(rpt_pe_file)) + t_obj = self.find_transition(rpt_pe2t_str(rpt_pe_file)) if not t_obj: common_err("%s: transition not found" % rpt_pe_file) return False @@ -1448,6 +1467,36 @@ self.logobj.set_log_timeframe(self.from_dt, self.to_dt) return True + def show_transition_tags(self, rpt_pe_file): + ''' + prints the tags for the transition + ''' + t_obj = self.find_transition(rpt_pe2t_str(rpt_pe_file)) + if not t_obj: + common_err("%s: transition not found" % rpt_pe_file) + return False + for tag in t_obj.tags: + print tag + return True + + def _set_transition_tags(self, transition): + # limit the log scope temporarily + self.logobj.set_log_timeframe(transition.start_ts, transition.end_ts) + + # search log, match regexes to tags + regexes = [ + re.compile(r"(error|unclean)", re.I), + re.compile(r"crmd.*notice:\s+Operation\s+([^:]+):\s+(?!ok)"), + ] + + for l in self.logobj.get_matches(regexes): + for rx in regexes: + m = rx.search(l) + if m: + transition.tags.add(m.group(1).lower()) + + self.logobj.set_log_timeframe(self.from_dt, self.to_dt) + def resource(self, *args): ''' Show resource relevant logs. @@ -1532,8 +1581,8 @@ a.append(a[0]) elif a is not None: a = [a, a] - l = [long and self.pe_detail_format(x) or self.pe_report_path(x) - for x in self.peinputs_l if pe_file_in_range(x.pe_file, a)] + l = [long and self.pe_detail_format(t_obj) or self.pe_report_path(t_obj) + for t_obj in self._transitions if pe_file_in_range(t_obj.pe_file, a)] if long: l = [self.pe_details_header, self.pe_details_separator] + l return l diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/scripts.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/scripts.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/scripts.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/scripts.py 2015-09-11 12:21:55.000000000 +0200 @@ -1278,12 +1278,6 @@ ok, _ = _valid_integer(value) if not ok: errors.append("%s=%s is not an integer" % (param.get('name'), value)) - elif type == 'port': - ok, ival = _valid_integer(value) - if not ok: - errors.append("%s=%s is not a valid port" % (param.get('name'), value)) - if ival < 0 or ival > 65535: - errors.append("%s=%s is out of port range" % (param.get('name'), value)) elif type == 'string': return value elif type == 'boolean': @@ -1294,12 +1288,21 @@ errors.append("%s=%s invalid resource identifier" % (param.get('name'), value)) except TypeError as e: errors.append("%s=%s %s" % (param.get('name'), value, str(e))) + elif type == 'enum': + if 'values' not in param: + errors.append("%s=%s enum without list of values" % (param.get('name'), value)) + else: + opts = param['values'] + if isinstance(opts, basestring): + opts = opts.replace(',', ' ').split(' ') + for v in opts: + if value.lower() == v.lower(): + return v + else: + errors.append("%s=%s does not match '%s'" % (param.get('name'), value, "|".join(opts))) elif type == 'ip_address': if not _valid_ip(value): errors.append("%s=%s is not an IP address" % (param.get('name'), value)) - elif type == 'email': - if not re.match(r'[^@]+@[^@]+', value): - errors.append("%s=%s is not a valid email address" % (param.get('value'), value)) elif type == 'ip_network': sp = value.rsplit('/', 1) if len(sp) == 1 and not (is_valid_ipv4_address(value) or is_valid_ipv6_address(value)): @@ -1308,6 +1311,15 @@ errors.append("%s=%s is not a valid IP network" % (param.get('name'), value)) else: errors.append("%s=%s is not a valid IP network" % (param.get('name'), value)) + elif type == 'port': + ok, ival = _valid_integer(value) + if not ok: + errors.append("%s=%s is not a valid port" % (param.get('name'), value)) + if ival < 0 or ival > 65535: + errors.append("%s=%s is out of port range" % (param.get('name'), value)) + elif type == 'email': + if not re.match(r'[^@]+@[^@]+', value): + errors.append("%s=%s is not a valid email address" % (param.get('name'), value)) else: errors.append("%s=%s is unknown type %s" % (param.get('name'), value, type)) return value diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/ui_history.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/ui_history.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/ui_history.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/ui_history.py 2015-09-11 12:21:55.000000000 +0200 @@ -61,9 +61,12 @@ to_dt = utils.parse_time(to_time) if not to_dt: return False - if to_dt and to_dt <= from_dt: - common_err("%s - %s: bad period" % (from_time, to_time)) - return False + if to_dt and from_dt: + if to_dt < from_dt: + from_dt, to_dt = to_dt, from_dt + elif to_dt == from_dt: + common_err("%s - %s: To and from dates cannot be the same" % (from_time, to_time)) + return False return crm_report().set_period(from_dt, to_dt) def _check_source(self, src): @@ -138,9 +141,8 @@ "usage: detail <detail_level>" self._init_source() detail_num = utils.convert2ints(detail_lvl) - if not (isinstance(detail_num, int) and int(detail_num) >= 0): - bad_usage(context.get_command_name(), detail_lvl) - return False + if detail_num is None or detail_num not in (0, 1): + context.fatal_error("Expected '0' or '1' (was '%s')" % (detail_lvl)) return crm_report().set_detail(detail_lvl) @command.skill_level('administrator') @@ -305,7 +307,7 @@ self._init_source() argl = list(args) subcmd = "show" - if argl and argl[0] in ("showdot", "log", "save"): + if argl and argl[0] in ("showdot", "log", "save", "tags"): subcmd = argl[0] del argl[0] if subcmd == "show": @@ -328,6 +330,8 @@ rc = self._display_dot(f) elif subcmd == "save": rc = self._pe2shadow(f, argl) + elif subcmd == "tags": + rc = crm_report().show_transition_tags(f) else: rc = crm_report().show_transition_log(f, True) return rc diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/utils.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/utils.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/utils.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/utils.py 2015-09-11 12:21:55.000000000 +0200 @@ -406,6 +406,7 @@ def rmdir_r(d): + # TODO: Make sure we're not deleting something we shouldn't! if d and os.path.isdir(d): shutil.rmtree(d) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/xmlbuilder.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/xmlbuilder.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/modules/xmlbuilder.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/modules/xmlbuilder.py 2015-09-11 12:21:55.000000000 +0200 @@ -51,7 +51,6 @@ """ <nvpair id-ref=<idref> [name=<name>]/> """ - print "nvpair_ref:", repr(idref), repr(name) nvp = new("nvpair") nvp.set('id-ref', idref) if name is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/scripts/mailto/main.yml new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/scripts/mailto/main.yml --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/scripts/mailto/main.yml 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/scripts/mailto/main.yml 2015-09-11 12:21:55.000000000 +0200 @@ -20,6 +20,9 @@ op stop timeout="10" op monitor interval="10" timeout="10" actions: + - install: + - mailx + shortdesc: Ensure mail package is installed - include: mailto - cib: | clone c-{{id}} {{id}} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/testcases/newfeatures.exp new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/testcases/newfeatures.exp --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/testcases/newfeatures.exp 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/testcases/newfeatures.exp 2015-09-11 12:21:55.000000000 +0200 @@ -9,8 +9,6 @@ .INP: primitive p0 Dummy params $p0-state:state=1 .INP: primitive p1 Dummy params rule role=Started date in start=2009-05-26 end=2010-05-26 or date gt 2014-01-01 state=2 .INP: primitive p2 Dummy params @p0-state -nvpair_ref: 'p0-state' None -nvpair_ref: 'p0-state' None .INP: property rule #uname eq node1 stonith-enabled=no .INP: tag tag1: p0 p1 p2 .INP: tag tag2 p0 p1 p2 @@ -43,4 +41,3 @@ .EXT pengine metadata .EXT cib metadata .INP: commit -nvpair_ref: 'p0-state' None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/testcases/scripts.exp new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/testcases/scripts.exp --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/testcases/scripts.exp 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/testcases/scripts.exp 2015-09-11 12:21:55.000000000 +0200 @@ -10,9 +10,11 @@ .EXT crm_resource --show-metadata systemd:haproxy ERROR: 2: Error when loading script haproxy: No meta-data for agent: systemd:haproxy .EXT crm_resource --show-metadata ocf:heartbeat:LVM +.EXT crm_resource --show-metadata ocf:heartbeat:MailTo .EXT crm_resource --show-metadata ocf:heartbeat:Raid1 Basic: +mailto MailTo virtual-ip Virtual IP Database: @@ -57,6 +59,7 @@ ERROR: 3: Error when loading script haproxy: No meta-data for agent: systemd:haproxy Basic: +mailto MailTo virtual-ip Virtual IP Database: @@ -128,6 +131,7 @@ init libvirt lvm +mailto nfsserver ocfs2 oracle @@ -162,6 +166,7 @@ init libvirt lvm +mailto nfsserver ocfs2 oracle @@ -196,6 +201,7 @@ init libvirt lvm +mailto nfsserver ocfs2 oracle @@ -237,8 +243,6 @@ CIDR netmask broadcast Broadcast address - lvs_support - Enable support for LVS DR diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/unittests/test_cliformat.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/unittests/test_cliformat.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/unittests/test_cliformat.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/unittests/test_cliformat.py 2015-09-11 12:21:55.000000000 +0200 @@ -244,3 +244,7 @@ roundtrip('role silly-role-2 read xpath:"//nodes//attributes" ' + 'deny type:nvpair deny ref:d0 deny type:nvpair') + +@with_setup(setup_func, teardown_func) +def test_topology_1114(): + roundtrip('fencing_topology attr:rack=1 node1,node2') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/unittests/test_parse.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/unittests/test_parse.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/unittests/test_parse.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/unittests/test_parse.py 2015-09-11 12:21:55.000000000 +0200 @@ -434,7 +434,7 @@ # num test nodes are 3 out = self.parser.parse('fencing_topology poison-pill power') - self.assertEqual(6, len(out)) + self.assertEqual("""<fencing-topology><fencing-level devices="poison-pill" index="1" target="ha-one"/><fencing-level devices="power" index="2" target="ha-one"/><fencing-level devices="poison-pill" index="1" target="ha-two"/><fencing-level devices="power" index="2" target="ha-two"/><fencing-level devices="poison-pill" index="1" target="ha-three"/><fencing-level devices="power" index="2" target="ha-three"/></fencing-topology>""", etree.tostring(out)) out = self.parser.parse('fencing_topology node-a: poison-pill power node-b: ipmi serial') self.assertEqual(4, len(out)) @@ -442,8 +442,21 @@ devs = ['stonith-vbox3-1-off', 'stonith-vbox3-2-off', 'stonith-vbox3-1-on', 'stonith-vbox3-2-on'] out = self.parser.parse('fencing_topology vbox4: %s' % ','.join(devs)) + print etree.tostring(out) self.assertEqual(1, len(out)) + def test_fencing_1114(self): + """ + Test node attribute fence target assignment + """ + out = self.parser.parse('fencing_topology attr:rack=1 poison-pill power') + expect = """<fencing-topology><fencing-level devices="poison-pill" index="1" target-attribute="rack" target-value="1"/><fencing-level devices="power" index="2" target-attribute="rack" target-value="1"/></fencing-topology>""" + self.assertEqual(expect, etree.tostring(out)) + + out = self.parser.parse('fencing_topology attr:rack=1 poison-pill,power') + expect = '<fencing-topology><fencing-level devices="poison-pill,power" index="1" target-attribute="rack" target-value="1"/></fencing-topology>' + self.assertEqual(expect, etree.tostring(out)) + def test_tag(self): out = self.parser.parse('tag tag1: one two three') self.assertEqual(out.get('id'), 'tag1') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/unittests/test_scripts.py new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/unittests/test_scripts.py --- old/crmsh-2.2.0~rc3+git.1441319359.d823416/test/unittests/test_scripts.py 2015-09-04 10:08:47.000000000 +0200 +++ new/crmsh-2.2.0~rc3+git.1441965248.a9a616d/test/unittests/test_scripts.py 2015-09-11 12:21:55.000000000 +0200 @@ -4,7 +4,7 @@ from os import path from pprint import pprint -from nose.tools import eq_, with_setup +from nose.tools import eq_, with_setup, assert_raises from lxml import etree from crmsh import scripts from crmsh import ra @@ -644,3 +644,80 @@ eq_(len(actions), 1) pprint(actions) assert actions[0]['text'] == "primitive SARUMAN apacho" + + +@with_setup(setup_func, teardown_func) +def test_enums_basic(): + a = '''--- +- version: 2.2 + category: Script + parameters: + - name: foo + required: true + type: enum + values: + - one + - two + - three + actions: + - cib: "{{foo}}" +''' + + script_a = scripts.load_script_string('test-a', a) + assert script_a is not None + + actions = scripts.verify(script_a, + {"foo": "one"}) + eq_(len(actions), 1) + pprint(actions) + assert actions[0]['text'] == "one" + + actions = scripts.verify(script_a, + {"foo": "three"}) + eq_(len(actions), 1) + pprint(actions) + assert actions[0]['text'] == "three" + + +@with_setup(setup_func, teardown_func) +def test_enums_fail(): + a = '''--- +- version: 2.2 + category: Script + parameters: + - name: foo + required: true + type: enum + values: + - one + - two + - three + actions: + - cib: "{{foo}}" +''' + script_a = scripts.load_script_string('test-a', a) + assert script_a is not None + + def ver(): + return scripts.verify(script_a, {"foo": "wrong"}) + assert_raises(ValueError, ver) + + +@with_setup(setup_func, teardown_func) +def test_enums_fail2(): + a = '''--- +- version: 2.2 + category: Script + parameters: + - name: foo + required: true + type: enum + actions: + - cib: "{{foo}}" +''' + script_a = scripts.load_script_string('test-a', a) + assert script_a is not None + + def ver(): + return scripts.verify(script_a, {"foo": "one"}) + assert_raises(ValueError, ver)
