Project

General

Profile

Download (19.2 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * parser_dhcpv6_leases.inc
4
 *
5
 * Copyright (c) 2017-2018 Anders Lind (anders.lind@gmail.com)
6
 * All rights reserved.
7
 *
8
 * part of pfSense (https://www.pfsense.org)
9
 * Copyright (c) 2004-2016 Rubicon Communications, LLC (Netgate)
10
 * Copyright (c) 2011 Seth Mos
11
 * All rights reserved.
12
 *
13
 * Licensed under the Apache License, Version 2.0 (the "License");
14
 * you may not use this file except in compliance with the License.
15
 * You may obtain a copy of the License at
16
 *
17
 * http://www.apache.org/licenses/LICENSE-2.0
18
 *
19
 * Unless required by applicable law or agreed to in writing, software
20
 * distributed under the License is distributed on an "AS IS" BASIS,
21
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22
 * See the License for the specific language governing permissions and
23
 * limitations under the License.
24
 */
25

    
26
require_once 'tools_for_debugging_n_testing.inc';
27
require_once 'parser_ipv6.inc';
28

    
29
/*
30
 * Moved over from status_dhcpv6_leases.php
31
 */
32
function parse_duid($duid_string) {
33
	$parsed_duid = array();
34
	for ($i = 0; $i < strlen($duid_string); $i++) {
35
		$s = substr($duid_string, $i, 1);
36
		if ($s == '\\') {
37
			$n = substr($duid_string, $i+1, 1);
38
			if (($n == '\\') || ($n == '"')) {
39
				$parsed_duid[] = sprintf("%02x", ord($n));
40
				$i += 1;
41
			} else {
42
				$n = substr($duid_string, $i+1, 3);
43
				if (preg_match('/[0-3][0-7]{2}/', $n)) {
44
					$parsed_duid[] = sprintf("%02x", octdec($n));
45
					$i += 3;
46
				}
47
			}
48
		} else {
49
			$parsed_duid[] = sprintf("%02x", ord($s));
50
		}
51
	}
52
	$iaid = array_slice($parsed_duid, 0, 4);
53
	$duid = array_slice($parsed_duid, 4);
54
	return array($iaid, $duid);
55
}
56

    
57
/*
58
 * Moved over from status_dhcpv6_leases.php
59
 */
60
function get_ndpdata() {
61
	exec("/usr/sbin/ndp -an", $rawdata);
62
	$ndpdata = array();
63
	foreach ($rawdata as $line) {
64
		$elements = preg_split('/\s+/ ', $line);
65
		if ($elements[1] != "(incomplete)") {
66
			$ndpent = array();
67
			$ip = trim(str_replace(array('(', ')'), '', $elements[0]));
68
			$ndpent['mac'] = trim($elements[1]);
69
			$ndpent['interface'] = trim($elements[2]);
70
			$ndpdata[$ip] = $ndpent;
71
		}
72
	}
73

    
74
	return $ndpdata;
75
}
76

    
77
const IA_NA = 1;
78
const IA_PD = 2;
79
const IA_TA = 3;
80

    
81
/*
82
 * returns false if it was not possible to parse the leases.
83
 */
84
function gui_parse_leases(&$pools, &$leases, &$prefixes, &$mappings, $raw_leases, $ndpdata, $lang_pack) {
85
	$amount_of_matches = preg_match_all('/'.base_regex.'/i', (string) $raw_leases, $lease_array, PREG_SET_ORDER);
86

    
87
	if ($amount_of_matches == false) {
88
		return false;
89
	}
90

    
91
	$amount_of_leases = 0;
92
	foreach ($lease_array as $val) {
93
		general_debug('---LEASE '.++$amount_of_leases.' START---');
94

    
95
		$iaid_duid = parse_duid($val['IAID_DUID']);
96

    
97
		general_debug($val, 'IAID_DUID');
98

    
99
		$entry = [ 'iaid' => hexdec(implode("", array_reverse($iaid_duid[0]))),
100
			   'duid' => implode(":", $iaid_duid[1]) ];
101

    
102
		general_debug($entry, 'iaid');
103
		general_debug($entry, 'duid');
104

    
105
		if (preg_match('/^ia-na$/i', $val['IA_ASSOC'])) {
106
			gui_parse_single_lease(IA_NA, $val['NEXT_LEVEL'], $lang_pack, $ndpdata, $mappings, $entry);
107
			$leases[] = $entry;
108
		} elseif (preg_match('/^ia-pd$/i', $val['IA_ASSOC'])) {
109
			gui_parse_single_lease(IA_PD, $val['NEXT_LEVEL'], $lang_pack, $ndpdata, $mappings, $entry);
110
			$prefixes[] = $entry;
111
		} elseif (preg_match('/^ia-ta$/i', $val['IA_ASSOC'])) {
112
			// Not used in the pfSense UI at the moment.
113
			general_debug('Notice: ia-ta is unhandled for now.');
114
		}
115

    
116
		general_debug("---LEASE $amount_of_leases END---\n");
117
	}
118

    
119
	// Handle failover - NOTE Experimental;
120
	// has not been tested on a live configured system.
121
	$amount_of_matches = preg_match_all('/'.failover_base_regex.'/i', $raw_leases, $lease_array, PREG_SET_ORDER);
122
	$amount_of_leases = 0;
123
	foreach ($lease_array as $val) {
124
		general_debug('---FAILOVER '.++$amount_of_leases.' START---');
125
		$entry = ['name' => $val['FPN']];
126
		general_debug($val, 'FPN');
127
		gui_parse_single_failover($val['NEXT_LEVEL'], $entry);
128
		$pools[] = $entry;
129
		general_debug("---FAILOVER $amount_of_leases END---\n");
130
	}
131

    
132
	general_debug('Leases', $leases);
133
	general_debug('Prefixes', $prefixes);
134
	general_debug('Mappings', $mappings);
135
	general_debug('Pools', $pools);
136
}
137

    
138
function gui_parse_single_lease($type, $content, $lang_pack, $ndpdata, &$mappings, &$entry) {
139
	switch ($type) {
140
		case IA_NA:
141
			preg_match_all('/'.na_regex.'/i', $content, $matches, PREG_SET_ORDER);
142
			break;
143
		case IA_PD:
144
			preg_match_all('/'.pd_regex.'/i', $content, $matches, PREG_SET_ORDER);
145
			break;
146
		// case IA_TA: // Not implemented in pfSense.
147
		default:
148
			general_debug('Error: Unhandled lease type.');
149
			return -1;
150
	}
151

    
152
	$amount_of_matches = 0;
153

    
154
	foreach ($matches as $val) {
155
		general_debug("\n  --Match number " . ++$amount_of_matches . ' start.');
156
		general_debug("Matched: " . $val[0]);
157

    
158
		check_value_set_entry($val, 'CLTT', $entry, 'start', true, function () use (&$val) {
159
			general_debug($val, 'WEEKDAY', 'DATE', 'YEAR', 'MONTH', 'DAY', 'TIME');
160
			return $val['DATE'] . ' ' . $val['TIME'];});
161

    
162
		if (setnlen($val, 'NEXT_LEVEL')) {
163
			general_debug($val, 'NEXT_LEVEL');
164

    
165
			gui_handle_ia($val['NEXT_LEVEL'], $entry, $lang_pack);
166
		}
167

    
168
		// For now it does not matter that this goes last.
169
		if ($type === IA_NA) {
170
			gui_parse_single_na_lease($val, $lang_pack, $ndpdata, $mappings, $entry);
171
		} elseif ($type === IA_PD) {
172
			gui_parse_single_pd_lease($val, $lang_pack, $entry);
173
		}
174

    
175
		general_debug('  --Match number ' . $amount_of_matches . ' end.');
176
	}
177
}
178

    
179
function gui_parse_single_na_lease($val, $lang_pack, $ndpdata, &$mappings, &$entry) {
180
	if (setnlen($val, 'ADDRESS')) {
181
	  	general_debug($val, 'ADDRESS');
182

    
183
		$space_result = parse_all_ipv6_to_array($val['ADDRESS']);
184

    
185
		if (count($space_result) == 1) {
186
	    		if (setnlen($space_result[0], 'MATCH')) {
187
				general_debug("We have a valid ipv6 address");
188
				if (check_n_set($entry, 'ip', $space_result[0]['MATCH'], true)) {
189
					// In general we trust that $lang_pack exists with values so no checking (either
190
					// it works for a certain language/translation or it does not (for all entries.)
191
					check_n_set($entry, 'type', $lang_pack['dynamic'], true);
192

    
193
					in_array($entry['ip'], array_keys($ndpdata))
194
						? check_n_set($entry, 'online',	$lang_pack['online'], true)
195
						: check_n_set($entry, 'online',	$lang_pack['offline'], true);
196

    
197
					// mappings - using DUID and IAID
198
					if (setnlen($entry, 'duid') && setnlen($entry, 'iaid')) {
199
						// We assume $mappings array exist - else nothing works.
200
						general_debug('Setting the mappings array for DUID ' . $entry['duid'] .
201
							      ' with IAID ' . $entry['iaid'] . ' as index with value ' .
202
							      $entry['ip']);
203
						check_n_set($mappings[$entry['duid']], $entry['iaid'], $entry['ip'], true);
204
					} else {
205
						general_debug('WARNING: DUID or IAID is missing in entry. Likely either has zero length content.');
206
					}
207
				} // else { } - we use overwrite set to true and entry must have been set.
208
			} else {
209
				general_debug('Unknown problem with IPv6 address. See output just above.');
210
			}
211
		} else {
212
			general_debug('WARNING: Count of IPv6 addresses is: '.count($space_result));
213
		}
214
	}
215
}
216

    
217
function gui_parse_single_pd_lease($val, $lang_pack, &$entry) {
218
	if (setnlen($val, 'SPACE')) {
219
		general_debug($val, 'SPACE');
220

    
221
		$space_result = parse_all_ipv6_to_array($val['SPACE']);
222

    
223
		if (count($space_result) == 1) {
224
			if (setnlen($space_result[0], 'MATCH')) {
225
				general_debug("We have a valid ipv6 address");
226

    
227
				if (setnlen($space_result[0], 'CMR6') &&
228
				    preg_match('/::$/', $space_result[0]['CMR6'])) {
229
					general_debug("that is properly terminated with ::");
230
				} else {
231
					general_debug(", but not a proper subnet that ends with ::");
232
				}
233

    
234
				check_value_set_entry($val, 'PREFIX', $entry, 'prefix', true,
235
					function () use (&$val, &$lang_pack, &$entry, &$space_result) {
236
					check_n_set($entry, 'type', $lang_pack['dynamic'], true);
237
					return $space_result[0]['MATCH'].'/'.$val['PREFIX'];});
238
			} else {
239
				general_debug('Unknown problem with IPv6 address. See output just above.');
240
			}
241
		} else {
242
			general_debug('WARNING: Count of IPv6 addresses is: '.count($space_result));
243
		}
244
	}
245
}
246

    
247
function gui_handle_ia($content, &$entry, $lang_pack) {
248
	preg_match_all('/'.ia_regex.'/i', $content, $ia_matches, PREG_SET_ORDER);
249

    
250
	/* The reason why we use foreach and not simply just try lookup content is due to the
251
	 * fact that we match with alternation different sets of values from different
252
	 * regex groups.
253
	 * In theory one set/group could be repeated inside $ia_matches which means we want to
254
	 * know if that is the case. check_n_set makes sure to check if the value exists already
255
	 * and returns debug info about that.
256
	 */
257
	foreach ($ia_matches as $ia_val) {
258
		if (setnlen($ia_val, 'BINDINGSTATE')) {
259
			switch ($ia_val['BINDINGSTATE']) {
260
				case "active":
261
					check_n_set($entry, 'act', $lang_pack['active'], true);
262
					break;
263
				case "expired":
264
					check_n_set($entry, 'act', $lang_pack['expired'], true);
265
					break;
266
				case "free":
267
					check_n_set($entry, 'act', $lang_pack['expired'], true);
268
					check_n_set($entry, 'online', $lang_pack['offline'], true);
269
					break;
270
				case "backup":
271
					check_n_set($entry, 'act', $lang_pack['reserved'], true);
272
					check_n_set($entry, 'online', $lang_pack['offline'], true);
273
					break;
274
				case "released":
275
					check_n_set($entry, 'act', $lang_pack['released'], true);
276
					check_n_set($entry, 'online', $lang_pack['offline'], true);
277
					break;
278
				default:
279
					general_debug('Notice: Binding state "' . $ia_val['BINDINGSTATE'] .
280
						      '" is not handled.');
281
					break;
282
			}
283

    
284
			general_debug($ia_val, 'BINDINGSTATE');
285
		}
286

    
287
		// does not seem to be used by lease gui so we do not set anything; we just debug.
288
		check_value_set_entry($ia_val, 'PREFLIFE');
289

    
290
		// does not seem to be used by lease gui so we do not set anything; we just debug.
291
		check_value_set_entry($ia_val, 'MAXLIFE');
292

    
293
		check_value_set_entry($ia_val, 'ENDS', $entry, 'end', true, function () use (&$ia_val) {
294
			general_debug($ia_val, 'WEEKDAY', 'DATE', 'YEAR', 'MONTH', 'DAY', 'TIME');
295
			return $ia_val['DATE'] . ' ' . $ia_val['TIME'];});
296
	}
297
}
298

    
299
function gui_parse_single_failover($content, &$entry) {
300
	preg_match_all('/'.failover_level1_regex.'/i', $content, $matches, PREG_SET_ORDER);
301

    
302
	/* The reason why we use foreach and not simply just try lookup content is due to the
303
	 * fact that we match with alternation different sets of values from different
304
	 * regex groups.
305
	 * In theory one set/group could be repeated inside $matches which means we want to
306
	 * know if that is the case. check_value_set_entry makes sure to check if the value
307
	 * exists already and returns debug info about that.
308
	 */
309
	foreach ($matches as $val) {
310
		check_value_set_entry($val, 'MYSTATE', $entry, 'mystate', true);
311
		check_value_set_entry($val, 'MYDATE', $entry, 'mydate', true,
312
				      function () use (&$val) {return $val['DATE'] . ' ' . $val['TIME'];});
313
		check_value_set_entry($val, 'PEERSTATE', $entry, 'peerstate', true);
314
		check_value_set_entry($val, 'PEERDATE', $entry, 'peerdate', true,
315
				      function () use (&$val) {return $val['PDATE'] . ' ' . $val['PTIME'];});
316
		/* Does not seem to be used by lease gui so we do not set anything; we just debug.
317
		 * mclt (Maximum Client Lead Time):
318
		 * https://kb.isc.org/article/AA-00502/0/A-Basic-Guide-to-Configuring-DHCP-Failover.html
319
		 * https://kb.isc.org/article/AA-00268/0/DHCP-Failover-and-MCLT-configuration-implications.html
320
		 */
321
		check_value_set_entry($val, 'MCLT');
322
	}
323
}
324

    
325
/* Main leases - Level 0
326
 * regex matches the outer/top level of a lease
327
 * We allow free spacing and comments with (?x)
328
 * We capture:
329
 * . the identity association of a temporary address and non-temporary address
330
 * and delegated prefix (IA-TA, IA-NA and IA-PD)
331
 * . the partly octal encoded iaid + duid (Identity association identifier + DHCP Unique Identifier)
332
 * . the content at the next level,
333
 * but our matching also reuses our
334
 * current match (RECURSE) to make sure
335
 * there is a balance to the curly braces {}
336
 * semicolons where there are no curly braces
337
 * and sub-content to curly braces - but neither the
338
 * next level and sub levels are directly
339
 * individually matched/captured (that is for later
340
 * regex matches to achieve!)
341
 *
342
 *	 \x5C = \
343
 *	 Octal numbers are the characters that are found in the IAID and DUID with a
344
 *	 backslash \ in front of them.
345
 *	 Hex values in ranges are those characters that are printed as characters (not
346
 *	 octal values):
347
 *	 [\x20\x21\x23-\x5B\x5D-\x7E]
348
 *	 1. See ascii table that includes hex values:
349
 *	    https://en.wikipedia.org/wiki/ASCII#Printable_characters
350
 *	    https://en.wikipedia.org/wiki/ASCII#Character_set
351
 *	 2. Check: https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=common/print.c
352
 *	 3. isprint is more limited than isascii: http://www.cplusplus.com/reference/cctype/isprint/
353
 */
354

    
355
/* Notice (?x) must be a the beginning of the line - cannot be tabbed in,
356
 * because free spacing mode must start at the very beginning (^) of the
357
 * content.
358
 */
359
const base_regex = <<<'BASE'
360
(?x)
361
	(?'IA_ASSOC'ia-(?>ta|na|pd))
362
	\s+"
363
	(?'IAID_DUID'
364
		(?>
365
				[\x20\x21\x23-\x5B\x5D-\x7E]
366
			|
367
				\x5C
368
				(?>
369
						"
370
					|
371
						\x5C
372
					|
373
						(?>00[0-7]|0[1-3][0-7]|177|[2-3][0-7]{2})
374
				)
375
		)*
376
	)
377
	"\s+
378
	\{
379
		(?'NEXT_LEVEL'
380
			(?>
381
				\s*
382
				(?>
383
						[^\{\}\n]+;
384
					|
385
						[^\{\}\n]+
386
						\s*
387
						\{
388
							(?&NEXT_LEVEL)
389
						\}
390
						\s*
391
				)*
392
			)*
393
		)
394
	\}
395
BASE;
396
/* Inside the: (?'NEXT_LEVEL' ... )
397
 * various white space characters where substituted with \s* to match all white
398
 * space, but at different places before the substitutions some of the white
399
 * space characters were as mentioned below and their purpose:
400
 * *  (star) needed for when no space or newline e.g. nested {} is used in e.g.:
401
 *    something {dww w{} w {d;}}
402
 * \n (new line) needed for quicker matching (here)
403
 * \  (space) needed so we eat all space before a faulty double {}:
404
 *    something {} {}
405
 *    , is repeated - which we consider illegal.
406
 * \ \n has been substituted with \s to cover more white space characters!
407
 */
408

    
409
/* https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=includes/dhcpd.h
410
 * struct lease etc. and various write_lease methods
411
 *
412
 * https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=server/db.c
413
 * method for write lease file content is found here e.g. write_lease()
414
 *
415
 * Notice lease time entries are written with C gmtime() as UTC:
416
 * https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=common/print.c
417
 * const char * print_time(TIME t) last else clause:
418
 * if (strftime(buf, sizeof(buf), "%w %Y/%m/%d %H:%M:%S;",
419
 *                              gmtime(&t)) == 0)
420
 */
421
const date_time = <<<'DATETIME'
422
	(?'WEEKDAY'[0-6])\s+
423
	(?'DATE'
424
		(?'YEAR'\d+)\/(?'MONTH'(?>0[1-9]|1[0-2]))\/
425
		(?'DAY'(?>0[1-9]|[1-2][0-9]|30|31))
426
	)\s+
427
	(?'TIME'(?>[0-1][0-9]|2[0-3]):(?>[0-5][0-9]):(?>[0-5][0-9]|60))
428
DATETIME;
429

    
430
// Level 1
431
const common_napd_top =
432
'(?x)
433
cltt\s+(?\'CLTT\''.date_time.');|';
434

    
435
# Perform real check of the IPv6 address/space somewhere else.
436
const pd_specific = <<<'PD'
437
iaprefix\s+(?'SPACE'[\.\da-fA-F:]*)\/(?'PREFIX'\d{1,3})\s+\{
438
PD;
439
const na_specific = <<<'NA'
440
iaaddr\s+(?'ADDRESS'[\.\da-fA-F:]*)\s+\{
441
NA;
442

    
443
/* Level 1
444
 * This part handles matching properly placed curly braces and semicolons.
445
 * If in doubt just do experiments! To achieve matching the curly braces we need
446
 * to check at all levels however we do just capture it all since matching the
447
 * individual components needs to be handled separatly at each level by other
448
 * regexes.
449
 * Same idea as with base_regex above, but not expanded to save space.
450
 */
451
const common_napd_bottom = <<<'COMMON'
452
(?'NEXT_LEVEL'(?>\s*
453
(?>[^\{\}\n]+;|
454
[^\{\}\n]+\s*\{
455
(?&NEXT_LEVEL)
456
\}\s*
457
)*)*)
458
\}
459
COMMON;
460

    
461
/* Level 2 stuff
462
 * binding state names defined in array binding_state_names
463
 * https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=server/stables.c
464
 * "free", "active", "expired", "released", "abandoned", "reset", "backup"
465
 * write_lease and write_ia use binding_state_names in:
466
 * https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=server/db.c
467
 * defined in https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=server/stables.c
468
 */
469
const ia_regex =
470
'(?x)
471
(binding\s+state\s+(?\'BINDINGSTATE\'free|active|expired|released|abandoned|reset|backup)|
472
preferred-life\s+(?\'PREFLIFE\'\d+)|
473
max-life\s+(?\'MAXLIFE\'\d+)|
474
ends\s+(?\'ENDS\''.date_time.'));\s*';
475

    
476
const pd_regex = common_napd_top."\n".pd_specific."\n".common_napd_bottom;
477
const na_regex = common_napd_top."\n".na_specific."\n".common_napd_bottom;
478

    
479
/* Print these out if you would like to test and debug with (online) tools such
480
   as https://regex101.com - you could also test and debug some parts mentioned
481
   above. Just use the raw lease file as input and see how they match. */
482

    
483
// NOTE THIS IS NOT TESTED ON A REAL FAILOVER CONFIGURED SYSTEM
484
/*
485
 * Handling of failover base - level 0
486
 * Documentation is misleading so the source of ISC DHCP server is the best
487
 * resource combined with this article:
488
 * https://deepthought.isc.org/article/AA-00252/31/Putting-the-working-server-in-a-failover-pair-into-partner-down-state.html
489
 * , that seems to be right! Whereas the documentation tells something different
490
 * in: https://www.isc.org/wp-content/uploads/2017/08/dhcp43leases.html
491
 * , it writes: peer state state at date;
492
 * For more about how we implement see under level 1 below.
493
 *
494
 * FPN = failover pair name
495
 * Relevant source places:
496
 * https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=server/db.c
497
 * method: write_failover_state
498
 *
499
 * Same idea as with base_regex above, but not expanded to save space.
500
 */
501
const failover_base_regex = <<<'FAILOVERBASE'
502
(?x)
503
	failover\s+peer\s+\"(?'FPN'.*)\"\s+state\s+\{
504
	(?'NEXT_LEVEL'(?>\s*
505
	(?>[^\{\}\n]+;|
506
	[^\{\}\n]+\s*\{
507
	(?&NEXT_LEVEL)
508
	\}\s*
509
	)*)*)
510
	\}
511
FAILOVERBASE;
512

    
513
/*
514
 * Handling of failover statements - level 1
515
 *
516
 * Definition of state names:
517
 * https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=server/failover.c;h=25e1b72b5ed1705981fa7867d7c6172daa27f5a0;hb=HEAD
518
 * const char *dhcp_failover_state_name_print (enum failover_state state)
519
 * https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=includes/failover.h
520
 * enum failover_state, that holds the enumerated state names and the typedef
521
 * of dhcp_failover_state_t with references to dhcp_failover_config_t me,
522
 * partner and the enumerated failover_state for saved_state that is used by
523
 * int write_failover_state (dhcp_failover_state_t *state) in:
524
 * https://source.isc.org/cgi-bin/gitweb.cgi?p=dhcp.git;a=blob;f=server/db.c
525
 * to write the actual failover info in our lease file!
526
 *
527
 */
528
const p_date_time = <<< 'PDATETIME'
529
	(?'PWEEKDAY'[0-6])\s+
530
	(?'PDATE'
531
		(?'PYEAR'\d+)\/(?'PMONTH'(?>0[1-9]|1[0-2]))\/
532
		(?'PDAY'(?>0[1-9]|[1-2][0-9]|30|31))
533
	)\s+
534
	(?'PTIME'(?>[0-1][0-9]|2[0-3]):(?>[0-5][0-9]):(?>[0-5][0-9]|60))
535
PDATETIME;
536

    
537
/* Layout of these matters. 'recover-...' must come before 'recover', because
538
 * we make use of atomic grouping in failover_level1 to save amount of steps to
539
 * match!
540
 */
541
const failover_states = 'unknown-state|partner-down|normal|conflict-done'.
542
			'|communications-interrupted|resolution-interrupted|potential-conflict'.
543
			'|recover-done|recover-wait|recover|shutdown|paused|startup';
544

    
545
const failover_level1_regex =
546
	'(?x)
547
	(
548
			my\s+state\s+(?\'MYSTATE\'(?>'.failover_states.'))\s+
549
			at\s+(?\'MYDATE\''.date_time.')
550
		|
551
			partner\s+state\s+(?\'PEERSTATE\'(?&MYSTATE))\s+
552
			at\s+(?\'PEERDATE\''.p_date_time.')
553
		|
554
			mclt\s+(?\'MCLT\'\d+)
555
	);\s*';
(36-36/60)