diff --git a/cts/cli/regression.dates.exp b/cts/cli/regression.dates.exp
index 3b8f8e3f5d..36b6cffe72 100644
--- a/cts/cli/regression.dates.exp
+++ b/cts/cli/regression.dates.exp
@@ -1,401 +1,409 @@
=#=#=#= Begin test: Invalid period - [] =#=#=#=
crm_time_parse_period error: No ISO 8601 time period given
iso8601: Invalid interval specified:
=#=#=#= End test: Invalid period - [] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - []
=#=#=#= Begin test: Invalid period - [2019-01-01 00:00:00Z] =#=#=#=
crm_time_parse_period error: '2019-01-01 00:00:00Z' is not a valid ISO 8601 time period because it has no duration or ending time
iso8601: Invalid interval specified: 2019-01-01 00:00:00Z
=#=#=#= End test: Invalid period - [2019-01-01 00:00:00Z] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [2019-01-01 00:00:00Z]
=#=#=#= Begin test: Invalid period - [2019-01-01 00:00:00Z/] =#=#=#=
parse_date error: No ISO 8601 date/time specification given
iso8601: Invalid interval specified: 2019-01-01 00:00:00Z/
=#=#=#= End test: Invalid period - [2019-01-01 00:00:00Z/] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [2019-01-01 00:00:00Z/]
=#=#=#= Begin test: Invalid period - [PT2S/P1M] =#=#=#=
crm_time_parse_period error: 'PT2S/P1M' is not a valid ISO 8601 time period because it has two durations
iso8601: Invalid interval specified: PT2S/P1M
=#=#=#= End test: Invalid period - [PT2S/P1M] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [PT2S/P1M]
=#=#=#= Begin test: Invalid period - [2019-13-01 00:00:00Z/P1M] =#=#=#=
parse_date error: '2019-13-01 00:00:00Z/P1M' is not a valid ISO 8601 date/time specification because '13' is not a valid month
iso8601: Invalid interval specified: 2019-13-01 00:00:00Z/P1M
=#=#=#= End test: Invalid period - [2019-13-01 00:00:00Z/P1M] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [2019-13-01 00:00:00Z/P1M]
=#=#=#= Begin test: Invalid period - [20191077T15/P1M] =#=#=#=
parse_date error: '20191077T15/P1M' is not a valid ISO 8601 date/time specification because '77' is not a valid day of the month
iso8601: Invalid interval specified: 20191077T15/P1M
=#=#=#= End test: Invalid period - [20191077T15/P1M] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [20191077T15/P1M]
=#=#=#= Begin test: Invalid period - [2019-10-01T25:00:00Z/P1M] =#=#=#=
crm_time_parse_sec error: 25:00:00Z/P1M is not a valid ISO 8601 time specification because 25 is not a valid hour
iso8601: Invalid interval specified: 2019-10-01T25:00:00Z/P1M
=#=#=#= End test: Invalid period - [2019-10-01T25:00:00Z/P1M] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [2019-10-01T25:00:00Z/P1M]
=#=#=#= Begin test: Invalid period - [2019-10-01T24:00:01Z/P1M] =#=#=#=
crm_time_parse_sec error: 24:00:01Z/P1M is not a valid ISO 8601 time specification because 24 is not a valid hour
iso8601: Invalid interval specified: 2019-10-01T24:00:01Z/P1M
=#=#=#= End test: Invalid period - [2019-10-01T24:00:01Z/P1M] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [2019-10-01T24:00:01Z/P1M]
=#=#=#= Begin test: Invalid period - [PT5H/20191001T007000Z] =#=#=#=
crm_time_parse_sec error: 007000Z is not a valid ISO 8601 time specification because 70 is not a valid minute
iso8601: Invalid interval specified: PT5H/20191001T007000Z
=#=#=#= End test: Invalid period - [PT5H/20191001T007000Z] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [PT5H/20191001T007000Z]
=#=#=#= Begin test: Invalid period - [2019-10-01 00:00:80Z/P1M] =#=#=#=
crm_time_parse_sec error: 00:00:80Z/P1M is not a valid ISO 8601 time specification because 80 is not a valid second
iso8601: Invalid interval specified: 2019-10-01 00:00:80Z/P1M
=#=#=#= End test: Invalid period - [2019-10-01 00:00:80Z/P1M] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [2019-10-01 00:00:80Z/P1M]
=#=#=#= Begin test: Invalid period - [2019-10-01 00:00:10 +25:00/P1M] =#=#=#=
crm_time_parse_sec error: 25:00/P1M is not a valid ISO 8601 time specification because 25 is not a valid hour
iso8601: Invalid interval specified: 2019-10-01 00:00:10 +25:00/P1M
=#=#=#= End test: Invalid period - [2019-10-01 00:00:10 +25:00/P1M] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [2019-10-01 00:00:10 +25:00/P1M]
=#=#=#= Begin test: Invalid period - [20191001T000010 -00:61/P1M] =#=#=#=
crm_time_parse_sec error: 00:61/P1M is not a valid ISO 8601 time specification because 61 is not a valid minute
iso8601: Invalid interval specified: 20191001T000010 -00:61/P1M
=#=#=#= End test: Invalid period - [20191001T000010 -00:61/P1M] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [20191001T000010 -00:61/P1M]
=#=#=#= Begin test: Invalid period - [P1Y/2019-02-29 00:00:00Z] =#=#=#=
parse_date error: '2019-02-29 00:00:00Z' is not a valid ISO 8601 date/time specification because '29' is not a valid day of the month
iso8601: Invalid interval specified: P1Y/2019-02-29 00:00:00Z
=#=#=#= End test: Invalid period - [P1Y/2019-02-29 00:00:00Z] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [P1Y/2019-02-29 00:00:00Z]
=#=#=#= Begin test: Invalid period - [2019-01-01 00:00:00Z/P] =#=#=#=
crm_time_parse_duration error: 'P' is not a valid ISO 8601 time duration because nothing follows 'P'
iso8601: Invalid interval specified: 2019-01-01 00:00:00Z/P
=#=#=#= End test: Invalid period - [2019-01-01 00:00:00Z/P] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [2019-01-01 00:00:00Z/P]
=#=#=#= Begin test: Invalid period - [P1Z/2019-02-20 00:00:00Z] =#=#=#=
crm_time_parse_duration error: 'P1Z/2019-02-20 00:00:00Z' is not a valid ISO 8601 time duration because 'Z' is not a valid time unit
iso8601: Invalid interval specified: P1Z/2019-02-20 00:00:00Z
=#=#=#= End test: Invalid period - [P1Z/2019-02-20 00:00:00Z] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [P1Z/2019-02-20 00:00:00Z]
=#=#=#= Begin test: Invalid period - [P1YM/2019-02-20 00:00:00Z] =#=#=#=
crm_time_parse_duration error: 'P1YM/2019-02-20 00:00:00Z' is not a valid ISO 8601 time duration because no valid integer at 'M/2019-02-20 00:00:00Z'
iso8601: Invalid interval specified: P1YM/2019-02-20 00:00:00Z
=#=#=#= End test: Invalid period - [P1YM/2019-02-20 00:00:00Z] - Invalid parameter (2) =#=#=#=
* Passed: iso8601 - Invalid period - [P1YM/2019-02-20 00:00:00Z]
=#=#=#= Begin test: '2005-040/2005-043' period =#=#=#=
Period: 2005-02-09 00:00:00Z to 2005-02-12 00:00:00Z
=#=#=#= End test: '2005-040/2005-043' period - OK (0) =#=#=#=
* Passed: iso8601 - '2005-040/2005-043' period
=#=#=#= Begin test: '2005-040/2005-043' period (XML) =#=#=#=
2005-02-09 00:00:00Z
2005-02-12 00:00:00Z
=#=#=#= End test: '2005-040/2005-043' period (XML) - OK (0) =#=#=#=
* Passed: iso8601 - '2005-040/2005-043' period (XML)
=#=#=#= Begin test: 2014-01-01 00:30:00 - 1 Hour =#=#=#=
Date: 2014-01-01 00:30:00Z
Duration: -3600 seconds (1 hour)
Duration ends at: 2013-12-31 23:30:00Z
=#=#=#= End test: 2014-01-01 00:30:00 - 1 Hour - OK (0) =#=#=#=
* Passed: iso8601 - 2014-01-01 00:30:00 - 1 Hour
=#=#=#= Begin test: Valid date - Feb 29 in leap year =#=#=#=
Date: 2020-02-29 00:00:00Z
=#=#=#= End test: Valid date - Feb 29 in leap year - OK (0) =#=#=#=
* Passed: iso8601 - Valid date - Feb 29 in leap year
=#=#=#= Begin test: Valid date - using 'T' and offset =#=#=#=
Date: 2019-12-01 18:12:11Z
=#=#=#= End test: Valid date - using 'T' and offset - OK (0) =#=#=#=
* Passed: iso8601 - Valid date - using 'T' and offset
=#=#=#= Begin test: 24:00:00 equivalent to 00:00:00 of next day =#=#=#=
Date: 2020-01-01 00:00:00Z
=#=#=#= End test: 24:00:00 equivalent to 00:00:00 of next day - OK (0) =#=#=#=
* Passed: iso8601 - 24:00:00 equivalent to 00:00:00 of next day
=#=#=#= Begin test: 2006-W01-7 =#=#=#=
Date: 2006-01-08 00:00:00Z
=#=#=#= End test: 2006-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2006-W01-7
=#=#=#= Begin test: 2006-W01-7 - round-trip =#=#=#=
Date: 2006-W01-7 00:00:00Z
=#=#=#= End test: 2006-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2006-W01-7 - round-trip
=#=#=#= Begin test: 2006-W01-1 =#=#=#=
Date: 2006-01-02 00:00:00Z
=#=#=#= End test: 2006-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2006-W01-1
=#=#=#= Begin test: 2006-W01-1 - round-trip =#=#=#=
Date: 2006-W01-1 00:00:00Z
=#=#=#= End test: 2006-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2006-W01-1 - round-trip
=#=#=#= Begin test: 2007-W01-7 =#=#=#=
Date: 2007-01-07 00:00:00Z
=#=#=#= End test: 2007-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2007-W01-7
=#=#=#= Begin test: 2007-W01-7 - round-trip =#=#=#=
Date: 2007-W01-7 00:00:00Z
=#=#=#= End test: 2007-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2007-W01-7 - round-trip
=#=#=#= Begin test: 2007-W01-1 =#=#=#=
Date: 2007-01-01 00:00:00Z
=#=#=#= End test: 2007-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2007-W01-1
=#=#=#= Begin test: 2007-W01-1 - round-trip =#=#=#=
Date: 2007-W01-1 00:00:00Z
=#=#=#= End test: 2007-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2007-W01-1 - round-trip
=#=#=#= Begin test: 2008-W01-7 =#=#=#=
Date: 2008-01-06 00:00:00Z
=#=#=#= End test: 2008-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2008-W01-7
=#=#=#= Begin test: 2008-W01-7 - round-trip =#=#=#=
Date: 2008-W01-7 00:00:00Z
=#=#=#= End test: 2008-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2008-W01-7 - round-trip
=#=#=#= Begin test: 2008-W01-1 =#=#=#=
Date: 2007-12-31 00:00:00Z
=#=#=#= End test: 2008-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2008-W01-1
=#=#=#= Begin test: 2008-W01-1 - round-trip =#=#=#=
Date: 2008-W01-1 00:00:00Z
=#=#=#= End test: 2008-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2008-W01-1 - round-trip
=#=#=#= Begin test: 2009-W01-7 =#=#=#=
Date: 2009-01-04 00:00:00Z
=#=#=#= End test: 2009-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2009-W01-7
=#=#=#= Begin test: 2009-W01-7 - round-trip =#=#=#=
Date: 2009-W01-7 00:00:00Z
=#=#=#= End test: 2009-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2009-W01-7 - round-trip
=#=#=#= Begin test: 2009-W01-1 =#=#=#=
Date: 2008-12-29 00:00:00Z
=#=#=#= End test: 2009-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2009-W01-1
=#=#=#= Begin test: 2009-W01-1 - round-trip =#=#=#=
Date: 2009-W01-1 00:00:00Z
=#=#=#= End test: 2009-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2009-W01-1 - round-trip
=#=#=#= Begin test: 2010-W01-7 =#=#=#=
Date: 2010-01-10 00:00:00Z
=#=#=#= End test: 2010-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2010-W01-7
=#=#=#= Begin test: 2010-W01-7 - round-trip =#=#=#=
Date: 2010-W01-7 00:00:00Z
=#=#=#= End test: 2010-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2010-W01-7 - round-trip
=#=#=#= Begin test: 2010-W01-1 =#=#=#=
Date: 2010-01-04 00:00:00Z
=#=#=#= End test: 2010-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2010-W01-1
=#=#=#= Begin test: 2010-W01-1 - round-trip =#=#=#=
Date: 2010-W01-1 00:00:00Z
=#=#=#= End test: 2010-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2010-W01-1 - round-trip
=#=#=#= Begin test: 2011-W01-7 =#=#=#=
Date: 2011-01-09 00:00:00Z
=#=#=#= End test: 2011-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2011-W01-7
=#=#=#= Begin test: 2011-W01-7 - round-trip =#=#=#=
Date: 2011-W01-7 00:00:00Z
=#=#=#= End test: 2011-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2011-W01-7 - round-trip
=#=#=#= Begin test: 2011-W01-1 =#=#=#=
Date: 2011-01-03 00:00:00Z
=#=#=#= End test: 2011-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2011-W01-1
=#=#=#= Begin test: 2011-W01-1 - round-trip =#=#=#=
Date: 2011-W01-1 00:00:00Z
=#=#=#= End test: 2011-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2011-W01-1 - round-trip
=#=#=#= Begin test: 2012-W01-7 =#=#=#=
Date: 2012-01-08 00:00:00Z
=#=#=#= End test: 2012-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2012-W01-7
=#=#=#= Begin test: 2012-W01-7 - round-trip =#=#=#=
Date: 2012-W01-7 00:00:00Z
=#=#=#= End test: 2012-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2012-W01-7 - round-trip
=#=#=#= Begin test: 2012-W01-1 =#=#=#=
Date: 2012-01-02 00:00:00Z
=#=#=#= End test: 2012-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2012-W01-1
=#=#=#= Begin test: 2012-W01-1 - round-trip =#=#=#=
Date: 2012-W01-1 00:00:00Z
=#=#=#= End test: 2012-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2012-W01-1 - round-trip
=#=#=#= Begin test: 2013-W01-7 =#=#=#=
Date: 2013-01-06 00:00:00Z
=#=#=#= End test: 2013-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2013-W01-7
=#=#=#= Begin test: 2013-W01-7 - round-trip =#=#=#=
Date: 2013-W01-7 00:00:00Z
=#=#=#= End test: 2013-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2013-W01-7 - round-trip
=#=#=#= Begin test: 2013-W01-1 =#=#=#=
Date: 2012-12-31 00:00:00Z
=#=#=#= End test: 2013-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2013-W01-1
=#=#=#= Begin test: 2013-W01-1 - round-trip =#=#=#=
Date: 2013-W01-1 00:00:00Z
=#=#=#= End test: 2013-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2013-W01-1 - round-trip
=#=#=#= Begin test: 2014-W01-7 =#=#=#=
Date: 2014-01-05 00:00:00Z
=#=#=#= End test: 2014-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2014-W01-7
=#=#=#= Begin test: 2014-W01-7 - round-trip =#=#=#=
Date: 2014-W01-7 00:00:00Z
=#=#=#= End test: 2014-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2014-W01-7 - round-trip
=#=#=#= Begin test: 2014-W01-1 =#=#=#=
Date: 2013-12-30 00:00:00Z
=#=#=#= End test: 2014-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2014-W01-1
=#=#=#= Begin test: 2014-W01-1 - round-trip =#=#=#=
Date: 2014-W01-1 00:00:00Z
=#=#=#= End test: 2014-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2014-W01-1 - round-trip
=#=#=#= Begin test: 2015-W01-7 =#=#=#=
Date: 2015-01-04 00:00:00Z
=#=#=#= End test: 2015-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2015-W01-7
=#=#=#= Begin test: 2015-W01-7 - round-trip =#=#=#=
Date: 2015-W01-7 00:00:00Z
=#=#=#= End test: 2015-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2015-W01-7 - round-trip
=#=#=#= Begin test: 2015-W01-1 =#=#=#=
Date: 2014-12-29 00:00:00Z
=#=#=#= End test: 2015-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2015-W01-1
=#=#=#= Begin test: 2015-W01-1 - round-trip =#=#=#=
Date: 2015-W01-1 00:00:00Z
=#=#=#= End test: 2015-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2015-W01-1 - round-trip
=#=#=#= Begin test: 2016-W01-7 =#=#=#=
Date: 2016-01-10 00:00:00Z
=#=#=#= End test: 2016-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2016-W01-7
=#=#=#= Begin test: 2016-W01-7 - round-trip =#=#=#=
Date: 2016-W01-7 00:00:00Z
=#=#=#= End test: 2016-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2016-W01-7 - round-trip
=#=#=#= Begin test: 2016-W01-1 =#=#=#=
Date: 2016-01-04 00:00:00Z
=#=#=#= End test: 2016-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2016-W01-1
=#=#=#= Begin test: 2016-W01-1 - round-trip =#=#=#=
Date: 2016-W01-1 00:00:00Z
=#=#=#= End test: 2016-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2016-W01-1 - round-trip
=#=#=#= Begin test: 2017-W01-7 =#=#=#=
Date: 2017-01-08 00:00:00Z
=#=#=#= End test: 2017-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2017-W01-7
=#=#=#= Begin test: 2017-W01-7 - round-trip =#=#=#=
Date: 2017-W01-7 00:00:00Z
=#=#=#= End test: 2017-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2017-W01-7 - round-trip
=#=#=#= Begin test: 2017-W01-1 =#=#=#=
Date: 2017-01-02 00:00:00Z
=#=#=#= End test: 2017-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2017-W01-1
=#=#=#= Begin test: 2017-W01-1 - round-trip =#=#=#=
Date: 2017-W01-1 00:00:00Z
=#=#=#= End test: 2017-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2017-W01-1 - round-trip
=#=#=#= Begin test: 2018-W01-7 =#=#=#=
Date: 2018-01-07 00:00:00Z
=#=#=#= End test: 2018-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2018-W01-7
=#=#=#= Begin test: 2018-W01-7 - round-trip =#=#=#=
Date: 2018-W01-7 00:00:00Z
=#=#=#= End test: 2018-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2018-W01-7 - round-trip
=#=#=#= Begin test: 2018-W01-1 =#=#=#=
Date: 2018-01-01 00:00:00Z
=#=#=#= End test: 2018-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2018-W01-1
=#=#=#= Begin test: 2018-W01-1 - round-trip =#=#=#=
Date: 2018-W01-1 00:00:00Z
=#=#=#= End test: 2018-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2018-W01-1 - round-trip
=#=#=#= Begin test: 2040-W01-7 =#=#=#=
Date: 2040-01-08 00:00:00Z
=#=#=#= End test: 2040-W01-7 - OK (0) =#=#=#=
* Passed: iso8601 - 2040-W01-7
=#=#=#= Begin test: 2040-W01-7 - round-trip =#=#=#=
Date: 2040-W01-7 00:00:00Z
=#=#=#= End test: 2040-W01-7 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2040-W01-7 - round-trip
=#=#=#= Begin test: 2040-W01-1 =#=#=#=
Date: 2040-01-02 00:00:00Z
=#=#=#= End test: 2040-W01-1 - OK (0) =#=#=#=
* Passed: iso8601 - 2040-W01-1
=#=#=#= Begin test: 2040-W01-1 - round-trip =#=#=#=
Date: 2040-W01-1 00:00:00Z
=#=#=#= End test: 2040-W01-1 - round-trip - OK (0) =#=#=#=
* Passed: iso8601 - 2040-W01-1 - round-trip
=#=#=#= Begin test: 2009-W53-07 =#=#=#=
Date: 2009-W53-7 00:00:00Z
=#=#=#= End test: 2009-W53-07 - OK (0) =#=#=#=
* Passed: iso8601 - 2009-W53-07
=#=#=#= Begin test: 2009-W53-07 (XML) =#=#=#=
2009-W53-7 00:00:00Z
=#=#=#= End test: 2009-W53-07 (XML) - OK (0) =#=#=#=
* Passed: iso8601 - 2009-W53-07 (XML)
=#=#=#= Begin test: epoch + 2 Years 5 Months 6 Minutes =#=#=#=
Date: 1970-01-01 00:00:00Z
Duration: 2 years 5 months 360 seconds (6 minutes)
Duration ends at: 1972-06-01 00:06:00Z
=#=#=#= End test: epoch + 2 Years 5 Months 6 Minutes - OK (0) =#=#=#=
* Passed: iso8601 - epoch + 2 Years 5 Months 6 Minutes
=#=#=#= Begin test: 2009-01-31 + 1 Month =#=#=#=
Date: 2009-01-31 00:00:00Z
Duration: 1 month
Duration ends at: 2009-02-28 00:00:00Z
=#=#=#= End test: 2009-01-31 + 1 Month - OK (0) =#=#=#=
* Passed: iso8601 - 2009-01-31 + 1 Month
=#=#=#= Begin test: 2009-01-31 + 2 Months =#=#=#=
Date: 2009-01-31 00:00:00Z
Duration: 2 months
Duration ends at: 2009-03-31 00:00:00Z
=#=#=#= End test: 2009-01-31 + 2 Months - OK (0) =#=#=#=
* Passed: iso8601 - 2009-01-31 + 2 Months
=#=#=#= Begin test: 2009-01-31 + 3 Months =#=#=#=
Date: 2009-01-31 00:00:00Z
Duration: 3 months
Duration ends at: 2009-04-30 00:00:00Z
=#=#=#= End test: 2009-01-31 + 3 Months - OK (0) =#=#=#=
* Passed: iso8601 - 2009-01-31 + 3 Months
+=#=#=#= Begin test: Test correct immediate plus-sign at timezone parsing =#=#=#=
+Date: 1556221374
+=#=#=#= End test: Test correct immediate plus-sign at timezone parsing - OK (0) =#=#=#=
+* Passed: iso8601 - Test correct immediate plus-sign at timezone parsing
+=#=#=#= Begin test: Like the previous, with exotic timezone to avoid false negative =#=#=#=
+Date: 1556212974
+=#=#=#= End test: Like the previous, with exotic timezone to avoid false negative - OK (0) =#=#=#=
+* Passed: iso8601 - Like the previous, with exotic timezone to avoid false negative
=#=#=#= Begin test: 2009-03-31 - 1 Month =#=#=#=
Date: 2009-03-31 00:00:00Z
Duration: -1 months
Duration ends at: 2009-02-28 01:00:00 +01:00
=#=#=#= End test: 2009-03-31 - 1 Month - OK (0) =#=#=#=
* Passed: iso8601 - 2009-03-31 - 1 Month
=#=#=#= Begin test: 2009-03-31 - 1 Month (XML) =#=#=#=
2009-03-31 00:00:00Z
-1 months
2009-02-28 01:00:00 +01:00
=#=#=#= End test: 2009-03-31 - 1 Month (XML) - OK (0) =#=#=#=
* Passed: iso8601 - 2009-03-31 - 1 Month (XML)
=#=#=#= Begin test: 2038-01-01 + 3 Months =#=#=#=
Date: 2038-01-01 00:00:00Z
Duration: 3 months
Duration ends at: 2038-04-01 00:00:00Z
=#=#=#= End test: 2038-01-01 + 3 Months - OK (0) =#=#=#=
* Passed: iso8601 - 2038-01-01 + 3 Months
=#=#=#= Begin test: 2038-01-01 + 3 Months (XML) =#=#=#=
2038-01-01 00:00:00Z
3 months
2038-04-01 00:00:00Z
=#=#=#= End test: 2038-01-01 + 3 Months (XML) - OK (0) =#=#=#=
* Passed: iso8601 - 2038-01-01 + 3 Months (XML)
diff --git a/cts/cts-cli.in b/cts/cts-cli.in
index c840ea023e..854f0e2e20 100644
--- a/cts/cts-cli.in
+++ b/cts/cts-cli.in
@@ -1,3425 +1,3429 @@
#!@PYTHON@
"""Regression tests for Pacemaker's command line tools."""
# pylint doesn't like the module name "cts-cli" which is an invalid complaint for this file
# but probably something we want to continue warning about elsewhere
# pylint: disable=invalid-name
# pacemaker imports need to come after we modify sys.path, which pylint will complain about.
# pylint: disable=wrong-import-position
# We know this is a very long file.
# pylint: disable=too-many-lines
__copyright__ = "Copyright 2024-2025 the Pacemaker project contributors"
__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY"
import argparse
from contextlib import contextmanager
from datetime import datetime, timedelta
import fileinput
from functools import partial
from gettext import ngettext
from multiprocessing import Pool, cpu_count
import os
import pathlib
import re
from shutil import copyfile
import signal
from string import Formatter
import subprocess
import sys
from tempfile import NamedTemporaryFile, TemporaryDirectory, mkstemp
import types
# These imports allow running from a source checkout after running `make`.
if os.path.exists("@abs_top_srcdir@/python"):
sys.path.insert(0, "@abs_top_srcdir@/python")
# pylint: disable=comparison-of-constants,comparison-with-itself,condition-evals-to-constant
if os.path.exists("@abs_top_builddir@/python") and "@abs_top_builddir@" != "@abs_top_srcdir@":
sys.path.insert(0, "@abs_top_builddir@/python")
from pacemaker._cts.environment import set_cts_path
from pacemaker._cts.errors import XmlValidationError
from pacemaker._cts.validate import validate
from pacemaker.buildoptions import BuildOptions
from pacemaker.exitstatus import ExitStatus
# Individual tool tests are split out, but can also be accessed as a group with "tools"
tools_tests = ["cibadmin", "crm_attribute", "crm_standby", "crm_resource",
"crm_ticket", "crmadmin", "crm_shadow", "crm_verify", "crm_simulate",
"crm_diff"]
# The default list of tests to run, in the order they should be run
default_tests = ["access_render", "daemons", "dates", "error_codes"] + tools_tests + \
["crm_mon", "acls", "validity", "upgrade", "rules", "feature_set"]
other_tests = ["agents"]
# The directory containing this program
test_home = os.path.dirname(os.path.realpath(__file__))
# Where test data is stored
cts_cli_data = f"{test_home}/cli"
# The name of the shadow CIB
SHADOW_NAME = "cts-cli"
# Arguments to pass to valgrind
VALGRIND_ARGS = ["-q", "--gen-suppressions=all", "--show-reachable=no", "--leak-check=full",
"--trace-children=no", "--time-stamp=yes", "--num-callers=20",
f"--suppressions={test_home}/valgrind-pcmk.suppressions"]
class PluralFormatter(Formatter):
"""
Special string formatting class for selecting singular vs. plurals.
Use like so:
fmt = PluralFormatter()
print(fmt.format("{0} {0}:plural,test,tests} succeeded", n_tests))
"""
def format_field(self, value, format_spec):
"""Convert a value to a formatted representation."""
if format_spec.startswith("plural,"):
eles = format_spec.split(',')
if len(eles) == 2:
singular = eles[1]
plural = singular + "s"
else:
singular = eles[1]
plural = eles[2]
return ngettext(singular, plural, value)
return super().format_field(value, format_spec)
def cleanup_shadow_dir():
"""Remove any previously created shadow CIB directory."""
subprocess.run(["crm_shadow", "--force", "--delete", SHADOW_NAME],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True)
def copy_existing_cib(existing):
"""
Generate a CIB by copying an existing one to a temporary location.
This is suitable for use with the cib_gen= parameter to the TestGroup class.
"""
(fp, new) = mkstemp(prefix="cts-cli.cib.xml.")
os.close(fp)
copyfile(existing, new)
return new
def current_cib():
"""Return the complete current CIB."""
with environ({"CIB_user": "root"}):
return subprocess.check_output(["cibadmin", "-Q"], encoding="utf-8")
def make_test_group(desc, cmd, **kwargs):
"""
Create a TestGroup that replicates the same test for multiple classes.
The given description, cmd, and kwargs will be passed as arguments to each
Test subclass. The resulting objects will then be added to a TestGroup
and returned.
The main purpose of this function is to be able to run the same test for
both text and XML formats without having to duplicate everything.
"""
tests = []
for c in [Test, ValidatingTest]:
# Insert "--output-as=" after the command name.
splitup = cmd.split()
splitup.insert(1, c.format_args)
obj = c(desc, " ".join(splitup), **kwargs)
tests.append(obj)
return TestGroup(tests)
def create_shadow_cib(shadow_dir, create_empty=True, validate_with=None,
valgrind=False):
"""
Create a shadow CIB file.
Keyword arguments:
create_empty -- If True, the shadow CIB will be empty. Otherwise, the
shadow CIB will be a copy of the currently active
cluster configuration.
validate_with -- If not None, the schema version to validate the CIB
against
valgrind -- If True, run the create operation under valgrind
"""
args = ["crm_shadow", "--batch", "--force"]
if create_empty:
args += ["--create-empty", SHADOW_NAME]
else:
args += ["--create", SHADOW_NAME]
if validate_with is not None:
args += ["--validate-with", validate_with]
if valgrind:
args = ["valgrind"] + VALGRIND_ARGS + args
os.environ["CIB_shadow_dir"] = shadow_dir
os.environ["CIB_shadow"] = SHADOW_NAME
subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True)
delete_shadow_resource_defaults()
def delete_shadow_resource_defaults():
"""Clear out the rsc_defaults section from a shadow CIB file."""
# A newly created empty CIB might or might not have a rsc_defaults section
# depending on whether the --with-resource-stickiness-default configure
# option was used. To ensure regression tests behave the same either way,
# delete any rsc_defaults after creating or erasing a CIB.
subprocess.run(["cibadmin", "--delete", "--xml-text", ""],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True)
# The above command might or might not bump the CIB version, so reset it
# to ensure future changes result in the same version for comparison.
reset_shadow_cib_version()
def reset_shadow_cib_version():
"""Set various version numbers in a shadow CIB file back to 0."""
with fileinput.input(files=[shadow_path()], inplace=True) as f:
for line in f:
line = re.sub('epoch="[0-9]*"', 'epoch="1"', line)
line = re.sub('num_updates="[0-9]*"', 'num_updates="0"', line)
line = re.sub('admin_epoch="[0-9]*"', 'admin_epoch="0"', line)
print(line, end='')
def run_cmd_list(cmds):
"""
Run one or more shell commands.
cmds can be:
* A string
* A Python function
* A list of the above
Raises subprocess.CalledProcessError on error.
"""
if cmds is None:
return
if isinstance(cmds, (str, types.FunctionType)):
cmds = [cmds]
for c in cmds:
if isinstance(c, types.FunctionType):
c()
else:
subprocess.run(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True, universal_newlines=True, check=True)
def sanitize_output(s):
"""
Replace content in the output expected to change between test runs.
This is stuff like version numbers, timestamps, source line numbers,
build options, system names and messages, etc.
"""
# A list of tuples of regular expressions and their replacements.
replacements = [
(r'Created new pacemaker-.* configuration', r'Created new pacemaker configuration'),
(r'Device not configured', r'No such device or address'),
(r'^Entity: line [0-9]+: ', r''),
(r'(Injecting attribute last-failure-ping#monitor_10000=)[0-9]*', r'\1'),
(r'Last change: .*', r'Last change:'),
(r'Last updated: .*', r'Last updated:'),
(r'^Migration will take effect until: .*', r'Migration will take effect until:'),
(r'(\* Possible values.*: .*)\(default: [^)]*\)', r'\1(default: )'),
(r"""-X '.*'""", r"""-X '...'"""),
(r' api-version="[^"]*"', r' api-version="X"'),
(r'\(apply_upgrade@.*\.c:[0-9]+\)', r'apply_upgrade'),
(r'\(invert_action@.*\.c:[0-9]+\)', r'invert_action'),
(r'\(pcmk__update_schema@.*\.c:[0-9]+\)', r'pcmk__update_schema'),
(r'(
"""
# Create a test CIB that has ACL roles
basic_tests = [
Test("Configure some ACLs", "cibadmin -M -o acls -p", update_cib=True,
stdin=acl_cib),
Test("Enable ACLs", "crm_attribute -n enable-acl -v true",
update_cib=True),
# Run cibadmin --show-access on the test CIB as an ACL-restricted user
Test("An instance of ACLs render (into color)",
"cibadmin --force --show-access=color -Q --user tony"),
Test("An instance of ACLs render (into namespacing)",
"cibadmin --force --show-access=namespace -Q --user tony"),
Test("An instance of ACLs render (into text)",
"cibadmin --force --show-access=text -Q --user tony"),
]
return [
ShadowTestGroup(basic_tests),
]
class DaemonsRegressionTest(RegressionTest):
"""A class for testing command line options of pacemaker daemons."""
@property
def name(self):
"""Return the name of this regression test."""
return "daemons"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
return [
Test("Get CIB manager metadata", "pacemaker-based metadata"),
Test("Get controller metadata", "pacemaker-controld metadata"),
Test("Get fencer metadata", "pacemaker-fenced metadata"),
Test("Get scheduler metadata", "pacemaker-schedulerd metadata"),
]
class DatesRegressionTest(RegressionTest):
"""A class for testing handling of ISO8601 dates."""
@property
def name(self):
"""Return the name of this regression test."""
return "dates"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
invalid_periods = [
"",
"2019-01-01 00:00:00Z", # Start with no end
"2019-01-01 00:00:00Z/", # Start with only a trailing slash
"PT2S/P1M", # Two durations
"2019-13-01 00:00:00Z/P1M", # Out-of-range month
"20191077T15/P1M", # Out-of-range day
"2019-10-01T25:00:00Z/P1M", # Out-of-range hour
"2019-10-01T24:00:01Z/P1M", # Hour 24 with anything but :00:00
"PT5H/20191001T007000Z", # Out-of-range minute
"2019-10-01 00:00:80Z/P1M", # Out-of-range second
"2019-10-01 00:00:10 +25:00/P1M", # Out-of-range offset hour
"20191001T000010 -00:61/P1M", # Out-of-range offset minute
"P1Y/2019-02-29 00:00:00Z", # Feb. 29 in non-leap-year
"2019-01-01 00:00:00Z/P", # Duration with no values
"P1Z/2019-02-20 00:00:00Z", # Invalid duration unit
"P1YM/2019-02-20 00:00:00Z", # No number for duration unit
]
# Ensure invalid period specifications are rejected
invalid_period_tests = []
for p in invalid_periods:
invalid_period_tests.append(Test(f"Invalid period - [{p}]",
f"iso8601 -p '{p}'",
expected_rc=ExitStatus.INVALID_PARAM))
year_tests = []
for y in ["06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "40"]:
year_tests.extend([
Test(f"20{y}-W01-7",
f"iso8601 -d '20{y}-W01-7 00Z'"),
Test(f"20{y}-W01-7 - round-trip",
f"iso8601 -d '20{y}-W01-7 00Z' -W -E '20{y}-W01-7 00:00:00Z'"),
Test(f"20{y}-W01-1",
f"iso8601 -d '20{y}-W01-1 00Z'"),
Test(f"20{y}-W01-1 - round-trip",
f"iso8601 -d '20{y}-W01-1 00Z' -W -E '20{y}-W01-1 00:00:00Z'")
])
return invalid_period_tests + [
make_test_group("'2005-040/2005-043' period", "iso8601 -p '2005-040/2005-043'"),
Test("2014-01-01 00:30:00 - 1 Hour",
"iso8601 -d '2014-01-01 00:30:00Z' -D P-1H -E '2013-12-31 23:30:00Z'"),
Test("Valid date - Feb 29 in leap year",
"iso8601 -d '2020-02-29 00:00:00Z' -E '2020-02-29 00:00:00Z'"),
Test("Valid date - using 'T' and offset",
"iso8601 -d '20191201T131211 -05:00' -E '2019-12-01 18:12:11Z'"),
Test("24:00:00 equivalent to 00:00:00 of next day",
"iso8601 -d '2019-12-31 24:00:00Z' -E '2020-01-01 00:00:00Z'"),
] + year_tests + [
make_test_group("2009-W53-07",
"iso8601 -d '2009-W53-7 00:00:00Z' -W -E '2009-W53-7 00:00:00Z'"),
Test("epoch + 2 Years 5 Months 6 Minutes",
"iso8601 -d 'epoch' -D P2Y5MT6M -E '1972-06-01 00:06:00Z'"),
Test("2009-01-31 + 1 Month",
"iso8601 -d '20090131T000000Z' -D P1M -E '2009-02-28 00:00:00Z'"),
Test("2009-01-31 + 2 Months",
"iso8601 -d '2009-01-31 00:00:00Z' -D P2M -E '2009-03-31 00:00:00Z'"),
Test("2009-01-31 + 3 Months",
"iso8601 -d '2009-01-31 00:00:00Z' -D P3M -E '2009-04-30 00:00:00Z'"),
+ Test("Test correct immediate plus-sign at timezone parsing",
+ "iso8601 -d '2019-04-25T21:42:54+0200' -S"),
+ Test("Like the previous, with exotic timezone to avoid false negative",
+ "iso8601 -d '2019-04-25T21:42:54+0420' -S"),
make_test_group("2009-03-31 - 1 Month",
"iso8601 -d '2009-03-31 01:00:00 +01:00' -D P-1M -E '2009-02-28 00:00:00Z'"),
make_test_group("2038-01-01 + 3 Months",
"iso8601 -d '2038-01-01 00:00:00Z' -D P3M -E '2038-04-01 00:00:00Z'"),
]
class ErrorCodeRegressionTest(RegressionTest):
"""A class for testing error code reporting."""
@property
def name(self):
"""Return the name of this regression test."""
return "error_codes"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
# Legacy return codes
#
# Don't test unknown legacy code. FreeBSD includes a colon in strerror(),
# while other distros do not.
legacy_tests = [
make_test_group("Get legacy return code", "crm_error 201"),
make_test_group("Get legacy return code (with name)", "crm_error -n 201"),
make_test_group("Get multiple legacy return codes", "crm_error 201 202"),
make_test_group("Get multiple legacy return codes (with names)",
"crm_error -n 201 202"),
# We can only rely on our custom codes, so we'll spot-check codes 201-209
Test("List legacy return codes (spot check)",
"crm_error -l | grep 20[1-9]"),
ValidatingTest("List legacy return codes (spot check)",
"crm_error -l --output-as=xml | grep -Ev '&1 < $(crm_shadow --file)"),
make_test_group("Require --force for CIB erasure", "cibadmin -E",
expected_rc=ExitStatus.UNSAFE, update_cib=True),
make_test_group("Allow CIB erasure with --force", "cibadmin -E --force"),
# Verify the output after erasure
make_test_group("Query CIB", "cibadmin -Q",
setup=delete_shadow_resource_defaults, update_cib=True),
]
# Add some stuff to the empty CIB so we know that erasing it did something.
basic_tests_setup = [
"""cibadmin -C -o nodes --xml-text ''""",
"""cibadmin -C -o crm_config --xml-text ''""",
"""cibadmin -C -o resources --xml-text ''"""
]
return [
ShadowTestGroup(basic_tests, setup=basic_tests_setup),
]
class CrmAttributeRegressionTest(RegressionTest):
"""A class for testing crm_attribute."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_attribute"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
options_tests = [
make_test_group("List all available options (invalid type)",
"crm_attribute --list-options=asdf",
expected_rc=ExitStatus.USAGE),
make_test_group("List non-advanced cluster options",
"crm_attribute --list-options=cluster"),
make_test_group("List all available cluster options",
"crm_attribute --list-options=cluster --all"),
Test("Return usage error if both -p and OCF_RESOURCE_INSTANCE are empty strings",
"crm_attribute -N cluster01 -p '' -G",
expected_rc=ExitStatus.USAGE),
]
value_update_tests = [
Test("Query the value of an attribute that does not exist",
"crm_attribute -n ABCD --query --quiet",
expected_rc=ExitStatus.NOSUCH),
Test("Configure something before erasing",
"crm_attribute -n test_attr -v 5", update_cib=True),
Test("Test '++' XML attribute update syntax",
"""cibadmin -M --score --xml-text=''""",
update_cib=True),
Test("Test '+=' XML attribute update syntax",
"""cibadmin -M --score --xml-text=''""",
update_cib=True),
make_test_group("Test '++' nvpair value update syntax",
"crm_attribute -n test_attr -v 'value++' --score",
update_cib=True),
make_test_group("Test '+=' nvpair value update syntax",
"crm_attribute -n test_attr -v 'value+=2' --score",
update_cib=True),
Test("Test '++' XML attribute update syntax (--score not set)",
"""cibadmin -M --xml-text=''""",
update_cib=True),
Test("Test '+=' XML attribute update syntax (--score not set)",
"""cibadmin -M --xml-text=''""",
update_cib=True),
make_test_group("Test '++' nvpair value update syntax (--score not set)",
"crm_attribute -n test_attr -v 'value++'",
update_cib=True),
make_test_group("Test '+=' nvpair value update syntax (--score not set)",
"crm_attribute -n test_attr -v 'value+=2'",
update_cib=True),
]
query_set_tests = [
Test("Set cluster option", "crm_attribute -n cluster-delay -v 60s",
update_cib=True),
Test("Query new cluster option",
"cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay"),
Test("Set no-quorum policy",
"crm_attribute -n no-quorum-policy -v ignore", update_cib=True),
Test("Delete nvpair",
"""cibadmin -D -o crm_config --xml-text ''""",
update_cib=True),
Test("Create operation should fail",
"""cibadmin -C -o crm_config --xml-text ''""",
expected_rc=ExitStatus.EXISTS, update_cib=True),
Test("Modify cluster options section",
"""cibadmin -M -o crm_config --xml-text ''""",
update_cib=True),
Test("Query updated cluster option",
"cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay",
update_cib=True),
Test("Set duplicate cluster option",
"crm_attribute -n cluster-delay -v 40s -s duplicate",
update_cib=True),
Test("Setting multiply defined cluster option should fail",
"crm_attribute -n cluster-delay -v 30s",
expected_rc=ExitStatus.MULTIPLE, update_cib=True),
Test("Set cluster option with -s",
"crm_attribute -n cluster-delay -v 30s -s duplicate",
update_cib=True),
Test("Delete cluster option with -i",
"crm_attribute -n cluster-delay -D -i cib-bootstrap-options-cluster-delay",
update_cib=True),
Test("Create node1 and bring it online",
"crm_simulate --live-check --in-place --node-up=node1",
update_cib=True),
Test("Create node attribute",
"crm_attribute -n ram -v 1024M -N node1 -t nodes",
update_cib=True),
Test("Query new node attribute",
"cibadmin -Q -o nodes | grep node1-ram",
update_cib=True),
Test("Create second node attribute",
"crm_attribute -n rattr -v XYZ -N node1 -t nodes",
update_cib=True),
Test("Query node attributes by pattern",
"crm_attribute -t nodes -P 'ra.*' -N node1 --query"),
Test("Update node attributes by pattern",
"crm_attribute -t nodes -P 'rat.*' -N node1 -v 10",
update_cib=True),
Test("Delete node attributes by pattern",
"crm_attribute -t nodes -P 'rat.*' -N node1 -D",
update_cib=True),
Test("Set a transient (fail-count) node attribute",
"crm_attribute -n fail-count-foo -v 3 -N node1 -t status",
update_cib=True),
Test("Query a fail count", "crm_failcount --query -r foo -N node1",
update_cib=True),
Test("Show node attributes with crm_simulate",
"crm_simulate --live-check --show-attrs"),
Test("Set a second transient node attribute",
"crm_attribute -n fail-count-bar -v 5 -N node1 -t status",
update_cib=True),
Test("Query transient node attributes by pattern",
"crm_attribute -t status -P fail-count -N node1 --query"),
Test("Update transient node attributes by pattern",
"crm_attribute -t status -P fail-count -N node1 -v 10",
update_cib=True),
Test("Delete transient node attributes by pattern",
"crm_attribute -t status -P fail-count -N node1 -D",
update_cib=True),
Test("crm_attribute given invalid delete usage",
"crm_attribute -t nodes -N node1 -D",
expected_rc=ExitStatus.USAGE),
Test("Set a utilization node attribute",
"crm_attribute -n cpu -v 1 -N node1 -z",
update_cib=True),
Test("Query utilization node attribute",
"crm_attribute --query -n cpu -N node1 -z"),
# This update will fail because it has version numbers
Test("Replace operation should fail",
"""cibadmin -Q | sed -e 's/epoch="[^"]*"/epoch="1"/' | cibadmin -R -p""",
expected_rc=ExitStatus.OLD),
]
promotable_tests = [
make_test_group("Query a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Delete a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -D"),
make_test_group("Query after deleting a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Update a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -v 1"),
make_test_group("Query after updating a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G"),
make_test_group("Update an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -v 5"),
make_test_group("Query after updating an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G"),
make_test_group("Delete an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -D"),
make_test_group("Query after deleting an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G",
expected_rc=ExitStatus.NOSUCH),
]
# Test for an issue with legacy command line parsing when the resource is
# specified in the environment (CLBZ#5509)
ocf_rsc_instance_tests = [
make_test_group("Update a promotable score attribute to -INFINITY",
"crm_attribute -N cluster01 -p -v -INFINITY",
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}),
make_test_group("Query after updating a promotable score attribute to -INFINITY",
"crm_attribute -N cluster01 -p -G",
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}),
Test("Try OCF_RESOURCE_INSTANCE if -p is specified with an empty string",
"crm_attribute -N cluster01 -p '' -G",
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}),
]
return options_tests + [
ShadowTestGroup(value_update_tests),
ShadowTestGroup(query_set_tests),
TestGroup(promotable_tests + ocf_rsc_instance_tests,
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"},
cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
]
class CrmStandbyRegressionTest(RegressionTest):
"""A class for testing crm_standby."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_standby"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
Test("Default standby value", "crm_standby -N node1 -G"),
Test("Set standby status", "crm_standby -N node1 -v true",
update_cib=True),
Test("Query standby value", "crm_standby -N node1 -G"),
Test("Delete standby value", "crm_standby -N node1 -D",
update_cib=True),
]
return [
ShadowTestGroup(basic_tests,
setup="""cibadmin -C -o nodes --xml-text ''"""),
]
class CrmResourceRegressionTest(RegressionTest):
"""A class for testing crm_resource."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_resource"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
options_tests = [
Test("crm_resource run with extra arguments", "crm_resource foo bar",
expected_rc=ExitStatus.USAGE),
Test("List all available resource options (invalid type)",
"crm_resource --list-options=asdf",
expected_rc=ExitStatus.USAGE),
Test("List all available resource options (invalid type)",
"crm_resource --list-options=asdf --output-as=xml",
expected_rc=ExitStatus.USAGE),
make_test_group("List non-advanced primitive meta-attributes",
"crm_resource --list-options=primitive"),
make_test_group("List all available primitive meta-attributes",
"crm_resource --list-options=primitive --all"),
make_test_group("List non-advanced fencing parameters",
"crm_resource --list-options=fencing"),
make_test_group("List all available fencing parameters",
"crm_resource --list-options=fencing --all"),
]
basic_tests = [
Test("Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True),
Test("crm_resource given both -r and resource config",
"crm_resource -r xyz --class ocf --provider pacemaker --agent Dummy",
expected_rc=ExitStatus.USAGE),
Test("crm_resource given resource config with invalid action",
"crm_resource --class ocf --provider pacemaker --agent Dummy -D",
expected_rc=ExitStatus.USAGE),
Test("Create a resource meta attribute",
"crm_resource -r dummy --meta -p is-managed -v false",
update_cib=True),
Test("Query a resource meta attribute",
"crm_resource -r dummy --meta -g is-managed",
update_cib=True),
Test("Remove a resource meta attribute",
"crm_resource -r dummy --meta -d is-managed",
update_cib=True),
ValidatingTest("Create another resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Stopped --output-as=xml"),
ValidatingTest("Show why a resource is not running",
"crm_resource -Y -r dummy --output-as=xml"),
ValidatingTest("Remove another resource meta attribute",
"crm_resource -r dummy --meta -d target-role --output-as=xml"),
ValidatingTest("Get a non-existent attribute from a resource element",
"crm_resource -r dummy --get-parameter nonexistent --element --output-as=xml"),
make_test_group("Get a non-existent attribute from a resource element",
"crm_resource -r dummy --get-parameter nonexistent --element",
update_cib=True),
Test("Get an existent attribute from a resource element",
"crm_resource -r dummy --get-parameter class --element",
update_cib=True),
ValidatingTest("Set a non-existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element --output-as=xml",
update_cib=True),
ValidatingTest("Set an existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element --output-as=xml",
update_cib=True),
ValidatingTest("Delete an existent attribute for a resource element",
"crm_resource -r dummy -d description --element --output-as=xml",
update_cib=True),
ValidatingTest("Delete a non-existent attribute for a resource element",
"crm_resource -r dummy -d description --element --output-as=xml",
update_cib=True),
Test("Set a non-existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element",
update_cib=True),
Test("Set an existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element",
update_cib=True),
Test("Delete an existent attribute for a resource element",
"crm_resource -r dummy -d description --element",
update_cib=True),
Test("Delete a non-existent attribute for a resource element",
"crm_resource -r dummy -d description --element",
update_cib=True),
Test("Create a resource attribute", "crm_resource -r dummy -p delay -v 10s",
update_cib=True),
make_test_group("List the configured resources", "crm_resource -L",
update_cib=True),
Test("Implicitly list the configured resources", "crm_resource"),
Test("List IDs of instantiated resources", "crm_resource -l"),
make_test_group("Show XML configuration of resource", "crm_resource -q -r dummy"),
Test("Require a destination when migrating a resource that is stopped",
"crm_resource -r dummy -M",
update_cib=True, expected_rc=ExitStatus.USAGE),
Test("Don't support migration to non-existent locations",
"crm_resource -r dummy -M -N i.do.not.exist",
update_cib=True, expected_rc=ExitStatus.NOSUCH),
Test("Create a fencing resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True),
Test("Bring resources online", "crm_simulate --live-check --in-place",
update_cib=True),
Test("Try to move a resource to its existing location",
"crm_resource -r dummy --move --node node1",
update_cib=True, expected_rc=ExitStatus.EXISTS),
Test("Try to move a resource that doesn't exist",
"crm_resource -r xyz --move --node node1",
expected_rc=ExitStatus.NOSUCH),
Test("Move a resource from its existing location",
"crm_resource -r dummy --move",
update_cib=True),
Test("Clear out constraints generated by --move",
"crm_resource -r dummy --clear",
update_cib=True),
Test("Ban a resource on unknown node",
"crm_resource -r dummy -B -N host1",
expected_rc=ExitStatus.NOSUCH),
Test("Create two more nodes and bring them online",
"crm_simulate --live-check --in-place --node-up=node2 --node-up=node3",
update_cib=True),
Test("Ban dummy from node1", "crm_resource -r dummy -B -N node1",
update_cib=True),
Test("Show where a resource is running", "crm_resource -r dummy -W"),
Test("Show constraints on a resource", "crm_resource -a -r dummy"),
ValidatingTest("Ban dummy from node2",
"crm_resource -r dummy -B -N node2 --output-as=xml",
update_cib=True),
Test("Relocate resources due to ban",
"crm_simulate --live-check --in-place -S",
update_cib=True),
ValidatingTest("Move dummy to node1",
"crm_resource -r dummy -M -N node1 --output-as=xml",
update_cib=True),
Test("Clear implicit constraints for dummy on node2",
"crm_resource -r dummy -U -N node2",
update_cib=True),
Test("Drop the status section",
"cibadmin -R -o status --xml-text ''"),
Test("Create a clone",
"""cibadmin -C -o resources --xml-text ''"""),
Test("Create a resource meta attribute",
"crm_resource -r test-primitive --meta -p is-managed -v false",
update_cib=True),
Test("Create a resource meta attribute in the primitive",
"crm_resource -r test-primitive --meta -p is-managed -v false --force",
update_cib=True),
Test("Update resource meta attribute with duplicates",
"crm_resource -r test-clone --meta -p is-managed -v true",
update_cib=True),
Test("Update resource meta attribute with duplicates (force clone)",
"crm_resource -r test-clone --meta -p is-managed -v true --force",
update_cib=True),
Test("Update child resource meta attribute with duplicates",
"crm_resource -r test-primitive --meta -p is-managed -v false",
update_cib=True),
Test("Delete resource meta attribute with duplicates",
"crm_resource -r test-clone --meta -d is-managed",
update_cib=True),
Test("Delete resource meta attribute in parent",
"crm_resource -r test-primitive --meta -d is-managed",
update_cib=True),
Test("Create a resource meta attribute in the primitive",
"crm_resource -r test-primitive --meta -p is-managed -v false --force",
update_cib=True),
Test("Update existing resource meta attribute",
"crm_resource -r test-clone --meta -p is-managed -v true",
update_cib=True),
Test("Create a resource meta attribute in the parent",
"crm_resource -r test-clone --meta -p is-managed -v true --force",
update_cib=True),
Test("Delete resource parent meta attribute (force)",
"crm_resource -r test-clone --meta -d is-managed --force",
update_cib=True),
# Restore meta-attributes before running this test
Test("Delete resource child meta attribute",
"crm_resource -r test-primitive --meta -d is-managed",
setup=["crm_resource -r test-primitive --meta -p is-managed -v true --force",
"crm_resource -r test-clone --meta -p is-managed -v true --force"],
update_cib=True),
Test("Create the dummy-group resource group",
"""cibadmin -C -o resources --xml-text '"""
""""""
""""""
"""'""",
update_cib=True),
Test("Create a resource meta attribute in dummy1",
"crm_resource -r dummy1 --meta -p is-managed -v true",
update_cib=True),
Test("Create a resource meta attribute in dummy-group",
"crm_resource -r dummy-group --meta -p is-managed -v false",
update_cib=True),
Test("Delete the dummy-group resource group",
"cibadmin -D -o resources --xml-text ''",
update_cib=True),
Test("Specify a lifetime when moving a resource",
"crm_resource -r dummy --move --node node2 --lifetime=PT1H",
update_cib=True),
Test("Try to move a resource previously moved with a lifetime",
"crm_resource -r dummy --move --node node1",
update_cib=True),
Test("Ban dummy from node1 for a short time",
"crm_resource -r dummy -B -N node1 --lifetime=PT1S",
update_cib=True),
Test("Remove expired constraints",
"sleep 2 && crm_resource --clear --expired",
update_cib=True),
# Clear has already been tested elsewhere, but we need to get rid of the
# constraints so testing delete works. It won't delete if there's still
# a reference to the resource somewhere.
Test("Clear all implicit constraints for dummy",
"crm_resource -r dummy -U",
update_cib=True),
Test("Set a node health strategy",
"crm_attribute -n node-health-strategy -v migrate-on-red",
update_cib=True),
Test("Set a node health attribute",
"crm_attribute -N node3 -n '#health-cts-cli' -v red",
update_cib=True),
ValidatingTest("Show why a resource is not running on an unhealthy node",
"crm_resource -N node3 -Y -r dummy --output-as=xml"),
Test("Delete a resource",
"crm_resource -D -r dummy -t primitive",
update_cib=True),
]
constraint_tests = []
for rsc in ["prim1", "prim2", "prim3", "prim4", "prim5", "prim6", "prim7",
"prim8", "prim9", "prim10", "prim11", "prim12", "prim13",
"group", "clone"]:
constraint_tests.extend([
make_test_group(f"Check locations and constraints for {rsc}",
f"crm_resource -a -r {rsc}"),
make_test_group(f"Recursively check locations and constraints for {rsc}",
f"crm_resource -A -r {rsc}"),
])
constraint_tests.extend([
Test("Check locations and constraints for group member (referring to group)",
"crm_resource -a -r gr2"),
Test("Check locations and constraints for group member (without referring to group)",
"crm_resource -a -r gr2 --force"),
])
colocation_tests = [
ValidatingTest("Set a meta-attribute for primitive and resources colocated with it",
"crm_resource -r prim5 --meta --set-parameter=target-role -v Stopped --recursive --output-as=xml"),
Test("Set a meta-attribute for group and resource colocated with it",
"crm_resource -r group --meta --set-parameter=target-role -v Stopped --recursive"),
ValidatingTest("Set a meta-attribute for clone and resource colocated with it",
"crm_resource -r clone --meta --set-parameter=target-role -v Stopped --recursive --output-as=xml"),
]
digest_tests = [
ValidatingTest("Show resource digests",
"crm_resource --digests -r rsc1 -N node1 --output-as=xml"),
Test("Show resource digests with overrides",
"crm_resource --digests -r rsc1 -N node1 --output-as=xml CRM_meta_interval=10000 CRM_meta_timeout=20000"),
make_test_group("Show resource operations", "crm_resource --list-operations"),
]
basic2_tests = [
make_test_group("List a promotable clone resource",
"crm_resource --locate -r promotable-clone"),
make_test_group("List the primitive of a promotable clone resource",
"crm_resource --locate -r promotable-rsc"),
make_test_group("List a single instance of a promotable clone resource",
"crm_resource --locate -r promotable-rsc:0"),
make_test_group("List another instance of a promotable clone resource",
"crm_resource --locate -r promotable-rsc:1"),
Test("Try to move an instance of a cloned resource",
"crm_resource -r promotable-rsc:0 --move --node cluster01",
expected_rc=ExitStatus.INVALID_PARAM),
]
basic_tests_setup = [
"crm_attribute -n no-quorum-policy -v ignore",
"crm_simulate --live-check --in-place --node-up=node1"
]
return options_tests + [
ShadowTestGroup(basic_tests, setup=basic_tests_setup),
TestGroup(constraint_tests, env={"CIB_file": f"{cts_cli_data}/constraints.xml"}),
TestGroup(colocation_tests, cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/constraints.xml")),
TestGroup(digest_tests, env={"CIB_file": f"{cts_cli_data}/crm_resource_digests.xml"}),
TestGroup(basic2_tests, env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
ValidatingTest("Check that CIB_file=\"-\" works - crm_resource",
"crm_resource --digests -r rsc1 -N node1 --output-as=xml",
env={"CIB_file": "-"},
stdin=pathlib.Path(f"{cts_cli_data}/crm_resource_digests.xml")),
]
class CrmTicketRegressionTest(RegressionTest):
"""A class for testing crm_ticket."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_ticket"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
Test("Default ticket granted state",
"crm_ticket -t ticketA -G granted -d false"),
Test("Set ticket granted state", "crm_ticket -t ticketA -r --force",
update_cib=True),
make_test_group("List ticket IDs", "crm_ticket -w"),
make_test_group("Query ticket state", "crm_ticket -t ticketA -q"),
make_test_group("Query ticket granted state",
"crm_ticket -t ticketA -G granted"),
Test("Delete ticket granted state",
"crm_ticket -t ticketA -D granted --force",
update_cib=True),
Test("Make a ticket standby", "crm_ticket -t ticketA -s",
update_cib=True),
Test("Query ticket standby state", "crm_ticket -t ticketA -G standby"),
Test("Activate a ticket", "crm_ticket -t ticketA -a",
update_cib=True),
make_test_group("List ticket details", "crm_ticket -L -t ticketA"),
Test("Add a second ticket", "crm_ticket -t ticketB -G granted -d false",
update_cib=True),
Test("Set second ticket granted state",
"crm_ticket -t ticketB -r --force",
update_cib=True),
make_test_group("List tickets", "crm_ticket -l"),
Test("Delete second ticket",
"""cibadmin --delete --xml-text ''""",
update_cib=True),
Test("Delete ticket standby state", "crm_ticket -t ticketA -D standby",
update_cib=True),
Test("Add a constraint to a ticket",
"""cibadmin -C -o constraints --xml-text ''""",
update_cib=True),
make_test_group("Query ticket constraints", "crm_ticket -t ticketA -c"),
Test("Delete ticket constraint",
"""cibadmin --delete --xml-text ''""",
update_cib=True),
]
basic_tests_setup = [
"""cibadmin -C -o crm_config --xml-text ''""",
"""cibadmin -C -o resources --xml-text ''"""
]
return [
ShadowTestGroup(basic_tests, setup=basic_tests_setup),
]
class CrmadminRegressionTest(RegressionTest):
"""A class for testing crmadmin."""
@property
def name(self):
"""Return the name of this regression test."""
return "crmadmin"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
make_test_group("List all nodes", "crmadmin -N"),
make_test_group("Minimally list all nodes", "crmadmin -N -q"),
Test("List all nodes as bash exports", "crmadmin -N -B"),
make_test_group("List cluster nodes",
"crmadmin -N cluster"),
make_test_group("List guest nodes",
"crmadmin -N guest"),
make_test_group("List remote nodes",
"crmadmin -N remote"),
make_test_group("List cluster,remote nodes",
"crmadmin -N cluster,remote"),
make_test_group("List guest,remote nodes",
"crmadmin -N guest,remote"),
]
return [
TestGroup(basic_tests,
env={"CIB_file": f"{cts_cli_data}/crmadmin-cluster-remote-guest-nodes.xml"}),
Test("Check that CIB_file=\"-\" works", "crmadmin -N",
env={"CIB_file": "-"},
stdin=pathlib.Path(f"{cts_cli_data}/crmadmin-cluster-remote-guest-nodes.xml")),
]
class CrmShadowRegressionTest(RegressionTest):
"""A class for testing crm_shadow."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_shadow"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
no_instance_tests = [
make_test_group("Get active shadow instance (no active instance)",
"crm_shadow --which",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's file name (no active instance)",
"crm_shadow --file",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's contents (no active instance)",
"crm_shadow --display",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's diff (no active instance)",
"crm_shadow --diff",
expected_rc=ExitStatus.NOSUCH),
]
# Create new shadow instance based on active CIB
# Don't use create_shadow_cib() here; test explicitly
new_instance_tests = [
make_test_group("Create copied shadow instance",
f"crm_shadow --create {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
# Query shadow instance based on active CIB
make_test_group("Get active shadow instance (copied)",
"crm_shadow --which"),
make_test_group("Get active shadow instance's file name (copied)",
"crm_shadow --file"),
make_test_group("Get active shadow instance's contents (copied)",
"crm_shadow --display"),
make_test_group("Get active shadow instance's diff (copied)",
"crm_shadow --diff"),
]
# Make some changes to the shadow file
modify_cib = """export CIB_file=$(crm_shadow --file) && """ \
"""cibadmin --modify --xml-text '' && """ \
"""cibadmin --delete --xml-text '' && """ \
"""cibadmin --create -o resources --xml-text '' && """ \
"""cibadmin --create -o status --xml-text ''"""
more_tests = [
# We can't use make_test_group() here because we only want to run
# the modify_cib setup code once, and make_test_group will pass all
# kwargs to every instance it creates.
Test("Get active shadow instance's diff (after changes)",
"crm_shadow --diff",
setup=modify_cib, expected_rc=ExitStatus.ERROR),
ValidatingTest("Get active shadow instance's diff (after changes)",
"crm_shadow --diff --output-as=xml",
expected_rc=ExitStatus.ERROR),
TestGroup([
# Commit the modified shadow CIB to a temp active CIB file
Test("Commit shadow instance",
f"crm_shadow --commit {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Commit shadow instance (force)",
f"crm_shadow --commit {SHADOW_NAME} --force"),
Test("Get active shadow instance's diff (after commit)",
"crm_shadow --diff",
expected_rc=ExitStatus.ERROR),
Test("Commit shadow instance (force) (all)",
f"crm_shadow --commit {SHADOW_NAME} --force --all"),
Test("Get active shadow instance's diff (after commit all)",
"crm_shadow --diff",
expected_rc=ExitStatus.ERROR),
], cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
TestGroup([
# Repeat sequence with XML output
ValidatingTest("Commit shadow instance",
f"crm_shadow --commit {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Commit shadow instance (force)",
f"crm_shadow --commit {SHADOW_NAME} --force --output-as=xml"),
ValidatingTest("Get active shadow instance's diff (after commit)",
"crm_shadow --diff --output-as=xml",
expected_rc=ExitStatus.ERROR),
ValidatingTest("Commit shadow instance (force) (all)",
f"crm_shadow --commit {SHADOW_NAME} --force --all --output-as=xml"),
ValidatingTest("Get active shadow instance's diff (after commit all)",
"crm_shadow --diff --output-as=xml",
expected_rc=ExitStatus.ERROR),
# Commit an inactive shadow instance with no active instance
make_test_group("Commit shadow instance (no active instance)",
f"crm_shadow --commit {SHADOW_NAME}",
env={"CIB_shadow": None},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (no active instance) (force)",
f"crm_shadow --commit {SHADOW_NAME} --force",
env={"CIB_shadow": None}),
# Commit an inactive shadow instance with an active instance
make_test_group("Commit shadow instance (mismatch)",
f"crm_shadow --commit {SHADOW_NAME}",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (mismatch) (force)",
f"crm_shadow --commit {SHADOW_NAME} --force",
env={"CIB_shadow": "nonexistent_shadow"}),
# Commit an active shadow instance whose shadow file is missing
make_test_group("Commit shadow instance (nonexistent shadow file)",
"crm_shadow --commit nonexistent_shadow",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (nonexistent shadow file) (force)",
"crm_shadow --commit nonexistent_shadow --force",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's diff (nonexistent shadow file)",
"crm_shadow --diff",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.NOSUCH),
# Commit an active shadow instance when the CIB file is missing
make_test_group("Commit shadow instance (nonexistent CIB file)",
f"crm_shadow --commit {SHADOW_NAME}",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --commit {SHADOW_NAME} --force",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"},
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's diff (nonexistent CIB file)",
"crm_shadow --diff",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"},
expected_rc=ExitStatus.NOSUCH),
], cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
]
delete_1_tests = [
# Delete an active shadow instance
Test("Delete shadow instance", f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (force)", f"crm_shadow --delete {SHADOW_NAME} --force"),
ShadowTestGroup([
ValidatingTest("Delete shadow instance",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
])
]
delete_2_tests = [
# Delete an inactive shadow instance with no active instance
Test("Delete shadow instance (no active instance)",
f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (no active instance) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force"),
]
delete_3_tests = [
ValidatingTest("Delete shadow instance (no active instance)",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (no active instance) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
]
delete_4_tests = [
# Delete an inactive shadow instance with an active instance
Test("Delete shadow instance (mismatch)",
f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (mismatch) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force"),
]
delete_5_tests = [
ValidatingTest("Delete shadow instance (mismatch)",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (mismatch) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
# Delete an active shadow instance whose shadow file is missing
Test("Delete shadow instance (nonexistent shadow file)",
"crm_shadow --delete nonexistent_shadow",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (nonexistent shadow file) (force)",
"crm_shadow --delete nonexistent_shadow --force"),
ValidatingTest("Delete shadow instance (nonexistent shadow file)",
"crm_shadow --delete nonexistent_shadow --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (nonexistent shadow file) (force)",
"crm_shadow --delete nonexistent_shadow --force --output-as=xml"),
]
delete_6_tests = [
# Delete an active shadow instance when the CIB file is missing
Test("Delete shadow instance (nonexistent CIB file)",
f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force"),
]
delete_7_tests = [
ValidatingTest("Delete shadow instance (nonexistent CIB file)",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
]
create_1_tests = [
# Create new shadow instance based on active CIB with no instance active
make_test_group("Create copied shadow instance (no active instance)",
f"crm_shadow --create {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": None}),
# Create new shadow instance based on active CIB with other instance active
make_test_group("Create copied shadow instance (mismatch)",
f"crm_shadow --create {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": "nonexistent_shadow"}),
# Create new shadow instance based on CIB (shadow file already exists)
make_test_group("Create copied shadow instance (file already exists)",
f"crm_shadow --create {SHADOW_NAME} --batch",
expected_rc=ExitStatus.CANTCREAT),
make_test_group("Create copied shadow instance (file already exists) (force)",
f"crm_shadow --create {SHADOW_NAME} --batch --force"),
# Create new shadow instance based on active CIB when the CIB file is missing
make_test_group("Create copied shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --create {SHADOW_NAME} --batch --force",
expected_rc=ExitStatus.NOSUCH,
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
]
create_2_tests = [
# Create new empty shadow instance
make_test_group("Create empty shadow instance",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
# Create empty shadow instance with no active instance
make_test_group("Create empty shadow instance (no active instance)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": None}),
# Create empty shadow instance with other instance active
make_test_group("Create empty shadow instance (mismatch)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": "nonexistent_shadow"}),
# Create empty shadow instance when the CIB file is missing
make_test_group("Create empty shadow instance (nonexistent CIB file)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
# Create empty shadow instance (shadow file already exists)
make_test_group("Create empty shadow instance (file already exists)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
expected_rc=ExitStatus.CANTCREAT),
make_test_group("Create empty shadow instance (file already exists) (force)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch --force"),
# Query shadow instance with an empty CIB.
# --which and --file queries were done earlier.
TestGroup([
make_test_group("Get active shadow instance's contents (empty CIB)",
"crm_shadow --display"),
make_test_group("Get active shadow instance's diff (empty CIB)",
"crm_shadow --diff",
expected_rc=ExitStatus.ERROR),
], setup=delete_shadow_resource_defaults),
]
reset_1_tests = [
Test("Resetting active shadow instance to active CIB requires force",
f"crm_shadow --reset {SHADOW_NAME} --batch",
expected_rc=ExitStatus.USAGE),
Test("Reset active shadow instance to active CIB",
f"crm_shadow --reset {SHADOW_NAME} --batch --force"),
Test("Active shadow instance no different from active CIB after reset",
"crm_shadow --diff"),
Test("Active shadow instance differs from active CIB after change",
"crm_shadow --diff",
setup="crm_attribute -n admin_epoch -v 99",
expected_rc=ExitStatus.ERROR),
ValidatingTest("Reset active shadow instance to active CIB",
f"crm_shadow --reset {SHADOW_NAME} --batch --force --output-as=xml"),
ValidatingTest("Active shadow instance no different from active CIB after reset",
"crm_shadow --diff --output-as=xml"),
ValidatingTest("Active shadow instance differs from active CIB after change",
"crm_shadow --diff --output-as=xml",
setup="crm_attribute -n admin_epoch -v 199",
expected_rc=ExitStatus.ERROR),
make_test_group("Reset shadow instance to active CIB with nonexistent shadow file",
f"crm_shadow --reset {SHADOW_NAME} --batch --force",
setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
Test("Active shadow instance no different from active CIB after force-reset",
"crm_shadow --diff"),
]
reset_2_tests = [
make_test_group("Reset inactive shadow instance (none active) to active CIB",
f"crm_shadow --reset {SHADOW_NAME} --force --batch"),
]
reset_3_tests = [
make_test_group("Reset inactive shadow instance while another instance active",
f"crm_shadow --reset {SHADOW_NAME} --batch --force"),
]
reset_4_tests = [
make_test_group("Reset shadow instance with nonexistent CIB",
f"crm_shadow --reset {SHADOW_NAME} --batch --force",
expected_rc=ExitStatus.NOSUCH),
]
# Switch shadow instances
switch_tests = [
make_test_group("Switch to new shadow instance",
f"crm_shadow --switch {SHADOW_NAME} --batch"),
TestGroup([
make_test_group("Switch to nonexistent shadow instance",
f"crm_shadow --switch {SHADOW_NAME} --batch",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Switch to nonexistent shadow instance (force)",
f"crm_shadow --switch {SHADOW_NAME} --batch --force",
expected_rc=ExitStatus.NOSUCH),
], setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
]
return no_instance_tests + [
ShadowTestGroup(new_instance_tests + more_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"},
create=False),
ShadowTestGroup(delete_1_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
ShadowTestGroup(delete_2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": None}),
ShadowTestGroup(delete_3_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": None}),
ShadowTestGroup(delete_4_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": "nonexistent_shadow"}),
ShadowTestGroup(delete_5_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": "nonexistent_shadow"}),
ShadowTestGroup(delete_6_tests,
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
ShadowTestGroup(delete_7_tests,
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
ShadowTestGroup(create_1_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"},
create=False),
ShadowTestGroup(create_2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"},
create=False),
ShadowTestGroup(reset_1_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
ShadowTestGroup(reset_2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": None}),
ShadowTestGroup(reset_3_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": "nonexistent_shadow"}),
ShadowTestGroup(reset_4_tests,
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
ShadowTestGroup(switch_tests,
env={"CIB_shadow": "nonexistent_shadow"},
create_empty=True),
]
class CrmVerifyRegressionTest(RegressionTest):
"""A class for testing crm_verify."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_verify"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
invalid_tests = [
make_test_group("Verify a file-specified invalid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml",
expected_rc=ExitStatus.CONFIG),
make_test_group("Verify a file-specified invalid configuration (verbose)",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml --verbose",
expected_rc=ExitStatus.CONFIG),
make_test_group("Verify a file-specified invalid configuration (quiet)",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml --quiet",
expected_rc=ExitStatus.CONFIG),
ValidatingTest("Verify another file-specified invalid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_no_stonith.xml --output-as=xml",
expected_rc=ExitStatus.CONFIG),
]
with open(f"{test_home}/cli/crm_mon.xml", encoding="utf-8") as f:
cib_contents = f.read()
valid_tests = [
ValidatingTest("Verify a file-specified valid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_mon.xml --output-as=xml"),
ValidatingTest("Verify a piped-in valid configuration",
"crm_verify -p --output-as=xml",
stdin=pathlib.Path(f"{cts_cli_data}/crm_mon.xml")),
ValidatingTest("Verbosely verify a file-specified valid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_mon.xml --output-as=xml --verbose"),
ValidatingTest("Verbosely verify a piped-in valid configuration",
"crm_verify -p --output-as=xml --verbose",
stdin=pathlib.Path(f"{cts_cli_data}/crm_mon.xml")),
ValidatingTest("Verify a string-supplied valid configuration",
f"crm_verify -X '{cib_contents}' --output-as=xml"),
ValidatingTest("Verbosely verify a string-supplied valid configuration",
f"crm_verify -X '{cib_contents}' --output-as=xml --verbose"),
]
return invalid_tests + valid_tests
class CrmSimulateRegressionTest(RegressionTest):
"""A class for testing crm_simulate."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_simulate"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
good_cib = """
"""
bad_cib = good_cib.replace("start", "break")
bad_version_cib = good_cib.replace("pacemaker-1.2", "pacemaker-9999.0")
recoverable_cib = good_cib.replace("", "")
no_version_cib = good_cib.replace('validate-with="pacemaker-1.2" ', "")
no_version_bad_cib = bad_version_cib.replace('epoch="3"', 'epoch="30"').replace("start", "break")
basic_tests = [
Test("Show allocation scores with crm_simulate",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml --show-scores --output-as=xml"),
Test("Show utilization with crm_simulate",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml --show-utilization"),
Test("Simulate injecting a failure",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml -S -i ping_monitor_10000@cluster02=1"),
Test("Simulate bringing a node down",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml -S --node-down=cluster01"),
Test("Simulate a node failing",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml -S --node-fail=cluster02"),
Test("Run crm_simulate with invalid CIB (enum violation)",
"crm_simulate -p -S",
stdin=bad_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
Test("Run crm_simulate with invalid CIB (unrecognized validate-with)",
"crm_simulate -p -S",
stdin=bad_version_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
Test("Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1)",
"crm_simulate -p -S",
stdin=recoverable_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"}),
Test("Run crm_simulate with valid CIB, but without validate-with attribute",
"crm_simulate -p -S",
stdin=no_version_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
Test("Run crm_simulate with invalid CIB, also without validate-with attribute",
"crm_simulate -p -S",
stdin=no_version_bad_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
]
return [
ShadowTestGroup(basic_tests, create=False,
env={"CIB_shadow": None}),
]
class CrmDiffRegressionTest(RegressionTest):
"""A class for testing crm_diff."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_diff"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
old_file = f"{cts_cli_data}/crm_diff_old.xml"
new_file = f"{cts_cli_data}/crm_diff_new.xml"
patch_file = f"{cts_cli_data}/crm_diff_patchset.xml"
cib_patch_file = f"{cts_cli_data}/crm_diff_patchset_cib.xml"
# Enclose the strings in quotes now rather than in the command lines
with open(f"{cts_cli_data}/crm_diff_old.xml", "r") as file:
old_str = f"'{file.read()}'"
with open(f"{cts_cli_data}/crm_diff_new.xml", "r") as file:
new_str = f"'{file.read()}'"
return [
make_test_group("Create an XML patchset from files",
f"crm_diff -o {old_file} -n {new_file}",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset from strings",
f"crm_diff -O {old_str} -N {new_str}",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset from old file, new string",
f"crm_diff -o {old_file} -N {new_str}",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset from old string, new file",
f"crm_diff -O {old_str} -n {new_file}",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset as CIB",
f"crm_diff -o {old_file} -n {new_file} --cib",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset with no versions",
f"crm_diff -o {old_file} -n {new_file} --no-version",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset as CIB, with no versions",
f"crm_diff -o {old_file} -n {new_file} --cib --no-version",
expected_rc=ExitStatus.USAGE),
# Patch must be a file (cannot be a string).
#
# patch_file was generated using the following command:
#
# # crm_diff -o {old_file} -n {new_file}
#
make_test_group("Apply an XML patchset to a file",
f"crm_diff -o {old_file} -p {patch_file}"),
make_test_group("Apply an XML patchset to a string",
f"crm_diff -O {old_str} -p {patch_file}"),
make_test_group("Apply an XML patchset as CIB",
f"crm_diff -o {old_file} -p {patch_file} --cib"),
make_test_group("Apply an XML patchset with no versions",
f"crm_diff -o {old_file} -p {patch_file} --no-version"),
make_test_group("Apply an XML patchset as CIB, with no versions",
f"crm_diff -o {old_file} -p {patch_file} --cib --no-version",
expected_rc=ExitStatus.USAGE),
# cib_patch_file was generated using the following command:
#
# # crm_diff -o {old_file} -n {new_file} --cib
#
# Thus a digest was added to the patchset, and attribute position
# changes were ignored.
#
# @FIXME Currently these all fail due to digest mismatch. The issue
# goes back to at least Pacemaker 1.1.24. However, note that they
# fail with a generic error code, not a digest error code.
#
# It seems reasonable that a patchset generated by crm_diff should
# possible to apply to the old XML using crm_diff.
make_test_group("Apply an XML patchset generated as CIB",
f"crm_diff -o {old_file} -p {cib_patch_file}",
expected_rc=ExitStatus.ERROR),
make_test_group("Apply an XML patchset generated as CIB, as CIB",
f"crm_diff -o {old_file} -p {cib_patch_file} --cib",
expected_rc=ExitStatus.ERROR),
make_test_group("Apply an XML patchset generated as CIB, with no versions",
f"crm_diff -o {old_file} -p {cib_patch_file} --no-version",
expected_rc=ExitStatus.ERROR),
# @TODO We could add tests where the old and new CIBs have the same
# version info. In that case, at the time of writing, generating a
# patchset with --cib and then trying to apply it will result in
# ExitStatus.OLD.
]
class CrmMonRegressionTest(RegressionTest):
"""A class for testing crm_mon."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_mon"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
make_test_group("Basic output", "crm_mon -1"),
make_test_group("Output without node section",
"crm_mon -1 --exclude=nodes"),
# The next test doesn't need to be performed for other output formats. It's
# really just a test to make sure that blank lines are correct.
Test("Output with only the node section",
"crm_mon -1 --exclude=all --include=nodes"),
# XML includes everything already so there's no need for a complete test
Test("Complete text output", "crm_mon -1 --include=all"),
# XML includes detailed output already
Test("Complete text output with detail", "crm_mon -1R --include=all"),
Test("Complete brief text output", "crm_mon -1 --include=all --brief"),
Test("Complete text output grouped by node",
"crm_mon -1 --include=all --group-by-node"),
# XML does not have a brief output option
Test("Complete brief text output grouped by node",
"crm_mon -1 --include=all --group-by-node --brief"),
ValidatingTest("Output grouped by node",
"crm_mon --output-as=xml --group-by-node"),
make_test_group("Complete output filtered by node",
"crm_mon -1 --include=all --node=cluster01"),
make_test_group("Complete output filtered by tag",
"crm_mon -1 --include=all --node=even-nodes"),
make_test_group("Complete output filtered by resource tag",
"crm_mon -1 --include=all --resource=fencing-rscs"),
make_test_group("Output filtered by node that doesn't exist",
"crm_mon -1 --node=blah"),
Test("Basic text output with inactive resources", "crm_mon -1 -r"),
# XML already includes inactive resources
Test("Basic text output with inactive resources, filtered by node",
"crm_mon -1 -r --node=cluster02"),
make_test_group("Complete output filtered by primitive resource",
"crm_mon -1 --include=all --resource=Fencing"),
make_test_group("Complete output filtered by group resource",
"crm_mon -1 --include=all --resource=exim-group"),
Test("Complete text output filtered by group resource member",
"crm_mon -1 --include=all --resource=Public-IP"),
ValidatingTest("Output filtered by group resource member",
"crm_mon --output-as=xml --resource=Email"),
make_test_group("Complete output filtered by clone resource",
"crm_mon -1 --include=all --resource=ping-clone"),
make_test_group("Complete output filtered by clone resource instance",
"crm_mon -1 --include=all --resource=ping"),
Test("Complete text output filtered by exact clone resource instance",
"crm_mon -1 --include=all --show-detail --resource=ping:0"),
ValidatingTest("Output filtered by exact clone resource instance",
"crm_mon --output-as=xml --resource=ping:1"),
make_test_group("Output filtered by resource that doesn't exist",
"crm_mon -1 --resource=blah"),
Test("Basic text output with inactive resources, filtered by tag",
"crm_mon -1 -r --resource=inactive-rscs"),
Test("Basic text output with inactive resources, filtered by bundle resource",
"crm_mon -1 -r --resource=httpd-bundle"),
ValidatingTest("Output filtered by inactive bundle resource",
"crm_mon --output-as=xml --resource=httpd-bundle"),
Test("Basic text output with inactive resources, filtered by bundled IP address resource",
"crm_mon -1 -r --resource=httpd-bundle-ip-192.168.122.131"),
ValidatingTest("Output filtered by bundled IP address resource",
"crm_mon --output-as=xml --resource=httpd-bundle-ip-192.168.122.132"),
Test("Basic text output with inactive resources, filtered by bundled container",
"crm_mon -1 -r --resource=httpd-bundle-docker-1"),
ValidatingTest("Output filtered by bundled container",
"crm_mon --output-as=xml --resource=httpd-bundle-docker-2"),
Test("Basic text output with inactive resources, filtered by bundle connection",
"crm_mon -1 -r --resource=httpd-bundle-0"),
ValidatingTest("Output filtered by bundle connection",
"crm_mon --output-as=xml --resource=httpd-bundle-0"),
Test("Basic text output with inactive resources, filtered by bundled primitive resource",
"crm_mon -1 -r --resource=httpd"),
ValidatingTest("Output filtered by bundled primitive resource",
"crm_mon --output-as=xml --resource=httpd"),
Test("Complete text output, filtered by clone name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-clone-group"),
ValidatingTest("Output, filtered by clone name in cloned group",
"crm_mon --output-as=xml --resource=mysql-clone-group"),
Test("Complete text output, filtered by group name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-group"),
ValidatingTest("Output, filtered by group name in cloned group",
"crm_mon --output-as=xml --resource=mysql-group"),
Test("Complete text output, filtered by exact group instance name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-group:1"),
ValidatingTest("Output, filtered by exact group instance name in cloned group",
"crm_mon --output-as=xml --resource=mysql-group:1"),
Test("Complete text output, filtered by primitive name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-proxy"),
ValidatingTest("Output, filtered by primitive name in cloned group",
"crm_mon --output-as=xml --resource=mysql-proxy"),
Test("Complete text output, filtered by exact primitive instance name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-proxy:1"),
ValidatingTest("Output, filtered by exact primitive instance name in cloned group",
"crm_mon --output-as=xml --resource=mysql-proxy:1"),
]
partial_tests = [
Test("Output of partially active resources", "crm_mon -1 --show-detail"),
ValidatingTest("Output of partially active resources", "crm_mon --output-as=xml"),
Test("Output of partially active resources, with inactive resources",
"crm_mon -1 -r --show-detail"),
# XML already includes inactive resources
Test("Complete brief text output, with inactive resources",
"crm_mon -1 -r --include=all --brief --show-detail"),
# XML does not have a brief output option
Test("Text output of partially active group", "crm_mon -1 --resource=partially-active-group"),
Test("Text output of partially active group, with inactive resources",
"crm_mon -1 --resource=partially-active-group -r"),
Test("Text output of active member of partially active group",
"crm_mon -1 --resource=dummy-1"),
Test("Text output of inactive member of partially active group",
"crm_mon -1 --resource=dummy-2 --show-detail"),
Test("Complete brief text output grouped by node, with inactive resources",
"crm_mon -1 -r --include=all --group-by-node --brief --show-detail"),
Test("Text output of partially active resources, with inactive resources, filtered by node",
"crm_mon -1 -r --node=cluster01"),
ValidatingTest("Output of partially active resources, filtered by node",
"crm_mon --output-as=xml --node=cluster01"),
]
unmanaged_tests = [
make_test_group("Output of active unmanaged resource on offline node",
"crm_mon -1"),
Test("Brief text output of active unmanaged resource on offline node",
"crm_mon -1 --brief"),
Test("Brief text output of active unmanaged resource on offline node, grouped by node",
"crm_mon -1 --brief --group-by-node"),
]
maint1_tests = [
make_test_group("Output of all resources with maintenance-mode enabled",
"crm_mon -1 -r",
setup="crm_attribute -n maintenance-mode -v true",
teardown="crm_attribute -n maintenance-mode -v false"),
make_test_group("Output of all resources with maintenance enabled for a node",
"crm_mon -1 -r",
setup="crm_attribute -n maintenance -N cluster02 -v true",
teardown="crm_attribute -n maintenance -N cluster02 -v false"),
]
maint2_tests = [
# The fence resource is excluded, for comparison
make_test_group("Output of all resources with maintenance meta attribute true",
"crm_mon -1 -r"),
]
t180_tests = [
Test("Text output of guest node's container on different node from its remote resource",
"crm_mon -1"),
Test("Complete text output of guest node's container on different node from its remote resource",
"crm_mon -1 --show-detail"),
]
return [
TestGroup(basic_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
Test("Check that CIB_file=\"-\" works", "crm_mon -1",
env={"CIB_file": "-"},
stdin=pathlib.Path(f"{cts_cli_data}/crm_mon.xml")),
TestGroup(partial_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-partial.xml"}),
TestGroup(unmanaged_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-unmanaged.xml"}),
TestGroup(maint1_tests,
cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
TestGroup(maint2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-rsc-maint.xml"}),
TestGroup(t180_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-T180.xml"}),
]
class AclsRegressionTest(RegressionTest):
"""A class for testing access control lists."""
@property
def name(self):
"""Return the name of this regression test."""
return "acls"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
acl_cib = """
"""
basic_tests = [
Test("Configure some ACLs", "cibadmin -M -o acls -p",
update_cib=True, stdin=acl_cib),
Test("Enable ACLs", "crm_attribute -n enable-acl -v true",
update_cib=True),
Test("Set cluster option", "crm_attribute -n no-quorum-policy -v ignore",
update_cib=True),
Test("New ACL role",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("New ACL target",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("Another ACL role",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("Another ACL target",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("Updated ACL",
"""cibadmin --replace -o acls --xml-text ''""",
update_cib=True),
]
no_acl_tests = [
Test("unknownguy: Query configuration", "cibadmin -Q",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("unknownguy: Set enable-acl",
"crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("unknownguy: Set stonith-enabled",
"crm_attribute -n stonith-enabled -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("unknownguy: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
]
deny_cib_tests = [
Test("l33t-haxor: Query configuration",
"cibadmin -Q",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Set enable-acl",
"crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Set stonith-enabled",
"crm_attribute -n stonith-enabled -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
]
observer_tests = [
Test("niceguy: Query configuration", "cibadmin -Q"),
Test("niceguy: Set enable-acl",
"crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("niceguy: Set stonith-enabled",
"crm_attribute -n stonith-enabled -v false",
update_cib=True),
Test("niceguy: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("root: Query configuration", "cibadmin -Q",
env={"CIB_user": "root"}),
Test("root: Set stonith-enabled", "crm_attribute -n stonith-enabled -v true",
update_cib=True, env={"CIB_user": "root"}),
Test("root: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True, env={"CIB_user": "root"}),
# For use with later tests
Test("root: Create another resource (with description)",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True, env={"CIB_user": "root"}),
]
deny_cib_2_tests = [
Test("l33t-haxor: Create a resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Stopped",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Query a resource meta attribute",
"crm_resource -r dummy --meta -g target-role",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Remove a resource meta attribute",
"crm_resource -r dummy --meta -d target-role",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
]
observer_2_tests = [
Test("niceguy: Create a resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Stopped",
update_cib=True),
Test("niceguy: Query a resource meta attribute",
"crm_resource -r dummy --meta -g target-role",
update_cib=True),
Test("niceguy: Remove a resource meta attribute",
"crm_resource -r dummy --meta -d target-role",
update_cib=True),
Test("niceguy: Create a resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Started",
update_cib=True),
]
read_meta_tests = [
Test("badidea: Query configuration - implied deny", "cibadmin -Q"),
]
deny_cib_3_tests = [
Test("betteridea: Query configuration - explicit deny", "cibadmin -Q"),
]
replace_tests = [
TestGroup([
AclTest("niceguy: Replace - remove acls",
"cibadmin --replace -p",
setup="cibadmin --delete --xml-text ''",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - create resource",
"cibadmin --replace -p",
setup="""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - modify attribute (deny)",
"cibadmin --replace -p",
setup="crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - delete attribute (deny)",
"cibadmin --replace -p",
setup="""cibadmin --replace --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - create attribute (deny)",
"cibadmin --replace -p",
setup="""cibadmin --modify --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
], env={"CIB_user": "niceguy"}),
# admin role
TestGroup([
AclTest("bob: Replace - create attribute (direct allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("bob: Replace - modify attribute (direct allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("bob: Replace - delete attribute (direct allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''"""),
], env={"CIB_user": "bob"}),
# super_user role
TestGroup([
AclTest("joe: Replace - create attribute (inherited allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("joe: Replace - modify attribute (inherited allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("joe: Replace - delete attribute (inherited allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''"""),
], env={"CIB_user": "joe"}),
# rsc_writer role
TestGroup([
AclTest("mike: Replace - create attribute (allow overrides deny)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("mike: Replace - modify attribute (allow overrides deny)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("mike: Replace - delete attribute (allow overrides deny)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''"""),
# Create an additional resource for deny-overrides-allow testing
AclTest("mike: Create another resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True),
], env={"CIB_user": "mike"}),
# rsc_denied role
TestGroup([
AclTest("chris: Replace - create attribute (deny overrides allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("chris: Replace - modify attribute (deny overrides allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("chris: Replace - delete attribute (deny overrides allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
], env={"CIB_user": "chris"}),
]
loop_tests = [
# no ACL
TestGroup(no_acl_tests, env={"CIB_user": "unknownguy"}),
# deny /cib permission
TestGroup(deny_cib_tests, env={"CIB_user": "l33t-haxor"}),
# observer role
TestGroup(observer_tests, env={"CIB_user": "niceguy"}),
# deny /cib permission
TestGroup(deny_cib_2_tests, env={"CIB_user": "l33t-haxor"}),
# observer role
TestGroup(observer_2_tests, env={"CIB_user": "niceguy"}),
# read //meta_attributes
TestGroup(read_meta_tests, env={"CIB_user": "badidea"}),
# deny /cib, read //meta_attributes
TestGroup(deny_cib_3_tests, env={"CIB_user": "betteridea"}),
] + replace_tests
return [
ShadowTestGroup(basic_tests + [
TestGroup(loop_tests,
env={"PCMK_trace_functions": "pcmk__check_acl,pcmk__apply_creation_acl"})]),
]
class ValidityRegressionTest(RegressionTest):
"""A class for testing CIB validity."""
@property
def name(self):
"""Return the name of this regression test."""
return "validity"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
# sanitize_output() strips out validate-with, so there's no point in
# outputting the CIB after tests that modify it
Test("Try to set unrecognized validate-with",
"cibadmin -M --xml-text ''",
expected_rc=ExitStatus.CONFIG),
Test("Try to remove validate-with attribute",
"cibadmin -R -p",
stdin=StdinCmd("""cibadmin -Q | sed 's#validate-with="[^"]*"##'"""),
expected_rc=ExitStatus.CONFIG),
Test("Try to use rsc_order first-action value disallowed by schema",
"cibadmin -M -o constraints --xml-text ''",
expected_rc=ExitStatus.CONFIG, update_cib=True),
Test("Try to use configuration legal only with schema after configured one",
"cibadmin -C -o configuration --xml-text ''",
expected_rc=ExitStatus.CONFIG, update_cib=True),
Test("Disable schema validation",
"cibadmin -M --xml-text ''",
expected_rc=ExitStatus.OK),
Test("Set invalid rsc_order first-action value (schema validation disabled)",
"cibadmin -M -o constraints --xml-text ''",
expected_rc=ExitStatus.OK, update_cib=True),
Test("Run crm_simulate with invalid rsc_order first-action "
"(schema validation disabled)",
"crm_simulate -SL",
expected_rc=ExitStatus.OK),
]
basic_tests_setup = [
"""cibadmin -C -o resources --xml-text ''""",
"""cibadmin -C -o resources --xml-text ''""",
"""cibadmin -C -o constraints --xml-text ''""",
]
return [
ShadowTestGroup(basic_tests, validate_with="pacemaker-1.2",
setup=basic_tests_setup,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema,invert_action"}),
]
class UpgradeRegressionTest(RegressionTest):
"""A class for testing upgrading the CIB."""
@property
def name(self):
"""Return the name of this regression test."""
return "upgrade"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
resource_cib = """
"""
basic_tests = [
Test("Set stonith-enabled=false", "crm_attribute -n stonith-enabled -v false",
update_cib=True),
Test("Configure the initial resource", "cibadmin -M -o resources -p",
update_cib=True, stdin=resource_cib),
Test("Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping)",
"cibadmin --upgrade --force -V -V",
update_cib=True),
Test("Query a resource instance attribute (shall survive)",
"crm_resource -r mySmartFuse -g requires",
update_cib=True),
]
return [
ShadowTestGroup(basic_tests, validate_with="pacemaker-2.10",
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"})
]
class RulesRegressionTest(RegressionTest):
"""A class for testing support for CIB rules."""
@property
def name(self):
"""Return the name of this regression test."""
return "rules"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
tomorrow = datetime.now() + timedelta(days=1)
rule_cib = f"""
"""
usage_tests = [
make_test_group("crm_rule given no arguments", "crm_rule",
expected_rc=ExitStatus.USAGE),
make_test_group("crm_rule given no rule to check", "crm_rule -c",
expected_rc=ExitStatus.USAGE),
make_test_group("crm_rule given invalid input XML",
"crm_rule -c -r blahblah -X invalidxml",
expected_rc=ExitStatus.DATAERR),
make_test_group("crm_rule given invalid input XML on stdin",
"crm_rule -c -r blahblah -X -",
stdin=StdinCmd("echo invalidxml"),
expected_rc=ExitStatus.DATAERR),
]
basic_tests = [
make_test_group("Try to check a rule that doesn't exist",
"crm_rule -c -r blahblah",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Try to check a rule that has too many date_expressions",
"crm_rule -c -r cli-rule-too-many-date-expressions",
expected_rc=ExitStatus.UNIMPLEMENT_FEATURE),
make_test_group("Verify basic rule is expired",
"crm_rule -c -r cli-prefer-rule-dummy-expired",
expected_rc=ExitStatus.EXPIRED),
make_test_group("Verify basic rule worked in the past",
"crm_rule -c -r cli-prefer-rule-dummy-expired -d 20180101"),
make_test_group("Verify basic rule is not yet in effect",
"crm_rule -c -r cli-prefer-rule-dummy-not-yet",
expected_rc=ExitStatus.NOT_YET_IN_EFFECT),
make_test_group("Verify date_spec rule with years has expired",
"crm_rule -c -r cli-prefer-rule-dummy-date_spec-only-years",
expected_rc=ExitStatus.EXPIRED),
make_test_group("Verify multiple rules at once",
"crm_rule -c -r cli-prefer-rule-dummy-not-yet -r cli-prefer-rule-dummy-date_spec-only-years",
expected_rc=ExitStatus.EXPIRED),
make_test_group("Verify date_spec rule with years is in effect",
"crm_rule -c -r cli-prefer-rule-dummy-date_spec-only-years -d 20190201"),
make_test_group("Try to check a rule whose date_spec does not contain years=",
"crm_rule -c -r cli-prefer-rule-dummy-date_spec-without-years",
expected_rc=ExitStatus.UNIMPLEMENT_FEATURE),
make_test_group("Try to check a rule with no date_expression",
"crm_rule -c -r cli-no-date_expression-rule",
expected_rc=ExitStatus.UNIMPLEMENT_FEATURE),
]
return usage_tests + [
TestGroup(basic_tests, cib_gen=partial(write_cib, rule_cib))
]
class FeatureSetRegressionTest(RegressionTest):
"""A class for testing support for version-specific features."""
@property
def name(self):
"""Return the name of this regression test."""
return "feature_set"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
# Import the test CIB
Test("Import the test CIB",
f"cibadmin --replace --xml-file {cts_cli_data}/crm_mon-feature_set.xml",
update_cib=True),
Test("Complete text output, no mixed status",
"crm_mon -1 --show-detail"),
ValidatingTest("Output, no mixed status", "crm_mon --output-as=xml"),
# Modify the CIB to fake that the cluster has mixed versions
Test("Fake inconsistent feature set",
"crm_attribute --node=cluster02 --name=#feature-set --update=3.15.0 --lifetime=reboot",
update_cib=True),
Test("Complete text output, mixed status",
"crm_mon -1 --show-detail"),
ValidatingTest("Output, mixed status", "crm_mon --output-as=xml"),
]
return [
ShadowTestGroup(basic_tests),
]
# Tests that depend on resource agents and must be run in an installed
# environment
class AgentRegressionTest(RegressionTest):
"""A class for testing resource agents."""
@property
def name(self):
"""Return the name of this regression test."""
return "agents"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
return [
make_test_group("Validate a valid resource configuration",
"crm_resource --validate --class ocf --provider pacemaker --agent Dummy"),
# Make the Dummy configuration invalid (op_sleep can't be a generic string)
make_test_group("Validate an invalid resource configuration",
"crm_resource --validate --class ocf --provider pacemaker --agent Dummy",
expected_rc=ExitStatus.NOT_CONFIGURED,
env={"OCF_RESKEY_op_sleep": "asdf"}),
]
def build_options():
"""Handle command line arguments."""
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
description="Command line tool regression tests",
epilog=f"Default tests: {' '.join(default_tests)}\n"
"Other tests: agents (must be run in an installed environment)")
parser.add_argument("-j", "--jobs", metavar="JOBS", default=cpu_count() - 1, type=int,
help="The number of tests to run simultaneously")
parser.add_argument("-p", "--path", metavar="DIR", action="append",
help="Look for executables in DIR (may be specified multiple times)")
parser.add_argument("-r", "--run-only", metavar="TEST", choices=default_tests + ["tools"] + other_tests,
action="append",
help="Run only specified tests (may be specified multiple times)")
parser.add_argument("-s", "--save", action="store_true",
help="Save actual output as expected output")
parser.add_argument("-v", "--valgrind", action="store_true",
help="Run all commands under valgrind")
parser.add_argument("-V", "--verbose", action="store_true",
help="Display any differences from expected output")
args = parser.parse_args()
if args.path is None:
args.path = []
return args
def setup_environment(valgrind):
"""Set various environment variables needed for operation."""
if valgrind:
os.environ["G_SLICE"] = "always-malloc"
# Ensure all command output is in portable locale for comparison
os.environ["LC_ALL"] = "C"
# Log test errors to stderr
os.environ["PCMK_stderr"] = "1"
# Because we will change the value of PCMK_trace_functions and then reset it
# back to some initial value at various points, it's easiest to assume it is
# defined but empty by default
if "PCMK_trace_functions" not in os.environ:
os.environ["PCMK_trace_functions"] = ""
def _run_one(valgrind, r):
"""Run and return a TestGroup object."""
# See comments in run_regression_tests.
r.run(valgrind=valgrind)
return r
def run_regression_tests(regs, jobs, valgrind=False):
"""Run the given tests and return the modified objects."""
executed = []
with Pool(processes=jobs) as pool:
# What we really want to do here is:
# pool.map(lambda r: r.run(),regs)
#
# However, multiprocessing uses pickle somehow in its operation, and python
# doesn't want to pickle a lambda (nor a nested function within this one).
# Thus, we need to use the _run_one wrapper at the file level just to call
# run(). Further, if we don't return the modified object from that and then
# return the list of modified objects here, it looks like the rest of the
# program will use the originals, before this was ever run.
executed = pool.map(partial(_run_one, valgrind), regs)
return executed
def results(regs, save, verbose):
"""Print the output from each regression test, returning the number whose output differs."""
output_differs = 0
if verbose:
print("\n\nResults")
sys.stdout.flush()
for r in regs:
r.write()
if save:
dest = f"{test_home}/cli/regression.{r.name}.exp"
copyfile(r.results_file, dest)
r.diff(verbose)
if not r.identical:
output_differs += 1
return output_differs
def summary(regs, output_differs, verbose):
"""Print the summary output for the entire test run."""
test_failures = 0
test_successes = 0
for r in regs:
test_failures += r.failures
test_successes += r.successes
print("\n\nSummary")
sys.stdout.flush()
# First, print all the Passed/Failed lines from each Test run.
for r in regs:
print("\n".join(r.summary))
fmt = PluralFormatter()
# Then, print information specific to each result possibility. Basically,
# if there were failures then we print the output differences, leave the
# failed output files in place, and exit with an error. Otherwise, clean up
# anything that passed.
if test_failures > 0 and output_differs > 0:
print(fmt.format("{0} {0:plural,test} failed; see output in:",
test_failures))
for r in regs:
r.process_results(verbose)
return ExitStatus.ERROR
if test_failures > 0:
print(fmt.format("{0} {0:plural,test} failed", test_failures))
for r in regs:
r.process_results(verbose)
return ExitStatus.ERROR
if output_differs:
print(fmt.format("{0} {0:plural,test} passed but output was "
"unexpected; see output in:", test_successes))
for r in regs:
r.process_results(verbose)
return ExitStatus.DIGEST
print(fmt.format("{0} {0:plural,test} passed", test_successes))
for r in regs:
r.cleanup()
return ExitStatus.OK
regression_classes = [
AccessRenderRegressionTest,
DaemonsRegressionTest,
DatesRegressionTest,
ErrorCodeRegressionTest,
CibadminRegressionTest,
CrmAttributeRegressionTest,
CrmStandbyRegressionTest,
CrmResourceRegressionTest,
CrmTicketRegressionTest,
CrmadminRegressionTest,
CrmShadowRegressionTest,
CrmVerifyRegressionTest,
CrmSimulateRegressionTest,
CrmDiffRegressionTest,
CrmMonRegressionTest,
AclsRegressionTest,
ValidityRegressionTest,
UpgradeRegressionTest,
RulesRegressionTest,
FeatureSetRegressionTest,
AgentRegressionTest,
]
def main():
"""Run command line regression tests as specified by arguments."""
opts = build_options()
setup_environment(opts.valgrind)
set_cts_path(extra=opts.path)
# Filter the list of all regression test classes to include only those that
# were requested on the command line. If empty, this defaults to default_tests.
if not opts.run_only:
opts.run_only = default_tests
if opts.run_only == ["tools"]:
opts.run_only = tools_tests
regs = []
for cls in regression_classes:
obj = cls()
if obj.name in opts.run_only:
regs.append(obj)
regs = run_regression_tests(regs, max(1, opts.jobs), valgrind=opts.valgrind)
output_differs = results(regs, opts.save, opts.verbose)
rc = summary(regs, output_differs, opts.verbose)
sys.exit(rc)
if __name__ == "__main__":
main()
# vim: set filetype=python:
diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c
index 2a9e3f60d2..2c7151862f 100644
--- a/lib/common/iso8601.c
+++ b/lib/common/iso8601.c
@@ -1,2356 +1,2362 @@
/*
* Copyright 2005-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
/*
* References:
* https://en.wikipedia.org/wiki/ISO_8601
* http://www.staff.science.uu.nl/~gent0113/calendar/isocalendar.htm
*/
#include
#include
#include
#include
#include
#include // INT_MIN, INT_MAX
#include
#include
#include // g_strchomp()
#include
#include
#include "crmcommon_private.h"
/*
* Andrew's code was originally written for OSes whose "struct tm" contains:
* long tm_gmtoff; :: Seconds east of UTC
* const char *tm_zone; :: Timezone abbreviation
* Some OSes lack these, instead having:
* time_t (or long) timezone;
:: "difference between UTC and local standard time"
* char *tzname[2] = { "...", "..." };
* I (David Lee) confess to not understanding the details. So my attempted
* generalisations for where their use is necessary may be flawed.
*
* 1. Does "difference between ..." subtract the same or opposite way?
* 2. Should it use "altzone" instead of "timezone"?
* 3. Should it use tzname[0] or tzname[1]? Interaction with timezone/altzone?
*/
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
# define GMTOFF(tm) ((tm)->tm_gmtoff)
#else
/* Note: extern variable; macro argument not actually used. */
# define GMTOFF(tm) (-timezone+daylight)
#endif
#define HOUR_SECONDS (60 * 60)
#define DAY_SECONDS (HOUR_SECONDS * 24)
/*!
* \internal
* \brief Validate a seconds/microseconds tuple
*
* The microseconds value must be in the correct range, and if both are nonzero
* they must have the same sign.
*
* \param[in] sec Seconds
* \param[in] usec Microseconds
*
* \return true if the seconds/microseconds tuple is valid, or false otherwise
*/
#define valid_sec_usec(sec, usec) \
((QB_ABS(usec) < QB_TIME_US_IN_SEC) \
&& (((sec) == 0) || ((usec) == 0) || (((sec) < 0) == ((usec) < 0))))
// A date/time or duration
struct crm_time_s {
int years; // Calendar year (date/time) or number of years (duration)
int months; // Number of months (duration only)
int days; // Ordinal day of year (date/time) or number of days (duration)
int seconds; // Seconds of day (date/time) or number of seconds (duration)
int offset; // Seconds offset from UTC (date/time only)
bool duration; // True if duration
};
static crm_time_t *parse_date(const char *date_str);
static crm_time_t *
crm_get_utc_time(const crm_time_t *dt)
{
crm_time_t *utc = NULL;
if (dt == NULL) {
errno = EINVAL;
return NULL;
}
utc = crm_time_new_undefined();
utc->years = dt->years;
utc->days = dt->days;
utc->seconds = dt->seconds;
utc->offset = 0;
if (dt->offset) {
crm_time_add_seconds(utc, -dt->offset);
} else {
/* Durations (which are the only things that can include months, never have a timezone */
utc->months = dt->months;
}
crm_time_log(LOG_TRACE, "utc-source", dt,
crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
crm_time_log(LOG_TRACE, "utc-target", utc,
crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
return utc;
}
crm_time_t *
crm_time_new(const char *date_time)
{
tzset();
if (date_time == NULL) {
return pcmk__copy_timet(time(NULL));
}
return parse_date(date_time);
}
/*!
* \brief Allocate memory for an uninitialized time object
*
* \return Newly allocated time object
* \note The caller is responsible for freeing the return value using
* crm_time_free().
*/
crm_time_t *
crm_time_new_undefined(void)
{
return (crm_time_t *) pcmk__assert_alloc(1, sizeof(crm_time_t));
}
/*!
* \brief Check whether a time object has been initialized yet
*
* \param[in] t Time object to check
*
- * \return TRUE if time object has been initialized, FALSE otherwise
+ * \return \c true if time object has been initialized, \c false otherwise
*/
bool
crm_time_is_defined(const crm_time_t *t)
{
// Any nonzero member indicates something has been done to t
return (t != NULL) && (t->years || t->months || t->days || t->seconds
|| t->offset || t->duration);
}
void
crm_time_free(crm_time_t * dt)
{
if (dt == NULL) {
return;
}
free(dt);
}
static int
year_days(int year)
{
int d = 365;
if (crm_time_leapyear(year)) {
d++;
}
return d;
}
/* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt :
*
* 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7)
* YY = (Y-1) % 100
* C = (Y-1) - YY
* G = YY + YY/4
* Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7)
*/
int
crm_time_january1_weekday(int year)
{
int YY = (year - 1) % 100;
int C = (year - 1) - YY;
int G = YY + YY / 4;
int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7);
crm_trace("YY=%d, C=%d, G=%d", YY, C, G);
crm_trace("January 1 %.4d: %d", year, jan1);
return jan1;
}
int
crm_time_weeks_in_year(int year)
{
int weeks = 52;
int jan1 = crm_time_january1_weekday(year);
/* if jan1 == thursday */
if (jan1 == 4) {
weeks++;
} else {
jan1 = crm_time_january1_weekday(year + 1);
/* if dec31 == thursday aka. jan1 of next year is a friday */
if (jan1 == 5) {
weeks++;
}
}
return weeks;
}
// Jan-Dec plus Feb of leap years
static int month_days[13] = {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29
};
/*!
* \brief Return number of days in given month of given year
*
* \param[in] month Ordinal month (1-12)
* \param[in] year Gregorian year
*
* \return Number of days in given month (0 if given month or year is invalid)
*/
int
crm_time_days_in_month(int month, int year)
{
if ((month < 1) || (month > 12) || (year < 1)) {
return 0;
}
if ((month == 2) && crm_time_leapyear(year)) {
month = 13;
}
return month_days[month - 1];
}
bool
crm_time_leapyear(int year)
{
- gboolean is_leap = FALSE;
+ bool is_leap = false;
if (year % 4 == 0) {
- is_leap = TRUE;
+ is_leap = true;
}
if (year % 100 == 0 && year % 400 != 0) {
- is_leap = FALSE;
+ is_leap = false;
}
return is_leap;
}
/*!
* \internal
* \brief Get ordinal day number of year corresponding to given date
*
* \param[in] y Year
* \param[in] m Month (1-12)
* \param[in] d Day of month (1-31)
*
* \return Day number of year \p y corresponding to month \p m and day \p d,
* or 0 for invalid arguments
*/
static int
get_ordinal_days(uint32_t y, uint32_t m, uint32_t d)
{
int result = 0;
CRM_CHECK((y > 0) && (y <= INT_MAX) && (m >= 1) && (m <= 12)
&& (d >= 1) && (d <= 31), return 0);
result = d;
for (int lpc = 1; lpc < m; lpc++) {
result += crm_time_days_in_month(lpc, y);
}
return result;
}
void
crm_time_log_alias(int log_level, const char *file, const char *function,
int line, const char *prefix, const crm_time_t *date_time,
int flags)
{
char *date_s = crm_time_as_string(date_time, flags);
if (log_level == LOG_STDOUT) {
printf("%s%s%s\n",
(prefix? prefix : ""), (prefix? ": " : ""), date_s);
} else {
do_crm_log_alias(log_level, file, function, line, "%s%s%s",
(prefix? prefix : ""), (prefix? ": " : ""), date_s);
}
free(date_s);
}
static void
crm_time_get_sec(int sec, uint32_t *h, uint32_t *m, uint32_t *s)
{
uint32_t hours, minutes, seconds;
seconds = QB_ABS(sec);
hours = seconds / HOUR_SECONDS;
seconds -= HOUR_SECONDS * hours;
minutes = seconds / 60;
seconds -= 60 * minutes;
crm_trace("%d == %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
sec, hours, minutes, seconds);
*h = hours;
*m = minutes;
*s = seconds;
}
int
crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m,
uint32_t *s)
{
crm_time_get_sec(dt->seconds, h, m, s);
return TRUE;
}
int
crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m)
{
uint32_t s;
crm_time_get_sec(dt->seconds, h, m, &s);
return TRUE;
}
long long
crm_time_get_seconds(const crm_time_t *dt)
{
int lpc;
crm_time_t *utc = NULL;
long long in_seconds = 0;
if (dt == NULL) {
return 0;
}
// @TODO This is inefficient if dt is already in UTC
utc = crm_get_utc_time(dt);
if (utc == NULL) {
return 0;
}
// @TODO We should probably use <= if dt is a duration
for (lpc = 1; lpc < utc->years; lpc++) {
long long dmax = year_days(lpc);
in_seconds += DAY_SECONDS * dmax;
}
/* utc->months can be set only for durations. By definition, the value
* varies depending on the (unknown) start date to which the duration will
* be applied. Assume 30-day months so that something vaguely sane happens
* in this case.
*/
if (utc->months > 0) {
in_seconds += DAY_SECONDS * 30 * (long long) (utc->months);
}
if (utc->days > 0) {
in_seconds += DAY_SECONDS * (long long) (utc->days - 1);
}
in_seconds += utc->seconds;
crm_time_free(utc);
return in_seconds;
}
#define EPOCH_SECONDS 62135596800ULL /* Calculated using crm_time_get_seconds() */
long long
crm_time_get_seconds_since_epoch(const crm_time_t *dt)
{
return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS);
}
int
crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m,
uint32_t *d)
{
int months = 0;
int days = dt->days;
if(dt->years != 0) {
for (months = 1; months <= 12 && days > 0; months++) {
int mdays = crm_time_days_in_month(months, dt->years);
if (mdays >= days) {
break;
} else {
days -= mdays;
}
}
} else if (dt->months) {
/* This is a duration including months, don't convert the days field */
months = dt->months;
} else {
/* This is a duration not including months, still don't convert the days field */
}
*y = dt->years;
*m = months;
*d = days;
crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days);
return TRUE;
}
int
crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d)
{
*y = dt->years;
*d = dt->days;
return TRUE;
}
int
crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w,
uint32_t *d)
{
/*
* Monday 29 December 2008 is written "2009-W01-1"
* Sunday 3 January 2010 is written "2009-W53-7"
*/
int year_num = 0;
int jan1 = crm_time_january1_weekday(dt->years);
int h = -1;
CRM_CHECK(dt->days > 0, return FALSE);
/* 6. Find the Weekday for Y M D */
h = dt->days + jan1 - 1;
*d = 1 + ((h - 1) % 7);
/* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */
if (dt->days <= (8 - jan1) && jan1 > 4) {
crm_trace("year--, jan1=%d", jan1);
year_num = dt->years - 1;
*w = crm_time_weeks_in_year(year_num);
} else {
year_num = dt->years;
}
/* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */
if (year_num == dt->years) {
int dmax = year_days(year_num);
int correction = 4 - *d;
if ((dmax - dt->days) < correction) {
crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction);
year_num = dt->years + 1;
*w = 1;
}
}
/* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */
if (year_num == dt->years) {
int j = dt->days + (7 - *d) + (jan1 - 1);
*w = j / 7;
if (jan1 > 4) {
*w -= 1;
}
}
*y = year_num;
crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32,
dt->years, dt->days, *y, *w, *d);
return TRUE;
}
#define DATE_MAX 128
/*!
* \internal
* \brief Print "." to a buffer
*
* \param[in] sec Seconds
* \param[in] usec Microseconds (must be of same sign as \p sec and of
* absolute value less than \c QB_TIME_US_IN_SEC)
* \param[in,out] buf Result buffer
*/
static inline void
sec_usec_as_string(long long sec, int usec, GString *buf)
{
/* A negative value smaller than -1 second should have the negative sign
* before the 0, not before the usec part
*/
if ((sec == 0) && (usec < 0)) {
g_string_append_c(buf, '-');
}
g_string_append_printf(buf, "%lld.%06d", sec, QB_ABS(usec));
}
/*!
* \internal
* \brief Get a string representation of a duration
*
* \param[in] dt Time object to interpret as a duration
* \param[in] usec Microseconds to add to \p dt
* \param[in] show_usec Whether to include microseconds in \p buf
* \param[in,out] buf Result buffer
*/
static void
duration_as_string(const crm_time_t *dt, int usec, bool show_usec, GString *buf)
{
pcmk__assert(valid_sec_usec(dt->seconds, usec));
if (dt->years) {
g_string_append_printf(buf, "%4d year%s ",
dt->years, pcmk__plural_s(dt->years));
}
if (dt->months) {
g_string_append_printf(buf, "%2d month%s ",
dt->months, pcmk__plural_s(dt->months));
}
if (dt->days) {
g_string_append_printf(buf, "%2d day%s ",
dt->days, pcmk__plural_s(dt->days));
}
// At least print seconds (and optionally usecs)
if ((buf->len == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) {
if (show_usec) {
sec_usec_as_string(dt->seconds, usec, buf);
} else {
g_string_append_printf(buf, "%d", dt->seconds);
}
g_string_append_printf(buf, " second%s", pcmk__plural_s(dt->seconds));
}
// More than one minute, so provide a more readable breakdown into units
if (QB_ABS(dt->seconds) >= 60) {
uint32_t h = 0;
uint32_t m = 0;
uint32_t s = 0;
uint32_t u = QB_ABS(usec);
bool print_sec_component = false;
crm_time_get_sec(dt->seconds, &h, &m, &s);
print_sec_component = ((s != 0) || (show_usec && (u != 0)));
g_string_append(buf, " (");
if (h) {
g_string_append_printf(buf, "%" PRIu32 " hour%s",
h, pcmk__plural_s(h));
if ((m != 0) || print_sec_component) {
g_string_append_c(buf, ' ');
}
}
if (m) {
g_string_append_printf(buf, "%" PRIu32 " minute%s",
m, pcmk__plural_s(m));
if (print_sec_component) {
g_string_append_c(buf, ' ');
}
}
if (print_sec_component) {
if (show_usec) {
sec_usec_as_string(s, u, buf);
} else {
g_string_append_printf(buf, "%" PRIu32, s);
}
g_string_append_printf(buf, " second%s",
pcmk__plural_s(dt->seconds));
}
g_string_append_c(buf, ')');
}
}
/*!
* \internal
* \brief Get a string representation of a time object
*
* \param[in] dt Time to convert to string
* \param[in] usec Microseconds to add to \p dt
* \param[in] flags Group of \c crm_time_* string format options
*
* \return Newly allocated string representation of \p dt plus \p usec
*
* \note The caller is responsible for freeing the return value using \c free().
*/
static char *
time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags)
{
crm_time_t *utc = NULL;
GString *buf = NULL;
char *result = NULL;
if (!crm_time_is_defined(dt)) {
return pcmk__str_copy("");
}
pcmk__assert(valid_sec_usec(dt->seconds, usec));
buf = g_string_sized_new(128);
/* Simple cases: as duration, seconds, or seconds since epoch.
* These never depend on time zone.
*/
if (pcmk_is_set(flags, crm_time_log_duration)) {
duration_as_string(dt, usec, pcmk_is_set(flags, crm_time_usecs), buf);
goto done;
}
if (pcmk_any_flags_set(flags, crm_time_seconds|crm_time_epoch)) {
long long seconds = 0;
if (pcmk_is_set(flags, crm_time_seconds)) {
seconds = crm_time_get_seconds(dt);
} else {
seconds = crm_time_get_seconds_since_epoch(dt);
}
if (pcmk_is_set(flags, crm_time_usecs)) {
sec_usec_as_string(seconds, usec, buf);
} else {
g_string_append_printf(buf, "%lld", seconds);
}
goto done;
}
// Convert to UTC if local timezone was not requested
if ((dt->offset != 0) && !pcmk_is_set(flags, crm_time_log_with_timezone)) {
crm_trace("UTC conversion");
utc = crm_get_utc_time(dt);
dt = utc;
}
// As readable string
if (pcmk_is_set(flags, crm_time_log_date)) {
if (pcmk_is_set(flags, crm_time_weeks)) { // YYYY-WW-D
uint32_t y = 0;
uint32_t w = 0;
uint32_t d = 0;
if (crm_time_get_isoweek(dt, &y, &w, &d)) {
g_string_append_printf(buf,
"%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32,
y, w, d);
}
} else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD
uint32_t y = 0;
uint32_t d = 0;
if (crm_time_get_ordinal(dt, &y, &d)) {
g_string_append_printf(buf, "%" PRIu32 "-%.3" PRIu32, y, d);
}
} else { // YYYY-MM-DD
uint32_t y = 0;
uint32_t m = 0;
uint32_t d = 0;
if (crm_time_get_gregorian(dt, &y, &m, &d)) {
g_string_append_printf(buf,
"%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32,
y, m, d);
}
}
}
if (pcmk_is_set(flags, crm_time_log_timeofday)) {
uint32_t h = 0, m = 0, s = 0;
if (buf->len > 0) {
g_string_append_c(buf, ' ');
}
if (crm_time_get_timeofday(dt, &h, &m, &s)) {
g_string_append_printf(buf,
"%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
h, m, s);
if (pcmk_is_set(flags, crm_time_usecs)) {
g_string_append_printf(buf, ".%06" PRIu32, QB_ABS(usec));
}
}
if (pcmk_is_set(flags, crm_time_log_with_timezone)
&& (dt->offset != 0)) {
crm_time_get_sec(dt->offset, &h, &m, &s);
g_string_append_printf(buf, " %c%.2" PRIu32 ":%.2" PRIu32,
((dt->offset < 0)? '-' : '+'), h, m);
} else {
g_string_append_c(buf, 'Z');
}
}
done:
crm_time_free(utc);
result = pcmk__str_copy(buf->str);
g_string_free(buf, TRUE);
return result;
}
/*!
* \brief Get a string representation of a \p crm_time_t object
*
* \param[in] dt Time to convert to string
* \param[in] flags Group of \p crm_time_* string format options
*
* \note The caller is responsible for freeing the return value using \p free().
*/
char *
crm_time_as_string(const crm_time_t *dt, int flags)
{
return time_as_string_common(dt, 0, flags);
}
/*!
* \internal
* \brief Determine number of seconds from an hour:minute:second string
*
* \param[in] time_str Time specification string
* \param[out] result Number of seconds equivalent to time_str
*
- * \return TRUE if specification was valid, FALSE (and set errno) otherwise
+ * \return \c true if specification was valid, \c false (and set errno) otherwise
* \note This may return the number of seconds in a day (which is out of bounds
* for a time object) if given 24:00:00.
*/
static bool
crm_time_parse_sec(const char *time_str, int *result)
{
int rc;
uint32_t hour = 0;
uint32_t minute = 0;
uint32_t second = 0;
*result = 0;
// Must have at least hour, but minutes and seconds are optional
rc = sscanf(time_str, "%" SCNu32 ":%" SCNu32 ":%" SCNu32,
&hour, &minute, &second);
if (rc == 1) {
rc = sscanf(time_str, "%2" SCNu32 "%2" SCNu32 "%2" SCNu32,
&hour, &minute, &second);
}
if (rc == 0) {
crm_err("%s is not a valid ISO 8601 time specification", time_str);
errno = EINVAL;
- return FALSE;
+ return false;
}
crm_trace("Got valid time: %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
hour, minute, second);
if ((hour == 24) && (minute == 0) && (second == 0)) {
// Equivalent to 00:00:00 of next day, return number of seconds in day
} else if (hour >= 24) {
crm_err("%s is not a valid ISO 8601 time specification "
"because %" PRIu32 " is not a valid hour", time_str, hour);
errno = EINVAL;
- return FALSE;
+ return false;
}
if (minute >= 60) {
crm_err("%s is not a valid ISO 8601 time specification "
"because %" PRIu32 " is not a valid minute", time_str, minute);
errno = EINVAL;
- return FALSE;
+ return false;
}
if (second >= 60) {
crm_err("%s is not a valid ISO 8601 time specification "
"because %" PRIu32 " is not a valid second", time_str, second);
errno = EINVAL;
- return FALSE;
+ return false;
}
*result = (hour * HOUR_SECONDS) + (minute * 60) + second;
- return TRUE;
+ return true;
}
static bool
crm_time_parse_offset(const char *offset_str, int *offset)
{
tzset();
if (offset_str == NULL) {
// Use local offset
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
time_t now = time(NULL);
struct tm *now_tm = localtime(&now);
#endif
int h_offset = GMTOFF(now_tm) / HOUR_SECONDS;
int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60;
if (h_offset < 0 && m_offset < 0) {
m_offset = 0 - m_offset;
}
*offset = (HOUR_SECONDS * h_offset) + (60 * m_offset);
- return TRUE;
+ return true;
}
if (offset_str[0] == 'Z') { // @TODO invalid if anything after?
*offset = 0;
- return TRUE;
+ return true;
}
*offset = 0;
if ((offset_str[0] == '+') || (offset_str[0] == '-')
|| isdigit((int)offset_str[0])) {
- gboolean negate = FALSE;
+ bool negate = false;
if (offset_str[0] == '+') {
offset_str++;
} else if (offset_str[0] == '-') {
- negate = TRUE;
+ negate = true;
offset_str++;
}
- if (crm_time_parse_sec(offset_str, offset) == FALSE) {
- return FALSE;
+ if (!crm_time_parse_sec(offset_str, offset)) {
+ return false;
}
if (negate) {
*offset = 0 - *offset;
}
} // @TODO else invalid?
- return TRUE;
+ return true;
}
/*!
* \internal
* \brief Parse the time portion of an ISO 8601 date/time string
*
* \param[in] time_str Time portion of specification (after any 'T')
* \param[in,out] a_time Time object to parse into
*
- * \return TRUE if valid time was parsed, FALSE (and set errno) otherwise
+ * \return \c true if valid time was parsed, \c false (and set errno) otherwise
* \note This may add a day to a_time (if the time is 24:00:00).
*/
static bool
crm_time_parse(const char *time_str, crm_time_t *a_time)
{
uint32_t h, m, s;
char *offset_s = NULL;
tzset();
- if (time_str) {
- if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) {
- return FALSE;
+ if (time_str != NULL) {
+ if (!crm_time_parse_sec(time_str, &(a_time->seconds))) {
+ return false;
}
+
offset_s = strstr(time_str, "Z");
+
+ /* @COMPAT: Spaces between the time and the offset are not supported
+ * by the standard according to section 3.4.1 and 4.2.5.2.
+ */
if (offset_s == NULL) {
- offset_s = strstr(time_str, " ");
- if (offset_s) {
- while (isspace(offset_s[0])) {
- offset_s++;
- }
+ offset_s = strpbrk(time_str, " +-");
+ }
+
+ if (offset_s != NULL) {
+ while (isspace(*offset_s)) {
+ offset_s++;
}
}
}
- if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) {
- return FALSE;
+ if (!crm_time_parse_offset(offset_s, &(a_time->offset))) {
+ return false;
}
crm_time_get_sec(a_time->offset, &h, &m, &s);
crm_trace("Got tz: %c%2." PRIu32 ":%.2" PRIu32,
(a_time->offset < 0)? '-' : '+', h, m);
if (a_time->seconds == DAY_SECONDS) {
// 24:00:00 == 00:00:00 of next day
a_time->seconds = 0;
crm_time_add_days(a_time, 1);
}
- return TRUE;
+ return true;
}
/*
* \internal
* \brief Parse a time object from an ISO 8601 date/time specification
*
* \param[in] date_str ISO 8601 date/time specification (or
* \c PCMK__VALUE_EPOCH)
*
* \return New time object on success, NULL (and set errno) otherwise
*/
static crm_time_t *
parse_date(const char *date_str)
{
const char *time_s = NULL;
crm_time_t *dt = NULL;
uint32_t year = 0U;
uint32_t month = 0U;
uint32_t day = 0U;
uint32_t week = 0U;
int rc = 0;
if (pcmk__str_empty(date_str)) {
crm_err("No ISO 8601 date/time specification given");
goto invalid;
}
if ((date_str[0] == 'T')
|| ((strlen(date_str) > 2) && (date_str[2] == ':'))) {
/* Just a time supplied - Infer current date */
dt = crm_time_new(NULL);
if (date_str[0] == 'T') {
time_s = date_str + 1;
} else {
time_s = date_str;
}
goto parse_time;
}
dt = crm_time_new_undefined();
if ((strncasecmp(PCMK__VALUE_EPOCH, date_str, 5) == 0)
&& ((date_str[5] == '\0')
|| (date_str[5] == '/')
|| isspace(date_str[5]))) {
dt->days = 1;
dt->years = 1970;
crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
return dt;
}
/* YYYY-MM-DD */
rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32 "-%" SCNu32 "",
&year, &month, &day);
if (rc == 1) {
/* YYYYMMDD */
rc = sscanf(date_str, "%4" SCNu32 "%2" SCNu32 "%2" SCNu32 "",
&year, &month, &day);
}
if (rc == 3) {
if ((month < 1U) || (month > 12U)) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
"because '%" PRIu32 "' is not a valid month",
date_str, month);
goto invalid;
} else if ((year < 1U) || (year > INT_MAX)) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
"because '%" PRIu32 "' is not a valid year",
date_str, year);
goto invalid;
} else if ((day < 1) || (day > INT_MAX)
|| (day > crm_time_days_in_month(month, year))) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
"because '%" PRIu32 "' is not a valid day of the month",
date_str, day);
goto invalid;
} else {
dt->years = year;
dt->days = get_ordinal_days(year, month, day);
crm_trace("Parsed Gregorian date '%.4" PRIu32 "-%.3d' "
"from date string '%s'", year, dt->days, date_str);
}
goto parse_time;
}
/* YYYY-DDD */
rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32, &year, &day);
if (rc == 2) {
if ((year < 1U) || (year > INT_MAX)) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
"because '%" PRIu32 "' is not a valid year",
date_str, year);
goto invalid;
} else if ((day < 1U) || (day > INT_MAX) || (day > year_days(year))) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
"because '%" PRIu32 "' is not a valid day of year %"
PRIu32 " (1-%d)",
date_str, day, year, year_days(year));
goto invalid;
}
crm_trace("Parsed ordinal year %d and days %d from date string '%s'",
year, day, date_str);
dt->days = day;
dt->years = year;
goto parse_time;
}
/* YYYY-Www-D */
rc = sscanf(date_str, "%" SCNu32 "-W%" SCNu32 "-%" SCNu32,
&year, &week, &day);
if (rc == 3) {
if ((week < 1U) || (week > crm_time_weeks_in_year(year))) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
"because '%" PRIu32 "' is not a valid week of year %"
PRIu32 " (1-%d)",
date_str, week, year, crm_time_weeks_in_year(year));
goto invalid;
} else if ((day < 1U) || (day > 7U)) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
"because '%" PRIu32 "' is not a valid day of the week",
date_str, day);
goto invalid;
} else {
/*
* See https://en.wikipedia.org/wiki/ISO_week_date
*
* Monday 29 December 2008 is written "2009-W01-1"
* Sunday 3 January 2010 is written "2009-W53-7"
* Saturday 27 September 2008 is written "2008-W37-6"
*
* If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it
* is in week 1. If 1 January is on a Friday, Saturday or Sunday,
* it is in week 52 or 53 of the previous year.
*/
int jan1 = crm_time_january1_weekday(year);
crm_trace("Parsed year %" PRIu32 " (Jan 1 = %d), week %" PRIu32
", and day %" PRIu32 " from date string '%s'",
year, jan1, week, day, date_str);
dt->years = year;
crm_time_add_days(dt, (week - 1) * 7);
if (jan1 <= 4) {
crm_time_add_days(dt, 1 - jan1);
} else {
crm_time_add_days(dt, 8 - jan1);
}
crm_time_add_days(dt, day);
}
goto parse_time;
}
crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str);
goto invalid;
parse_time:
if (time_s == NULL) {
time_s = date_str + strspn(date_str, "0123456789-W");
if ((time_s[0] == ' ') || (time_s[0] == 'T')) {
++time_s;
} else {
time_s = NULL;
}
}
- if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) {
+ if ((time_s != NULL) && !crm_time_parse(time_s, dt)) {
goto invalid;
}
crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
- if (crm_time_check(dt) == FALSE) {
+ if (!crm_time_check(dt)) {
crm_err("'%s' is not a valid ISO 8601 date/time specification",
date_str);
goto invalid;
}
return dt;
invalid:
crm_time_free(dt);
errno = EINVAL;
return NULL;
}
// Parse an ISO 8601 numeric value and return number of characters consumed
static int
parse_int(const char *str, int *result)
{
unsigned int lpc;
int offset = (str[0] == 'T')? 1 : 0;
bool negate = false;
*result = 0;
// @TODO This cannot handle combinations of these characters
switch (str[offset]) {
case '.':
case ',':
return 0; // Fractions are not supported
case '-':
negate = true;
offset++;
break;
case '+':
case ':':
offset++;
break;
default:
break;
}
for (lpc = 0; (lpc < 10) && isdigit(str[offset]); lpc++) {
const int digit = str[offset++] - '0';
if ((*result * 10LL + digit) > INT_MAX) {
return 0; // Overflow
}
*result = *result * 10 + digit;
}
if (negate) {
*result = 0 - *result;
}
return (lpc > 0)? offset : 0;
}
/*!
* \brief Parse a time duration from an ISO 8601 duration specification
*
* \param[in] period_s ISO 8601 duration specification (optionally followed by
* whitespace, after which the rest of the string will be
* ignored)
*
* \return New time object on success, NULL (and set errno) otherwise
* \note It is the caller's responsibility to return the result using
* crm_time_free().
*/
crm_time_t *
crm_time_parse_duration(const char *period_s)
{
- gboolean is_time = FALSE;
+ bool is_time = false;
crm_time_t *diff = NULL;
if (pcmk__str_empty(period_s)) {
crm_err("No ISO 8601 time duration given");
goto invalid;
}
if (period_s[0] != 'P') {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because it does not start with a 'P'", period_s);
goto invalid;
}
if ((period_s[1] == '\0') || isspace(period_s[1])) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because nothing follows 'P'", period_s);
goto invalid;
}
diff = crm_time_new_undefined();
for (const char *current = period_s + 1;
current[0] && (current[0] != '/') && !isspace(current[0]);
++current) {
int an_int = 0, rc;
long long result = 0LL;
if (current[0] == 'T') {
/* A 'T' separates year/month/day from hour/minute/seconds. We don't
* require it strictly, but just use it to differentiate month from
* minutes.
*/
- is_time = TRUE;
+ is_time = true;
continue;
}
// An integer must be next
rc = parse_int(current, &an_int);
if (rc == 0) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because no valid integer at '%s'", period_s, current);
goto invalid;
}
current += rc;
// A time unit must be next (we're not strict about the order)
switch (current[0]) {
case 'Y':
diff->years = an_int;
break;
case 'M':
if (!is_time) { // Months
diff->months = an_int;
} else { // Minutes
result = diff->seconds + an_int * 60LL;
if ((result < INT_MIN) || (result > INT_MAX)) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too %s",
period_s, current - rc,
((result > 0)? "large" : "small"));
goto invalid;
} else {
diff->seconds = (int) result;
}
}
break;
case 'W':
result = diff->days + an_int * 7LL;
if ((result < INT_MIN) || (result > INT_MAX)) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too %s",
period_s, current - rc,
((result > 0)? "large" : "small"));
goto invalid;
} else {
diff->days = (int) result;
}
break;
case 'D':
result = diff->days + (long long) an_int;
if ((result < INT_MIN) || (result > INT_MAX)) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too %s",
period_s, current - rc,
((result > 0)? "large" : "small"));
goto invalid;
} else {
diff->days = (int) result;
}
break;
case 'H':
result = diff->seconds + (long long) an_int * HOUR_SECONDS;
if ((result < INT_MIN) || (result > INT_MAX)) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too %s",
period_s, current - rc,
((result > 0)? "large" : "small"));
goto invalid;
} else {
diff->seconds = (int) result;
}
break;
case 'S':
result = diff->seconds + (long long) an_int;
if ((result < INT_MIN) || (result > INT_MAX)) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too %s",
period_s, current - rc,
((result > 0)? "large" : "small"));
goto invalid;
} else {
diff->seconds = (int) result;
}
break;
case '\0':
crm_err("'%s' is not a valid ISO 8601 time duration "
"because no units after %d", period_s, an_int);
goto invalid;
default:
crm_err("'%s' is not a valid ISO 8601 time duration "
"because '%c' is not a valid time unit",
period_s, current[0]);
goto invalid;
}
}
if (!crm_time_is_defined(diff)) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because no amounts and units given", period_s);
goto invalid;
}
diff->duration = TRUE;
return diff;
invalid:
crm_time_free(diff);
errno = EINVAL;
return NULL;
}
/*!
* \brief Parse a time period from an ISO 8601 interval specification
*
* \param[in] period_str ISO 8601 interval specification (start/end,
* start/duration, or duration/end)
*
* \return New time period object on success, NULL (and set errno) otherwise
* \note The caller is responsible for freeing the result using
* crm_time_free_period().
*/
crm_time_period_t *
crm_time_parse_period(const char *period_str)
{
const char *original = period_str;
crm_time_period_t *period = NULL;
if (pcmk__str_empty(period_str)) {
crm_err("No ISO 8601 time period given");
goto invalid;
}
tzset();
period = pcmk__assert_alloc(1, sizeof(crm_time_period_t));
if (period_str[0] == 'P') {
period->diff = crm_time_parse_duration(period_str);
if (period->diff == NULL) {
goto error;
}
} else {
period->start = parse_date(period_str);
if (period->start == NULL) {
goto error;
}
}
period_str = strstr(original, "/");
if (period_str) {
++period_str;
if (period_str[0] == 'P') {
if (period->diff != NULL) {
crm_err("'%s' is not a valid ISO 8601 time period "
"because it has two durations",
original);
goto invalid;
}
period->diff = crm_time_parse_duration(period_str);
if (period->diff == NULL) {
goto error;
}
} else {
period->end = parse_date(period_str);
if (period->end == NULL) {
goto error;
}
}
} else if (period->diff != NULL) {
// Only duration given, assume start is now
period->start = crm_time_new(NULL);
} else {
// Only start given
crm_err("'%s' is not a valid ISO 8601 time period "
"because it has no duration or ending time",
original);
goto invalid;
}
if (period->start == NULL) {
period->start = crm_time_subtract(period->end, period->diff);
} else if (period->end == NULL) {
period->end = crm_time_add(period->start, period->diff);
}
- if (crm_time_check(period->start) == FALSE) {
+ if (!crm_time_check(period->start)) {
crm_err("'%s' is not a valid ISO 8601 time period "
"because the start is invalid", period_str);
goto invalid;
}
- if (crm_time_check(period->end) == FALSE) {
+ if (!crm_time_check(period->end)) {
crm_err("'%s' is not a valid ISO 8601 time period "
"because the end is invalid", period_str);
goto invalid;
}
return period;
invalid:
errno = EINVAL;
error:
crm_time_free_period(period);
return NULL;
}
/*!
* \brief Free a dynamically allocated time period object
*
* \param[in,out] period Time period to free
*/
void
crm_time_free_period(crm_time_period_t *period)
{
if (period) {
crm_time_free(period->start);
crm_time_free(period->end);
crm_time_free(period->diff);
free(period);
}
}
void
crm_time_set(crm_time_t *target, const crm_time_t *source)
{
crm_trace("target=%p, source=%p", target, source);
CRM_CHECK(target != NULL && source != NULL, return);
target->years = source->years;
target->days = source->days;
target->months = source->months; /* Only for durations */
target->seconds = source->seconds;
target->offset = source->offset;
crm_time_log(LOG_TRACE, "source", source,
crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
crm_time_log(LOG_TRACE, "target", target,
crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
}
static void
ha_set_tm_time(crm_time_t *target, const struct tm *source)
{
int h_offset = 0;
int m_offset = 0;
/* Ensure target is fully initialized */
target->years = 0;
target->months = 0;
target->days = 0;
target->seconds = 0;
target->offset = 0;
target->duration = FALSE;
if (source->tm_year > 0) {
/* years since 1900 */
target->years = 1900;
crm_time_add_years(target, source->tm_year);
}
if (source->tm_yday >= 0) {
/* days since January 1 [0-365] */
target->days = 1 + source->tm_yday;
}
if (source->tm_hour >= 0) {
target->seconds += HOUR_SECONDS * source->tm_hour;
}
if (source->tm_min >= 0) {
target->seconds += 60 * source->tm_min;
}
if (source->tm_sec >= 0) {
target->seconds += source->tm_sec;
}
/* tm_gmtoff == offset from UTC in seconds */
h_offset = GMTOFF(source) / HOUR_SECONDS;
m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60;
crm_trace("Time offset is %lds (%.2d:%.2d)",
GMTOFF(source), h_offset, m_offset);
target->offset += HOUR_SECONDS * h_offset;
target->offset += 60 * m_offset;
}
void
crm_time_set_timet(crm_time_t *target, const time_t *source)
{
ha_set_tm_time(target, localtime(source));
}
/*!
* \internal
* \brief Set one time object to another if the other is earlier
*
* \param[in,out] target Time object to set
* \param[in] source Time object to use if earlier
*/
void
pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source)
{
if ((target != NULL) && (source != NULL)
&& (!crm_time_is_defined(target)
|| (crm_time_compare(source, target) < 0))) {
crm_time_set(target, source);
}
}
crm_time_t *
pcmk_copy_time(const crm_time_t *source)
{
crm_time_t *target = crm_time_new_undefined();
crm_time_set(target, source);
return target;
}
/*!
* \internal
* \brief Convert a \p time_t time to a \p crm_time_t time
*
* \param[in] source Time to convert
*
* \return A \p crm_time_t object representing \p source
*/
crm_time_t *
pcmk__copy_timet(time_t source)
{
crm_time_t *target = crm_time_new_undefined();
crm_time_set_timet(target, &source);
return target;
}
crm_time_t *
crm_time_add(const crm_time_t *dt, const crm_time_t *value)
{
crm_time_t *utc = NULL;
crm_time_t *answer = NULL;
if ((dt == NULL) || (value == NULL)) {
errno = EINVAL;
return NULL;
}
answer = pcmk_copy_time(dt);
utc = crm_get_utc_time(value);
if (utc == NULL) {
crm_time_free(answer);
return NULL;
}
crm_time_add_years(answer, utc->years);
crm_time_add_months(answer, utc->months);
crm_time_add_days(answer, utc->days);
crm_time_add_seconds(answer, utc->seconds);
crm_time_free(utc);
return answer;
}
/*!
* \internal
* \brief Return the XML attribute name corresponding to a time component
*
* \param[in] component Component to check
*
* \return XML attribute name corresponding to \p component, or NULL if
* \p component is invalid
*/
const char *
pcmk__time_component_attr(enum pcmk__time_component component)
{
switch (component) {
case pcmk__time_years:
return PCMK_XA_YEARS;
case pcmk__time_months:
return PCMK_XA_MONTHS;
case pcmk__time_weeks:
return PCMK_XA_WEEKS;
case pcmk__time_days:
return PCMK_XA_DAYS;
case pcmk__time_hours:
return PCMK_XA_HOURS;
case pcmk__time_minutes:
return PCMK_XA_MINUTES;
case pcmk__time_seconds:
return PCMK_XA_SECONDS;
default:
return NULL;
}
}
typedef void (*component_fn_t)(crm_time_t *, int);
/*!
* \internal
* \brief Get the addition function corresponding to a time component
* \param[in] component Component to check
*
* \return Addition function corresponding to \p component, or NULL if
* \p component is invalid
*/
static component_fn_t
component_fn(enum pcmk__time_component component)
{
switch (component) {
case pcmk__time_years:
return crm_time_add_years;
case pcmk__time_months:
return crm_time_add_months;
case pcmk__time_weeks:
return crm_time_add_weeks;
case pcmk__time_days:
return crm_time_add_days;
case pcmk__time_hours:
return crm_time_add_hours;
case pcmk__time_minutes:
return crm_time_add_minutes;
case pcmk__time_seconds:
return crm_time_add_seconds;
default:
return NULL;
}
}
/*!
* \internal
* \brief Add the value of an XML attribute to a time object
*
* \param[in,out] t Time object to add to
* \param[in] component Component of \p t to add to
* \param[in] xml XML with value to add
*
* \return Standard Pacemaker return code
*/
int
pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component,
const xmlNode *xml)
{
long long value;
const char *attr = pcmk__time_component_attr(component);
component_fn_t add = component_fn(component);
if ((t == NULL) || (attr == NULL) || (add == NULL)) {
return EINVAL;
}
if (xml == NULL) {
return pcmk_rc_ok;
}
if (pcmk__scan_ll(pcmk__xe_get(xml, attr), &value, 0LL) != pcmk_rc_ok) {
return pcmk_rc_unpack_error;
}
if ((value < INT_MIN) || (value > INT_MAX)) {
return ERANGE;
}
if (value != 0LL) {
add(t, (int) value);
}
return pcmk_rc_ok;
}
crm_time_t *
crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value)
{
crm_time_t *utc = NULL;
crm_time_t *answer = NULL;
if ((dt == NULL) || (value == NULL)) {
errno = EINVAL;
return NULL;
}
utc = crm_get_utc_time(value);
if (utc == NULL) {
return NULL;
}
answer = crm_get_utc_time(dt);
if (answer == NULL) {
crm_time_free(utc);
return NULL;
}
answer->duration = TRUE;
crm_time_add_years(answer, -utc->years);
if(utc->months != 0) {
crm_time_add_months(answer, -utc->months);
}
crm_time_add_days(answer, -utc->days);
crm_time_add_seconds(answer, -utc->seconds);
crm_time_free(utc);
return answer;
}
crm_time_t *
crm_time_subtract(const crm_time_t *dt, const crm_time_t *value)
{
crm_time_t *utc = NULL;
crm_time_t *answer = NULL;
if ((dt == NULL) || (value == NULL)) {
errno = EINVAL;
return NULL;
}
utc = crm_get_utc_time(value);
if (utc == NULL) {
return NULL;
}
answer = pcmk_copy_time(dt);
crm_time_add_years(answer, -utc->years);
if(utc->months != 0) {
crm_time_add_months(answer, -utc->months);
}
crm_time_add_days(answer, -utc->days);
crm_time_add_seconds(answer, -utc->seconds);
crm_time_free(utc);
return answer;
}
/*!
* \brief Check whether a time object represents a sensible date/time
*
* \param[in] dt Date/time object to check
*
* \return \c true if years, days, and seconds are sensible, \c false otherwise
*/
bool
crm_time_check(const crm_time_t *dt)
{
return (dt != NULL)
&& (dt->days > 0) && (dt->days <= year_days(dt->years))
&& (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS);
}
#define do_cmp_field(l, r, field) \
if(rc == 0) { \
if(l->field > r->field) { \
crm_trace("%s: %d > %d", \
#field, l->field, r->field); \
rc = 1; \
} else if(l->field < r->field) { \
crm_trace("%s: %d < %d", \
#field, l->field, r->field); \
rc = -1; \
} \
}
int
crm_time_compare(const crm_time_t *a, const crm_time_t *b)
{
int rc = 0;
crm_time_t *t1 = crm_get_utc_time(a);
crm_time_t *t2 = crm_get_utc_time(b);
if ((t1 == NULL) && (t2 == NULL)) {
rc = 0;
} else if (t1 == NULL) {
rc = -1;
} else if (t2 == NULL) {
rc = 1;
} else {
do_cmp_field(t1, t2, years);
do_cmp_field(t1, t2, days);
do_cmp_field(t1, t2, seconds);
}
crm_time_free(t1);
crm_time_free(t2);
return rc;
}
/*!
* \brief Add a given number of seconds to a date/time or duration
*
* \param[in,out] a_time Date/time or duration to add seconds to
* \param[in] extra Number of seconds to add
*/
void
crm_time_add_seconds(crm_time_t *a_time, int extra)
{
int days = extra / DAY_SECONDS;
pcmk__assert(a_time != NULL);
crm_trace("Adding %d seconds (including %d whole day%s) to %d",
extra, days, pcmk__plural_s(days), a_time->seconds);
a_time->seconds += extra % DAY_SECONDS;
// Check whether the addition crossed a day boundary
if (a_time->seconds > DAY_SECONDS) {
++days;
a_time->seconds -= DAY_SECONDS;
} else if (a_time->seconds < 0) {
--days;
a_time->seconds += DAY_SECONDS;
}
crm_time_add_days(a_time, days);
}
#define ydays(t) (crm_time_leapyear((t)->years)? 366 : 365)
/*!
* \brief Add days to a date/time
*
* \param[in,out] a_time Time to modify
* \param[in] extra Number of days to add (may be negative to subtract)
*/
void
crm_time_add_days(crm_time_t *a_time, int extra)
{
pcmk__assert(a_time != NULL);
crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days);
if (extra > 0) {
while ((a_time->days + (long long) extra) > ydays(a_time)) {
if ((a_time->years + 1LL) > INT_MAX) {
a_time->days = ydays(a_time); // Clip to latest we can handle
return;
}
extra -= ydays(a_time);
a_time->years++;
}
} else if (extra < 0) {
const int min_days = a_time->duration? 0 : 1;
while ((a_time->days + (long long) extra) < min_days) {
if ((a_time->years - 1) < 1) {
a_time->days = 1; // Clip to earliest we can handle (no BCE)
return;
}
a_time->years--;
extra += ydays(a_time);
}
}
a_time->days += extra;
}
void
crm_time_add_months(crm_time_t * a_time, int extra)
{
int lpc;
uint32_t y, m, d, dmax;
crm_time_get_gregorian(a_time, &y, &m, &d);
crm_trace("Adding %d months to %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32,
extra, y, m, d);
if (extra > 0) {
for (lpc = extra; lpc > 0; lpc--) {
m++;
if (m == 13) {
m = 1;
y++;
}
}
} else {
for (lpc = -extra; lpc > 0; lpc--) {
m--;
if (m == 0) {
m = 12;
y--;
}
}
}
dmax = crm_time_days_in_month(m, y);
if (dmax < d) {
/* Preserve day-of-month unless the month doesn't have enough days */
d = dmax;
}
crm_trace("Calculated %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d);
a_time->years = y;
a_time->days = get_ordinal_days(y, m, d);
crm_time_get_gregorian(a_time, &y, &m, &d);
crm_trace("Got %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d);
}
void
crm_time_add_minutes(crm_time_t * a_time, int extra)
{
crm_time_add_seconds(a_time, extra * 60);
}
void
crm_time_add_hours(crm_time_t * a_time, int extra)
{
crm_time_add_seconds(a_time, extra * HOUR_SECONDS);
}
void
crm_time_add_weeks(crm_time_t * a_time, int extra)
{
crm_time_add_days(a_time, extra * 7);
}
void
crm_time_add_years(crm_time_t * a_time, int extra)
{
pcmk__assert(a_time != NULL);
if ((extra > 0) && ((a_time->years + (long long) extra) > INT_MAX)) {
a_time->years = INT_MAX;
} else if ((extra < 0) && ((a_time->years + (long long) extra) < 1)) {
a_time->years = 1; // Clip to earliest we can handle (no BCE)
} else {
a_time->years += extra;
}
}
/* The high-resolution variant of time object was added to meet an immediate
* need, and is kept internal API.
*
* @TODO The long-term goal is to come up with a clean, unified design for a
* time type (or types) that meets all the various needs, to replace
* crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t).
*/
static pcmk__time_hr_t *
time_to_hr(const crm_time_t *dt)
{
pcmk__time_hr_t *hr_dt = NULL;
pcmk__assert(dt != NULL);
hr_dt = pcmk__assert_alloc(1, sizeof(pcmk__time_hr_t));
hr_dt->years = dt->years;
hr_dt->months = dt->months;
hr_dt->days = dt->days;
hr_dt->seconds = dt->seconds;
hr_dt->offset = dt->offset;
hr_dt->duration = dt->duration;
return hr_dt;
}
/*!
* \internal
* \brief Return the current time as a high-resolution time
*
* \param[out] epoch If not NULL, this will be set to seconds since epoch
*
* \return Newly allocated high-resolution time set to the current time
*/
pcmk__time_hr_t *
pcmk__time_hr_now(time_t *epoch)
{
struct timespec tv;
crm_time_t dt;
pcmk__time_hr_t *hr;
qb_util_timespec_from_epoch_get(&tv);
if (epoch != NULL) {
*epoch = tv.tv_sec;
}
crm_time_set_timet(&dt, &(tv.tv_sec));
hr = time_to_hr(&dt);
hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC;
return hr;
}
pcmk__time_hr_t *
pcmk__time_hr_new(const char *date_time)
{
pcmk__time_hr_t *hr_dt = NULL;
if (date_time == NULL) {
hr_dt = pcmk__time_hr_now(NULL);
} else {
crm_time_t *dt = parse_date(date_time);
hr_dt = time_to_hr(dt);
crm_time_free(dt);
}
return hr_dt;
}
void
pcmk__time_hr_free(pcmk__time_hr_t * hr_dt)
{
free(hr_dt);
}
static void
ha_get_tm_time(struct tm *target, const pcmk__time_hr_t *source)
{
*target = (struct tm) {
.tm_year = source->years - 1900,
/* source->days is day of year, but we assign it to tm_mday instead of
* tm_yday. mktime() fixes it. See the mktime(3) man page for details.
*/
.tm_mday = source->days,
// mktime() converts this to hours/minutes/seconds appropriately
.tm_sec = source->seconds,
// Don't adjust DST here; let mktime() try to determine DST status
.tm_isdst = -1,
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
.tm_gmtoff = source->offset
#endif
};
mktime(target);
}
/*!
* \internal
* \brief Convert a struct tm to a \c GDateTime
*
* \param[in] tm Time object to convert
* \param[in] offset Offset from UTC (in seconds)
*
* \return Newly allocated \c GDateTime object corresponding to \p tm, or
* \c NULL on error
*
* \note The caller is responsible for freeing the return value using
* \c g_date_time_unref().
*/
static GDateTime *
get_g_date_time(const struct tm *tm, int offset)
{
// Accept an offset argument in case tm lacks a tm_gmtoff member
char buf[sizeof("+hh:mm")] = { '\0', };
const char *offset_s = NULL;
GTimeZone *tz = NULL;
GDateTime *dt = NULL;
if (QB_ABS(offset) <= DAY_SECONDS) {
uint32_t hours = 0;
uint32_t minutes = 0;
uint32_t seconds = 0;
int rc = 0;
crm_time_get_sec(offset, &hours, &minutes, &seconds);
rc = snprintf(buf, sizeof(buf), "%c%02" PRIu32 ":%02" PRIu32,
((offset >= 0)? '+' : '-'), hours, minutes);
pcmk__assert(rc == (sizeof(buf) - 1));
offset_s = buf;
} else {
// offset out of range; use NULL as offset_s
CRM_LOG_ASSERT(QB_ABS(offset) <= DAY_SECONDS);
}
/* @FIXME @COMPAT As of glib 2.68, g_time_zone_new() is deprecated in favor
* of g_time_zone_new_identifier(). However, calling
* g_time_zone_new_identifier() results in compiler warnings, even on a
* system with glib 2.84 installed. It is unclear why.
*
* The *_new_identifier() function was added (and the *_new() function
* deprecated) in version 2.68. They have the same signature. Ideally, we
* would choose which function to call here and below based the installed
* glib version using a CPP guard.
*/
tz = g_time_zone_new(offset_s);
dt = g_date_time_new(tz, tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec);
g_time_zone_unref(tz);
return dt;
}
/*!
* \internal
* \brief Expand a date/time format string, with support for fractional seconds
*
* \param[in] format Date/time format string compatible with
* \c g_date_time_format(), with additional support for
* \c "%N" for fractional seconds
* \param[in] hr_dt Time value to format
*
* \return Newly allocated string with formatted string, or \c NULL on error
*
* \note This function falls back to trying \c strftime() with a fixed-size
* buffer if \c g_date_time_format() fails. This fallback will be removed
* in a future release.
*/
char *
pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt)
{
int scanned_pos = 0; // How many characters of format have been parsed
int printed_pos = 0; // How many characters of format have been processed
GString *buf = NULL;
char *result = NULL;
struct tm tm = { 0, };
GDateTime *gdt = NULL;
if (format == NULL) {
return NULL;
}
buf = g_string_sized_new(128);
ha_get_tm_time(&tm, hr_dt);
gdt = get_g_date_time(&tm, hr_dt->offset);
if (gdt == NULL) {
goto done;
}
while (format[scanned_pos] != '\0') {
int fmt_pos = 0; // Index after last character to pass as-is
int frac_digits = 0; // %N specifier's width field value (if any)
gchar *tmp_fmt_s = NULL;
gchar *date_s = NULL;
// Look for next format specifier
const char *mark_s = strchr(&format[scanned_pos], '%');
if (mark_s == NULL) {
// No more specifiers, so pass remaining string to strftime() as-is
scanned_pos = strlen(format);
fmt_pos = scanned_pos;
} else {
fmt_pos = mark_s - format; // Index of %
// Skip % and any width field
scanned_pos = fmt_pos + 1;
while (isdigit(format[scanned_pos])) {
scanned_pos++;
}
switch (format[scanned_pos]) {
case '\0': // Literal % and possibly digits at end of string
fmt_pos = scanned_pos; // Pass remaining string as-is
break;
case 'N': // %[width]N
/* Fractional seconds. This was supposed to represent
* nanoseconds. However, we only store times at microsecond
* resolution, and the width field support makes this a
* general fractional component specifier rather than a
* nanoseconds specifier.
*
* Further, since we cap the width at 6 digits, a user
* cannot display times at greater than microsecond
* resolution.
*
* A leading zero in the width field is ignored, not treated
* as "use zero-padding." For example, "%03N" and "%3N"
* produce the same result.
*/
scanned_pos++;
// Parse width field
frac_digits = atoi(&format[fmt_pos + 1]);
frac_digits = QB_MAX(frac_digits, 0);
frac_digits = QB_MIN(frac_digits, 6);
break;
default: // Some other specifier
if (format[++scanned_pos] != '\0') { // More to parse
continue;
}
fmt_pos = scanned_pos; // Pass remaining string as-is
break;
}
}
tmp_fmt_s = g_strndup(&format[printed_pos], fmt_pos - printed_pos);
date_s = g_date_time_format(gdt, tmp_fmt_s);
if (date_s == NULL) {
char compat_date_s[1024] = { '\0' };
size_t nbytes = 0;
// @COMPAT Drop this fallback
crm_warn("Could not format time using format string '%s' with "
"g_date_time_format(); trying strftime(). In a future "
"release, use of strftime() as a fallback will be removed",
format);
#ifdef HAVE_FORMAT_NONLITERAL
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif // HAVE_FORMAT_NONLITERAL
nbytes = strftime(compat_date_s, sizeof(compat_date_s), tmp_fmt_s,
&tm);
#ifdef HAVE_FORMAT_NONLITERAL
#pragma GCC diagnostic pop
#endif // HAVE_FORMAT_NONLITERAL
if (nbytes == 0) {
// Truncation, empty string, or error; impossible to discern
crm_err("Could not format time using format string '%s'",
format);
// Ensure we return NULL
g_string_truncate(buf, 0);
g_free(tmp_fmt_s);
goto done;
}
date_s = g_strdup(compat_date_s);
}
g_string_append(buf, date_s);
g_free(date_s);
g_free(tmp_fmt_s);
printed_pos = scanned_pos;
if (frac_digits != 0) {
// Descending powers of 10 (10^5 down to 10^0)
static const int powers[6] = { 1e5, 1e4, 1e3, 1e2, 1e1, 1e0 };
// Sanity check to ensure array access is in bounds
pcmk__assert((frac_digits > 0) && (frac_digits <= 6));
/* Append fractional seconds at the requested resolution, truncated
* toward zero. We're basically converting from microseconds to
* another unit here. For example, suppose the width field
* (frac_digits) is 3. This means "use millisecond resolution." Then
* we need to divide our microseconds value by 10^3, which is
* powers[3 - 1].
*
* If the width field is 6 (microsecond resolution), then we divide
* our microseconds value by 10^0 == 1, which is powers[6 - 1].
*/
g_string_append_printf(buf, "%0*d", frac_digits,
hr_dt->useconds / powers[frac_digits - 1]);
}
}
done:
if (buf->len > 0) {
result = pcmk__str_copy(buf->str);
}
g_string_free(buf, TRUE);
if (gdt != NULL) {
g_date_time_unref(gdt);
}
return result;
}
/*!
* \internal
* \brief Return a human-friendly string corresponding to an epoch time value
*
* \param[in] source Pointer to epoch time value (or \p NULL for current time)
* \param[in] flags Group of \p crm_time_* flags controlling display format
* (0 to use \p ctime() with newline removed)
*
* \return String representation of \p source on success (may be empty depending
* on \p flags; guaranteed not to be \p NULL)
*
* \note The caller is responsible for freeing the return value using \p free().
*/
char *
pcmk__epoch2str(const time_t *source, uint32_t flags)
{
time_t epoch_time = (source == NULL)? time(NULL) : *source;
if (flags == 0) {
return pcmk__str_copy(g_strchomp(ctime(&epoch_time)));
} else {
crm_time_t dt;
crm_time_set_timet(&dt, &epoch_time);
return crm_time_as_string(&dt, flags);
}
}
/*!
* \internal
* \brief Return a human-friendly string corresponding to seconds-and-
* nanoseconds value
*
* Time is shown with microsecond resolution if \p crm_time_usecs is in \p
* flags.
*
* \param[in] ts Time in seconds and nanoseconds (or \p NULL for current
* time)
* \param[in] flags Group of \p crm_time_* flags controlling display format
*
* \return String representation of \p ts on success (may be empty depending on
* \p flags; guaranteed not to be \p NULL)
*
* \note The caller is responsible for freeing the return value using \p free().
*/
char *
pcmk__timespec2str(const struct timespec *ts, uint32_t flags)
{
struct timespec tmp_ts;
crm_time_t dt;
if (ts == NULL) {
qb_util_timespec_from_epoch_get(&tmp_ts);
ts = &tmp_ts;
}
crm_time_set_timet(&dt, &ts->tv_sec);
return time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags);
}
/*!
* \internal
* \brief Given a millisecond interval, return a log-friendly string
*
* \param[in] interval_ms Interval in milliseconds
*
* \return Readable version of \p interval_ms
*
* \note The return value is a pointer to static memory that may be overwritten
* by later calls to this function.
*/
const char *
pcmk__readable_interval(guint interval_ms)
{
#define MS_IN_S (1000)
#define MS_IN_M (MS_IN_S * 60)
#define MS_IN_H (MS_IN_M * 60)
#define MS_IN_D (MS_IN_H * 24)
#define MAXSTR sizeof("..d..h..m..s...ms")
static char str[MAXSTR];
GString *buf = NULL;
if (interval_ms == 0) {
return "0s";
}
buf = g_string_sized_new(128);
if (interval_ms >= MS_IN_D) {
g_string_append_printf(buf, "%ud", interval_ms / MS_IN_D);
interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D;
}
if (interval_ms >= MS_IN_H) {
g_string_append_printf(buf, "%uh", interval_ms / MS_IN_H);
interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H;
}
if (interval_ms >= MS_IN_M) {
g_string_append_printf(buf, "%um", interval_ms / MS_IN_M);
interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M;
}
// Ns, N.NNNs, or NNNms
if (interval_ms >= MS_IN_S) {
g_string_append_printf(buf, "%u", interval_ms / MS_IN_S);
interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S;
if (interval_ms > 0) {
g_string_append_printf(buf, ".%03u", interval_ms);
}
g_string_append_c(buf, 's');
} else if (interval_ms > 0) {
g_string_append_printf(buf, "%ums", interval_ms);
}
pcmk__assert(buf->len < sizeof(str));
strncpy(str, buf->str, sizeof(str) - 1);
g_string_free(buf, TRUE);
return str;
}
diff --git a/tools/iso8601.c b/tools/iso8601.c
index 7bb9578b74..21e1fb5529 100644
--- a/tools/iso8601.c
+++ b/tools/iso8601.c
@@ -1,460 +1,460 @@
/*
* Copyright 2005-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#define SUMMARY "Display and parse ISO 8601 dates and times"
static pcmk__supported_format_t formats[] = {
PCMK__SUPPORTED_FORMAT_NONE,
PCMK__SUPPORTED_FORMAT_TEXT,
PCMK__SUPPORTED_FORMAT_XML,
{ NULL, NULL, NULL }
};
struct {
char *date_time_s;
gchar *duration_s;
gchar *expected_s;
gchar *period_s;
int print_options;
} options;
#define INDENT " "
static gboolean
date_now_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
if (pcmk__str_any_of(option_name, "--now", "-n", NULL)) {
pcmk__str_update(&options.date_time_s, "now");
} else if (pcmk__str_any_of(option_name, "--date", "-d", NULL)) {
pcmk__str_update(&options.date_time_s, optarg);
}
return TRUE;
}
static gboolean
modifier_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
if (pcmk__str_any_of(option_name, "--seconds", "-s", NULL)) {
options.print_options |= crm_time_seconds;
} else if (pcmk__str_any_of(option_name, "--epoch", "-S", NULL)) {
options.print_options |= crm_time_epoch;
} else if (pcmk__str_any_of(option_name, "--local", "-L", NULL)) {
options.print_options |= crm_time_log_with_timezone;
} else if (pcmk__str_any_of(option_name, "--ordinal", "-O", NULL)) {
options.print_options |= crm_time_ordinal;
} else if (pcmk__str_any_of(option_name, "--week", "-W", NULL)) {
options.print_options |= crm_time_weeks;
}
return TRUE;
}
static GOptionEntry command_entries[] = {
{ "now", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, date_now_cb,
"Display the current date/time",
NULL },
{ "date", 'd', 0, G_OPTION_ARG_CALLBACK, date_now_cb,
"Parse an ISO 8601 date/time (for example,\n"
- INDENT "'2019-09-24 00:30:00 +01:00' or '2019-040')",
+ INDENT "'2019-09-24 00:30:00+01:00' or '2019-040')",
"DATE" },
{ "period", 'p', 0, G_OPTION_ARG_STRING, &options.period_s,
"Parse an ISO 8601 period (interval) with start time (for example,\n"
INDENT "'2005-040/2005-043')",
"PERIOD" },
{ "duration", 'D', 0, G_OPTION_ARG_STRING, &options.duration_s,
"Parse an ISO 8601 duration (for example, 'P1M')",
"DURATION" },
{ "expected", 'E', 0, G_OPTION_ARG_STRING, &options.expected_s,
"Exit with error status if result does not match this text.\n"
INDENT "Requires: -n or -d",
"TEXT" },
{ NULL }
};
static GOptionEntry modifier_entries[] = {
{ "seconds", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
"Show result as a seconds since 0000-001 00:00:00Z",
NULL },
{ "epoch", 'S', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
"Show result as a seconds since EPOCH (1970-001 00:00:00Z)",
NULL },
{ "local", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
"Show result as a 'local' date/time",
NULL },
{ "ordinal", 'O', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
"Show result as an 'ordinal' date/time",
NULL },
{ "week", 'W', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, modifier_cb,
"Show result as an 'calendar week' date/time",
NULL },
{ NULL }
};
PCMK__OUTPUT_ARGS("date", "const char *", "crm_time_t *", "int")
static int
date_default(pcmk__output_t *out, va_list args)
{
const char *prefix = va_arg(args, const char *);
crm_time_t *date = va_arg(args, crm_time_t *);
int opts = va_arg(args, int);
char *date_s = NULL;
opts |= crm_time_log_date | crm_time_log_timeofday;
date_s = crm_time_as_string(date, opts);
out->info(out, "%s: %s", prefix, date_s);
free(date_s);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("date", "const char *", "crm_time_t *", "int")
static int
date_xml(pcmk__output_t *out, va_list args)
{
const char *prefix G_GNUC_UNUSED = va_arg(args, const char *);
crm_time_t *date = va_arg(args, crm_time_t *);
int opts = va_arg(args, int);
char *date_s = NULL;
opts |= crm_time_log_date | crm_time_log_timeofday;
date_s = crm_time_as_string(date, opts);
pcmk__output_create_xml_text_node(out, PCMK_XE_DATE, date_s);
free(date_s);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("duration", "crm_time_t *", "int")
static int
duration_default(pcmk__output_t *out, va_list args)
{
crm_time_t *time = va_arg(args, crm_time_t *);
int opts = va_arg(args, int);
char *date_s = crm_time_as_string(time, opts | crm_time_log_duration);
out->info(out, "Duration: %s", date_s);
free(date_s);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("duration", "crm_time_t *", "int")
static int
duration_xml(pcmk__output_t *out, va_list args)
{
crm_time_t *time = va_arg(args, crm_time_t *);
int opts = va_arg(args, int);
char *date_s = crm_time_as_string(time, opts | crm_time_log_duration);
pcmk__output_create_xml_text_node(out, PCMK_XE_DURATION, date_s);
free(date_s);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("duration_ends", "crm_time_t *", "int")
static int
duration_ends_default(pcmk__output_t *out, va_list args)
{
crm_time_t *time = va_arg(args, crm_time_t *);
int opts = va_arg(args, int);
char *date_s = NULL;
opts |= crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone;
date_s = crm_time_as_string(time, opts);
out->info(out, "Duration ends at: %s", date_s);
free(date_s);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("duration_ends", "crm_time_t *", "int")
static int
duration_ends_xml(pcmk__output_t *out, va_list args)
{
crm_time_t *time = va_arg(args, crm_time_t *);
int opts = va_arg(args, int);
char *date_s = NULL;
opts |= crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone;
date_s = crm_time_as_string(time, opts);
pcmk__output_create_xml_text_node(out, PCMK_XE_DURATION_ENDS, date_s);
free(date_s);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("period", "crm_time_period_t *", "int")
static int
period_default(pcmk__output_t *out, va_list args)
{
crm_time_period_t *period = va_arg(args, crm_time_period_t *);
int opts = va_arg(args, int);
char *start = NULL;
char *end = NULL;
opts |= crm_time_log_date | crm_time_log_timeofday;
start = crm_time_as_string(period->start, opts);
if (start == NULL) {
return pcmk_rc_no_output;
}
end = crm_time_as_string(period->end, opts);
if (end == NULL) {
free(start);
return pcmk_rc_no_output;
}
out->info(out, "Period: %s to %s", start, end);
free(start);
free(end);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("period", "crm_time_period_t *", "int")
static int
period_xml(pcmk__output_t *out, va_list args)
{
crm_time_period_t *period = va_arg(args, crm_time_period_t *);
int opts = va_arg(args, int);
char *start = NULL;
char *end = NULL;
opts |= crm_time_log_date | crm_time_log_timeofday;
start = crm_time_as_string(period->start, opts);
if (start == NULL) {
return pcmk_rc_no_output;
}
end = crm_time_as_string(period->end, opts);
if (end == NULL) {
free(start);
return pcmk_rc_no_output;
}
pcmk__output_xml_create_parent(out, PCMK_XE_PERIOD, NULL);
pcmk__output_create_xml_text_node(out, PCMK_XE_START, start);
pcmk__output_create_xml_text_node(out, PCMK_XE_END, end);
free(start);
free(end);
return pcmk_rc_ok;
}
static GOptionContext *
build_arg_context(pcmk__common_args_t *args, GOptionGroup **group)
{
GOptionContext *context = NULL;
const char *description = "For more information on the ISO 8601 standard, see " \
"https://en.wikipedia.org/wiki/ISO_8601";
context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
g_option_context_set_description(context, description);
pcmk__add_arg_group(context, "commands", "Commands:",
"Show command options", command_entries);
pcmk__add_arg_group(context, "modifiers", "Output modifiers:",
"Show output modifiers", modifier_entries);
return context;
}
static pcmk__message_entry_t fmt_functions[] = {
{ "date", "default", date_default },
{ "date", "xml", date_xml },
{ "duration", "default", duration_default },
{ "duration", "xml", duration_xml },
{ "duration_ends", "default", duration_ends_default },
{ "duration_ends", "xml", duration_ends_xml },
{ "period", "default", period_default },
{ "period", "xml", period_xml },
{ NULL, NULL, NULL }
};
int
main(int argc, char **argv)
{
int rc = pcmk_rc_ok;
crm_exit_t exit_code = CRM_EX_OK;
crm_time_t *duration = NULL;
crm_time_t *date_time = NULL;
GError *error = NULL;
pcmk__output_t *out = NULL;
GOptionGroup *output_group = NULL;
pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
GOptionContext *context = build_arg_context(args, &output_group);
gchar **processed_args = pcmk__cmdline_preproc(argv, "dpDE");
pcmk__register_formats(output_group, formats);
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
pcmk__cli_init_logging("iso8601", args->verbosity);
rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error creating output format %s: %s", args->output_ty,
pcmk_rc_str(rc));
goto done;
}
if (args->version) {
out->version(out);
goto done;
}
pcmk__register_messages(out, fmt_functions);
if (pcmk__str_eq("now", options.date_time_s, pcmk__str_casei)) {
date_time = crm_time_new(NULL);
if (date_time == NULL) {
exit_code = CRM_EX_SOFTWARE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Internal error: couldn't determine 'now'!");
goto done;
}
out->message(out, "date", "Current date/time", date_time,
options.print_options);
} else if (options.date_time_s) {
date_time = crm_time_new(options.date_time_s);
if (date_time == NULL) {
exit_code = CRM_EX_INVALID_PARAM;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Invalid date/time specified: %s", options.date_time_s);
goto done;
}
out->message(out, "date", "Date", date_time, options.print_options);
}
if (options.duration_s) {
duration = crm_time_parse_duration(options.duration_s);
if (duration == NULL) {
exit_code = CRM_EX_INVALID_PARAM;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Invalid duration specified: %s", options.duration_s);
goto done;
}
out->message(out, "duration", duration, options.print_options);
}
if (options.period_s) {
crm_time_period_t *period = crm_time_parse_period(options.period_s);
if (period == NULL) {
exit_code = CRM_EX_INVALID_PARAM;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Invalid interval specified: %s", options.period_s);
goto done;
}
out->message(out, "period", period, options.print_options);
crm_time_free_period(period);
}
if (date_time && duration) {
crm_time_t *later = crm_time_add(date_time, duration);
if (later == NULL) {
exit_code = CRM_EX_SOFTWARE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Unable to calculate ending time of %s plus %s",
options.date_time_s, options.duration_s);
goto done;
}
out->message(out, "duration_ends", later, options.print_options);
if (options.expected_s) {
char *dt_s = crm_time_as_string(later,
options.print_options | crm_time_log_date |
crm_time_log_timeofday);
if (!pcmk__str_eq(options.expected_s, dt_s, pcmk__str_casei)) {
exit_code = CRM_EX_ERROR;
goto done;
}
free(dt_s);
}
crm_time_free(later);
} else if (date_time && options.expected_s) {
char *dt_s = crm_time_as_string(date_time,
options.print_options | crm_time_log_date | crm_time_log_timeofday);
if (!pcmk__str_eq(options.expected_s, dt_s, pcmk__str_casei)) {
exit_code = CRM_EX_ERROR;
goto done;
}
free(dt_s);
}
done:
crm_time_free(date_time);
crm_time_free(duration);
g_strfreev(processed_args);
pcmk__free_arg_context(context);
free(options.date_time_s);
g_free(options.duration_s);
g_free(options.expected_s);
g_free(options.period_s);
pcmk__output_and_clear_error(&error, out);
if (out != NULL) {
out->finish(out, exit_code, true, NULL);
pcmk__output_free(out);
}
pcmk__unregister_formats();
crm_exit(exit_code);
}