r4235 usePreparedUpdateBlade(): add a space for a cleaner SQL syntax (like it already...
[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 $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 require_once 'deviceconfig.php';
344 if ('' == $breed = detectDeviceBreed ($object_id))
345 throw new RTGatewayError ('device breed unknown');
346 global $gwpushxlator;
347 // FIXME: this is a perfect place to log intended changes
348 gwDeployDeviceConfig ($object_id, $breed, unix2dos ($gwpushxlator[$breed] ($pseudocode)));
349 }
350
351 function gwRetrieveDeviceConfig ($object_id, $command)
352 {
353 require_once 'deviceconfig.php';
354 global $gwrxlator;
355 if (!array_key_exists ($command, $gwrxlator))
356 throw new RTGatewayError ('command unknown');
357 $breed = detectDeviceBreed ($object_id);
358 if (!array_key_exists ($breed, $gwrxlator[$command]))
359 throw new RTGatewayError ('device breed unknown');
360 $objectInfo = spotEntity ('object', $object_id);
361 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
362 if (count ($endpoints) == 0)
363 throw new RTGatewayError ('no management address set');
364 if (count ($endpoints) > 1)
365 throw new RTGatewayError ('cannot pick management address');
366 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
367 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
368 try
369 {
370 queryGateway ('deviceconfig', array ("${command} ${endpoint} ${breed} ${tmpfilename}"));
371 $configtext = file_get_contents ($tmpfilename);
372 unlink ($tmpfilename);
373 }
374 catch (RTGatewayError $e)
375 {
376 unlink ($tmpfilename);
377 throw $e;
378 }
379 if ($configtext === FALSE)
380 throw new RTGatewayError ('failed to read temporary file');
381 // Being here means it was alright.
382 return $gwrxlator[$command][$breed] (dos2unix ($configtext));
383 }
384
385 function gwDeployDeviceConfig ($object_id, $breed, $text)
386 {
387 if ($text == '')
388 throw new InvalidArgException ('text', '', 'deploy text is empty');
389 $objectInfo = spotEntity ('object', $object_id);
390 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
391 if (count ($endpoints) == 0)
392 throw new RTGatewayError ('no management address set');
393 if (count ($endpoints) > 1)
394 throw new RTGatewayError ('cannot pick management address');
395 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
396 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
397 if (FALSE === file_put_contents ($tmpfilename, $text))
398 {
399 unlink ($tmpfilename);
400 throw new RTGatewayError ('failed to write to temporary file');
401 }
402 try
403 {
404 queryGateway ('deviceconfig', array ("deploy ${endpoint} ${breed} ${tmpfilename}"));
405 unlink ($tmpfilename);
406 }
407 catch (RTGatewayError $e)
408 {
409 unlink ($tmpfilename);
410 throw $e;
411 }
412 }
413
414 ?>