r4985 NX-OS 6.0 gateways support
[racktables] / wwwroot / 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 $breedfunc = array
18 (
19 'ios12-getcdpstatus-main' => 'ios12ReadCDPStatus',
20 'ios12-getlldpstatus-main' => 'ios12ReadLLDPStatus',
21 'ios12-get8021q-main' => 'ios12ReadVLANConfig',
22 'ios12-get8021q-top' => 'ios12ScanTopLevel',
23 'ios12-get8021q-readport' => 'ios12PickSwitchportCommand',
24 'ios12-get8021q-readvlan' => 'ios12PickVLANCommand',
25 'ios12-getportstatus-main' => 'ciscoReadInterfaceStatus',
26 'ios12-getmaclist-main' => 'ios12ReadMacList',
27 'ios12-xlatepushq-main' => 'ios12TranslatePushQueue',
28 'ios12-getallconf-main' => 'ios12SpotConfigText',
29 'fdry5-get8021q-main' => 'fdry5ReadVLANConfig',
30 'fdry5-get8021q-top' => 'fdry5ScanTopLevel',
31 'fdry5-get8021q-readvlan' => 'fdry5PickVLANSubcommand',
32 'fdry5-get8021q-readport' => 'fdry5PickInterfaceSubcommand',
33 'fdry5-xlatepushq-main' => 'fdry5TranslatePushQueue',
34 'fdry5-getallconf-main' => 'fdry5SpotConfigText',
35 'vrp53-getlldpstatus-main' => 'vrp5xReadLLDPStatus',
36 'vrp53-get8021q-main' => 'vrp53ReadVLANConfig',
37 'vrp53-get8021q-top' => 'vrp53ScanTopLevel',
38 'vrp53-get8021q-readport' => 'vrp53PickInterfaceSubcommand',
39 'vrp53-getportstatus-main' => 'vrpReadInterfaceStatus',
40 'vrp53-getmaclist-main' => 'vrp53ReadMacList',
41 'vrp53-xlatepushq-main' => 'vrp53TranslatePushQueue',
42 'vrp53-getallconf-main' => 'vrp5xSpotConfigText',
43 'vrp55-getlldpstatus-main' => 'vrp5xReadLLDPStatus',
44 'vrp55-get8021q-main' => 'vrp55Read8021QConfig',
45 'vrp55-getportstatus-main' => 'vrpReadInterfaceStatus',
46 'vrp55-getmaclist-main' => 'vrp55ReadMacList',
47 'vrp55-xlatepushq-main' => 'vrp55TranslatePushQueue',
48 'vrp55-getallconf-main' => 'vrp5xSpotConfigText',
49 'nxos4-getcdpstatus-main' => 'ios12ReadCDPStatus',
50 'nxos4-getlldpstatus-main' => 'nxos4ReadLLDPStatus',
51 'nxos4-get8021q-main' => 'nxos4Read8021QConfig',
52 'nxos4-get8021q-top' => 'nxos4ScanTopLevel',
53 'nxos4-get8021q-readport' => 'nxos4PickSwitchportCommand',
54 'nxos4-getportstatus-main' => 'ciscoReadInterfaceStatus',
55 'nxos4-getmaclist-main' => 'nxos4ReadMacList',
56 'nxos4-xlatepushq-main' => 'nxos4TranslatePushQueue',
57 'nxos4-getallconf-main' => 'nxos4SpotConfigText',
58 'xos12-getlldpstatus-main' => 'xos12ReadLLDPStatus',
59 'xos12-get8021q-main' => 'xos12Read8021QConfig',
60 'xos12-xlatepushq-main' => 'xos12TranslatePushQueue',
61 'xos12-getallconf-main' => 'xos12SpotConfigText',
62 'jun10-get8021q-main' => 'jun10Read8021QConfig',
63 'jun10-xlatepushq-main' => 'jun10TranslatePushQueue',
64 'jun10-getallconf-main' => 'jun10SpotConfigText',
65 'ftos8-xlatepushq-main' => 'ftos8TranslatePushQueue',
66 'ftos8-getlldpstatus-main' => 'ftos8ReadLLDPStatus',
67 'ftos8-getmaclist-main' => 'ftos8ReadMacList',
68 'ftos8-getportstatus-main' => 'ftos8ReadInterfaceStatus',
69 'ftos8-get8021q-main' => 'ftos8Read8021QConfig',
70 'ftos8-getallconf-main' => 'ftos8SpotConfigText',
71 );
72
73 // This function launches specified gateway with specified
74 // command-line arguments and feeds it with the commands stored
75 // in the second arg as array.
76 // The answers are stored in another array, which is returned
77 // by this function. In the case when a gateway cannot be found,
78 // finishes prematurely or exits with non-zero return code,
79 // a single-item array is returned with the only "ERR" record,
80 // which explains the reason.
81 function queryGateway ($gwname, $questions)
82 {
83 global $racktables_gwdir;
84 $execpath = "${racktables_gwdir}/{$gwname}/main";
85 $dspec = array
86 (
87 0 => array ("pipe", "r"),
88 1 => array ("pipe", "w"),
89 2 => array ("file", "/dev/null", "a")
90 );
91 $pipes = array();
92 $gateway = proc_open ($execpath, $dspec, $pipes);
93 if (!is_resource ($gateway))
94 return array ('ERR proc_open() failed in ' . __FUNCTION__);
95
96 // Dialogue starts. Send all questions.
97 foreach ($questions as $q)
98 fwrite ($pipes[0], "$q\n");
99 fclose ($pipes[0]);
100
101 // Fetch replies.
102 $answers = array ();
103 while (!feof($pipes[1]))
104 {
105 $a = fgets ($pipes[1]);
106 if (!strlen ($a))
107 continue;
108 // Somehow I got a space appended at the end. Kick it.
109 $answers[] = trim ($a);
110 }
111 fclose($pipes[1]);
112
113 $retval = proc_close ($gateway);
114 if ($retval != 0)
115 throw new RTGatewayError ("gateway failed with code ${retval}");
116 if (!count ($answers))
117 throw new RTGatewayError ('no response from gateway');
118 if (count ($answers) != count ($questions))
119 throw new RTGatewayError ('protocol violation');
120 foreach ($answers as $a)
121 if (strpos ($a, 'OK!') !== 0)
122 throw new RTGatewayError ("subcommand failed with status: ${a}");
123 return $answers;
124 }
125
126 // This functions returns an array for VLAN list, and an array for port list (both
127 // form another array themselves) and another one with MAC address list.
128 // The ports in the latter array are marked with either VLAN ID or 'trunk'.
129 // We don't sort the port list, as the gateway is believed to have done this already
130 // (or at least the underlying switch software ought to). This is important, as the
131 // port info is transferred to/from form not by names, but by numbers.
132 function getSwitchVLANs ($object_id = 0)
133 {
134 global $remote_username;
135 $objectInfo = spotEntity ('object', $object_id);
136 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
137 if (count ($endpoints) == 0)
138 throw new RTGatewayError ('no management address set');
139 if (count ($endpoints) > 1)
140 throw new RTGatewayError ('cannot pick management address');
141 $hwtype = $swtype = 'unknown';
142 foreach (getAttrValues ($object_id) as $record)
143 {
144 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
145 $swtype = str_replace (' ', '+', execGMarker ($record['o_value']));
146 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
147 $hwtype = str_replace (' ', '+', execGMarker ($record['o_value']));
148 }
149 $endpoint = str_replace (' ', '+', $endpoints[0]);
150 $commands = array
151 (
152 "connect ${endpoint} ${hwtype} ${swtype} ${remote_username}",
153 'listvlans',
154 'listports',
155 'listmacs'
156 );
157 $data = queryGateway ('switchvlans', $commands);
158 if (strpos ($data[0], 'OK!') !== 0)
159 throw new RTGatewayError ("gateway failed with status: ${data[0]}.");
160 // Now we have VLAN list in $data[1] and port list in $data[2]. Let's sort this out.
161 $tmp = array_unique (explode (';', substr ($data[1], strlen ('OK!'))));
162 if (count ($tmp) == 0)
163 throw new RTGatewayError ('gateway returned no records');
164 $vlanlist = array();
165 foreach ($tmp as $record)
166 {
167 list ($vlanid, $vlandescr) = explode ('=', $record);
168 $vlanlist[$vlanid] = $vlandescr;
169 }
170 $portlist = array();
171 foreach (explode (';', substr ($data[2], strlen ('OK!'))) as $pair)
172 {
173 list ($portname, $pair2) = explode ('=', $pair);
174 list ($status, $vlanid) = explode (',', $pair2);
175 $portlist[] = array ('portname' => $portname, 'status' => $status, 'vlanid' => $vlanid);
176 }
177 if (count ($portlist) == 0)
178 throw new RTGatewayError ('gateway returned no records');
179 $maclist = array();
180 foreach (explode (';', substr ($data[3], strlen ('OK!'))) as $pair)
181 if (preg_match ('/^([^=]+)=(.+)/', $pair, $m))
182 {
183 $macaddr = $m[1];
184 list ($vlanid, $ifname) = explode ('@', $m[2]);
185 $maclist[$ifname][$vlanid][] = $macaddr;
186 }
187 return array ($vlanlist, $portlist, $maclist);
188 }
189
190 function setSwitchVLANs ($object_id = 0, $setcmd)
191 {
192 global $remote_username;
193 $objectInfo = spotEntity ('object', $object_id);
194 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
195 if (count ($endpoints) == 0)
196 throw new RTGatewayError ('no management address set');
197 if (count ($endpoints) > 1)
198 throw new RTGatewayError ('cannot pick management address');
199 $hwtype = $swtype = 'unknown';
200 foreach (getAttrValues ($object_id) as $record)
201 {
202 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
203 $swtype = strtr (execGMarker ($record['o_value']), ' ', '+');
204 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
205 $hwtype = strtr (execGMarker ($record['o_value']), ' ', '+');
206 }
207 $endpoint = str_replace (' ', '+', $endpoints[0]);
208 $data = queryGateway
209 (
210 'switchvlans',
211 array ("connect ${endpoint} ${hwtype} ${swtype} ${remote_username}", $setcmd)
212 );
213 // Finally we can parse the response into message array.
214 foreach (explode (';', substr ($data[1], strlen ('OK!'))) as $text)
215 {
216 $message = 'gw: ' . substr ($text, 2);
217 if (strpos ($text, 'I!') === 0)
218 showSuccess ($message); // generic gateway success
219 elseif (strpos ($text, 'W!') === 0)
220 showWarning ($message); // generic gateway warning
221 elseif (strpos ($text, 'E!') === 0)
222 showError ($message); // generic gateway error
223 else // All improperly formatted messages must be treated as error conditions.
224 showError ('unexpected line from gw: ' . $text);
225 }
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 // On success returns the text string printed by sendfile handler
231 // On failure throws an exception
232 function gwSendFile ($endpoint, $handlername, $filetext = array())
233 {
234 $result = '';
235 if (! is_array ($filetext))
236 throw new InvalidArgException ('filetext', '(suppressed)', 'is not an array');
237 global $remote_username;
238 $tmpnames = array();
239 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
240 $command = "submit ${remote_username} ${endpoint} ${handlername}";
241 foreach ($filetext as $text)
242 {
243 $name = tempnam ('', 'RackTables-sendfile-');
244 $tmpnames[] = $name;
245 if (FALSE === $name or FALSE === file_put_contents ($name, $text))
246 {
247 foreach ($tmpnames as $name)
248 unlink ($name);
249 throw new RTGatewayError ('failed to write to temporary file');
250 }
251 $command .= " ${name}";
252 }
253 try
254 {
255 $answers = queryGateway ('sendfile', array ($command));
256 $result = preg_replace ('/^OK!\s*/', '', array_shift ($answers));
257 foreach ($tmpnames as $name)
258 unlink ($name);
259 }
260 catch (RTGatewayError $e)
261 {
262 foreach ($tmpnames as $name)
263 unlink ($name);
264 throw new RTGatewayError ("Sending $handlername to $endpoint: " . $e->getMessage());
265 }
266 return $result;
267 }
268
269 // Query something through a gateway and get some text in return. Return that text.
270 function gwRecvFile ($endpoint, $handlername, &$output)
271 {
272 global $remote_username;
273 $tmpfilename = tempnam ('', 'RackTables-sendfile-');
274 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
275 try
276 {
277 queryGateway ('sendfile', array ("submit ${remote_username} ${endpoint} ${handlername} ${tmpfilename}"));
278 $output = file_get_contents ($tmpfilename);
279 unlink ($tmpfilename);
280 }
281 catch (RTGatewayError $e)
282 {
283 unlink ($tmpfilename);
284 throw $e;
285 }
286 if ($output === FALSE)
287 throw new RTGatewayError ('failed to read temporary file');
288 }
289
290 function gwSendFileToObject ($object_id, $handlername, $filetext = '')
291 {
292 if (!mb_strlen ($handlername))
293 throw new InvalidArgException ('$handlername');
294 $objectInfo = spotEntity ('object', $object_id);
295 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
296 if (count ($endpoints) == 0)
297 throw new RTGatewayError ('no management address set');
298 if (count ($endpoints) > 1)
299 throw new RTGatewayError ('cannot pick management address');
300 return gwSendFile (str_replace (' ', '+', $endpoints[0]), $handlername, array ($filetext));
301 }
302
303 function gwRecvFileFromObject ($object_id, $handlername, &$output)
304 {
305 if (!mb_strlen ($handlername))
306 throw new InvalidArgException ('$handlername');
307 $objectInfo = spotEntity ('object', $object_id);
308 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
309 if (count ($endpoints) == 0)
310 throw new RTGatewayError ('no management address set');
311 if (count ($endpoints) > 1)
312 throw new RTGatewayError ('cannot pick management address');
313 gwRecvFile (str_replace (' ', '+', $endpoints[0]), $handlername, $output);
314 }
315
316 function detectDeviceBreed ($object_id)
317 {
318 $breed_by_swcode = array
319 (
320 251 => 'ios12',
321 252 => 'ios12',
322 254 => 'ios12',
323 963 => 'nxos4', // NX-OS 4.0
324 964 => 'nxos4', // NX-OS 4.1
325 1365 => 'nxos4', // NX-OS 4.2
326 1410 => 'nxos4', // NX-OS 5.0, seems compatible
327 1411 => 'nxos4', // NX-OS 5.1
328 1643 => 'nxos4', // NX-OS 6.0
329 1352 => 'xos12',
330 1360 => 'vrp53',
331 1361 => 'vrp55',
332 1369 => 'vrp55', // VRP versions 5.5 and 5.7 seem to be compatible
333 1363 => 'fdry5',
334 1367 => 'jun10', # 10S
335 1597 => 'jun10', # 10R
336 1598 => 'jun10', # 11R
337 1594 => 'ftos8',
338 );
339 foreach (getAttrValues ($object_id) as $record)
340 if ($record['id'] == 4 and array_key_exists ($record['key'], $breed_by_swcode))
341 return $breed_by_swcode[$record['key']];
342 return '';
343 }
344
345 function validBreedFunction ($breed, $command)
346 {
347 global $breedfunc;
348 return array_key_exists ("${breed}-${command}-main", $breedfunc);
349 }
350
351 function assertBreedFunction ($breed, $command)
352 {
353 global $breedfunc;
354 if (! array_key_exists ("${breed}-${command}-main", $breedfunc))
355 throw new RTGatewayError ('unsupported command for this breed');
356 }
357
358 function gwRetrieveDeviceConfig ($object_id, $command)
359 {
360 require_once 'deviceconfig.php';
361 global $breedfunc;
362 $breed = detectDeviceBreed ($object_id);
363 assertBreedFunction ($breed, $command);
364 $objectInfo = spotEntity ('object', $object_id);
365 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
366 if (count ($endpoints) == 0)
367 throw new RTGatewayError ('no management address set');
368 if (count ($endpoints) > 1)
369 throw new RTGatewayError ('cannot pick management address');
370 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
371 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
372 try
373 {
374 queryGateway ('deviceconfig', array ("${command} ${endpoint} ${breed} ${tmpfilename}"));
375 $configtext = file_get_contents ($tmpfilename);
376 unlink ($tmpfilename);
377 }
378 catch (RTGatewayError $e)
379 {
380 unlink ($tmpfilename);
381 throw $e;
382 }
383 if ($configtext === FALSE)
384 throw new RTGatewayError ('failed to read temporary file');
385 // Being here means it was alright.
386 return $breedfunc["${breed}-${command}-main"] (dos2unix ($configtext));
387 }
388
389 function gwDeployDeviceConfig ($object_id, $breed, $text)
390 {
391 if ($text == '')
392 throw new InvalidArgException ('text', '', 'deploy text is empty');
393 $objectInfo = spotEntity ('object', $object_id);
394 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
395 if (count ($endpoints) == 0)
396 throw new RTGatewayError ('no management address set');
397 if (count ($endpoints) > 1)
398 throw new RTGatewayError ('cannot pick management address');
399 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
400 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
401 if (FALSE === file_put_contents ($tmpfilename, $text))
402 {
403 unlink ($tmpfilename);
404 throw new RTGatewayError ('failed to write to temporary file');
405 }
406 try
407 {
408 queryGateway ('deviceconfig', array ("deploy ${endpoint} ${breed} ${tmpfilename}"));
409 unlink ($tmpfilename);
410 }
411 catch (RTGatewayError $e)
412 {
413 unlink ($tmpfilename);
414 throw $e;
415 }
416 }
417
418 ?>