Project

General

Profile

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

    
28
require_once 'tools_for_debugging_n_testing.inc';
29
require_once 'parser_ipv6.inc';
30

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

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

    
76
	return $ndpdata;
77
}
78

    
79
const IA_NA = 1;
80
const IA_PD = 2;
81
const IA_TA = 3;
82

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

    
89
	if ($amount_of_matches == false) {
90
		return false;
91
	}
92

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

    
97
		$iaid_duid = parse_duid($val['IAID_DUID']);
98

    
99
		general_debug($val, 'IAID_DUID');
100

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

    
104
		general_debug($entry, 'iaid');
105
		general_debug($entry, 'duid');
106

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

    
118
		general_debug("---LEASE $amount_of_leases END---\n");
119
	}
120

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

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

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

    
154
	$amount_of_matches = 0;
155

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

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

    
164
		if (setnlen($val, 'NEXT_LEVEL')) {
165
			general_debug($val, 'NEXT_LEVEL');
166

    
167
			gui_handle_ia($val['NEXT_LEVEL'], $entry, $lang_pack);
168
		}
169

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

    
177
		general_debug('  --Match number ' . $amount_of_matches . ' end.');
178
	}
179
}
180

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

    
185
		$space_result = parse_all_ipv6_to_array($val['ADDRESS']);
186

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

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

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

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

    
223
		$space_result = parse_all_ipv6_to_array($val['SPACE']);
224

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

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

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

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

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

    
286
			general_debug($ia_val, 'BINDINGSTATE');
287
		}
288

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

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

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

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

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

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

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

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

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

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

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

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

    
478
const pd_regex = common_napd_top."\n".pd_specific."\n".common_napd_bottom;
479
const na_regex = common_napd_top."\n".na_specific."\n".common_napd_bottom;
480

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

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

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

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

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