r3714 gwSendFile(): throw exception in case of error, employ file_put_contents()...
[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']['ios12'] = 'ios12ReadCDPStatus';
19 $gwrxlator['get8021q'] = array
20 (
21 'ios12' => 'ios12ReadVLANConfig',
22 'fdry5' => 'fdry5ReadVLANConfig',
23 'vrp53' => 'vrp53ReadVLANConfig',
24 'nxos4' => 'nxos4Read8021QConfig',
25 );
26
27 // This function launches specified gateway with specified
28 // command-line arguments and feeds it with the commands stored
29 // in the second arg as array.
30 // The answers are stored in another array, which is returned
31 // by this function. In the case when a gateway cannot be found,
32 // finishes prematurely or exits with non-zero return code,
33 // a single-item array is returned with the only "ERR" record,
34 // which explains the reason.
35 function queryGateway ($gwname, $questions)
36 {
37 $execpath = "./gateways/{$gwname}/main";
38 $dspec = array
39 (
40 0 => array ("pipe", "r"),
41 1 => array ("pipe", "w"),
42 2 => array ("file", "/dev/null", "a")
43 );
44 $pipes = array();
45 $gateway = proc_open ($execpath, $dspec, $pipes);
46 if (!is_resource ($gateway))
47 return array ('ERR proc_open() failed in ' . __FUNCTION__);
48
49 // Dialogue starts. Send all questions.
50 foreach ($questions as $q)
51 fwrite ($pipes[0], "$q\n");
52 fclose ($pipes[0]);
53
54 // Fetch replies.
55 $answers = array ();
56 while (!feof($pipes[1]))
57 {
58 $a = fgets ($pipes[1]);
59 if (!strlen ($a))
60 continue;
61 // Somehow I got a space appended at the end. Kick it.
62 $answers[] = trim ($a);
63 }
64 fclose($pipes[1]);
65
66 $retval = proc_close ($gateway);
67 if ($retval != 0)
68 throw new Exception ("gateway failed with code ${retval}", E_GW_FAILURE);
69 if (!count ($answers))
70 throw new Exception ('no response from gateway', E_GW_FAILURE);
71 if (count ($answers) != count ($questions))
72 throw new Exception ('protocol violation', E_GW_FAILURE);
73 foreach ($answers as $a)
74 if (strpos ($a, 'OK!') !== 0)
75 throw new Exception ("subcommand failed with status: ${a}", E_GW_FAILURE);
76 return $answers;
77 }
78
79 // This functions returns an array for VLAN list, and an array for port list (both
80 // form another array themselves) and another one with MAC address list.
81 // The ports in the latter array are marked with either VLAN ID or 'trunk'.
82 // We don't sort the port list, as the gateway is believed to have done this already
83 // (or at least the underlying switch software ought to). This is important, as the
84 // port info is transferred to/from form not by names, but by numbers.
85 function getSwitchVLANs ($object_id = 0)
86 {
87 global $remote_username;
88 $objectInfo = spotEntity ('object', $object_id);
89 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
90 if (count ($endpoints) == 0)
91 throw new Exception ('no management address set', E_GW_FAILURE);
92 if (count ($endpoints) > 1)
93 throw new Exception ('cannot pick management address', E_GW_FAILURE);
94 $hwtype = $swtype = 'unknown';
95 foreach (getAttrValues ($object_id) as $record)
96 {
97 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
98 $swtype = str_replace (' ', '+', execGMarker ($record['o_value']));
99 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
100 $hwtype = str_replace (' ', '+', execGMarker ($record['o_value']));
101 }
102 $endpoint = str_replace (' ', '+', $endpoints[0]);
103 $commands = array
104 (
105 "connect ${endpoint} ${hwtype} ${swtype} ${remote_username}",
106 'listvlans',
107 'listports',
108 'listmacs'
109 );
110 $data = queryGateway ('switchvlans', $commands);
111 if (strpos ($data[0], 'OK!') !== 0)
112 throw new Exception ("gateway failed with status: ${data[0]}.", E_GW_FAILURE);
113 // Now we have VLAN list in $data[1] and port list in $data[2]. Let's sort this out.
114 $tmp = array_unique (explode (';', substr ($data[1], strlen ('OK!'))));
115 if (count ($tmp) == 0)
116 throw new Exception ('gateway returned no records', E_GW_FAILURE);
117 $vlanlist = array();
118 foreach ($tmp as $record)
119 {
120 list ($vlanid, $vlandescr) = explode ('=', $record);
121 $vlanlist[$vlanid] = $vlandescr;
122 }
123 $portlist = array();
124 foreach (explode (';', substr ($data[2], strlen ('OK!'))) as $pair)
125 {
126 list ($portname, $pair2) = explode ('=', $pair);
127 list ($status, $vlanid) = explode (',', $pair2);
128 $portlist[] = array ('portname' => $portname, 'status' => $status, 'vlanid' => $vlanid);
129 }
130 if (count ($portlist) == 0)
131 throw new Exception ('gateway returned no records', E_GW_FAILURE);
132 $maclist = array();
133 foreach (explode (';', substr ($data[3], strlen ('OK!'))) as $pair)
134 {
135 list ($macaddr, $pair2) = explode ('=', $pair);
136 if (!strlen ($pair2))
137 continue;
138 list ($vlanid, $ifname) = explode ('@', $pair2);
139 $maclist[$ifname][$vlanid][] = $macaddr;
140 }
141 return array ($vlanlist, $portlist, $maclist);
142 }
143
144 function setSwitchVLANs ($object_id = 0, $setcmd)
145 {
146 global $remote_username;
147 if ($object_id <= 0)
148 return oneLiner (160); // invalid arguments
149 $objectInfo = spotEntity ('object', $object_id);
150 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
151 if (count ($endpoints) == 0)
152 return oneLiner (161); // endpoint not found
153 if (count ($endpoints) > 1)
154 return oneLiner (162); // can't pick an address
155 $hwtype = $swtype = 'unknown';
156 foreach (getAttrValues ($object_id) as $record)
157 {
158 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
159 $swtype = strtr (execGMarker ($record['o_value']), ' ', '+');
160 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
161 $hwtype = strtr (execGMarker ($record['o_value']), ' ', '+');
162 }
163 $endpoint = str_replace (' ', '+', $endpoints[0]);
164 $data = queryGateway
165 (
166 'switchvlans',
167 array ("connect ${endpoint} ${hwtype} ${swtype} ${remote_username}", $setcmd)
168 );
169 // Finally we can parse the response into message array.
170 $log_m = array();
171 foreach (explode (';', substr ($data[1], strlen ('OK!'))) as $text)
172 {
173 if (strpos ($text, 'C!') === 0)
174 {
175 $tmp = explode ('!', $text);
176 array_shift ($tmp);
177 $code = array_shift ($tmp);
178 $log_m[] = count ($tmp) ? array ('c' => $code, 'a' => $tmp) : array ('c' => $code); // gateway-encoded message
179 }
180 elseif (strpos ($text, 'I!') === 0)
181 $log_m[] = array ('c' => 62, 'a' => array (substr ($text, 2))); // generic gateway success
182 elseif (strpos ($text, 'W!') === 0)
183 $log_m[] = array ('c' => 202, 'a' => array (substr ($text, 2))); // generic gateway warning
184 else // All improperly formatted messages must be treated as error conditions.
185 $log_m[] = array ('c' => 166, 'a' => array (substr ($text, 2))); // generic gateway error
186 }
187 return $log_m;
188 }
189
190 // Drop a file off RackTables platform. The gateway will catch the file and pass it to the given
191 // installer script.
192 function gwSendFile ($endpoint, $handlername, $filetext = array())
193 {
194 global $remote_username;
195 $tmpnames = array();
196 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
197 $command = "submit ${remote_username} ${endpoint} ${handlername}";
198 foreach ($filetext as $text)
199 {
200 $name = tempnam ('', 'RackTables-sendfile-');
201 $tmpnames[] = $name;
202 if (FALSE === $name or FALSE === file_put_contents ($name, $text))
203 {
204 foreach ($tmpnames as $name)
205 unlink ($name);
206 throw new Exception ('failed to write to temporary file', E_GW_FAILURE);
207 }
208 $command .= " ${name}";
209 }
210 $outputlines = queryGateway
211 (
212 'sendfile',
213 array ($command)
214 );
215 foreach ($tmpnames as $name)
216 unlink ($name);
217 }
218
219 // Query something through a gateway and get some text in return. Return that text.
220 function gwRecvFile ($endpoint, $handlername, &$output)
221 {
222 global $remote_username;
223 $tmpfilename = tempnam ('', 'RackTables-sendfile-');
224 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
225 $outputlines = queryGateway
226 (
227 'sendfile',
228 array ("submit ${remote_username} ${endpoint} ${handlername} ${tmpfilename}")
229 );
230 $output = file_get_contents ($tmpfilename);
231 unlink ($tmpfilename);
232 // Being here means having 'OK!' in the response.
233 return oneLiner (66, array ($handlername)); // ignore provided "Ok" text
234 }
235
236 function gwSendFileToObject ($object_id = 0, $handlername, $filetext = '')
237 {
238 $objectInfo = spotEntity ('object', $object_id);
239 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
240 if (count ($endpoints) == 0)
241 throw new Exception ('no management address set', E_GW_FAILURE);
242 if (count ($endpoints) > 1)
243 throw new Exception ('cannot pick management address', E_GW_FAILURE);
244 gwSendFile (str_replace (' ', '+', $endpoints[0]), $handlername, array ($filetext));
245 }
246
247 function gwRecvFileFromObject ($object_id = 0, $handlername, &$output)
248 {
249 global $remote_username;
250 if ($object_id <= 0 or !strlen ($handlername))
251 return oneLiner (160); // invalid arguments
252 $objectInfo = spotEntity ('object', $object_id);
253 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
254 if (count ($endpoints) == 0)
255 return oneLiner (161); // endpoint not found
256 if (count ($endpoints) > 1)
257 return oneLiner (162); // can't pick an address
258 $endpoint = str_replace (' ', '+', $endpoints[0]);
259 return gwRecvFile ($endpoint, $handlername, $output);
260 }
261
262 function detectDeviceBreed ($object_id)
263 {
264 foreach (getAttrValues ($object_id) as $record)
265 {
266 if
267 (
268 $record['name'] == 'SW type' &&
269 strlen ($record['o_value']) &&
270 preg_match ('/^Cisco IOS 12\./', execGMarker ($record['o_value']))
271 )
272 return 'ios12';
273 if
274 (
275 $record['name'] == 'SW type' &&
276 strlen ($record['o_value']) &&
277 preg_match ('/^Cisco NX-OS 4\./', execGMarker ($record['o_value']))
278 )
279 return 'nxos4';
280 if
281 (
282 $record['name'] == 'HW type' &&
283 strlen ($record['o_value']) &&
284 preg_match ('/^Foundry FastIron GS /', execGMarker ($record['o_value']))
285 )
286 return 'fdry5';
287 if
288 (
289 $record['name'] == 'HW type' &&
290 strlen ($record['o_value']) &&
291 preg_match ('/^Huawei Quidway S53/', execGMarker ($record['o_value']))
292 )
293 return 'vrp53';
294 }
295 return '';
296 }
297
298 function getRunning8021QConfig ($object_id)
299 {
300 $ret = gwRetrieveDeviceConfig ($object_id, 'get8021q');
301 // Once there is no default VLAN in the parsed data, it means
302 // something else was parsed instead of config text.
303 if (!in_array (VLAN_DFL_ID, $ret['vlanlist']))
304 throw new Exception ('communication with device failed', E_GW_FAILURE);
305 return $ret;
306 }
307
308 function getRunningCDPStatus ($object_id)
309 {
310 return gwRetrieveDeviceConfig ($object_id, 'getcdpstatus');
311 }
312
313 function setDevice8021QConfig ($object_id, $pseudocode)
314 {
315 if ('' == $breed = detectDeviceBreed ($object_id))
316 throw new Exception ('device breed unknown', E_GW_FAILURE);
317 $xlator = array
318 (
319 'ios12' => 'ios12TranslatePushQueue',
320 'fdry5' => 'fdry5TranslatePushQueue',
321 'vrp53' => 'vrp53TranslatePushQueue',
322 'nxos4' => 'ios12TranslatePushQueue', // employ syntax compatibility
323 );
324 gwDeployDeviceConfig ($object_id, $breed, unix2dos ($xlator[$breed] ($pseudocode)));
325 }
326
327 function gwRetrieveDeviceConfig ($object_id, $command)
328 {
329 global $gwrxlator;
330 if (!array_key_exists ($command, $gwrxlator))
331 throw new Exception ('command unknown', E_GW_FAILURE);
332 $breed = detectDeviceBreed ($object_id);
333 if (!array_key_exists ($breed, $gwrxlator[$command]))
334 throw new Exception ('device breed unknown', E_GW_FAILURE);
335 $objectInfo = spotEntity ('object', $object_id);
336 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
337 if (count ($endpoints) == 0)
338 throw new Exception ('no management address set', E_GW_FAILURE);
339 if (count ($endpoints) > 1)
340 throw new Exception ('cannot pick management address', E_GW_FAILURE);
341 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
342 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
343 $outputlines = queryGateway
344 (
345 'deviceconfig',
346 array ("${command} ${endpoint} ${breed} ${tmpfilename}")
347 );
348 $configtext = dos2unix (file_get_contents ($tmpfilename));
349 unlink ($tmpfilename);
350 // Being here means it was alright.
351 return $gwrxlator[$command][$breed] ($configtext);
352 }
353
354 function gwDeployDeviceConfig ($object_id, $breed, $text)
355 {
356 $objectInfo = spotEntity ('object', $object_id);
357 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
358 if (count ($endpoints) == 0)
359 throw new Exception ('no management address set', E_GW_FAILURE);
360 if (count ($endpoints) > 1)
361 throw new Exception ('cannot pick management address', E_GW_FAILURE);
362 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
363 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
364 if (FALSE === file_put_contents ($tmpfilename, $text))
365 {
366 unlink ($tmpfilename);
367 throw new Exception ('failed to write to temporary file', E_GW_FAILURE);
368 }
369 $outputlines = queryGateway
370 (
371 'deviceconfig',
372 array ("deploy ${endpoint} ${breed} ${tmpfilename}")
373 );
374 unlink ($tmpfilename);
375 }
376
377 // Read provided output of "show cdp neighbors detail" command and
378 // return a list of records with (translated) local port name,
379 // remote device name and (translated) remote port name.
380 function ios12ReadCDPStatus ($input)
381 {
382 $ret = array();
383 $procfunc = 'ios12ScanCDPTopLevel';
384 foreach (explode ("\n", $input) as $line)
385 $procfunc = $procfunc ($ret, $line);
386 return $ret;
387 }
388
389 function ios12ReadVLANConfig ($input)
390 {
391 $ret = array
392 (
393 'vlanlist' => array(),
394 'portdata' => array(),
395 );
396 $procfunc = 'ios12ScanTopLevel';
397 foreach (explode ("\n", $input) as $line)
398 $procfunc = $procfunc ($ret, $line);
399 return $ret;
400 }
401
402 function ios12ScanTopLevel (&$work, $line)
403 {
404 $matches = array();
405 switch (TRUE)
406 {
407 case (preg_match ('@^interface ((Ethernet|FastEthernet|GigabitEthernet|TenGigabitEthernet|Port-channel)[[:digit:]]+(/[[:digit:]]+)*)$@', $line, $matches)):
408 $work['current'] = array ('port_name' => ios12ShortenIfName ($matches[1]));
409 return 'ios12PickSwitchportCommand'; // switch to interface block reading
410 case (preg_match ('/^VLAN Name Status Ports$/', $line, $matches)):
411 return 'ios12PickVLANCommand';
412 default:
413 return __FUNCTION__; // continue scan
414 }
415 }
416
417 function ios12PickSwitchportCommand (&$work, $line)
418 {
419 if ($line[0] != ' ') // end of interface section
420 {
421 // save work, if it makes sense
422 switch (TRUE)
423 {
424 case $work['current']['ignore']:
425 $work['portdata'][$work['current']['port_name']] = array
426 (
427 'mode' => 'none',
428 'allowed' => array(),
429 'native' => 0,
430 );
431 break;
432 case 'access' == $work['current']['mode']:
433 if (!array_key_exists ('access vlan', $work['current']))
434 $work['current']['access vlan'] = 1;
435 $work['portdata'][$work['current']['port_name']] = array
436 (
437 'mode' => 'access',
438 'allowed' => array ($work['current']['access vlan']),
439 'native' => $work['current']['access vlan'],
440 );
441 break;
442 case 'trunk' == $work['current']['mode']:
443 if (!array_key_exists ('trunk native vlan', $work['current']))
444 $work['current']['trunk native vlan'] = 1;
445 if (!array_key_exists ('trunk allowed vlan', $work['current']))
446 $work['current']['trunk allowed vlan'] = range (VLAN_MIN_ID, VLAN_MAX_ID);
447 // Having configured VLAN as "native" doesn't mean anything
448 // as long as it's not listed on the "allowed" line.
449 $effective_native = in_array
450 (
451 $work['current']['trunk native vlan'],
452 $work['current']['trunk allowed vlan']
453 ) ? $work['current']['trunk native vlan'] : 0;
454 $work['portdata'][$work['current']['port_name']] = array
455 (
456 'mode' => 'trunk',
457 'allowed' => $work['current']['trunk allowed vlan'],
458 'native' => $effective_native,
459 );
460 break;
461 default:
462 // dot1q-tunnel, dynamic, private-vlan or even none --
463 // show in returned config and let user decide, if they
464 // want to fix device config or work around these ports
465 // by means of VST.
466 $work['portdata'][$work['current']['port_name']] = array
467 (
468 'mode' => 'none',
469 'allowed' => array(),
470 'native' => 0,
471 );
472 break;
473 }
474 unset ($work['current']);
475 return 'ios12ScanTopLevel';
476 }
477 // not yet
478 $matches = array();
479 switch (TRUE)
480 {
481 case (preg_match ('@^ switchport mode (.+)$@', $line, $matches)):
482 $work['current']['mode'] = $matches[1];
483 break;
484 case (preg_match ('@^ switchport access vlan (.+)$@', $line, $matches)):
485 $work['current']['access vlan'] = $matches[1];
486 break;
487 case (preg_match ('@^ switchport trunk native vlan (.+)$@', $line, $matches)):
488 $work['current']['trunk native vlan'] = $matches[1];
489 break;
490 case (preg_match ('@^ switchport trunk allowed vlan add (.+)$@', $line, $matches)):
491 $work['current']['trunk allowed vlan'] = array_merge
492 (
493 $work['current']['trunk allowed vlan'],
494 iosParseVLANString ($matches[1])
495 );
496 break;
497 case (preg_match ('@^ switchport trunk allowed vlan (.+)$@', $line, $matches)):
498 $work['current']['trunk allowed vlan'] = iosParseVLANString ($matches[1]);
499 break;
500 case preg_match ('@^ channel-group @', $line):
501 // port-channel subinterface config follows that of the master interface
502 case preg_match ('@^ ip address @', $line):
503 // L3 interface does no switchport functions
504 $work['current']['ignore'] = TRUE;
505 break;
506 default: // suppress warning on irrelevant config clause
507 }
508 return __FUNCTION__;
509 }
510
511 function ios12PickVLANCommand (&$work, $line)
512 {
513 $matches = array();
514 switch (TRUE)
515 {
516 case ($line == '---- -------------------------------- --------- -------------------------------'):
517 // ignore the rest of VLAN table header;
518 break;
519 case (preg_match ('@! END OF VLAN LIST$@', $line)):
520 return 'ios12ScanTopLevel';
521 case (preg_match ('@^([[:digit:]]+) {1,4}.{32} active @', $line, $matches)):
522 if (!array_key_exists ($matches[1], $work['vlanlist']))
523 $work['vlanlist'][] = $matches[1];
524 break;
525 default:
526 }
527 return __FUNCTION__;
528 }
529
530 // Another finite automata to read a dialect of Foundry configuration.
531 function fdry5ReadVLANConfig ($input)
532 {
533 $ret = array
534 (
535 'vlanlist' => array(),
536 'portdata' => array(),
537 );
538 $procfunc = 'fdry5ScanTopLevel';
539 foreach (explode ("\n", $input) as $line)
540 $procfunc = $procfunc ($ret, $line);
541 return $ret;
542 }
543
544 function fdry5ScanTopLevel (&$work, $line)
545 {
546 $matches = array();
547 switch (TRUE)
548 {
549 case (preg_match ('@^vlan ([[:digit:]]+)( name .+)? (by port)$@', $line, $matches)):
550 if (!array_key_exists ($matches[1], $work['vlanlist']))
551 $work['vlanlist'][] = $matches[1];
552 $work['current'] = array ('vlan_id' => $matches[1]);
553 return 'fdry5PickVLANSubcommand';
554 case (preg_match ('@^interface ethernet ([[:digit:]]+/[[:digit:]]+/[[:digit:]]+)$@', $line, $matches)):
555 $work['current'] = array ('port_name' => 'e' . $matches[1]);
556 return 'fdry5PickInterfaceSubcommand';
557 default:
558 return __FUNCTION__;
559 }
560 }
561
562 function fdry5PickVLANSubcommand (&$work, $line)
563 {
564 if ($line[0] != ' ') // end of VLAN section
565 {
566 unset ($work['current']);
567 return 'fdry5ScanTopLevel';
568 }
569 // not yet
570 $matches = array();
571 switch (TRUE)
572 {
573 case (preg_match ('@^ tagged (.+)$@', $line, $matches)):
574 // add current VLAN to 'allowed' list of each mentioned port
575 foreach (fdry5ParsePortString ($matches[1]) as $port_name)
576 if (array_key_exists ($port_name, $work['portdata']))
577 $work['portdata'][$port_name]['allowed'][] = $work['current']['vlan_id'];
578 else
579 $work['portdata'][$port_name] = array
580 (
581 'mode' => 'trunk',
582 'allowed' => array ($work['current']['vlan_id']),
583 'native' => 0, // can be updated later
584 );
585 $work['portdata'][$port_name]['mode'] = 'trunk';
586 break;
587 case (preg_match ('@^ untagged (.+)$@', $line, $matches)):
588 // replace 'native' column of each mentioned port with current VLAN ID
589 foreach (fdry5ParsePortString ($matches[1]) as $port_name)
590 {
591 if (array_key_exists ($port_name, $work['portdata']))
592 {
593 $work['portdata'][$port_name]['native'] = $work['current']['vlan_id'];
594 $work['portdata'][$port_name]['allowed'][] = $work['current']['vlan_id'];
595 }
596 else
597 $work['portdata'][$port_name] = array
598 (
599 'mode' => 'access',
600 'allowed' => array ($work['current']['vlan_id']),
601 'native' => $work['current']['vlan_id'],
602 );
603 // Untagged ports are initially assumed to be access ports, and
604 // when this assumption is right, this is the final port mode state.
605 // When the port is dual-mode one, this is detected and justified
606 // later in "interface" section of config text.
607 $work['portdata'][$port_name]['mode'] = 'access';
608 }
609 break;
610 default: // nom-nom
611 }
612 return __FUNCTION__;
613 }
614
615 function fdry5PickInterfaceSubcommand (&$work, $line)
616 {
617 if ($line[0] != ' ') // end of interface section
618 {
619 if (array_key_exists ('dual-mode', $work['current']))
620 {
621 if (array_key_exists ($work['current']['port_name'], $work['portdata']))
622 // update existing record
623 $work['portdata'][$work['current']['port_name']]['native'] = $work['current']['dual-mode'];
624 else
625 // add new
626 $work['portdata'][$work['current']['port_name']] = array
627 (
628 'allowed' => array ($work['current']['dual-mode']),
629 'native' => $work['current']['dual-mode'],
630 );
631 // a dual-mode port is always considered a trunk port
632 // (but not in the IronWare's meaning of "trunk") regardless of
633 // number of assigned tagged VLANs
634 $work['portdata'][$work['current']['port_name']]['mode'] = 'trunk';
635 }
636 unset ($work['current']);
637 return 'fdry5ScanTopLevel';
638 }
639 $matches = array();
640 switch (TRUE)
641 {
642 case (preg_match ('@^ dual-mode( +[[:digit:]]+ *)?$@', $line, $matches)):
643 // default VLAN ID for dual-mode command is 1
644 $work['current']['dual-mode'] = strlen (trim ($matches[1])) ? trim ($matches[1]) : 1;
645 break;
646 // FIXME: trunk/link-aggregate/ip address pulls port from 802.1Q field
647 default: // nom-nom
648 }
649 return __FUNCTION__;
650 }
651
652 function fdry5ParsePortString ($string)
653 {
654 $ret = array();
655 $tokens = explode (' ', trim ($string));
656 while (count ($tokens))
657 {
658 $letters = array_shift ($tokens); // "ethe", "to"
659 $numbers = array_shift ($tokens); // "x", "x/x", "x/x/x"
660 switch ($letters)
661 {
662 case 'ethe':
663 if ($prev_numbers != NULL)
664 $ret[] = 'e' . $prev_numbers;
665 $prev_numbers = $numbers;
666 break;
667 case 'to':
668 $ret = array_merge ($ret, fdry5GenPortRange ($prev_numbers, $numbers));
669 $prev_numbers = NULL; // no action on next token
670 break;
671 default: // ???
672 return array();
673 }
674 }
675 // flush delayed item
676 if ($prev_numbers != NULL)
677 $ret[] = 'e' . $prev_numbers;
678 return $ret;
679 }
680
681 // Take two indices in form "x", "x/x" or "x/x/x" and return the range of
682 // ports spanning from the first to the last. The switch software makes it
683 // easier to perform, because "ethe x/x/x to y/y/y" ranges never cross
684 // unit/slot boundary (every index except the last remains constant).
685 function fdry5GenPortRange ($from, $to)
686 {
687 $matches = array();
688 if (1 !== preg_match ('@^([[:digit:]]+/)?([[:digit:]]+/)?([[:digit:]]+)$@', $from, $matches))
689 return array();
690 $prefix = 'e' . $matches[1] . $matches[2];
691 $from_idx = $matches[3];
692 if (1 !== preg_match ('@^([[:digit:]]+/)?([[:digit:]]+/)?([[:digit:]]+)$@', $to, $matches))
693 return array();
694 $to_idx = $matches[3];
695 for ($i = $from_idx; $i <= $to_idx; $i++)
696 $ret[] = $prefix . $i;
697 return $ret;
698 }
699
700 // an implementation for Huawei syntax
701 function vrp53ReadVLANConfig ($input)
702 {
703 $ret = array
704 (
705 'vlanlist' => array(),
706 'portdata' => array(),
707 );
708 $procfunc = 'vrp53ScanTopLevel';
709 foreach (explode ("\n", $input) as $line)
710 $procfunc = $procfunc ($ret, $line);
711 return $ret;
712 }
713
714 function vrp53ScanTopLevel (&$work, $line)
715 {
716 $matches = array();
717 switch (TRUE)
718 {
719 case (preg_match ('@^ vlan batch (.+)$@', $line, $matches)):
720 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
721 $work['vlanlist'][] = $vlan_id;
722 return __FUNCTION__;
723 case (preg_match ('@^interface ((GigabitEthernet|XGigabitEthernet|Eth-Trunk)([[:digit:]]+(/[[:digit:]]+)*))$@', $line, $matches)):
724 $matches[1] = preg_replace ('@^GigabitEthernet(.+)$@', 'gi\\1', $matches[1]);
725 $matches[1] = preg_replace ('@^XGigabitEthernet(.+)$@', 'xg\\1', $matches[1]);
726 $matches[1] = preg_replace ('@^Eth-Trunk(.+)$@', 'et\\1', $matches[1]);
727 $work['current'] = array ('port_name' => $matches[1]);
728 return 'vrp53PickInterfaceSubcommand';
729 default:
730 return __FUNCTION__;
731 }
732 }
733
734 function vrp53ParseVLANString ($string)
735 {
736 $string = preg_replace ('/ to /', '-', $string);
737 $string = preg_replace ('/ /', ',', $string);
738 return iosParseVLANString ($string);
739 }
740
741 function vrp53PickInterfaceSubcommand (&$work, $line)
742 {
743 if ($line[0] == '#') // end of interface section
744 {
745 // Configuration Guide - Ethernet 3.3.4:
746 // "By default, the interface type is hybrid."
747 if (!array_key_exists ('link-type', $work['current']))
748 $work['current']['link-type'] = 'hybrid';
749 if (!array_key_exists ('allowed', $work['current']))
750 $work['current']['allowed'] = array();
751 if (!array_key_exists ('native', $work['current']))
752 $work['current']['native'] = 0;
753 switch ($work['current']['link-type'])
754 {
755 case 'access':
756 // VRP does not assign access ports to VLAN1 by default,
757 // leaving them blocked.
758 $work['portdata'][$work['current']['port_name']] =
759 $work['current']['native'] ? array
760 (
761 'allowed' => $work['current']['allowed'],
762 'native' => $work['current']['native'],
763 'mode' => 'access',
764 ) : array
765 (
766 'mode' => 'none',
767 'allowed' => array(),
768 'native' => 0,
769 );
770 break;
771 case 'trunk':
772 $work['portdata'][$work['current']['port_name']] = array
773 (
774 'allowed' => $work['current']['allowed'],
775 'native' => 0,
776 'mode' => 'trunk',
777 );
778 break;
779 case 'hybrid':
780 $work['portdata'][$work['current']['port_name']] = array
781 (
782 'allowed' => $work['current']['allowed'],
783 'native' => $work['current']['native'],
784 'mode' => 'trunk',
785 );
786 break;
787 default: // dot1q-tunnel ?
788 }
789 unset ($work['current']);
790 return 'vrp53ScanTopLevel';
791 }
792 $matches = array();
793 switch (TRUE)
794 {
795 case (preg_match ('@^ port default vlan ([[:digit:]]+)$@', $line, $matches)):
796 $work['current']['native'] = $matches[1];
797 if (!array_key_exists ('allowed', $work['current']))
798 $work['current']['allowed'] = array();
799 if (!in_array ($matches[1], $work['current']['allowed']))
800 $work['current']['allowed'][] = $matches[1];
801 break;
802 case (preg_match ('@^ port link-type (.+)$@', $line, $matches)):
803 $work['current']['link-type'] = $matches[1];
804 break;
805 case (preg_match ('@^ port trunk allow-pass vlan (.+)$@', $line, $matches)):
806 if (!array_key_exists ('allowed', $work['current']))
807 $work['current']['allowed'] = array();
808 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
809 if (!in_array ($vlan_id, $work['current']['allowed']))
810 $work['current']['allowed'][] = $vlan_id;
811 break;
812 // TODO: make sure, that a port with "eth-trunk" clause always ends up in "none" mode
813 default: // nom-nom
814 }
815 return __FUNCTION__;
816 }
817
818 function nxos4Read8021QConfig ($input)
819 {
820 $ret = array
821 (
822 'vlanlist' => array(),
823 'portdata' => array(),
824 );
825 $procfunc = 'nxos4ScanTopLevel';
826 foreach (explode ("\n", $input) as $line)
827 $procfunc = $procfunc ($ret, $line);
828 return $ret;
829 }
830
831 function nxos4ScanTopLevel (&$work, $line)
832 {
833 $matches = array();
834 switch (TRUE)
835 {
836 case (preg_match ('@^interface ((Ethernet)[[:digit:]]+(/[[:digit:]]+)*)$@', $line, $matches)):
837 $matches[1] = preg_replace ('@^Ethernet(.+)$@', 'e\\1', $matches[1]);
838 $work['current'] = array ('port_name' => $matches[1]);
839 return 'nxos4PickSwitchportCommand';
840 case (preg_match ('@^vlan ([[:digit:]]+)$@', $line, $matches)):
841 $work['vlanlist'][] = $matches[1];
842 return 'nxos4PickVLANs';
843 default:
844 return __FUNCTION__; // continue scan
845 }
846 }
847
848 function nxos4PickVLANs (&$work, $line)
849 {
850 switch (TRUE)
851 {
852 case ($line == ''): // end of VLAN list
853 return 'nxos4ScanTopLevel';
854 case (preg_match ('@^vlan ([[:digit:]]+)$@', $line, $matches)):
855 $work['vlanlist'][] = $matches[1];
856 default: // VLAN name or any other text
857 return __FUNCTION__;
858 }
859 }
860
861 function nxos4PickSwitchportCommand (&$work, $line)
862 {
863 if ($line == '') // end of interface section
864 {
865 // fill in defaults
866 // below assumes "system default switchport" mode set on the device
867 if (!array_key_exists ('mode', $work['current']))
868 $work['current']['mode'] = 'access';
869 // save work, if it makes sense
870 switch ($work['current']['mode'])
871 {
872 case 'access':
873 if (!array_key_exists ('access vlan', $work['current']))
874 $work['current']['access vlan'] = 1;
875 $work['portdata'][$work['current']['port_name']] = array
876 (
877 'mode' => 'access',
878 'allowed' => array ($work['current']['access vlan']),
879 'native' => $work['current']['access vlan'],
880 );
881 break;
882 case 'trunk':
883 if (!array_key_exists ('trunk native vlan', $work['current']))
884 $work['current']['trunk native vlan'] = 1;
885 if (!array_key_exists ('trunk allowed vlan', $work['current']))
886 $work['current']['trunk allowed vlan'] = range (VLAN_MIN_ID, VLAN_MAX_ID);
887 // Having configured VLAN as "native" doesn't mean anything
888 // as long as it's not listed on the "allowed" line.
889 $effective_native = in_array
890 (
891 $work['current']['trunk native vlan'],
892 $work['current']['trunk allowed vlan']
893 ) ? $work['current']['trunk native vlan'] : 0;
894 $work['portdata'][$work['current']['port_name']] = array
895 (
896 'mode' => 'trunk',
897 'allowed' => $work['current']['trunk allowed vlan'],
898 'native' => $effective_native,
899 );
900 break;
901 default:
902 // dot1q-tunnel, dynamic, private-vlan --- skip these
903 }
904 unset ($work['current']);
905 return 'nxos4ScanTopLevel';
906 }
907 // not yet
908 $matches = array();
909 switch (TRUE)
910 {
911 case (preg_match ('@^ switchport mode (.+)$@', $line, $matches)):
912 $work['current']['mode'] = $matches[1];
913 break;
914 case (preg_match ('@^ switchport access vlan (.+)$@', $line, $matches)):
915 $work['current']['access vlan'] = $matches[1];
916 break;
917 case (preg_match ('@^ switchport trunk native vlan (.+)$@', $line, $matches)):
918 $work['current']['trunk native vlan'] = $matches[1];
919 break;
920 case (preg_match ('@^ switchport trunk allowed vlan add (.+)$@', $line, $matches)):
921 $work['current']['trunk allowed vlan'] = array_merge
922 (
923 $work['current']['trunk allowed vlan'],
924 iosParseVLANString ($matches[1])
925 );
926 break;
927 case (preg_match ('@^ switchport trunk allowed vlan (.+)$@', $line, $matches)):
928 $work['current']['trunk allowed vlan'] = iosParseVLANString ($matches[1]);
929 break;
930 default: // suppress warning on irrelevant config clause
931 }
932 return __FUNCTION__;
933 }
934
935 // Get a list of VLAN management pseudo-commands and return a text
936 // of real vendor-specific commands, which implement the work.
937 // This work is done in two rounds:
938 // 1. For "add allowed" and "rem allowed" commands detect continuous
939 // sequences of VLAN IDs and replace them with ranges of form "A-B",
940 // where B>A.
941 // 2. Iterate over the resulting list and produce real CLI commands.
942 function ios12TranslatePushQueue ($queue)
943 {
944 $ret = "configure terminal\n";
945 foreach ($queue as $cmd)
946 switch ($cmd['opcode'])
947 {
948 case 'create VLAN':
949 $ret .= "vlan ${cmd['arg1']}\nexit\n";
950 break;
951 case 'destroy VLAN':
952 $ret .= "no vlan ${cmd['arg1']}\n";
953 break;
954 case 'add allowed':
955 case 'rem allowed':
956 $clause = $cmd['opcode'] == 'add allowed' ? 'add' : 'remove';
957 $ret .= "interface ${cmd['port']}\n";
958 foreach (listToRanges ($cmd['vlans']) as $range)
959 $ret .= "switchport trunk allowed vlan ${clause} " .
960 ($range['from'] == $range['to'] ? $range['to'] : "${range['from']}-${range['to']}") .
961 "\n";
962 $ret .= "exit\n";
963 break;
964 case 'set native':
965 $ret .= "interface ${cmd['arg1']}\nswitchport trunk native vlan ${cmd['arg2']}\nexit\n";
966 break;
967 case 'unset native':
968 $ret .= "interface ${cmd['arg1']}\nno switchport trunk native vlan ${cmd['arg2']}\nexit\n";
969 break;
970 case 'set access':
971 $ret .= "interface ${cmd['arg1']}\nswitchport access vlan ${cmd['arg2']}\nexit\n";
972 break;
973 case 'unset access':
974 $ret .= "interface ${cmd['arg1']}\nno switchport access vlan\nexit\n";
975 break;
976 case 'set mode':
977 $ret .= "interface ${cmd['arg1']}\nswitchport mode ${cmd['arg2']}\n";
978 if ($cmd['arg2'] == 'trunk')
979 $ret .= "no switchport trunk native vlan\nswitchport trunk allowed vlan none\n";
980 $ret .= "exit\n";
981 break;
982 }
983 $ret .= "end\n";
984 if (getConfigVar ('8021Q_WRI_AFTER_CONFT') == 'yes')
985 $ret .= "write memory\n";
986 return $ret;
987 }
988
989 function fdry5TranslatePushQueue ($queue)
990 {
991 $ret = "conf t\n";
992 foreach ($queue as $cmd)
993 switch ($cmd['opcode'])
994 {
995 case 'create VLAN':
996 $ret .= "vlan ${cmd['arg1']}\nexit\n";
997 break;
998 case 'destroy VLAN':
999 $ret .= "no vlan ${cmd['arg1']}\n";
1000 break;
1001 case 'add allowed':
1002 foreach ($cmd['vlans'] as $vlan_id)
1003 $ret .= "vlan ${vlan_id}\ntagged ${cmd['port']}\nexit\n";
1004 break;
1005 case 'rem allowed':
1006 foreach ($cmd['vlans'] as $vlan_id)
1007 $ret .= "vlan ${vlan_id}\nno tagged ${cmd['port']}\nexit\n";
1008 break;
1009 case 'set native':
1010 $ret .= "interface ${cmd['arg1']}\ndual-mode ${cmd['arg2']}\nexit\n";
1011 break;
1012 case 'unset native':
1013 $ret .= "interface ${cmd['arg1']}\nno dual-mode ${cmd['arg2']}\nexit\n";
1014 break;
1015 case 'set access':
1016 $ret .= "vlan ${cmd['arg2']}\nuntagged ${cmd['arg1']}\nexit\n";
1017 break;
1018 case 'unset access':
1019 $ret .= "vlan ${cmd['arg2']}\nno untagged ${cmd['arg1']}\nexit\n";
1020 break;
1021 case 'set mode': // NOP
1022 break;
1023 }
1024 $ret .= "end\n";
1025 if (getConfigVar ('8021Q_WRI_AFTER_CONFT') == 'yes')
1026 $ret .= "write memory\n";
1027 return $ret;
1028 }
1029
1030 function vrp53TranslatePushQueue ($queue)
1031 {
1032 $ret = "system-view\n";
1033 foreach ($queue as $cmd)
1034 switch ($cmd['opcode'])
1035 {
1036 case 'create VLAN':
1037 $ret .= "vlan ${cmd['arg1']}\nquit\n";
1038 break;
1039 case 'destroy VLAN':
1040 $ret .= "undo vlan ${cmd['arg1']}\n";
1041 break;
1042 case 'add allowed':
1043 case 'rem allowed':
1044 $clause = $cmd['opcode'] == 'add allowed' ? '' : 'undo ';
1045 $ret .= "interface ${cmd['port']}\n";
1046 foreach (listToRanges ($cmd['vlans']) as $range)
1047 $ret .= "${clause}port trunk allow-pass vlan " .
1048 ($range['from'] == $range['to'] ? $range['to'] : "${range['from']} to ${range['to']}") .
1049 "\n";
1050 $ret .= "quit\n";
1051 break;
1052 case 'set native':
1053 case 'set access':
1054 $ret .= "interface ${cmd['arg1']}\nport default vlan ${cmd['arg2']}\nquit\n";
1055 break;
1056 case 'unset native':
1057 case 'unset access':
1058 $ret .= "interface ${cmd['arg1']}\nundo port default vlan\nquit\n";
1059 break;
1060 case 'set mode':
1061 $modemap = array ('access' => 'access', 'trunk' => 'hybrid');
1062 $ret .= "interface ${cmd['arg1']}\nport link-type " . $modemap[$cmd['arg2']] . "\n";
1063 if ($cmd['arg2'] == 'hybrid')
1064 $ret .= "undo port default vlan\nundo port trunk allow-pass vlan all\n";
1065 $ret .= "quit\n";
1066 break;
1067 }
1068 $ret .= "return\n";
1069 if (getConfigVar ('8021Q_WRI_AFTER_CONFT') == 'yes')
1070 $ret .= "save\nY\n";
1071 return $ret;
1072 }
1073
1074 function ios12ScanCDPTopLevel (&$work, $line)
1075 {
1076 $matches = array();
1077 switch (TRUE)
1078 {
1079 case preg_match ('/^Device ID: (.+)$/', $line, $matches):
1080 $work['current'] = array ('device' => $matches[1]);
1081 return 'ios12ScanCDPEntry';
1082 default:
1083 return __FUNCTION__; // continue scan
1084 }
1085 }
1086
1087 function ios12ScanCDPEntry (&$work, $line)
1088 {
1089 $matches = array();
1090 switch (TRUE)
1091 {
1092 case preg_match ('/^Interface: (.+), Port ID \(outgoing port\): (.+)$/', $line, $matches):
1093 $work[ios12ShortenIfName ($matches[1])] = array
1094 (
1095 'device' => $work['current']['device'],
1096 'port' => ios12ShortenIfName ($matches[2]),
1097 );
1098 unset ($work['current']);
1099 return 'ios12ScanCDPTopLevel';
1100 default:
1101 }
1102 return __FUNCTION__;
1103 }
1104
1105 ?>