r4105 setSwitchVLANs(): replace log array tricks with oneLiner/mergeLogs
[racktables] / inc / gateways.php
1 <?php
2 /*
3 *
4 * This file contains gateway functions for RackTables.
5 * A gateway is an external executable, which provides
6 * read-only or read-write access to some external entities.
7 * Each gateway accepts its own list of command-line args
8 * and then reads its stdin for requests. Each request consists
9 * of one line and results in exactly one line of reply.
10 * The replies must have the following syntax:
11 * OK<space>any text up to the end of the line
12 * ERR<space>any text up to the end of the line
13 *
14 */
15
16 // translating functions maps
17 $gwrxlator = array();
18 $gwrxlator['getcdpstatus'] = array
19 (
20 'ios12' => 'ios12ReadCDPStatus',
21 'nxos4' => 'ios12ReadCDPStatus',
22 );
23 $gwrxlator['getlldpstatus'] = array
24 (
25 'ios12' => 'ios12ReadLLDPStatus',
26 'xos12' => 'xos12ReadLLDPStatus',
27 'vrp53' => 'vrp53ReadLLDPStatus',
28 'vrp55' => 'vrp55ReadLLDPStatus',
29 );
30 $gwrxlator['get8021q'] = array
31 (
32 'ios12' => 'ios12ReadVLANConfig',
33 'fdry5' => 'fdry5ReadVLANConfig',
34 'vrp53' => 'vrp53ReadVLANConfig',
35 'vrp55' => 'vrp55Read8021QConfig',
36 'nxos4' => 'nxos4Read8021QConfig',
37 'xos12' => 'xos12Read8021QConfig',
38 );
39 $gwrxlator['getportstatus'] = array
40 (
41 'ios12' => 'ciscoReadInterfaceStatus',
42 'vrp53' => 'vrpReadInterfaceStatus',
43 'vrp55' => 'vrpReadInterfaceStatus',
44 'nxos4' => 'ciscoReadInterfaceStatus',
45 );
46 $gwrxlator['getmaclist'] = array
47 (
48 'ios12' => 'ios12ReadMacList',
49 'vrp53' => 'vrp53ReadMacList',
50 'vrp55' => 'vrp55ReadMacList',
51 'nxos4' => 'nxos4ReadMacList',
52 );
53
54 $gwrxlator['gethndp']['vrp53'] = 'vrp53ReadHNDPStatus';
55
56 $gwpushxlator = array
57 (
58 'ios12' => 'ios12TranslatePushQueue',
59 'fdry5' => 'fdry5TranslatePushQueue',
60 'vrp53' => 'vrp53TranslatePushQueue',
61 'vrp55' => 'vrp55TranslatePushQueue',
62 'nxos4' => 'ios12TranslatePushQueue', // employ syntax compatibility
63 'xos12' => 'xos12TranslatePushQueue',
64 );
65
66 // This function launches specified gateway with specified
67 // command-line arguments and feeds it with the commands stored
68 // in the second arg as array.
69 // The answers are stored in another array, which is returned
70 // by this function. In the case when a gateway cannot be found,
71 // finishes prematurely or exits with non-zero return code,
72 // a single-item array is returned with the only "ERR" record,
73 // which explains the reason.
74 function queryGateway ($gwname, $questions)
75 {
76 $execpath = "./gateways/{$gwname}/main";
77 $dspec = array
78 (
79 0 => array ("pipe", "r"),
80 1 => array ("pipe", "w"),
81 2 => array ("file", "/dev/null", "a")
82 );
83 $pipes = array();
84 $gateway = proc_open ($execpath, $dspec, $pipes);
85 if (!is_resource ($gateway))
86 return array ('ERR proc_open() failed in ' . __FUNCTION__);
87
88 // Dialogue starts. Send all questions.
89 foreach ($questions as $q)
90 fwrite ($pipes[0], "$q\n");
91 fclose ($pipes[0]);
92
93 // Fetch replies.
94 $answers = array ();
95 while (!feof($pipes[1]))
96 {
97 $a = fgets ($pipes[1]);
98 if (!strlen ($a))
99 continue;
100 // Somehow I got a space appended at the end. Kick it.
101 $answers[] = trim ($a);
102 }
103 fclose($pipes[1]);
104
105 $retval = proc_close ($gateway);
106 if ($retval != 0)
107 throw new RTGatewayError ("gateway failed with code ${retval}");
108 if (!count ($answers))
109 throw new RTGatewayError ('no response from gateway');
110 if (count ($answers) != count ($questions))
111 throw new RTGatewayError ('protocol violation');
112 foreach ($answers as $a)
113 if (strpos ($a, 'OK!') !== 0)
114 throw new RTGatewayError ("subcommand failed with status: ${a}");
115 return $answers;
116 }
117
118 // This functions returns an array for VLAN list, and an array for port list (both
119 // form another array themselves) and another one with MAC address list.
120 // The ports in the latter array are marked with either VLAN ID or 'trunk'.
121 // We don't sort the port list, as the gateway is believed to have done this already
122 // (or at least the underlying switch software ought to). This is important, as the
123 // port info is transferred to/from form not by names, but by numbers.
124 function getSwitchVLANs ($object_id = 0)
125 {
126 global $remote_username;
127 $objectInfo = spotEntity ('object', $object_id);
128 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
129 if (count ($endpoints) == 0)
130 throw new RTGatewayError ('no management address set');
131 if (count ($endpoints) > 1)
132 throw new RTGatewayError ('cannot pick management address');
133 $hwtype = $swtype = 'unknown';
134 foreach (getAttrValues ($object_id) as $record)
135 {
136 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
137 $swtype = str_replace (' ', '+', execGMarker ($record['o_value']));
138 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
139 $hwtype = str_replace (' ', '+', execGMarker ($record['o_value']));
140 }
141 $endpoint = str_replace (' ', '+', $endpoints[0]);
142 $commands = array
143 (
144 "connect ${endpoint} ${hwtype} ${swtype} ${remote_username}",
145 'listvlans',
146 'listports',
147 'listmacs'
148 );
149 $data = queryGateway ('switchvlans', $commands);
150 if (strpos ($data[0], 'OK!') !== 0)
151 throw new RTGatewayError ("gateway failed with status: ${data[0]}.");
152 // Now we have VLAN list in $data[1] and port list in $data[2]. Let's sort this out.
153 $tmp = array_unique (explode (';', substr ($data[1], strlen ('OK!'))));
154 if (count ($tmp) == 0)
155 throw new RTGatewayError ('gateway returned no records');
156 $vlanlist = array();
157 foreach ($tmp as $record)
158 {
159 list ($vlanid, $vlandescr) = explode ('=', $record);
160 $vlanlist[$vlanid] = $vlandescr;
161 }
162 $portlist = array();
163 foreach (explode (';', substr ($data[2], strlen ('OK!'))) as $pair)
164 {
165 list ($portname, $pair2) = explode ('=', $pair);
166 list ($status, $vlanid) = explode (',', $pair2);
167 $portlist[] = array ('portname' => $portname, 'status' => $status, 'vlanid' => $vlanid);
168 }
169 if (count ($portlist) == 0)
170 throw new RTGatewayError ('gateway returned no records');
171 $maclist = array();
172 foreach (explode (';', substr ($data[3], strlen ('OK!'))) as $pair)
173 {
174 list ($macaddr, $pair2) = explode ('=', $pair);
175 if (!strlen ($pair2))
176 continue;
177 list ($vlanid, $ifname) = explode ('@', $pair2);
178 $maclist[$ifname][$vlanid][] = $macaddr;
179 }
180 return array ($vlanlist, $portlist, $maclist);
181 }
182
183 function setSwitchVLANs ($object_id = 0, $setcmd)
184 {
185 global $remote_username;
186 $objectInfo = spotEntity ('object', $object_id);
187 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
188 if (count ($endpoints) == 0)
189 throw new RTGatewayError ('no management address set');
190 if (count ($endpoints) > 1)
191 throw new RTGatewayError ('cannot pick management address');
192 $hwtype = $swtype = 'unknown';
193 foreach (getAttrValues ($object_id) as $record)
194 {
195 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
196 $swtype = strtr (execGMarker ($record['o_value']), ' ', '+');
197 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
198 $hwtype = strtr (execGMarker ($record['o_value']), ' ', '+');
199 }
200 $endpoint = str_replace (' ', '+', $endpoints[0]);
201 $data = queryGateway
202 (
203 'switchvlans',
204 array ("connect ${endpoint} ${hwtype} ${swtype} ${remote_username}", $setcmd)
205 );
206 // Finally we can parse the response into message array.
207 $log = emptyLog();
208 foreach (explode (';', substr ($data[1], strlen ('OK!'))) as $text)
209 {
210 if (strpos ($text, 'C!') === 0)
211 {
212 // gateway-encoded message
213 $tmp = explode ('!', $text);
214 array_shift ($tmp);
215 $code = array_shift ($tmp);
216 $log = mergeLogs ($log, oneLiner ($code, $tmp));
217 }
218 elseif (strpos ($text, 'I!') === 0)
219 $log = mergeLogs ($log, oneLiner (62, array (substr ($text, 2)))); // generic gateway success
220 elseif (strpos ($text, 'W!') === 0)
221 $log = mergeLogs ($log, oneLiner (202, array (substr ($text, 2)))); // generic gateway warning
222 else // All improperly formatted messages must be treated as error conditions.
223 $log = mergeLogs ($log, oneLiner (166, array (substr ($text, 2)))); // generic gateway error
224 }
225 return $log;
226 }
227
228 // Drop a file off RackTables platform. The gateway will catch the file and pass it to the given
229 // installer script.
230 function gwSendFile ($endpoint, $handlername, $filetext = array())
231 {
232 global $remote_username;
233 $tmpnames = array();
234 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
235 $command = "submit ${remote_username} ${endpoint} ${handlername}";
236 foreach ($filetext as $text)
237 {
238 $name = tempnam ('', 'RackTables-sendfile-');
239 $tmpnames[] = $name;
240 if (FALSE === $name or FALSE === file_put_contents ($name, $text))
241 {
242 foreach ($tmpnames as $name)
243 unlink ($name);
244 throw new RTGatewayError ('failed to write to temporary file');
245 }
246 $command .= " ${name}";
247 }
248 try
249 {
250 queryGateway ('sendfile', array ($command));
251 foreach ($tmpnames as $name)
252 unlink ($name);
253 }
254 catch (RTGatewayError $e)
255 {
256 foreach ($tmpnames as $name)
257 unlink ($name);
258 throw $e;
259 }
260 }
261
262 // Query something through a gateway and get some text in return. Return that text.
263 function gwRecvFile ($endpoint, $handlername, &$output)
264 {
265 global $remote_username;
266 $tmpfilename = tempnam ('', 'RackTables-sendfile-');
267 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
268 try
269 {
270 queryGateway ('sendfile', array ("submit ${remote_username} ${endpoint} ${handlername} ${tmpfilename}"));
271 $output = file_get_contents ($tmpfilename);
272 unlink ($tmpfilename);
273 }
274 catch (RTGatewayError $e)
275 {
276 unlink ($tmpfilename);
277 throw $e;
278 }
279 if ($output === FALSE)
280 throw new RTGatewayError ('failed to read temporary file');
281 }
282
283 function gwSendFileToObject ($object_id, $handlername, $filetext = '')
284 {
285 if (!mb_strlen ($handlername))
286 throw new InvalidArgException ('$handlername');
287 $objectInfo = spotEntity ('object', $object_id);
288 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
289 if (count ($endpoints) == 0)
290 throw new RTGatewayError ('no management address set');
291 if (count ($endpoints) > 1)
292 throw new RTGatewayError ('cannot pick management address');
293 gwSendFile (str_replace (' ', '+', $endpoints[0]), $handlername, array ($filetext));
294 }
295
296 function gwRecvFileFromObject ($object_id, $handlername, &$output)
297 {
298 if (!mb_strlen ($handlername))
299 throw new InvalidArgException ('$handlername');
300 $objectInfo = spotEntity ('object', $object_id);
301 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
302 if (count ($endpoints) == 0)
303 throw new RTGatewayError ('no management address set');
304 if (count ($endpoints) > 1)
305 throw new RTGatewayError ('cannot pick management address');
306 gwRecvFile (str_replace (' ', '+', $endpoints[0]), $handlername, $output);
307 }
308
309 function detectDeviceBreed ($object_id)
310 {
311 $breed_by_swcode = array
312 (
313 251 => 'ios12',
314 252 => 'ios12',
315 254 => 'ios12',
316 963 => 'nxos4',
317 964 => 'nxos4',
318 1365 => 'nxos4',
319 1352 => 'xos12',
320 1360 => 'vrp53',
321 1361 => 'vrp55',
322 1369 => 'vrp55', // VRP versions 5.5 and 5.7 seem to be compatible
323 1363 => 'fdry5',
324 );
325 foreach (getAttrValues ($object_id) as $record)
326 if ($record['id'] == 4 and array_key_exists ($record['key'], $breed_by_swcode))
327 return $breed_by_swcode[$record['key']];
328 return '';
329 }
330
331 function getRunning8021QConfig ($object_id)
332 {
333 $ret = gwRetrieveDeviceConfig ($object_id, 'get8021q');
334 // Once there is no default VLAN in the parsed data, it means
335 // something else was parsed instead of config text.
336 if (!in_array (VLAN_DFL_ID, $ret['vlanlist']))
337 throw new RTGatewayError ('communication with device failed');
338 return $ret;
339 }
340
341 function setDevice8021QConfig ($object_id, $pseudocode)
342 {
343 if ('' == $breed = detectDeviceBreed ($object_id))
344 throw new RTGatewayError ('device breed unknown');
345 global $gwpushxlator;
346 // FIXME: this is a perfect place to log intended changes
347 gwDeployDeviceConfig ($object_id, $breed, unix2dos ($gwpushxlator[$breed] ($pseudocode)));
348 }
349
350 function gwRetrieveDeviceConfig ($object_id, $command)
351 {
352 global $gwrxlator;
353 if (!array_key_exists ($command, $gwrxlator))
354 throw new RTGatewayError ('command unknown');
355 $breed = detectDeviceBreed ($object_id);
356 if (!array_key_exists ($breed, $gwrxlator[$command]))
357 throw new RTGatewayError ('device breed unknown');
358 $objectInfo = spotEntity ('object', $object_id);
359 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
360 if (count ($endpoints) == 0)
361 throw new RTGatewayError ('no management address set');
362 if (count ($endpoints) > 1)
363 throw new RTGatewayError ('cannot pick management address');
364 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
365 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
366 try
367 {
368 queryGateway ('deviceconfig', array ("${command} ${endpoint} ${breed} ${tmpfilename}"));
369 $configtext = file_get_contents ($tmpfilename);
370 unlink ($tmpfilename);
371 }
372 catch (RTGatewayError $e)
373 {
374 unlink ($tmpfilename);
375 throw $e;
376 }
377 if ($configtext === FALSE)
378 throw new RTGatewayError ('failed to read temporary file');
379 // Being here means it was alright.
380 return $gwrxlator[$command][$breed] (dos2unix ($configtext));
381 }
382
383 function gwDeployDeviceConfig ($object_id, $breed, $text)
384 {
385 if ($text == '')
386 throw new InvalidArgException ('text', '', 'deploy text is empty');
387 $objectInfo = spotEntity ('object', $object_id);
388 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
389 if (count ($endpoints) == 0)
390 throw new RTGatewayError ('no management address set');
391 if (count ($endpoints) > 1)
392 throw new RTGatewayError ('cannot pick management address');
393 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
394 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
395 if (FALSE === file_put_contents ($tmpfilename, $text))
396 {
397 unlink ($tmpfilename);
398 throw new RTGatewayError ('failed to write to temporary file');
399 }
400 try
401 {
402 queryGateway ('deviceconfig', array ("deploy ${endpoint} ${breed} ${tmpfilename}"));
403 unlink ($tmpfilename);
404 }
405 catch (RTGatewayError $e)
406 {
407 unlink ($tmpfilename);
408 throw $e;
409 }
410 }
411
412 // Read provided output of "show cdp neighbors detail" command and
413 // return a list of records with (translated) local port name,
414 // remote device name and (translated) remote port name.
415 function ios12ReadCDPStatus ($input)
416 {
417 $ret = array();
418 foreach (explode ("\n", $input) as $line)
419 {
420 $matches = array();
421 switch (TRUE)
422 {
423 case preg_match ('/^Device ID:\s*([A-Za-z0-9][A-Za-z0-9\.\-]*)/', $line, $matches):
424 case preg_match ('/^System Name:\s*([A-Za-z0-9][A-Za-z0-9\.\-]*)/', $line, $matches):
425 $ret['current']['device'] = $matches[1];
426 break;
427 case preg_match ('/^Interface: (.+), ?Port ID \(outgoing port\): (.+)$/', $line, $matches):
428 if (array_key_exists ('device', $ret['current']))
429 $ret[ios12ShortenIfName ($matches[1])][] = array
430 (
431 'device' => $ret['current']['device'],
432 'port' => ios12ShortenIfName ($matches[2]),
433 );
434 unset ($ret['current']);
435 break;
436 default:
437 }
438 }
439 unset ($ret['current']);
440 return $ret;
441 }
442
443 function ios12ReadLLDPStatus ($input)
444 {
445 $ret = array();
446 $got_header = FALSE;
447 foreach (explode ("\n", $input) as $line)
448 {
449 if (preg_match ("/^Device ID/", $line))
450 $got_header = TRUE;
451
452 if (!$got_header)
453 continue;
454
455 $matches = preg_split ('/\s+/', $line);
456
457 switch (count ($matches))
458 {
459 case 5:
460 list ($remote_name, $local_port, $ttl, $caps, $remote_port) = $matches;
461 $local_port = ios12ShortenIfName ($local_port);
462 $remote_port = ios12ShortenIfName ($remote_port);
463 $ret[$local_port][] = array
464 (
465 'device' => $remote_name,
466 'port' => $remote_port,
467 );
468 break;
469 default:
470 }
471 }
472 return $ret;
473 }
474
475 function xos12ReadLLDPStatus ($input)
476 {
477 $ret = array();
478 foreach (explode ("\n", $input) as $line)
479 {
480 $matches = array();
481 switch (TRUE)
482 {
483 case preg_match ('/^LLDP Port ([[:digit:]]+) detected \d+ neighbor$/', $line, $matches):
484 $ret['current']['local_port'] = ios12ShortenIfName ($matches[1]);
485 break;
486 case preg_match ('/^ Port ID : "(.+)"$/', $line, $matches):
487 $ret['current']['remote_port'] = ios12ShortenIfName ($matches[1]);
488 break;
489 case preg_match ('/^ - System Name: "(.+)"$/', $line, $matches):
490 if
491 (
492 array_key_exists ('current', $ret) and
493 array_key_exists ('local_port', $ret['current']) and
494 array_key_exists ('remote_port', $ret['current'])
495 )
496 $ret[$ret['current']['local_port']][] = array
497 (
498 'device' => $matches[1],
499 'port' => $ret['current']['remote_port'],
500 );
501 unset ($ret['current']);
502 default:
503 }
504 }
505 unset ($ret['current']);
506 return $ret;
507 }
508
509 function vrp53ReadLLDPStatus ($input)
510 {
511 $ret = array();
512 foreach (explode ("\n", $input) as $line)
513 {
514 $matches = array();
515 switch (TRUE)
516 {
517 case preg_match ('/^(.+) has \d+ neighbors:$/', $line, $matches):
518 $ret['current']['local_port'] = ios12ShortenIfName ($matches[1]);
519 break;
520 case preg_match ('/^(PortIdSubtype|PortId): ([^ ]+)/', $line, $matches):
521 $ret['current'][$matches[1]] = $matches[2];
522 break;
523 case preg_match ('/^SysName: (.+)$/', $line, $matches):
524 if
525 (
526 array_key_exists ('current', $ret) and
527 array_key_exists ('PortIdSubtype', $ret['current']) and
528 ($ret['current']['PortIdSubtype'] == 'interfaceAlias' or $ret['current']['PortIdSubtype'] == 'interfaceName') and
529 array_key_exists ('PortId', $ret['current']) and
530 array_key_exists ('local_port', $ret['current'])
531 )
532 $ret[$ret['current']['local_port']][] = array
533 (
534 'device' => $matches[1],
535 'port' => ios12ShortenIfName ($ret['current']['PortId']),
536 );
537 unset ($ret['current']);
538 break;
539 default:
540 }
541 }
542 unset ($ret['current']);
543 return $ret;
544 }
545
546 function vrp55ReadLLDPStatus ($input)
547 {
548 $ret = array();
549 foreach (explode ("\n", $input) as $line)
550 {
551 $matches = array();
552 switch (TRUE)
553 {
554 case preg_match ('/^(.+) has \d+ neighbors:$/', $line, $matches):
555 $ret['current']['local_port'] = ios12ShortenIfName ($matches[1]);
556 break;
557 case preg_match ('/^Port ID type :([^ ]+)/', $line, $matches):
558 $ret['current']['PortIdSubtype'] = $matches[1];
559 break;
560 case preg_match ('/^Port ID :(.+)$/', $line, $matches):
561 $ret['current']['PortId'] = $matches[1];
562 break;
563 case preg_match ('/^System name :(.+)$/', $line, $matches):
564 if
565 (
566 array_key_exists ('current', $ret) and
567 array_key_exists ('PortIdSubtype', $ret['current']) and
568 ($ret['current']['PortIdSubtype'] == 'interfaceAlias' or $ret['current']['PortIdSubtype'] == 'interfaceName') and
569 array_key_exists ('PortId', $ret['current']) and
570 array_key_exists ('local_port', $ret['current'])
571 )
572 $ret[$ret['current']['local_port']][] = array
573 (
574 'device' => $matches[1],
575 'port' => ios12ShortenIfName ($ret['current']['PortId']),
576 );
577 unset ($ret['current']);
578 break;
579 default:
580 }
581 }
582 unset ($ret['current']);
583 return $ret;
584 }
585
586 function vrp53ReadHNDPStatus ($input)
587 {
588 $ret = array();
589 foreach (explode ("\n", $input) as $line)
590 {
591 $matches = array();
592 switch (TRUE)
593 {
594 case preg_match ('/^ Interface: (.+)$/', $line, $matches):
595 $ret['current']['local_port'] = ios12ShortenIfName ($matches[1]);
596 break;
597 case preg_match ('/^ Port Name : (.+)$/', $line, $matches):
598 $ret['current']['remote_port'] = ios12ShortenIfName ($matches[1]);
599 break;
600 case preg_match ('/^ Device Name : (.+)$/', $line, $matches):
601 if
602 (
603 array_key_exists ('current', $ret) and
604 array_key_exists ('local_port', $ret['current']) and
605 array_key_exists ('remote_port', $ret['current'])
606 )
607 $ret[$ret['current']['local_port']][] = array
608 (
609 'device' => $matches[1],
610 'port' => $ret['current']['remote_port'],
611 );
612 unset ($ret['current']);
613 break;
614 default:
615 }
616 }
617 unset ($ret['current']);
618 return $ret;
619 }
620
621 function ios12ReadVLANConfig ($input)
622 {
623 $ret = array
624 (
625 'vlanlist' => array(),
626 'portdata' => array(),
627 );
628 $procfunc = 'ios12ScanTopLevel';
629 foreach (explode ("\n", $input) as $line)
630 $procfunc = $procfunc ($ret, $line);
631 return $ret;
632 }
633
634 function ios12ScanTopLevel (&$work, $line)
635 {
636 $matches = array();
637 switch (TRUE)
638 {
639 case (preg_match ('@^interface ((Ethernet|FastEthernet|GigabitEthernet|TenGigabitEthernet|Port-channel)[[:digit:]]+(/[[:digit:]]+)*)$@', $line, $matches)):
640 $work['current'] = array ('port_name' => ios12ShortenIfName ($matches[1]));
641 $work['current']['config'][] = array ('type' => 'line-header', 'line' => $line);
642 return 'ios12PickSwitchportCommand'; // switch to interface block reading
643 case (preg_match ('/^VLAN Name Status Ports$/', $line, $matches)):
644 return 'ios12PickVLANCommand';
645 default:
646 return __FUNCTION__; // continue scan
647 }
648 }
649
650 function ios12PickSwitchportCommand (&$work, $line)
651 {
652 if ($line[0] != ' ') // end of interface section
653 {
654 // save work, if it makes sense
655 switch ($work['current']['mode'])
656 {
657 case 'access':
658 if (!array_key_exists ('access vlan', $work['current']))
659 $work['current']['access vlan'] = 1;
660 $work['portdata'][$work['current']['port_name']] = array
661 (
662 'mode' => 'access',
663 'allowed' => array ($work['current']['access vlan']),
664 'native' => $work['current']['access vlan'],
665 );
666 break;
667 case 'trunk':
668 if (!array_key_exists ('trunk native vlan', $work['current']))
669 $work['current']['trunk native vlan'] = 1;
670 if (!array_key_exists ('trunk allowed vlan', $work['current']))
671 $work['current']['trunk allowed vlan'] = range (VLAN_MIN_ID, VLAN_MAX_ID);
672 // Having configured VLAN as "native" doesn't mean anything
673 // as long as it's not listed on the "allowed" line.
674 $effective_native = in_array
675 (
676 $work['current']['trunk native vlan'],
677 $work['current']['trunk allowed vlan']
678 ) ? $work['current']['trunk native vlan'] : 0;
679 $work['portdata'][$work['current']['port_name']] = array
680 (
681 'mode' => 'trunk',
682 'allowed' => $work['current']['trunk allowed vlan'],
683 'native' => $effective_native,
684 );
685 break;
686 case 'SKIP':
687 break;
688 case 'IP':
689 default:
690 // dot1q-tunnel, dynamic, private-vlan or even none --
691 // show in returned config and let user decide, if they
692 // want to fix device config or work around these ports
693 // by means of VST.
694 $work['portdata'][$work['current']['port_name']] = array
695 (
696 'mode' => 'none',
697 'allowed' => array(),
698 'native' => 0,
699 );
700 break;
701 }
702 if (isset ($work['portdata'][$work['current']['port_name']]))
703 $work['portdata'][$work['current']['port_name']]['config'] = $work['current']['config'];
704 unset ($work['current']);
705 return 'ios12ScanTopLevel';
706 }
707 // not yet
708 $matches = array();
709 $line_class = 'line-8021q';
710 switch (TRUE)
711 {
712 case (preg_match ('@^ switchport mode (.+)$@', $line, $matches)):
713 $work['current']['mode'] = $matches[1];
714 break;
715 case (preg_match ('@^ switchport access vlan (.+)$@', $line, $matches)):
716 $work['current']['access vlan'] = $matches[1];
717 break;
718 case (preg_match ('@^ switchport trunk native vlan (.+)$@', $line, $matches)):
719 $work['current']['trunk native vlan'] = $matches[1];
720 break;
721 case (preg_match ('@^ switchport trunk allowed vlan add (.+)$@', $line, $matches)):
722 $work['current']['trunk allowed vlan'] = array_merge
723 (
724 $work['current']['trunk allowed vlan'],
725 iosParseVLANString ($matches[1])
726 );
727 break;
728 case (preg_match ('@^ switchport trunk allowed vlan (.+)$@', $line, $matches)):
729 $work['current']['trunk allowed vlan'] = iosParseVLANString ($matches[1]);
730 break;
731 case preg_match ('@^ channel-group @', $line):
732 // port-channel subinterface config follows that of the master interface
733 $work['current']['mode'] = 'SKIP';
734 break;
735 case preg_match ('@^ ip address @', $line):
736 // L3 interface does no switchport functions
737 $work['current']['mode'] = 'IP';
738 break;
739 default: // suppress warning on irrelevant config clause
740 $line_class = 'line-other';
741 }
742 $work['current']['config'][] = array ('type' => $line_class, 'line' => $line);
743 return __FUNCTION__;
744 }
745
746 function ios12PickVLANCommand (&$work, $line)
747 {
748 $matches = array();
749 switch (TRUE)
750 {
751 case ($line == '---- -------------------------------- --------- -------------------------------'):
752 // ignore the rest of VLAN table header;
753 break;
754 case (preg_match ('@! END OF VLAN LIST$@', $line)):
755 return 'ios12ScanTopLevel';
756 case (preg_match ('@^([[:digit:]]+) {1,4}.{32} active @', $line, $matches)):
757 if (!array_key_exists ($matches[1], $work['vlanlist']))
758 $work['vlanlist'][] = $matches[1];
759 break;
760 default:
761 }
762 return __FUNCTION__;
763 }
764
765 // Another finite automata to read a dialect of Foundry configuration.
766 function fdry5ReadVLANConfig ($input)
767 {
768 $ret = array
769 (
770 'vlanlist' => array(),
771 'portdata' => array(),
772 );
773 $procfunc = 'fdry5ScanTopLevel';
774 foreach (explode ("\n", $input) as $line)
775 $procfunc = $procfunc ($ret, $line);
776 return $ret;
777 }
778
779 function fdry5ScanTopLevel (&$work, $line)
780 {
781 $matches = array();
782 switch (TRUE)
783 {
784 case (preg_match ('@^vlan ([[:digit:]]+)( name .+)? (by port)$@', $line, $matches)):
785 if (!array_key_exists ($matches[1], $work['vlanlist']))
786 $work['vlanlist'][] = $matches[1];
787 $work['current'] = array ('vlan_id' => $matches[1]);
788 return 'fdry5PickVLANSubcommand';
789 case (preg_match ('@^interface ethernet ([[:digit:]]+/[[:digit:]]+/[[:digit:]]+)$@', $line, $matches)):
790 $work['current'] = array ('port_name' => 'e' . $matches[1]);
791 return 'fdry5PickInterfaceSubcommand';
792 default:
793 return __FUNCTION__;
794 }
795 }
796
797 function fdry5PickVLANSubcommand (&$work, $line)
798 {
799 if ($line[0] != ' ') // end of VLAN section
800 {
801 unset ($work['current']);
802 return 'fdry5ScanTopLevel';
803 }
804 // not yet
805 $matches = array();
806 switch (TRUE)
807 {
808 case (preg_match ('@^ tagged (.+)$@', $line, $matches)):
809 // add current VLAN to 'allowed' list of each mentioned port
810 foreach (fdry5ParsePortString ($matches[1]) as $port_name)
811 if (array_key_exists ($port_name, $work['portdata']))
812 $work['portdata'][$port_name]['allowed'][] = $work['current']['vlan_id'];
813 else
814 $work['portdata'][$port_name] = array
815 (
816 'mode' => 'trunk',
817 'allowed' => array ($work['current']['vlan_id']),
818 'native' => 0, // can be updated later
819 );
820 $work['portdata'][$port_name]['mode'] = 'trunk';
821 break;
822 case (preg_match ('@^ untagged (.+)$@', $line, $matches)):
823 // replace 'native' column of each mentioned port with current VLAN ID
824 foreach (fdry5ParsePortString ($matches[1]) as $port_name)
825 {
826 if (array_key_exists ($port_name, $work['portdata']))
827 {
828 $work['portdata'][$port_name]['native'] = $work['current']['vlan_id'];
829 $work['portdata'][$port_name]['allowed'][] = $work['current']['vlan_id'];
830 }
831 else
832 $work['portdata'][$port_name] = array
833 (
834 'mode' => 'access',
835 'allowed' => array ($work['current']['vlan_id']),
836 'native' => $work['current']['vlan_id'],
837 );
838 // Untagged ports are initially assumed to be access ports, and
839 // when this assumption is right, this is the final port mode state.
840 // When the port is dual-mode one, this is detected and justified
841 // later in "interface" section of config text.
842 $work['portdata'][$port_name]['mode'] = 'access';
843 }
844 break;
845 default: // nom-nom
846 }
847 return __FUNCTION__;
848 }
849
850 function fdry5PickInterfaceSubcommand (&$work, $line)
851 {
852 if ($line[0] != ' ') // end of interface section
853 {
854 if (array_key_exists ('dual-mode', $work['current']))
855 {
856 if (array_key_exists ($work['current']['port_name'], $work['portdata']))
857 // update existing record
858 $work['portdata'][$work['current']['port_name']]['native'] = $work['current']['dual-mode'];
859 else
860 // add new
861 $work['portdata'][$work['current']['port_name']] = array
862 (
863 'allowed' => array ($work['current']['dual-mode']),
864 'native' => $work['current']['dual-mode'],
865 );
866 // a dual-mode port is always considered a trunk port
867 // (but not in the IronWare's meaning of "trunk") regardless of
868 // number of assigned tagged VLANs
869 $work['portdata'][$work['current']['port_name']]['mode'] = 'trunk';
870 }
871 unset ($work['current']);
872 return 'fdry5ScanTopLevel';
873 }
874 $matches = array();
875 switch (TRUE)
876 {
877 case (preg_match ('@^ dual-mode( +[[:digit:]]+ *)?$@', $line, $matches)):
878 // default VLAN ID for dual-mode command is 1
879 $work['current']['dual-mode'] = strlen (trim ($matches[1])) ? trim ($matches[1]) : 1;
880 break;
881 // FIXME: trunk/link-aggregate/ip address pulls port from 802.1Q field
882 default: // nom-nom
883 }
884 return __FUNCTION__;
885 }
886
887 function fdry5ParsePortString ($string)
888 {
889 $ret = array();
890 $tokens = explode (' ', trim ($string));
891 while (count ($tokens))
892 {
893 $letters = array_shift ($tokens); // "ethe", "to"
894 $numbers = array_shift ($tokens); // "x", "x/x", "x/x/x"
895 switch ($letters)
896 {
897 case 'ethe':
898 if ($prev_numbers != NULL)
899 $ret[] = 'e' . $prev_numbers;
900 $prev_numbers = $numbers;
901 break;
902 case 'to':
903 $ret = array_merge ($ret, fdry5GenPortRange ($prev_numbers, $numbers));
904 $prev_numbers = NULL; // no action on next token
905 break;
906 default: // ???
907 return array();
908 }
909 }
910 // flush delayed item
911 if ($prev_numbers != NULL)
912 $ret[] = 'e' . $prev_numbers;
913 return $ret;
914 }
915
916 // Take two indices in form "x", "x/x" or "x/x/x" and return the range of
917 // ports spanning from the first to the last. The switch software makes it
918 // easier to perform, because "ethe x/x/x to y/y/y" ranges never cross
919 // unit/slot boundary (every index except the last remains constant).
920 function fdry5GenPortRange ($from, $to)
921 {
922 $matches = array();
923 if (1 !== preg_match ('@^([[:digit:]]+/)?([[:digit:]]+/)?([[:digit:]]+)$@', $from, $matches))
924 return array();
925 $prefix = 'e' . $matches[1] . $matches[2];
926 $from_idx = $matches[3];
927 if (1 !== preg_match ('@^([[:digit:]]+/)?([[:digit:]]+/)?([[:digit:]]+)$@', $to, $matches))
928 return array();
929 $to_idx = $matches[3];
930 for ($i = $from_idx; $i <= $to_idx; $i++)
931 $ret[] = $prefix . $i;
932 return $ret;
933 }
934
935 // an implementation for Huawei syntax
936 function vrp53ReadVLANConfig ($input)
937 {
938 $ret = array
939 (
940 'vlanlist' => array(),
941 'portdata' => array(),
942 );
943 $procfunc = 'vrp53ScanTopLevel';
944 foreach (explode ("\n", $input) as $line)
945 $procfunc = $procfunc ($ret, $line);
946 return $ret;
947 }
948
949 function vrp53ScanTopLevel (&$work, $line)
950 {
951 $matches = array();
952 switch (TRUE)
953 {
954 case (preg_match ('@^ vlan batch (.+)$@', $line, $matches)):
955 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
956 $work['vlanlist'][] = $vlan_id;
957 return __FUNCTION__;
958 case (preg_match ('@^interface ((GigabitEthernet|XGigabitEthernet|Eth-Trunk)([[:digit:]]+(/[[:digit:]]+)*))$@', $line, $matches)):
959 $matches[1] = preg_replace ('@^GigabitEthernet(.+)$@', 'gi\\1', $matches[1]);
960 $matches[1] = preg_replace ('@^XGigabitEthernet(.+)$@', 'xg\\1', $matches[1]);
961 $matches[1] = preg_replace ('@^Eth-Trunk(.+)$@', 'et\\1', $matches[1]);
962 $work['current'] = array ('port_name' => $matches[1]);
963 $work['current']['config'][] = array ('type' => 'line-header', 'line' => $line);
964 return 'vrp53PickInterfaceSubcommand';
965 default:
966 return __FUNCTION__;
967 }
968 }
969
970 function vrp53ParseVLANString ($string)
971 {
972 $string = preg_replace ('/ to /', '-', $string);
973 $string = preg_replace ('/ /', ',', $string);
974 return iosParseVLANString ($string);
975 }
976
977 function vrp53PickInterfaceSubcommand (&$work, $line)
978 {
979 if ($line[0] == '#') // end of interface section
980 {
981 // Configuration Guide - Ethernet 3.3.4:
982 // "By default, the interface type is hybrid."
983 if (!array_key_exists ('link-type', $work['current']))
984 $work['current']['link-type'] = 'hybrid';
985 if (!array_key_exists ('allowed', $work['current']))
986 $work['current']['allowed'] = array();
987 if (!array_key_exists ('native', $work['current']))
988 $work['current']['native'] = 0;
989 switch ($work['current']['link-type'])
990 {
991 case 'access':
992 // VRP does not assign access ports to VLAN1 by default,
993 // leaving them blocked.
994 $work['portdata'][$work['current']['port_name']] =
995 $work['current']['native'] ? array
996 (
997 'allowed' => $work['current']['allowed'],
998 'native' => $work['current']['native'],
999 'mode' => 'access',
1000 ) : array
1001 (
1002 'mode' => 'none',
1003 'allowed' => array(),
1004 'native' => 0,
1005 );
1006 break;
1007 case 'trunk':
1008 $work['portdata'][$work['current']['port_name']] = array
1009 (
1010 'allowed' => $work['current']['allowed'],
1011 'native' => 0,
1012 'mode' => 'trunk',
1013 );
1014 break;
1015 case 'hybrid':
1016 $work['portdata'][$work['current']['port_name']] = array
1017 (
1018 'allowed' => $work['current']['allowed'],
1019 'native' => $work['current']['native'],
1020 'mode' => 'trunk',
1021 );
1022 break;
1023 default: // dot1q-tunnel ?
1024 }
1025 if (isset ($work['portdata'][$work['current']['port_name']]))
1026 $work['portdata'][$work['current']['port_name']]['config'] = $work['current']['config'];
1027 unset ($work['current']);
1028 return 'vrp53ScanTopLevel';
1029 }
1030 $matches = array();
1031 $line_class = 'line-8021q';
1032 switch (TRUE)
1033 {
1034 case (preg_match ('@^ port default vlan ([[:digit:]]+)$@', $line, $matches)):
1035 $work['current']['native'] = $matches[1];
1036 if (!array_key_exists ('allowed', $work['current']))
1037 $work['current']['allowed'] = array();
1038 if (!in_array ($matches[1], $work['current']['allowed']))
1039 $work['current']['allowed'][] = $matches[1];
1040 break;
1041 case (preg_match ('@^ port link-type (.+)$@', $line, $matches)):
1042 $work['current']['link-type'] = $matches[1];
1043 break;
1044 case (preg_match ('@^ port trunk allow-pass vlan (.+)$@', $line, $matches)):
1045 if (!array_key_exists ('allowed', $work['current']))
1046 $work['current']['allowed'] = array();
1047 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
1048 if (!in_array ($vlan_id, $work['current']['allowed']))
1049 $work['current']['allowed'][] = $vlan_id;
1050 break;
1051 // TODO: make sure, that a port with "eth-trunk" clause always ends up in "none" mode
1052 default: // nom-nom
1053 $line_class = 'line-other';
1054 }
1055 $work['current']['config'][] = array('type' => $line_class, 'line' => $line);
1056 return __FUNCTION__;
1057 }
1058
1059 function vrp55Read8021QConfig ($input)
1060 {
1061 $ret = array
1062 (
1063 'vlanlist' => array (1), // VRP 5.50 hides VLAN1 from config text
1064 'portdata' => array(),
1065 );
1066 foreach (explode ("\n", $input) as $line)
1067 {
1068 $matches = array();
1069 // top level
1070 if (!array_key_exists ('current', $ret))
1071 {
1072 switch (TRUE)
1073 {
1074 case (preg_match ('@^ vlan batch (.+)$@', $line, $matches)):
1075 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
1076 $ret['vlanlist'][] = $vlan_id;
1077 break;
1078 case (preg_match ('@^interface ((GigabitEthernet|XGigabitEthernet|Eth-Trunk)([[:digit:]]+(/[[:digit:]]+)*))$@', $line, $matches)):
1079 $matches[1] = preg_replace ('@^GigabitEthernet(.+)$@', 'gi\\1', $matches[1]);
1080 $matches[1] = preg_replace ('@^XGigabitEthernet(.+)$@', 'xg\\1', $matches[1]);
1081 $ret['current'] = array ('port_name' => $matches[1]);
1082 $ret['current']['config'][] = array ('type' => 'line-header', 'line' => $line);
1083 break;
1084 }
1085 continue;
1086 }
1087 // inside an interface block
1088 $line_class = 'line-8021q';
1089 switch (TRUE)
1090 {
1091 case preg_match ('/^ port (link-type )?hybrid /', $line):
1092 throw new RTGatewayError ("unsupported configuration: ${line}");
1093 case preg_match ('/^ port link-type (.+)$/', $line, $matches):
1094 $ret['current']['link-type'] = $matches[1];
1095 break;
1096 // Native VLAN is configured differently for each link-type case, but
1097 // VRP is known to filter off clauses, which don't make sense for
1098 // current link-type. This way any interface section should contain
1099 // only one kind of "set native" clause (but if this constraint breaks,
1100 // we get a problem).
1101 case preg_match ('/^ port (default|trunk pvid) vlan ([[:digit:]]+)$/', $line, $matches):
1102 $ret['current']['native'] = $matches[2];
1103 if (!array_key_exists ('allowed', $ret['current']))
1104 $ret['current']['allowed'] = array();
1105 if (!in_array ($ret['current']['native'], $ret['current']['allowed']))
1106 $ret['current']['allowed'][] = $ret['current']['native'];
1107 break;
1108 case preg_match ('/^ port trunk allow-pass vlan (.+)$/', $line, $matches):
1109 if (!array_key_exists ('allowed', $ret['current']))
1110 $ret['current']['allowed'] = array();
1111 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
1112 if (!in_array ($vlan_id, $ret['current']['allowed']))
1113 $ret['current']['allowed'][] = $vlan_id;
1114 break;
1115 case $line == ' undo portswitch':
1116 case preg_match ('/^ ip address /', $line):
1117 $ret['current']['link-type'] = 'IP';
1118 break;
1119 case preg_match ('/^ eth-trunk /', $line):
1120 $ret['current']['link-type'] = 'SKIP';
1121 break;
1122 case substr ($line, 0, 1) == '#': // end of interface section
1123 if (!array_key_exists ('link-type', $ret['current']))
1124 throw new RTGatewayError ('unsupported configuration: link-type is neither trunk nor access for ' . $ret['current']['port_name']);
1125 if (!array_key_exists ('allowed', $ret['current']))
1126 $ret['current']['allowed'] = array();
1127 if (!array_key_exists ('native', $ret['current']))
1128 $ret['current']['native'] = 0;
1129 switch ($ret['current']['link-type'])
1130 {
1131 case 'access':
1132 // In VRP 5.50 an access port has default VLAN ID == 1
1133 $ret['portdata'][$ret['current']['port_name']] =
1134 $ret['current']['native'] ? array
1135 (
1136 'mode' => 'access',
1137 'allowed' => $ret['current']['allowed'],
1138 'native' => $ret['current']['native'],
1139 ) : array
1140 (
1141 'mode' => 'access',
1142 'allowed' => array (VLAN_DFL_ID),
1143 'native' => VLAN_DFL_ID,
1144 );
1145 break;
1146 case 'trunk':
1147 $ret['portdata'][$ret['current']['port_name']] = array
1148 (
1149 'mode' => 'trunk',
1150 'allowed' => $ret['current']['allowed'],
1151 'native' => $ret['current']['native'],
1152 );
1153 break;
1154 case 'IP':
1155 $ret['portdata'][$ret['current']['port_name']] = array
1156 (
1157 'mode' => 'none',
1158 'allowed' => array(),
1159 'native' => 0,
1160 );
1161 break;
1162 case 'SKIP':
1163 default: // dot1q-tunnel ?
1164 }
1165 if (isset ($ret['portdata'][$ret['current']['port_name']]))
1166 $ret['portdata'][$ret['current']['port_name']]['config'] = $ret['current']['config'];
1167 unset ($ret['current']);
1168 continue 2;
1169 default: // nom-nom
1170 $line_class = 'line-other';
1171 }
1172 $ret['current']['config'][] = array ('type' => $line_class, 'line' => $line);
1173 }
1174 return $ret;
1175 }
1176
1177 function nxos4Read8021QConfig ($input)
1178 {
1179 $ret = array
1180 (
1181 'vlanlist' => array(),
1182 'portdata' => array(),
1183 );
1184 $procfunc = 'nxos4ScanTopLevel';
1185 foreach (explode ("\n", $input) as $line)
1186 $procfunc = $procfunc ($ret, $line);
1187 return $ret;
1188 }
1189
1190 function nxos4ScanTopLevel (&$work, $line)
1191 {
1192 $matches = array();
1193 switch (TRUE)
1194 {
1195 case (preg_match ('@^interface ((Ethernet|Port-channel)[[:digit:]]+(/[[:digit:]]+)*)$@i', $line, $matches)):
1196 $matches[1] = preg_replace ('@^Ethernet(.+)$@i', 'e\\1', $matches[1]);
1197 $matches[1] = preg_replace ('@^Port-channel(.+)$@i', 'po\\1', $matches[1]);
1198 $work['current'] = array ('port_name' => $matches[1]);
1199 $work['current']['config'][] = array ('type' => 'line-header', 'line' => $line);
1200 return 'nxos4PickSwitchportCommand';
1201 case (preg_match ('@^vlan (.+)$@', $line, $matches)):
1202 foreach (iosParseVLANString ($matches[1]) as $vlan_id)
1203 $work['vlanlist'][] = $vlan_id;
1204 return __FUNCTION__;
1205 default:
1206 return __FUNCTION__; // continue scan
1207 }
1208 }
1209
1210 function nxos4PickSwitchportCommand (&$work, $line)
1211 {
1212 if ($line == '') // end of interface section
1213 {
1214 // fill in defaults
1215 if (!array_key_exists ('mode', $work['current']))
1216 $work['current']['mode'] = 'access';
1217 // save work, if it makes sense
1218 switch ($work['current']['mode'])
1219 {
1220 case 'access':
1221 if (!array_key_exists ('access vlan', $work['current']))
1222 $work['current']['access vlan'] = 1;
1223 $work['portdata'][$work['current']['port_name']] = array
1224 (
1225 'mode' => 'access',
1226 'allowed' => array ($work['current']['access vlan']),
1227 'native' => $work['current']['access vlan'],
1228 );
1229 break;
1230 case 'trunk':
1231 if (!array_key_exists ('trunk native vlan', $work['current']))
1232 $work['current']['trunk native vlan'] = 1;
1233 // FIXME: NX-OS reserves VLANs 3968 through 4047 plus 4094 for itself
1234 if (!array_key_exists ('trunk allowed vlan', $work['current']))
1235 $work['current']['trunk allowed vlan'] = range (VLAN_MIN_ID, VLAN_MAX_ID);
1236 // Having configured VLAN as "native" doesn't mean anything
1237 // as long as it's not listed on the "allowed" line.
1238 $effective_native = in_array
1239 (
1240 $work['current']['trunk native vlan'],
1241 $work['current']['trunk allowed vlan']
1242 ) ? $work['current']['trunk native vlan'] : 0;
1243 $work['portdata'][$work['current']['port_name']] = array
1244 (
1245 'mode' => 'trunk',
1246 'allowed' => $work['current']['trunk allowed vlan'],
1247 'native' => $effective_native,
1248 );
1249 break;
1250 case 'SKIP':
1251 case 'fex-fabric': // associated port-channel
1252 break;
1253 default:
1254 // dot1q-tunnel, dynamic, private-vlan
1255 $work['portdata'][$work['current']['port_name']] = array
1256 (
1257 'mode' => 'none',
1258 'allowed' => array(),
1259 'native' => 0,
1260 );
1261 // unset (routed), dot1q-tunnel, dynamic, private-vlan --- skip these
1262 }
1263 if (isset ($work['portdata'][$work['current']['port_name']]))
1264 $work['portdata'][$work['current']['port_name']]['config'] = $work['current']['config'];
1265 unset ($work['current']);
1266 return 'nxos4ScanTopLevel';
1267 }
1268 // not yet
1269 $matches = array();
1270 $line_class = 'line-8021q';
1271 switch (TRUE)
1272 {
1273 case (preg_match ('@^ switchport mode (.+)$@', $line, $matches)):
1274 $work['current']['mode'] = $matches[1];
1275 break;
1276 case (preg_match ('@^ switchport access vlan (.+)$@', $line, $matches)):
1277 $work['current']['access vlan'] = $matches[1];
1278 break;
1279 case (preg_match ('@^ switchport trunk native vlan (.+)$@', $line, $matches)):
1280 $work['current']['trunk native vlan'] = $matches[1];
1281 break;
1282 case (preg_match ('@^ switchport trunk allowed vlan add (.+)$@', $line, $matches)):
1283 $work['current']['trunk allowed vlan'] = array_merge
1284 (
1285 $work['current']['trunk allowed vlan'],
1286 iosParseVLANString ($matches[1])
1287 );
1288 break;
1289 case (preg_match ('@^ switchport trunk allowed vlan (.+)$@', $line, $matches)):
1290 $work['current']['trunk allowed vlan'] = iosParseVLANString ($matches[1]);
1291 break;
1292 case preg_match ('/^ +channel-group /', $line):
1293 $work['current']['mode'] = 'SKIP';
1294 break;
1295 default: // suppress warning on irrelevant config clause
1296 $line_class = 'line-other';
1297 }
1298 $work['current']['config'][] = array ('type' => $line_class, 'line' => $line);
1299 return __FUNCTION__;
1300 }
1301
1302 // Get a list of VLAN management pseudo-commands and return a text
1303 // of real vendor-specific commands, which implement the work.
1304 // This work is done in two rounds:
1305 // 1. For "add allowed" and "rem allowed" commands detect continuous
1306 // sequences of VLAN IDs and replace them with ranges of form "A-B",
1307 // where B>A.
1308 // 2. Iterate over the resulting list and produce real CLI commands.
1309 function ios12TranslatePushQueue ($queue)
1310 {
1311 $ret = '';
1312 foreach ($queue as $cmd)
1313 switch ($cmd['opcode'])
1314 {
1315 case 'create VLAN':
1316 $ret .= "vlan ${cmd['arg1']}\nexit\n";
1317 break;
1318 case 'destroy VLAN':
1319 $ret .= "no vlan ${cmd['arg1']}\n";
1320 break;
1321 case 'add allowed':
1322 case 'rem allowed':
1323 $clause = $cmd['opcode'] == 'add allowed' ? 'add' : 'remove';
1324 $ret .= "interface ${cmd['port']}\n";
1325 foreach (listToRanges ($cmd['vlans']) as $range)
1326 $ret .= "switchport trunk allowed vlan ${clause} " .
1327 ($range['from'] == $range['to'] ? $range['to'] : "${range['from']}-${range['to']}") .
1328 "\n";
1329 $ret .= "exit\n";
1330 break;
1331 case 'set native':
1332 $ret .= "interface ${cmd['arg1']}\nswitchport trunk native vlan ${cmd['arg2']}\nexit\n";
1333 break;
1334 case 'unset native':
1335 $ret .= "interface ${cmd['arg1']}\nno switchport trunk native vlan ${cmd['arg2']}\nexit\n";
1336 break;
1337 case 'set access':
1338 $ret .= "interface ${cmd['arg1']}\nswitchport access vlan ${cmd['arg2']}\nexit\n";
1339 break;
1340 case 'unset access':
1341 $ret .= "interface ${cmd['arg1']}\nno switchport access vlan\nexit\n";
1342 break;
1343 case 'set mode':
1344 $ret .= "interface ${cmd['arg1']}\nswitchport mode ${cmd['arg2']}\n";
1345 if ($cmd['arg2'] == 'trunk')
1346 $ret .= "no switchport trunk native vlan\nswitchport trunk allowed vlan none\n";
1347 $ret .= "exit\n";
1348 break;
1349 case 'begin configuration':
1350 $ret .= "configure terminal\n";
1351 break;
1352 case 'end configuration':
1353 $ret .= "end\n";
1354 break;
1355 case 'save configuration':
1356 $ret .= "copy running-config startup-config\n\n";
1357 break;
1358 default:
1359 throw new InvalidArgException ('opcode', $cmd['opcode']);
1360 }
1361 return $ret;
1362 }
1363
1364 function fdry5TranslatePushQueue ($queue)
1365 {
1366 $ret = '';
1367 foreach ($queue as $cmd)
1368 switch ($cmd['opcode'])
1369 {
1370 case 'create VLAN':
1371 $ret .= "vlan ${cmd['arg1']}\nexit\n";
1372 break;
1373 case 'destroy VLAN':
1374 $ret .= "no vlan ${cmd['arg1']}\n";
1375 break;
1376 case 'add allowed':
1377 foreach ($cmd['vlans'] as $vlan_id)
1378 $ret .= "vlan ${vlan_id}\ntagged ${cmd['port']}\nexit\n";
1379 break;
1380 case 'rem allowed':
1381 foreach ($cmd['vlans'] as $vlan_id)
1382 $ret .= "vlan ${vlan_id}\nno tagged ${cmd['port']}\nexit\n";
1383 break;
1384 case 'set native':
1385 $ret .= "interface ${cmd['arg1']}\ndual-mode ${cmd['arg2']}\nexit\n";
1386 break;
1387 case 'unset native':
1388 $ret .= "interface ${cmd['arg1']}\nno dual-mode ${cmd['arg2']}\nexit\n";
1389 break;
1390 case 'set access':
1391 $ret .= "vlan ${cmd['arg2']}\nuntagged ${cmd['arg1']}\nexit\n";
1392 break;
1393 case 'unset access':
1394 $ret .= "vlan ${cmd['arg2']}\nno untagged ${cmd['arg1']}\nexit\n";
1395 break;
1396 case 'set mode': // NOP
1397 break;
1398 case 'begin configuration':
1399 $ret .= "conf t\n";
1400 break;
1401 case 'end configuration':
1402 $ret .= "end\n";
1403 break;
1404 case 'save configuration':
1405 $ret .= "write memory\n";
1406 break;
1407 default:
1408 throw new InvalidArgException ('opcode', $cmd['opcode']);
1409 }
1410 return $ret;
1411 }
1412
1413 function vrp53TranslatePushQueue ($queue)
1414 {
1415 $ret = '';
1416 foreach ($queue as $cmd)
1417 switch ($cmd['opcode'])
1418 {
1419 case 'create VLAN':
1420 $ret .= "vlan ${cmd['arg1']}\nquit\n";
1421 break;
1422 case 'destroy VLAN':
1423 $ret .= "undo vlan ${cmd['arg1']}\n";
1424 break;
1425 case 'add allowed':
1426 case 'rem allowed':
1427 $clause = $cmd['opcode'] == 'add allowed' ? '' : 'undo ';
1428 $ret .= "interface ${cmd['port']}\n";
1429 foreach (listToRanges ($cmd['vlans']) as $range)
1430 $ret .= "${clause}port trunk allow-pass vlan " .
1431 ($range['from'] == $range['to'] ? $range['to'] : "${range['from']} to ${range['to']}") .
1432 "\n";
1433 $ret .= "quit\n";
1434 break;
1435 case 'set native':
1436 case 'set access':
1437 $ret .= "interface ${cmd['arg1']}\nport default vlan ${cmd['arg2']}\nquit\n";
1438 break;
1439 case 'unset native':
1440 case 'unset access':
1441 $ret .= "interface ${cmd['arg1']}\nundo port default vlan\nquit\n";
1442 break;
1443 case 'set mode':
1444 $modemap = array ('access' => 'access', 'trunk' => 'hybrid');
1445 $ret .= "interface ${cmd['arg1']}\nport link-type " . $modemap[$cmd['arg2']] . "\n";
1446 if ($cmd['arg2'] == 'hybrid')
1447 $ret .= "undo port default vlan\nundo port trunk allow-pass vlan all\n";
1448 $ret .= "quit\n";
1449 break;
1450 case 'begin configuration':
1451 $ret .= "system-view\n";
1452 break;
1453 case 'end configuration':
1454 $ret .= "return\n";
1455 break;
1456 case 'save configuration':
1457 $ret .= "save\nY\n";
1458 break;
1459 default:
1460 throw new InvalidArgException ('opcode', $cmd['opcode']);
1461 }
1462 return $ret;
1463 }
1464
1465 function vrp55TranslatePushQueue ($queue)
1466 {
1467 $ret = '';
1468 foreach ($queue as $cmd)
1469 switch ($cmd['opcode'])
1470 {
1471 case 'create VLAN':
1472 if ($cmd['arg1'] != 1)
1473 $ret .= "vlan ${cmd['arg1']}\nquit\n";
1474 break;
1475 case 'destroy VLAN':
1476 if ($cmd['arg1'] != 1)
1477 $ret .= "undo vlan ${cmd['arg1']}\n";
1478 break;
1479 case 'add allowed':
1480 case 'rem allowed':
1481 $undo = $cmd['opcode'] == 'add allowed' ? '' : 'undo ';
1482 $ret .= "interface ${cmd['port']}\n";
1483 foreach (listToRanges ($cmd['vlans']) as $range)
1484 $ret .= "${undo}port trunk allow-pass vlan " .
1485 ($range['from'] == $range['to'] ? $range['to'] : "${range['from']} to ${range['to']}") .
1486 "\n";
1487 $ret .= "quit\n";
1488 break;
1489 case 'set native':
1490 $ret .= "interface ${cmd['arg1']}\nport trunk pvid vlan ${cmd['arg2']}\nquit\n";
1491 break;
1492 case 'set access':
1493 $ret .= "interface ${cmd['arg1']}\nport default vlan ${cmd['arg2']}\nquit\n";
1494 break;
1495 case 'unset native':
1496 $ret .= "interface ${cmd['arg1']}\nundo port trunk pvid vlan\nquit\n";
1497 break;
1498 case 'unset access':
1499 $ret .= "interface ${cmd['arg1']}\nundo port default vlan\nquit\n";
1500 break;
1501 case 'set mode':
1502 // VRP 5.50's meaning of "trunk" is much like the one of IOS
1503 // (unlike the way VRP 5.30 defines "trunk" and "hybrid"),
1504 // but it is necessary to undo configured VLANs on a port
1505 // for mode change command to succeed.
1506 $undo = array
1507 (
1508 'access' => "undo port trunk allow-pass vlan all\n" .
1509 "port trunk allow-pass vlan 1\n" .
1510 "undo port trunk pvid vlan\n",
1511 'trunk' => "undo port default vlan\n",
1512 );
1513 $ret .= "interface ${cmd['arg1']}\n" . $undo[$cmd['arg2']];
1514 $ret .= "port link-type ${cmd['arg2']}\nquit\n";
1515 break;
1516 case 'begin configuration':
1517 $ret .= "system-view\n";
1518 break;
1519 case 'end configuration':
1520 $ret .= "return\n";
1521 break;
1522 case 'save configuration':
1523 $ret .= "save\nY\n";
1524 break;
1525 default:
1526 throw new InvalidArgException ('opcode', $cmd['opcode']);
1527 }
1528 return $ret;
1529 }
1530
1531 function xos12TranslatePushQueue ($queue)
1532 {
1533 $ret = '';
1534 foreach ($queue as $cmd)
1535 switch ($cmd['opcode'])
1536 {
1537 case 'create VLAN':
1538 $ret .= "create vlan VLAN${cmd['arg1']}\n";
1539 $ret .= "configure vlan VLAN${cmd['arg1']} tag ${cmd['arg1']}\n";
1540 break;
1541 case 'destroy VLAN':
1542 $ret .= "delete vlan VLAN${cmd['arg1']}\n";
1543 break;
1544 case 'add allowed':
1545 foreach ($cmd['vlans'] as $vlan_id)
1546 {
1547 $vlan_name = $vlan_id == 1 ? 'Default' : "VLAN${vlan_id}";
1548 $ret .= "configure vlan ${vlan_name} add ports ${cmd['port']} tagged\n";
1549 }
1550 break;
1551 case 'rem allowed':
1552 foreach ($cmd['vlans'] as $vlan_id)
1553 {
1554 $vlan_name = $vlan_id == 1 ? 'Default' : "VLAN${vlan_id}";
1555 $ret .= "configure vlan ${vlan_name} delete ports ${cmd['port']}\n";
1556 }
1557 break;
1558 case 'set native':
1559 $vlan_name = $cmd['arg2'] == 1 ? 'Default' : "VLAN${cmd['arg2']}";
1560 $ret .= "configure vlan ${vlan_name} delete ports ${cmd['arg1']}\n";
1561 $ret .= "configure vlan ${vlan_name} add ports ${cmd['arg1']} untagged\n";
1562 break;
1563 case 'unset native':
1564 $vlan_name = $cmd['arg2'] == 1 ? 'Default' : "VLAN${cmd['arg2']}";
1565 $ret .= "configure vlan ${vlan_name} delete ports ${cmd['arg1']}\n";
1566 $ret .= "configure vlan ${vlan_name} add ports ${cmd['arg1']} tagged\n";
1567 break;
1568 case 'set access':
1569 $vlan_name = $cmd['arg2'] == 1 ? 'Default' : "VLAN${cmd['arg2']}";
1570 $ret .= "configure vlan ${vlan_name} add ports ${cmd['arg1']} untagged\n";
1571 break;
1572 case 'unset access':
1573 $vlan_name = $cmd['arg2'] == 1 ? 'Default' : "VLAN${cmd['arg2']}";
1574 $ret .= "configure vlan ${vlan_name} delete ports ${cmd['arg1']}\n";
1575 break;
1576 case 'set mode':
1577 case 'begin configuration':
1578 case 'end configuration':
1579 break; // NOP
1580 case 'save configuration':
1581 $ret .= "save configuration\ny\n";
1582 break;
1583 default:
1584 throw new InvalidArgException ('opcode', $cmd['opcode']);
1585 }
1586 return $ret;
1587 }
1588
1589 function xos12Read8021QConfig ($input)
1590 {
1591 $ret = array
1592 (
1593 'vlanlist' => array (1),
1594 'portdata' => array(),
1595 );
1596 foreach (explode ("\n", $input) as $line)
1597 {
1598 $matches = array();
1599 switch (TRUE)
1600 {
1601 case (preg_match ('/^create vlan "([[:alnum:]]+)"$/', $line, $matches)):
1602 if (!preg_match ('/^VLAN[[:digit:]]+$/', $matches[1]))
1603 throw new RTGatewayError ('unsupported VLAN name ' . $matches[1]);
1604 break;
1605 case (preg_match ('/^configure vlan ([[:alnum:]]+) tag ([[:digit:]]+)$/', $line, $matches)):
1606 if (strtolower ($matches[1]) == 'default')
1607 throw new RTGatewayError ('default VLAN tag must be 1');
1608 if ($matches[1] != 'VLAN' . $matches[2])
1609 throw new RTGatewayError ("VLAN name ${matches[1]} does not match its tag ${matches[2]}");
1610 $ret['vlanlist'][] = $matches[2];
1611 break;
1612 case (preg_match ('/^configure vlan ([[:alnum:]]+) add ports (.+) (tagged|untagged) */', $line, $matches)):
1613 $submatch = array();
1614 if ($matches[1] == 'Default')
1615 $matches[1] = 'VLAN1';
1616 if (!preg_match ('/^VLAN([[:digit:]]+)$/', $matches[1], $submatch))
1617 throw new RTGatewayError ('unsupported VLAN name ' . $matches[1]);
1618 $vlan_id = $submatch[1];
1619 foreach (iosParseVLANString ($matches[2]) as $port_name)
1620 {
1621 if (!array_key_exists ($port_name, $ret['portdata']))
1622 $ret['portdata'][$port_name] = array
1623 (
1624 'mode' => 'trunk',
1625 'allowed' => array(),
1626 'native' => 0,
1627 );
1628 $ret['portdata'][$port_name]['allowed'][] = $vlan_id;
1629 if ($matches[3] == 'untagged')
1630 $ret['portdata'][$port_name]['native'] = $vlan_id;
1631 }
1632 break;
1633 default:
1634 }
1635 }
1636 return $ret;
1637 }
1638
1639 function ciscoReadInterfaceStatus ($text)
1640 {
1641 $result = array();
1642 $state = 'headerSearch';
1643 foreach (explode ("\n", $text) as $line)
1644 {
1645 switch ($state)
1646 {
1647 case 'headerSearch':
1648 if (preg_match('/^Port\s+Name\s+Status/', $line))
1649 {
1650 $name_field_borders = getColumnCoordinates($line, 'Name');
1651 if (isset ($name_field_borders['from']))
1652 $state = 'readPort';
1653 }
1654 break;
1655 case 'readPort':
1656 $portname = ios12ShortenIfName (trim (substr ($line, 0, $name_field_borders['from'])));
1657 $rest = trim (substr ($line, $name_field_borders['from'] + $name_field_borders['length'] + 1));
1658 $field_list = preg_split('/\s+/', $rest);
1659 if (count ($field_list) < 4)
1660 break;
1661 list ($status_raw, $vlan, $duplex, $speed, $type) = $field_list;
1662 if ($status_raw == 'connected' || $status_raw == 'up')
1663 $status = 'up';
1664 elseif ($status_raw == 'notconnect' || $status_raw == 'down')
1665 $status = 'down';
1666 else
1667 $status = 'disabled';
1668 $result[$portname] = array
1669 (
1670 'status' => $status,
1671 'speed' => $speed,
1672 'duplex' => $duplex,
1673 );
1674 break;
1675 }
1676 }
1677 return $result;
1678 }
1679
1680 function vrpReadInterfaceStatus ($text)
1681 {
1682 $result = array();
1683 $state = 'headerSearch';
1684 foreach (explode ("\n", $text) as $line)
1685 {
1686 switch ($state)
1687 {
1688 case 'headerSearch':
1689 if (preg_match('/^Interface\s+Phy\w*\s+Protocol/i', $line))
1690 $state = 'readPort';
1691 break;
1692 case 'readPort':
1693 if (preg_match('/[\$><\]]/', $line))
1694 break 2;
1695 $field_list = preg_split('/\s+/', $line);
1696 if (count ($field_list) < 7)
1697 break;
1698 list ($portname, $status_raw) = $field_list;
1699 $portname = ios12ShortenIfName ($portname);
1700
1701 if ($status_raw == 'up' || $status_raw == 'down')
1702 $status = $status_raw;
1703 else
1704 $status = 'disabled';
1705 $result[$portname] = array
1706 (
1707 'status' => $status,
1708 );
1709 break;
1710 }
1711 }
1712 return $result;
1713 }
1714
1715 function maclist_sort ($a, $b)
1716 {
1717 if ($a['vid'] == $b['vid'])
1718 return 0;
1719 return ($a['vid'] < $b['vid']) ? -1 : 1;
1720 }
1721
1722 function ios12ReadMacList ($text)
1723 {
1724 $result = array();
1725 $state = 'headerSearch';
1726 foreach (explode ("\n", $text) as $line)
1727 {
1728 switch ($state)
1729 {
1730 case 'headerSearch':
1731 if (preg_match('/Vlan\s+Mac Address\s+Type.*Ports?\s*$/i', $line))
1732 $state = 'readPort';
1733 break;
1734 case 'readPort':
1735 if (! preg_match ('/(\d+)\s+([a-f0-9]{4}\.[a-f0-9]{4}\.[a-f0-9]{4})\s.*?(\S+)$/', trim ($line), $matches))
1736 break;
1737 $portname = ios12ShortenIfName ($matches[3]);
1738 $result[$portname][] = array
1739 (
1740 'mac' => $matches[2],
1741 'vid' => $matches[1],
1742 );
1743 break;
1744 }
1745 }
1746 foreach ($result as $portname => &$maclist)
1747 usort ($maclist, 'maclist_sort');
1748 return $result;
1749 }
1750
1751 function nxos4ReadMacList ($text)
1752 {
1753 $result = array();
1754 $state = 'headerSearch';
1755 foreach (explode ("\n", $text) as $line)
1756 {
1757 switch ($state)
1758 {
1759 case 'headerSearch':
1760 if (preg_match('/VLAN\s+MAC Address\s+Type\s+age\s+Secure\s+NTFY\s+Ports/i', $line))
1761 $state = 'readPort';
1762 break;
1763 case 'readPort':
1764 if (! preg_match ('/(\d+)\s+([a-f0-9]{4}\.[a-f0-9]{4}\.[a-f0-9]{4})\s.*?(\S+)$/', trim ($line), $matches))
1765 break;
1766 $portname = ios12ShortenIfName ($matches[3]);
1767 $result[$portname][] = array
1768 (
1769 'mac' => $matches[2],
1770 'vid' => $matches[1],
1771 );
1772 break;
1773 }
1774 }
1775 foreach ($result as $portname => &$maclist)
1776 usort ($maclist, 'maclist_sort');
1777 return $result;
1778 }
1779
1780 function vrp53ReadMacList ($text)
1781 {
1782 $result = array();
1783 $state = 'headerSearch';
1784 foreach (explode ("\n", $text) as $line)
1785 {
1786 switch ($state)
1787 {
1788 case 'headerSearch':
1789 if (preg_match('/MAC Address\s+VLAN\/VSI\s+Port/i', $line))
1790 $state = 'readPort';
1791 break;
1792 case 'readPort':
1793 if (! preg_match ('/([a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4})\s+(\d+)\s+(\S+)/', trim ($line), $matches))
1794 break;
1795 $portname = ios12ShortenIfName ($matches[3]);
1796 $result[$portname][] = array
1797 (
1798 'mac' => str_replace ('-', '.', $matches[1]),
1799 'vid' => $matches[2],
1800 );
1801 break;
1802 }
1803 }
1804 foreach ($result as $portname => &$maclist)
1805 usort ($maclist, 'maclist_sort');
1806 return $result;
1807 }
1808
1809 function vrp55ReadMacList ($text)
1810 {
1811 $result = array();
1812 $state = 'headerSearch';
1813 foreach (explode ("\n", $text) as $line)
1814 {
1815 switch ($state)
1816 {
1817 case 'headerSearch':
1818 if (preg_match('/MAC Address\s+VLAN\/\S*\s+PEVLAN\s+CEVLAN\s+Port/i', $line))
1819 $state = 'readPort';
1820 break;
1821 case 'readPort':
1822 if (! preg_match ('/([a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4})\s+(\d+)(?:\s+\S+){2}\s+(\S+)/', trim ($line), $matches))
1823 break;
1824 $portname = ios12ShortenIfName ($matches[3]);
1825 $result[$portname][] = array
1826 (
1827 'mac' => str_replace ('-', '.', $matches[1]),
1828 'vid' => $matches[2],
1829 );
1830 break;
1831 }
1832 }
1833 foreach ($result as $portname => &$maclist)
1834 usort ($maclist, 'maclist_sort');
1835 return $result;
1836 }
1837
1838 ?>