r4254 Move "gateway" directory out of "wwwroot" and provide a mean to reconfigure
[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 $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 global $racktables_gwdir;
77 $execpath = "${racktables_gwdir}/gateways/{$gwname}/main";
78 $dspec = array
79 (
80 0 => array ("pipe", "r"),
81 1 => array ("pipe", "w"),
82 2 => array ("file", "/dev/null", "a")
83 );
84 $pipes = array();
85 $gateway = proc_open ($execpath, $dspec, $pipes);
86 if (!is_resource ($gateway))
87 return array ('ERR proc_open() failed in ' . __FUNCTION__);
88
89 // Dialogue starts. Send all questions.
90 foreach ($questions as $q)
91 fwrite ($pipes[0], "$q\n");
92 fclose ($pipes[0]);
93
94 // Fetch replies.
95 $answers = array ();
96 while (!feof($pipes[1]))
97 {
98 $a = fgets ($pipes[1]);
99 if (!strlen ($a))
100 continue;
101 // Somehow I got a space appended at the end. Kick it.
102 $answers[] = trim ($a);
103 }
104 fclose($pipes[1]);
105
106 $retval = proc_close ($gateway);
107 if ($retval != 0)
108 throw new RTGatewayError ("gateway failed with code ${retval}");
109 if (!count ($answers))
110 throw new RTGatewayError ('no response from gateway');
111 if (count ($answers) != count ($questions))
112 throw new RTGatewayError ('protocol violation');
113 foreach ($answers as $a)
114 if (strpos ($a, 'OK!') !== 0)
115 throw new RTGatewayError ("subcommand failed with status: ${a}");
116 return $answers;
117 }
118
119 // This functions returns an array for VLAN list, and an array for port list (both
120 // form another array themselves) and another one with MAC address list.
121 // The ports in the latter array are marked with either VLAN ID or 'trunk'.
122 // We don't sort the port list, as the gateway is believed to have done this already
123 // (or at least the underlying switch software ought to). This is important, as the
124 // port info is transferred to/from form not by names, but by numbers.
125 function getSwitchVLANs ($object_id = 0)
126 {
127 global $remote_username;
128 $objectInfo = spotEntity ('object', $object_id);
129 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
130 if (count ($endpoints) == 0)
131 throw new RTGatewayError ('no management address set');
132 if (count ($endpoints) > 1)
133 throw new RTGatewayError ('cannot pick management address');
134 $hwtype = $swtype = 'unknown';
135 foreach (getAttrValues ($object_id) as $record)
136 {
137 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
138 $swtype = str_replace (' ', '+', execGMarker ($record['o_value']));
139 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
140 $hwtype = str_replace (' ', '+', execGMarker ($record['o_value']));
141 }
142 $endpoint = str_replace (' ', '+', $endpoints[0]);
143 $commands = array
144 (
145 "connect ${endpoint} ${hwtype} ${swtype} ${remote_username}",
146 'listvlans',
147 'listports',
148 'listmacs'
149 );
150 $data = queryGateway ('switchvlans', $commands);
151 if (strpos ($data[0], 'OK!') !== 0)
152 throw new RTGatewayError ("gateway failed with status: ${data[0]}.");
153 // Now we have VLAN list in $data[1] and port list in $data[2]. Let's sort this out.
154 $tmp = array_unique (explode (';', substr ($data[1], strlen ('OK!'))));
155 if (count ($tmp) == 0)
156 throw new RTGatewayError ('gateway returned no records');
157 $vlanlist = array();
158 foreach ($tmp as $record)
159 {
160 list ($vlanid, $vlandescr) = explode ('=', $record);
161 $vlanlist[$vlanid] = $vlandescr;
162 }
163 $portlist = array();
164 foreach (explode (';', substr ($data[2], strlen ('OK!'))) as $pair)
165 {
166 list ($portname, $pair2) = explode ('=', $pair);
167 list ($status, $vlanid) = explode (',', $pair2);
168 $portlist[] = array ('portname' => $portname, 'status' => $status, 'vlanid' => $vlanid);
169 }
170 if (count ($portlist) == 0)
171 throw new RTGatewayError ('gateway returned no records');
172 $maclist = array();
173 foreach (explode (';', substr ($data[3], strlen ('OK!'))) as $pair)
174 {
175 list ($macaddr, $pair2) = explode ('=', $pair);
176 if (!strlen ($pair2))
177 continue;
178 list ($vlanid, $ifname) = explode ('@', $pair2);
179 $maclist[$ifname][$vlanid][] = $macaddr;
180 }
181 return array ($vlanlist, $portlist, $maclist);
182 }
183
184 function setSwitchVLANs ($object_id = 0, $setcmd)
185 {
186 global $remote_username;
187 $objectInfo = spotEntity ('object', $object_id);
188 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
189 if (count ($endpoints) == 0)
190 throw new RTGatewayError ('no management address set');
191 if (count ($endpoints) > 1)
192 throw new RTGatewayError ('cannot pick management address');
193 $hwtype = $swtype = 'unknown';
194 foreach (getAttrValues ($object_id) as $record)
195 {
196 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
197 $swtype = strtr (execGMarker ($record['o_value']), ' ', '+');
198 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
199 $hwtype = strtr (execGMarker ($record['o_value']), ' ', '+');
200 }
201 $endpoint = str_replace (' ', '+', $endpoints[0]);
202 $data = queryGateway
203 (
204 'switchvlans',
205 array ("connect ${endpoint} ${hwtype} ${swtype} ${remote_username}", $setcmd)
206 );
207 // Finally we can parse the response into message array.
208 $log = emptyLog();
209 foreach (explode (';', substr ($data[1], strlen ('OK!'))) as $text)
210 {
211 if (strpos ($text, 'C!') === 0)
212 {
213 // gateway-encoded message
214 $tmp = explode ('!', $text);
215 array_shift ($tmp);
216 $code = array_shift ($tmp);
217 $log = mergeLogs ($log, oneLiner ($code, $tmp));
218 }
219 elseif (strpos ($text, 'I!') === 0)
220 $log = mergeLogs ($log, oneLiner (62, array (substr ($text, 2)))); // generic gateway success
221 elseif (strpos ($text, 'W!') === 0)
222 $log = mergeLogs ($log, oneLiner (202, array (substr ($text, 2)))); // generic gateway warning
223 else // All improperly formatted messages must be treated as error conditions.
224 $log = mergeLogs ($log, oneLiner (166, array (substr ($text, 2)))); // generic gateway error
225 }
226 return $log;
227 }
228
229 // Drop a file off RackTables platform. The gateway will catch the file and pass it to the given
230 // installer script.
231 function gwSendFile ($endpoint, $handlername, $filetext = array())
232 {
233 if (! is_array ($filetext))
234 throw new InvalidArgException ('filetext', '(suppressed)', 'is not an array');
235 global $remote_username;
236 $tmpnames = array();
237 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
238 $command = "submit ${remote_username} ${endpoint} ${handlername}";
239 foreach ($filetext as $text)
240 {
241 $name = tempnam ('', 'RackTables-sendfile-');
242 $tmpnames[] = $name;
243 if (FALSE === $name or FALSE === file_put_contents ($name, $text))
244 {
245 foreach ($tmpnames as $name)
246 unlink ($name);
247 throw new RTGatewayError ('failed to write to temporary file');
248 }
249 $command .= " ${name}";
250 }
251 try
252 {
253 queryGateway ('sendfile', array ($command));
254 foreach ($tmpnames as $name)
255 unlink ($name);
256 }
257 catch (RTGatewayError $e)
258 {
259 foreach ($tmpnames as $name)
260 unlink ($name);
261 throw $e;
262 }
263 }
264
265 // Query something through a gateway and get some text in return. Return that text.
266 function gwRecvFile ($endpoint, $handlername, &$output)
267 {
268 global $remote_username;
269 $tmpfilename = tempnam ('', 'RackTables-sendfile-');
270 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
271 try
272 {
273 queryGateway ('sendfile', array ("submit ${remote_username} ${endpoint} ${handlername} ${tmpfilename}"));
274 $output = file_get_contents ($tmpfilename);
275 unlink ($tmpfilename);
276 }
277 catch (RTGatewayError $e)
278 {
279 unlink ($tmpfilename);
280 throw $e;
281 }
282 if ($output === FALSE)
283 throw new RTGatewayError ('failed to read temporary file');
284 }
285
286 function gwSendFileToObject ($object_id, $handlername, $filetext = '')
287 {
288 if (!mb_strlen ($handlername))
289 throw new InvalidArgException ('$handlername');
290 $objectInfo = spotEntity ('object', $object_id);
291 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
292 if (count ($endpoints) == 0)
293 throw new RTGatewayError ('no management address set');
294 if (count ($endpoints) > 1)
295 throw new RTGatewayError ('cannot pick management address');
296 gwSendFile (str_replace (' ', '+', $endpoints[0]), $handlername, array ($filetext));
297 }
298
299 function gwRecvFileFromObject ($object_id, $handlername, &$output)
300 {
301 if (!mb_strlen ($handlername))
302 throw new InvalidArgException ('$handlername');
303 $objectInfo = spotEntity ('object', $object_id);
304 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
305 if (count ($endpoints) == 0)
306 throw new RTGatewayError ('no management address set');
307 if (count ($endpoints) > 1)
308 throw new RTGatewayError ('cannot pick management address');
309 gwRecvFile (str_replace (' ', '+', $endpoints[0]), $handlername, $output);
310 }
311
312 function detectDeviceBreed ($object_id)
313 {
314 $breed_by_swcode = array
315 (
316 251 => 'ios12',
317 252 => 'ios12',
318 254 => 'ios12',
319 963 => 'nxos4',
320 964 => 'nxos4',
321 1365 => 'nxos4',
322 1352 => 'xos12',
323 1360 => 'vrp53',
324 1361 => 'vrp55',
325 1369 => 'vrp55', // VRP versions 5.5 and 5.7 seem to be compatible
326 1363 => 'fdry5',
327 );
328 foreach (getAttrValues ($object_id) as $record)
329 if ($record['id'] == 4 and array_key_exists ($record['key'], $breed_by_swcode))
330 return $breed_by_swcode[$record['key']];
331 return '';
332 }
333
334 function getRunning8021QConfig ($object_id)
335 {
336 $ret = gwRetrieveDeviceConfig ($object_id, 'get8021q');
337 // Once there is no default VLAN in the parsed data, it means
338 // something else was parsed instead of config text.
339 if (!in_array (VLAN_DFL_ID, $ret['vlanlist']))
340 throw new RTGatewayError ('communication with device failed');
341 return $ret;
342 }
343
344 function setDevice8021QConfig ($object_id, $pseudocode)
345 {
346 require_once 'deviceconfig.php';
347 if ('' == $breed = detectDeviceBreed ($object_id))
348 throw new RTGatewayError ('device breed unknown');
349 global $gwpushxlator;
350 // FIXME: this is a perfect place to log intended changes
351 gwDeployDeviceConfig ($object_id, $breed, unix2dos ($gwpushxlator[$breed] ($pseudocode)));
352 }
353
354 function gwRetrieveDeviceConfig ($object_id, $command)
355 {
356 require_once 'deviceconfig.php';
357 global $gwrxlator;
358 if (!array_key_exists ($command, $gwrxlator))
359 throw new RTGatewayError ('command unknown');
360 $breed = detectDeviceBreed ($object_id);
361 if (!array_key_exists ($breed, $gwrxlator[$command]))
362 throw new RTGatewayError ('device breed unknown');
363 $objectInfo = spotEntity ('object', $object_id);
364 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
365 if (count ($endpoints) == 0)
366 throw new RTGatewayError ('no management address set');
367 if (count ($endpoints) > 1)
368 throw new RTGatewayError ('cannot pick management address');
369 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
370 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
371 try
372 {
373 queryGateway ('deviceconfig', array ("${command} ${endpoint} ${breed} ${tmpfilename}"));
374 $configtext = file_get_contents ($tmpfilename);
375 unlink ($tmpfilename);
376 }
377 catch (RTGatewayError $e)
378 {
379 unlink ($tmpfilename);
380 throw $e;
381 }
382 if ($configtext === FALSE)
383 throw new RTGatewayError ('failed to read temporary file');
384 // Being here means it was alright.
385 return $gwrxlator[$command][$breed] (dos2unix ($configtext));
386 }
387
388 function gwDeployDeviceConfig ($object_id, $breed, $text)
389 {
390 if ($text == '')
391 throw new InvalidArgException ('text', '', 'deploy text is empty');
392 $objectInfo = spotEntity ('object', $object_id);
393 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
394 if (count ($endpoints) == 0)
395 throw new RTGatewayError ('no management address set');
396 if (count ($endpoints) > 1)
397 throw new RTGatewayError ('cannot pick management address');
398 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
399 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
400 if (FALSE === file_put_contents ($tmpfilename, $text))
401 {
402 unlink ($tmpfilename);
403 throw new RTGatewayError ('failed to write to temporary file');
404 }
405 try
406 {
407 queryGateway ('deviceconfig', array ("deploy ${endpoint} ${breed} ${tmpfilename}"));
408 unlink ($tmpfilename);
409 }
410 catch (RTGatewayError $e)
411 {
412 unlink ($tmpfilename);
413 throw $e;
414 }
415 }
416
417 ?>