r4236 gwSendFile(): add type check
[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 if (! is_array ($filetext))
233 throw new InvalidArgException ('filetext', '(suppressed)', 'is not an array');
234 global $remote_username;
235 $tmpnames = array();
236 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
237 $command = "submit ${remote_username} ${endpoint} ${handlername}";
238 foreach ($filetext as $text)
239 {
240 $name = tempnam ('', 'RackTables-sendfile-');
241 $tmpnames[] = $name;
242 if (FALSE === $name or FALSE === file_put_contents ($name, $text))
243 {
244 foreach ($tmpnames as $name)
245 unlink ($name);
246 throw new RTGatewayError ('failed to write to temporary file');
247 }
248 $command .= " ${name}";
249 }
250 try
251 {
252 queryGateway ('sendfile', array ($command));
253 foreach ($tmpnames as $name)
254 unlink ($name);
255 }
256 catch (RTGatewayError $e)
257 {
258 foreach ($tmpnames as $name)
259 unlink ($name);
260 throw $e;
261 }
262 }
263
264 // Query something through a gateway and get some text in return. Return that text.
265 function gwRecvFile ($endpoint, $handlername, &$output)
266 {
267 global $remote_username;
268 $tmpfilename = tempnam ('', 'RackTables-sendfile-');
269 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
270 try
271 {
272 queryGateway ('sendfile', array ("submit ${remote_username} ${endpoint} ${handlername} ${tmpfilename}"));
273 $output = file_get_contents ($tmpfilename);
274 unlink ($tmpfilename);
275 }
276 catch (RTGatewayError $e)
277 {
278 unlink ($tmpfilename);
279 throw $e;
280 }
281 if ($output === FALSE)
282 throw new RTGatewayError ('failed to read temporary file');
283 }
284
285 function gwSendFileToObject ($object_id, $handlername, $filetext = '')
286 {
287 if (!mb_strlen ($handlername))
288 throw new InvalidArgException ('$handlername');
289 $objectInfo = spotEntity ('object', $object_id);
290 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
291 if (count ($endpoints) == 0)
292 throw new RTGatewayError ('no management address set');
293 if (count ($endpoints) > 1)
294 throw new RTGatewayError ('cannot pick management address');
295 gwSendFile (str_replace (' ', '+', $endpoints[0]), $handlername, array ($filetext));
296 }
297
298 function gwRecvFileFromObject ($object_id, $handlername, &$output)
299 {
300 if (!mb_strlen ($handlername))
301 throw new InvalidArgException ('$handlername');
302 $objectInfo = spotEntity ('object', $object_id);
303 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
304 if (count ($endpoints) == 0)
305 throw new RTGatewayError ('no management address set');
306 if (count ($endpoints) > 1)
307 throw new RTGatewayError ('cannot pick management address');
308 gwRecvFile (str_replace (' ', '+', $endpoints[0]), $handlername, $output);
309 }
310
311 function detectDeviceBreed ($object_id)
312 {
313 $breed_by_swcode = array
314 (
315 251 => 'ios12',
316 252 => 'ios12',
317 254 => 'ios12',
318 963 => 'nxos4',
319 964 => 'nxos4',
320 1365 => 'nxos4',
321 1352 => 'xos12',
322 1360 => 'vrp53',
323 1361 => 'vrp55',
324 1369 => 'vrp55', // VRP versions 5.5 and 5.7 seem to be compatible
325 1363 => 'fdry5',
326 );
327 foreach (getAttrValues ($object_id) as $record)
328 if ($record['id'] == 4 and array_key_exists ($record['key'], $breed_by_swcode))
329 return $breed_by_swcode[$record['key']];
330 return '';
331 }
332
333 function getRunning8021QConfig ($object_id)
334 {
335 $ret = gwRetrieveDeviceConfig ($object_id, 'get8021q');
336 // Once there is no default VLAN in the parsed data, it means
337 // something else was parsed instead of config text.
338 if (!in_array (VLAN_DFL_ID, $ret['vlanlist']))
339 throw new RTGatewayError ('communication with device failed');
340 return $ret;
341 }
342
343 function setDevice8021QConfig ($object_id, $pseudocode)
344 {
345 require_once 'deviceconfig.php';
346 if ('' == $breed = detectDeviceBreed ($object_id))
347 throw new RTGatewayError ('device breed unknown');
348 global $gwpushxlator;
349 // FIXME: this is a perfect place to log intended changes
350 gwDeployDeviceConfig ($object_id, $breed, unix2dos ($gwpushxlator[$breed] ($pseudocode)));
351 }
352
353 function gwRetrieveDeviceConfig ($object_id, $command)
354 {
355 require_once 'deviceconfig.php';
356 global $gwrxlator;
357 if (!array_key_exists ($command, $gwrxlator))
358 throw new RTGatewayError ('command unknown');
359 $breed = detectDeviceBreed ($object_id);
360 if (!array_key_exists ($breed, $gwrxlator[$command]))
361 throw new RTGatewayError ('device breed unknown');
362 $objectInfo = spotEntity ('object', $object_id);
363 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
364 if (count ($endpoints) == 0)
365 throw new RTGatewayError ('no management address set');
366 if (count ($endpoints) > 1)
367 throw new RTGatewayError ('cannot pick management address');
368 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
369 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
370 try
371 {
372 queryGateway ('deviceconfig', array ("${command} ${endpoint} ${breed} ${tmpfilename}"));
373 $configtext = file_get_contents ($tmpfilename);
374 unlink ($tmpfilename);
375 }
376 catch (RTGatewayError $e)
377 {
378 unlink ($tmpfilename);
379 throw $e;
380 }
381 if ($configtext === FALSE)
382 throw new RTGatewayError ('failed to read temporary file');
383 // Being here means it was alright.
384 return $gwrxlator[$command][$breed] (dos2unix ($configtext));
385 }
386
387 function gwDeployDeviceConfig ($object_id, $breed, $text)
388 {
389 if ($text == '')
390 throw new InvalidArgException ('text', '', 'deploy text is empty');
391 $objectInfo = spotEntity ('object', $object_id);
392 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
393 if (count ($endpoints) == 0)
394 throw new RTGatewayError ('no management address set');
395 if (count ($endpoints) > 1)
396 throw new RTGatewayError ('cannot pick management address');
397 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
398 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
399 if (FALSE === file_put_contents ($tmpfilename, $text))
400 {
401 unlink ($tmpfilename);
402 throw new RTGatewayError ('failed to write to temporary file');
403 }
404 try
405 {
406 queryGateway ('deviceconfig', array ("deploy ${endpoint} ${breed} ${tmpfilename}"));
407 unlink ($tmpfilename);
408 }
409 catch (RTGatewayError $e)
410 {
411 unlink ($tmpfilename);
412 throw $e;
413 }
414 }
415
416 ?>