add reference to COPYING
[racktables] / wwwroot / inc / remote.php
CommitLineData
7cb57a03
AA
1<?php
2
cddbb9fd
DO
3# This file is a part of RackTables, a datacenter and server room management
4# framework. See accompanying file "COPYING" for the full copyright and
5# licensing information.
6
7cb57a03
AA
7function queryDevice ($object_id, $command)
8{
9 $breed = detectDeviceBreed ($object_id);
10 if (empty ($breed))
11 throw new RTGatewayError ("Can not determine device breed");
12
13 if (! validBreedFunction ($breed, $command))
14 throw new RTGatewayError ("unsupported command '$command' for the breed '$breed'");
15
16 require_once 'deviceconfig.php';
17 global $breedfunc;
18 if (! is_callable ($breedfunc["$breed-$command-main"]))
19 throw new RTGatewayError ("undeclared function '" . $breedfunc["$breed-$command-main"] . "'");
20 $query = translateDeviceCommands ($object_id, array (array ('opcode' => $command)));
21 if ($command == 'xlatepushq')
22 return $query;
23 else
24 {
25 $answer = queryTerminal ($object_id, $query, FALSE);
26 return $breedfunc["$breed-$command-main"] ($answer);
27 }
28}
29
30function translateDeviceCommands ($object_id, $crq, $vlan_names = NULL)
31{
32 require_once 'deviceconfig.php';
33 $breed = detectDeviceBreed ($object_id);
34 if (empty ($breed))
35 throw new RTGatewayError ("Can not determine device breed");
36
37 if (! validBreedFunction ($breed, 'xlatepushq'))
38 throw new RTGatewayError ("unsupported command 'xlatepushq' for the breed '$breed'");
39
40 global $breedfunc;
41 return $breedfunc["$breed-xlatepushq-main"] ($object_id, $crq, $vlan_names);
42}
43
44// This function returns a text output received from the device
45// You can override connection settings by implement a callback named 'terminal_settings'.
46// Errors are thrown as exceptions if not $tolerate_remote_errors, and shown as warnings otherwise.
47function queryTerminal ($object_id, $commands, $tolerate_remote_errors = TRUE)
48{
49 $objectInfo = spotEntity ('object', $object_id);
50 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
51 if (count ($endpoints) == 0)
52 throw new RTGatewayError ('no management address set');
53 if (count ($endpoints) > 1)
54 throw new RTGatewayError ('cannot pick management address');
55
5d2988fb 56 // telnet prompt and mode specification
7cb57a03
AA
57 switch ($breed = detectDeviceBreed ($object_id))
58 {
77b7e03c
DO
59 case 'ios12':
60 case 'fdry5':
7a0e9964 61 case 'ftos8':
a3447df9 62 $protocol = 'netcat'; // default is netcat mode
349394c1 63 $prompt = '^(Login|Username|Password): $|^\S+[>#]$'; // set the prompt in case user would like to specify telnet protocol
7cb57a03 64 break;
d4a957e0
DO
65 case 'air12':
66 $protocol = 'telnet'; # Aironet IOS is broken
67 $prompt = '^(Username|Password): $|^\S+[>#]$';
68 break;
7cb57a03
AA
69 case 'vrp53':
70 case 'vrp55':
5d2988fb 71 $protocol = 'telnet';
338465a8 72 $prompt = '^\[[^[\]]+\]$|^<[^<>]+>$|^(Username|Password):$|(?:\[Y\/N\]|\(Y\/N\)\[[YN]\]):?$';
7cb57a03
AA
73 break;
74 case 'nxos4':
5d2988fb 75 $protocol = 'telnet';
7cb57a03
AA
76 $prompt = '[>:#] $';
77 break;
78 case 'xos12':
5d2988fb 79 $protocol = 'telnet';
18997a72 80 $prompt = ': $|\.\d+ # $|\?\s*\([Yy]\/[Nn]\)\s*$';
7cb57a03
AA
81 break;
82 case 'jun10':
5d2988fb 83 $protocol = 'telnet';
7cb57a03
AA
84 $prompt = '^login: $|^Password:$|^\S+@\S+[>#] $';
85 break;
85703049
DO
86 case 'eos4':
87 $protocol = 'telnet'; # strict RFC854 implementation, netcat won't work
88 $prompt = '^(\xf2?login|Username|Password): $|^\S+[>#]$';
89 break;
919bb8b6
DO
90 case 'ros11':
91 $protocol = 'netcat'; # see ftos8 case
92 $prompt = '^(User Name|\rPassword):$|^\r?\S+# $';
93 break;
7a0e9964
AA
94 case 'iosxr4':
95 $protocol = 'telnet';
96 $prompt = '^\r?(Login|Username|Password): $|^\r?\S+[>#]$';
97 break;
be91a564
JS
98 case 'ucs':
99 $protocol = 'ucssdk';
100 break;
5d2988fb
AA
101 default:
102 $protocol = 'netcat';
103 $prompt = NULL;
7cb57a03
AA
104 }
105
106 // set the default settings before calling user-defined callback
107 $settings = array
108 (
109 'hostname' => $endpoints[0],
a3447df9 110 'protocol' => $protocol,
7cb57a03
AA
111 'port' => NULL,
112 'prompt' => $prompt,
113 'username' => NULL,
114 'password' => NULL,
ada8a703 115 'timeout' => 15,
7cb57a03 116 'connect_timeout' => 2,
a9edde6c 117 'prompt_delay' => 0.001, # 1ms
7cb57a03
AA
118 'sudo_user' => NULL,
119 'identity_file' => NULL,
120 );
121 if (is_callable ('terminal_settings'))
122 call_user_func ('terminal_settings', $objectInfo, array (&$settings)); // override settings
123
01b0c68f
AA
124 if (! isset ($settings['port']) and $settings['protocol'] == 'netcat')
125 $settings['port'] = 23;
126
7cb57a03
AA
127 $params = array ( $settings['hostname'] );
128 $params_from_settings = array();
129 switch ($settings['protocol'])
130 {
131 case 'telnet':
01b0c68f
AA
132 case 'netcat':
133 // prepend command list with vendor-specific disabling pager command
7cb57a03
AA
134 switch ($breed)
135 {
136 case 'ios12':
137 $commands = "terminal length 0\n" . $commands;
138 break;
139 case 'nxos4':
52bf6129 140 case 'air12':
0a2d7ff1 141 case 'ftos8':
7cb57a03
AA
142 $commands = "terminal length 0\nterminal no monitor\n" . $commands;
143 break;
144 case 'xos12':
145 $commands = "disable clipaging\n" . $commands;
146 break;
147 case 'vrp55':
148 $commands = "screen-length 0 temporary\n" . $commands;
149 break;
150 case 'fdry5':
151 $commands = "skip-page-display\n" . $commands;
152 break;
153 case 'jun10':
184ec25d 154 $commands = "set cli screen-length 0\n" . $commands;
7cb57a03 155 break;
85703049
DO
156 case 'eos4':
157 $commands = "enable\nno terminal monitor\nterminal length 0\n" . $commands;
158 break;
919bb8b6
DO
159 case 'ros11':
160 $commands = "terminal datadump\n" . $commands;
161 $commands .= "\n\n"; # temporary workaround for telnet server
162 break;
7a0e9964
AA
163 case 'iosxr4':
164 $commands = "terminal length 0\nterminal monitor disable\n" . $commands;
165 break;
fc3a6bd0
IE
166 case 'dlink':
167 $commands = "disable clipaging\n" . $commands;
168 break;
7cb57a03
AA
169 }
170 // prepend telnet commands by credentials
171 if (isset ($settings['password']))
172 $commands = $settings['password'] . "\n" . $commands;
173 if (isset ($settings['username']))
174 $commands = $settings['username'] . "\n" . $commands;
01b0c68f
AA
175 // command-line options are specific to client: telnet or netcat
176 switch ($settings['protocol'])
177 {
178 case 'telnet':
179 $params_from_settings['port'] = 'port';
180 $params_from_settings['prompt'] = 'prompt';
181 $params_from_settings['connect-timeout'] = 'connect_timeout';
182 $params_from_settings['timeout'] = 'timeout';
183 $params_from_settings['prompt-delay'] = 'prompt_delay';
184 break;
185 case 'netcat':
56930ac4 186 $params_from_settings['p'] = 'port';
01b0c68f 187 $params_from_settings['w'] = 'timeout';
56930ac4 188 $params_from_settings['b'] = 'ncbin';
01b0c68f
AA
189 break;
190 }
7cb57a03
AA
191 break;
192 case 'ssh':
193 $params_from_settings['port'] = 'port';
424604b4 194 $params_from_settings['proto'] = 'proto';
7cb57a03
AA
195 $params_from_settings['username'] = 'username';
196 $params_from_settings['i'] = 'identity_file';
197 $params_from_settings['sudo-user'] = 'sudo_user';
198 $params_from_settings['connect-timeout'] = 'connect_timeout';
199 break;
be91a564
JS
200 case 'ucssdk': # remote XML through a Python backend
201 $params = array(); # reset
202 # UCS in its current implementation besides the terminal_settings() provides
203 # an additional username/password feed through the HTML from. Whenever the
204 # user provides the credentials through the form, use these instead of the
205 # credentials [supposedly] set by terminal_settings().
206 if ($script_mode != TRUE && ! isCheckSet ('use_terminal_settings'))
207 {
208 genericAssertion ('ucs_login', 'string');
209 genericAssertion ('ucs_password', 'string');
210 $settings['username'] = $_REQUEST['ucs_login'];
211 $settings['password'] = $_REQUEST['ucs_password'];
212 }
213 foreach (array ('hostname', 'username', 'password') as $item)
214 if (empty ($settings[$item]))
215 throw new RTGatewayError ("${item} not available, check terminal_settings()");
216 $commands = "login ${settings['hostname']} ${settings['username']} ${settings['password']}\n" . $commands;
217 break;
7cb57a03 218 default:
c0b84dfb 219 throw RTGatewayError ("Invalid terminal protocol '${settings['protocol']}' specified");
7cb57a03
AA
220 }
221 foreach ($params_from_settings as $param_name => $setting_name)
222 if (isset ($settings[$setting_name]))
01b0c68f
AA
223 if (is_int ($param_name))
224 $params[] = $settings[$setting_name];
225 else
226 $params[$param_name] = $settings[$setting_name];
7cb57a03
AA
227
228 $ret_code = callScript ($settings['protocol'], $params, $commands, $out, $errors);
01b0c68f 229 if ($settings['protocol'] != 'ssh' || ! $tolerate_remote_errors)
7cb57a03
AA
230 {
231 if (! empty ($errors))
e75026f8 232 throw new RTGatewayError ("${settings['protocol']} error: " . rtrim ($errors));
7cb57a03
AA
233 elseif ($ret_code !== 0)
234 throw new RTGatewayError ("${settings['protocol']} error: result code $ret_code");
235 }
236 elseif (! empty ($errors)) // ssh and not tolerate and non-empty $errors
237 foreach (explode ("\n", $errors) as $line)
238 if (strlen ($line))
239 showWarning ("${settings['protocol']} ${settings['hostname']}: $line");
240 return strtr($out, array("\r" => "")); // cut ^M symbols
241}
242
243function callScript ($gwname, $params, $in, &$out, &$errors)
244{
245 global $racktables_gwdir, $local_gwdir;
246 if (isset ($local_gwdir) && file_exists ("$local_gwdir/$gwname"))
247 $dir = $local_gwdir;
248 elseif (isset ($racktables_gwdir) && file_exists ("$racktables_gwdir/$gwname"))
249 $dir = $racktables_gwdir;
250 if (! isset ($dir))
c0b84dfb 251 throw new RTGatewayError ("Could not find the gateway file called '$gwname'");
7cb57a03
AA
252
253 $cmd_line = "./$gwname";
254 foreach ($params as $key => $value)
255 {
256 if (! isset ($value))
257 continue;
258 if (preg_match ('/^\d+$/', $key))
259 $cmd_line .= " " . escapeshellarg ($value);
260 else
261 {
262 if (strlen ($key) == 1)
263 $cmd_line .= " " . escapeshellarg ("-$key") . " " . escapeshellarg ($value);
264 else
265 $cmd_line .= " " . escapeshellarg("--$key=$value");
266 }
267 }
424604b4 268
7cb57a03
AA
269 $pipes = array();
270 $child = proc_open
271 (
272 $cmd_line,
273 array (
274 0 => array ('pipe', 'r'),
275 1 => array ('pipe', 'w'),
276 2 => array ('pipe', 'w'),
277 ),
278 $pipes,
279 $dir
280 );
281 if (! is_resource ($child))
c0b84dfb 282 throw new RTGatewayError ("cant execute $dir/$gwname");
15da1b37
AA
283
284 $buff_size = 4096;
285 $write_left = array ($pipes[0]);
286 $read_left = array ($pipes[1], $pipes[2]);
287 $write_fd = $write_left;
288 $read_fd = $read_left;
289 $except_fd = array();
e9222a8a
AA
290 $out = '';
291 $errors = '';
15da1b37
AA
292 while ((! empty ($read_fd) || ! empty ($write_fd)) && stream_select ($read_fd, $write_fd, $except_fd, NULL))
293 {
294 foreach ($write_fd as $fd)
295 {
296 $written = fwrite ($fd, $in, $buff_size);
297 $in = substr ($in, $written);
298 if ($written == 0 || empty ($in))
299 {
300 // close input fd
301 $write_left = array_diff ($write_left, array ($fd));
302 fclose ($fd);
303 }
304 }
305 foreach ($read_fd as $fd)
306 {
307 $str = fread ($fd, $buff_size);
308 if (strlen ($str) == 0)
309 {
310 // close output fd
311 $read_left = array_diff ($read_left, array ($fd));
312 fclose ($fd);
313 }
314 else
315 {
316 if ($fd == $pipes[1])
317 $out .= $str;
318 elseif ($fd == $pipes[2])
319 $errors .= $str;
320 }
321 }
322
323 $write_fd = $write_left;
324 $read_fd = $read_left;
325 }
326
7da04382 327 return proc_close ($child);
7cb57a03
AA
328}
329
bc93236a
AA
330function getRunning8021QConfig ($object_id)
331{
332 $ret = queryDevice ($object_id, 'get8021q');
333 // Once there is no default VLAN in the parsed data, it means
334 // something else was parsed instead of config text.
1c0a61e2 335 if (!in_array (VLAN_DFL_ID, $ret['vlanlist']) || empty ($ret['portdata']))
bc93236a
AA
336 throw new RTGatewayError ('communication with device failed');
337 return $ret;
338}
339
340function setDevice8021QConfig ($object_id, $pseudocode, $vlan_names)
341{
342 // FIXME: this is a perfect place to log intended changes
343 // $object_id argument isn't used by default translating functions, but
344 // may come in handy for overloaded versions of these.
9f1da7de 345 $commands = translateDeviceCommands ($object_id, $pseudocode, $vlan_names);
dc68fd36
AA
346 $breed = detectDeviceBreed ($object_id);
347 $output = queryTerminal ($object_id, $commands, FALSE);
348
349 // throw an exception if Juniper did not allow to enter config mode or to commit changes
350 if ($breed == 'jun10')
351 {
352 if (preg_match ('/>\s*configure exclusive\s*$[^#>]*?^error:/sm', $output))
353 throw new RTGatewayError ("Configuration is locked by other user");
354 elseif (preg_match ('/#\s*commit\s*$([^#]*?^error: .*?)$/sm', $output, $m))
355 throw new RTGatewayError ("Commit failed: ${m[1]}");
356 }
bc93236a
AA
357}
358
7cb57a03 359?>