blob: fe6e1e150cb246582486271bd9d8aea4881f4004 [file] [log] [blame]
mblighdcd57a82007-07-11 23:06:47 +00001#!/usr/bin/python
2#
3# Copyright 2007 Google Inc. Released under the GPL v2
4
mbligh7d2bde82007-08-02 16:26:10 +00005"""
6This module defines the SSHHost class.
mblighdcd57a82007-07-11 23:06:47 +00007
8Implementation details:
9You should import the "hosts" package instead of importing each type of host.
10
11 SSHHost: a remote machine with a ssh access
12"""
13
mbligh7d2bde82007-08-02 16:26:10 +000014__author__ = """
15mbligh@google.com (Martin J. Bligh),
mblighdcd57a82007-07-11 23:06:47 +000016poirier@google.com (Benjamin Poirier),
mbligh7d2bde82007-08-02 16:26:10 +000017stutsman@google.com (Ryan Stutsman)
18"""
mblighdcd57a82007-07-11 23:06:47 +000019
20
mblighde384372007-10-17 04:25:37 +000021import types, os, sys, signal, subprocess, time, re, socket
mbligh5f876ad2007-10-12 23:59:53 +000022import base_classes, utils, errors, bootloader
mblighdcd57a82007-07-11 23:06:47 +000023
24
25class SSHHost(base_classes.RemoteHost):
mbligh7d2bde82007-08-02 16:26:10 +000026 """
27 This class represents a remote machine controlled through an ssh
mblighdcd57a82007-07-11 23:06:47 +000028 session on which you can run programs.
mbligh7d2bde82007-08-02 16:26:10 +000029
mblighdcd57a82007-07-11 23:06:47 +000030 It is not the machine autoserv is running on. The machine must be
31 configured for password-less login, for example through public key
32 authentication.
mbligh7d2bde82007-08-02 16:26:10 +000033
mbligh3409ee72007-10-16 23:58:33 +000034 It includes support for controlling the machine through a serial
35 console on which you can run programs. If such a serial console is
36 set up on the machine then capabilities such as hard reset and
37 boot strap monitoring are available. If the machine does not have a
38 serial console available then ordinary SSH-based commands will
39 still be available, but attempts to use extensions such as
40 console logging or hard reset will fail silently.
41
mblighdcd57a82007-07-11 23:06:47 +000042 Implementation details:
43 This is a leaf class in an abstract class hierarchy, it must
44 implement the unimplemented methods in parent classes.
45 """
mbligh7d2bde82007-08-02 16:26:10 +000046
mbligh0faf91f2007-10-18 03:10:48 +000047 SSH_BASE_COMMAND = 'ssh -a'
48
mblighde384372007-10-17 04:25:37 +000049 def __init__(self, hostname, user="root", port=22, initialize=True,
mblighd2fc50f2007-10-23 22:38:00 +000050 conmux_log="console.log", conmux_server=None, conmux_attach=None,
51 netconsole_log="netconsole.log", netconsole_port=6666):
mbligh7d2bde82007-08-02 16:26:10 +000052 """
53 Construct a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000054
55 Args:
56 hostname: network hostname or address of remote machine
57 user: user to log in as on the remote machine
58 port: port the ssh daemon is listening on on the remote
59 machine
mbligh9708f732007-10-18 03:18:54 +000060 """
mblighdcd57a82007-07-11 23:06:47 +000061 self.hostname= hostname
62 self.user= user
63 self.port= port
64 self.tmp_dirs= []
mbligh137a05c2007-10-04 15:56:51 +000065 self.initialize = initialize
mbligh91334902007-09-28 01:47:59 +000066
mbligh9708f732007-10-18 03:18:54 +000067 super(SSHHost, self).__init__()
68
mbligh3409ee72007-10-16 23:58:33 +000069 self.conmux_server = conmux_server
70 self.conmux_attach = self.__find_console_attach(conmux_attach)
71 self.logger_pid = None
mblighde384372007-10-17 04:25:37 +000072 self.__start_console_log(conmux_log)
mbligh3409ee72007-10-16 23:58:33 +000073
mbligha0452c82007-08-08 20:24:57 +000074 self.bootloader = bootloader.Bootloader(self)
mbligh7d2bde82007-08-02 16:26:10 +000075
mblighde384372007-10-17 04:25:37 +000076 self.__init_netconsole_params(netconsole_port)
77 self.netlogger_pid = None
78 self.__start_netconsole_log(netconsole_log, netconsole_port)
79 self.__load_netconsole_module()
80
mbligh7d2bde82007-08-02 16:26:10 +000081
mblighdcd57a82007-07-11 23:06:47 +000082 def __del__(self):
mbligh7d2bde82007-08-02 16:26:10 +000083 """
84 Destroy a SSHHost object
mblighdcd57a82007-07-11 23:06:47 +000085 """
86 for dir in self.tmp_dirs:
87 try:
88 self.run('rm -rf "%s"' % (utils.sh_escape(dir)))
89 except errors.AutoservRunError:
90 pass
mblighde384372007-10-17 04:25:37 +000091 # kill the console logger
mbligh7364ae42007-10-18 03:20:34 +000092 if getattr(self, 'logger_pid', None):
mbligh3409ee72007-10-16 23:58:33 +000093 try:
94 pgid = os.getpgid(self.logger_pid)
95 os.killpg(pgid, signal.SIGTERM)
96 except OSError:
97 pass
mblighde384372007-10-17 04:25:37 +000098 # kill the netconsole logger
mbligh7364ae42007-10-18 03:20:34 +000099 if getattr(self, 'netlogger_pid', None):
mblighde384372007-10-17 04:25:37 +0000100 try:
101 os.kill(self.netlogger_pid, signal.SIGTERM)
102 except OSError:
103 pass
104
105
106 def __init_netconsole_params(self, port):
107 """
108 Connect to the remote machine and determine the values to use for the
109 required netconsole parameters.
110 """
111 self.__netconsole_param = ""
112 # PROBLEM: on machines with multiple IPs this may not make any sense
113 # It also doesn't work with IPv6
114 remote_ip = socket.gethostbyname(self.hostname)
115 local_ip = socket.gethostbyname(socket.gethostname())
116 # Get the gateway of the remote machine
117 try:
118 traceroute = self.run('traceroute -n %s' % local_ip)
119 except errors.AutoservRunError:
120 return
121 first_node = traceroute.stdout.split("\n")[0]
122 match = re.search(r'\s+((\d+\.){3}\d+)\s+', first_node)
123 if match:
124 router_ip = match.group(1)
125 else:
126 return
127 # Look up the MAC address of the gateway
128 try:
129 self.run('ping -c 1 %s' % router_ip)
130 arp = self.run('arp -n -a %s' % router_ip)
131 except errors.AutoservRunError:
132 return
133 match = re.search(r'\s+(([0-9A-F]{2}:){5}[0-9A-F]{2})\s+', arp.stdout)
134 if match:
135 gateway_mac = match.group(1)
136 else:
137 return
138 self.__netconsole_param = 'netconsole=@%s/,%s@%s/%s' % (remote_ip,
139 port,
140 local_ip,
141 gateway_mac)
142
143
144 def __start_netconsole_log(self, logfilename, port):
145 """
146 Log the output of netconsole to a specified file
147 """
148 if logfilename == None:
149 return
150 cmd = ['nc', '-u', '-l', '-p', str(port)]
mblighd2fc50f2007-10-23 22:38:00 +0000151 logger = subprocess.Popen(cmd, stdout=open(logfilename, "a", 0))
mblighde384372007-10-17 04:25:37 +0000152 self.netlogger_pid = logger.pid
153
154
155 def __load_netconsole_module(self):
156 """
157 Make a best effort to load the netconsole module.
158
159 Note that loading the module can fail even when the remote machine is
160 working correctly if netconsole is already compiled into the kernel
161 and started.
162 """
163 try:
164 self.run('modprobe netconsole %s' % self.__netconsole_param)
165 except errors.AutoservRunError:
166 # if it fails there isn't much we can do, just keep going
167 pass
168
169
170 def __unload_netconsole_module(self):
171 try:
172 self.run('modprobe -r netconsole')
173 except errors.AutoservRunError:
174 pass
mbligh3409ee72007-10-16 23:58:33 +0000175
176
177 def __wait_for_restart(self, timeout):
178 self.wait_down(60) # Make sure he's dead, Jim
179 self.wait_up(timeout)
180 time.sleep(2) # this is needed for complete reliability
181 self.wait_up(timeout)
182 print "Reboot complete"
183
184
185 def hardreset(self, timeout=600, wait=True):
186 """
187 Reach out and slap the box in the power switch
188 """
mblighba81c682007-10-25 15:35:59 +0000189 command_ran = self.__console_run(r"'~$hardreset'")
190 if not command_ran:
191 raise errors.AutoservUnsupportedError
192 if wait:
193 self.__wait_for_restart(timeout)
mbligh3409ee72007-10-16 23:58:33 +0000194
195
196 def __start_console_log(self, logfilename):
197 """
198 Log the output of the console session to a specified file
199 """
200 if logfilename == None:
201 return
202 if not self.conmux_attach or not os.path.exists(self.conmux_attach):
203 return
204 if self.conmux_server:
205 to = '%s/%s' % (self.conmux_server, self.hostname)
206 else:
207 to = self.hostname
mblighd2fc50f2007-10-23 22:38:00 +0000208 cmd = [self.conmux_attach, to, 'cat -']
mbligh3409ee72007-10-16 23:58:33 +0000209 logger = subprocess.Popen(cmd,
mblighd2fc50f2007-10-23 22:38:00 +0000210 stdout=open(logfilename, 'a', 0),
mbligh3409ee72007-10-16 23:58:33 +0000211 stderr=open('/dev/null', 'w'),
212 preexec_fn=lambda: os.setpgid(0, 0))
213 self.logger_pid = logger.pid
214
215
216 def __find_console_attach(self, conmux_attach):
217 if conmux_attach:
218 return conmux_attach
219 try:
220 res = utils.run('which conmux-attach')
221 if res.exit_status == 0:
222 return res.stdout.strip()
223 except errors.AutoservRunError, e:
224 pass
mbligh9708f732007-10-18 03:18:54 +0000225 autotest_conmux = os.path.join(self.serverdir, '..',
mbligh3409ee72007-10-16 23:58:33 +0000226 'conmux', 'conmux-attach')
mbligh9708f732007-10-18 03:18:54 +0000227 autotest_conmux_alt = os.path.join(self.serverdir,
mbligh3409ee72007-10-16 23:58:33 +0000228 '..', 'autotest',
229 'conmux', 'conmux-attach')
230 locations = [autotest_conmux,
231 autotest_conmux_alt,
232 '/usr/local/conmux/bin/conmux-attach',
233 '/usr/bin/conmux-attach']
234 for l in locations:
235 if os.path.exists(l):
236 return l
237
238 print "WARNING: conmux-attach not found on autoserv server"
239 return None
240
241
242 def __console_run(self, cmd):
243 """
244 Send a command to the conmux session
245 """
246 if not self.conmux_attach or not os.path.exists(self.conmux_attach):
247 return False
248 if self.conmux_server:
249 to = '%s/%s' % (self.conmux_server, self.hostname)
250 else:
251 to = self.hostname
252 cmd = '%s %s echo %s 2> /dev/null' % (self.conmux_attach,
253 to,
254 cmd)
255 result = os.system(cmd)
256 return result == 0
mbligh7d2bde82007-08-02 16:26:10 +0000257
258
mblighe6647d12007-10-17 00:00:01 +0000259 def ssh_command(self):
260 """Construct an ssh command with proper args for this host."""
mbligh0faf91f2007-10-18 03:10:48 +0000261 return r'%s -l %s -p %d %s' % (self.SSH_BASE_COMMAND,
262 self.user,
263 self.port,
264 self.hostname)
mblighe6647d12007-10-17 00:00:01 +0000265
266
mblighcf965b02007-07-25 16:49:45 +0000267 def run(self, command, timeout=None, ignore_status=False):
mbligh7d2bde82007-08-02 16:26:10 +0000268 """
269 Run a command on the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000270
271 Args:
272 command: the command line string
273 timeout: time limit in seconds before attempting to
274 kill the running process. The run() function
275 will take a few seconds longer than 'timeout'
276 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000277 ignore_status: do not raise an exception, no matter
278 what the exit code of the command is.
mblighdcd57a82007-07-11 23:06:47 +0000279
280 Returns:
281 a hosts.base_classes.CmdResult object
282
283 Raises:
284 AutoservRunError: the exit code of the command
285 execution was not 0
286 """
287 #~ print "running %s" % (command,)
mblighe6647d12007-10-17 00:00:01 +0000288 result= utils.run(r'%s "%s"' % (self.ssh_command(),
289 utils.sh_escape(command)),
290 timeout, ignore_status)
mblighdcd57a82007-07-11 23:06:47 +0000291 return result
mbligh7d2bde82007-08-02 16:26:10 +0000292
293
mbligha0452c82007-08-08 20:24:57 +0000294 def reboot(self, timeout=600, label=None, kernel_args=None, wait=True):
mbligh7d2bde82007-08-02 16:26:10 +0000295 """
296 Reboot the remote host.
mbligh8b85dfb2007-08-28 09:50:31 +0000297
mbligha0452c82007-08-08 20:24:57 +0000298 Args:
299 timeout
mbligh8b85dfb2007-08-28 09:50:31 +0000300 """
mblighde384372007-10-17 04:25:37 +0000301 # forcibly include the "netconsole" kernel arg
302 if self.__netconsole_param:
303 if kernel_args is None:
304 kernel_args = self.__netconsole_param
305 else:
306 kernel_args += " " + self.__netconsole_param
307 # unload the (possibly loaded) module to avoid shutdown issues
308 self.__unload_netconsole_module()
mbligha0452c82007-08-08 20:24:57 +0000309 if label or kernel_args:
310 self.bootloader.install_boottool()
311 if label:
312 self.bootloader.set_default(label)
313 if kernel_args:
314 if not label:
315 default = int(self.bootloader.get_default())
316 label = self.bootloader.get_titles()[default]
317 self.bootloader.add_args(label, kernel_args)
mblighd742a222007-09-30 01:27:06 +0000318 print "Reboot: initiating reboot"
mbligha0452c82007-08-08 20:24:57 +0000319 self.run('reboot')
320 if wait:
mbligh3409ee72007-10-16 23:58:33 +0000321 self.__wait_for_restart(timeout)
mblighde384372007-10-17 04:25:37 +0000322 self.__load_netconsole_module() # if the builtin fails
mbligha0452c82007-08-08 20:24:57 +0000323
mbligh7d2bde82007-08-02 16:26:10 +0000324
mblighdcd57a82007-07-11 23:06:47 +0000325 def get_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000326 """
327 Copy files from the remote host to a local path.
mblighdcd57a82007-07-11 23:06:47 +0000328
329 Directories will be copied recursively.
330 If a source component is a directory with a trailing slash,
331 the content of the directory will be copied, otherwise, the
332 directory itself and its content will be copied. This
333 behavior is similar to that of the program 'rsync'.
334
335 Args:
336 source: either
337 1) a single file or directory, as a string
338 2) a list of one or more (possibly mixed)
339 files or directories
340 dest: a file or a directory (if source contains a
341 directory or more than one element, you must
342 supply a directory dest)
343
344 Raises:
345 AutoservRunError: the scp command failed
346 """
347 if isinstance(source, types.StringTypes):
348 source= [source]
349
350 processed_source= []
351 for entry in source:
352 if entry.endswith('/'):
353 format_string= '%s@%s:"%s*"'
354 else:
355 format_string= '%s@%s:"%s"'
356 entry= format_string % (self.user, self.hostname,
357 utils.scp_remote_escape(entry))
358 processed_source.append(entry)
359
360 processed_dest= os.path.abspath(dest)
361 if os.path.isdir(dest):
362 processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
363 else:
364 processed_dest= utils.sh_escape(processed_dest)
365
366 utils.run('scp -rpq %s "%s"' % (
367 " ".join(processed_source),
368 processed_dest))
mbligh7d2bde82007-08-02 16:26:10 +0000369
370
mblighdcd57a82007-07-11 23:06:47 +0000371 def send_file(self, source, dest):
mbligh7d2bde82007-08-02 16:26:10 +0000372 """
373 Copy files from a local path to the remote host.
mblighdcd57a82007-07-11 23:06:47 +0000374
375 Directories will be copied recursively.
376 If a source component is a directory with a trailing slash,
377 the content of the directory will be copied, otherwise, the
378 directory itself and its content will be copied. This
379 behavior is similar to that of the program 'rsync'.
380
381 Args:
382 source: either
383 1) a single file or directory, as a string
384 2) a list of one or more (possibly mixed)
385 files or directories
386 dest: a file or a directory (if source contains a
387 directory or more than one element, you must
388 supply a directory dest)
389
390 Raises:
391 AutoservRunError: the scp command failed
392 """
393 if isinstance(source, types.StringTypes):
394 source= [source]
395
396 processed_source= []
397 for entry in source:
398 if entry.endswith('/'):
399 format_string= '"%s/"*'
400 else:
401 format_string= '"%s"'
402 entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
403 processed_source.append(entry)
mbligh7d2bde82007-08-02 16:26:10 +0000404
mblighe6647d12007-10-17 00:00:01 +0000405 result = utils.run(r'%s rsync -h' % self.ssh_command(),
406 ignore_status=True)
mblighd5669092007-08-27 19:01:05 +0000407
mbligh0faf91f2007-10-18 03:10:48 +0000408 remote_dest = '%s@%s:"%s"' % (
409 self.user, self.hostname,
410 utils.scp_remote_escape(dest))
mblighd5669092007-08-27 19:01:05 +0000411 if result.exit_status == 0:
mbligh0faf91f2007-10-18 03:10:48 +0000412 utils.run('rsync --rsh="%s" -az %s %s' % (
413 self.SSH_BASE_COMMAND, " ".join(processed_source),
414 remote_dest))
mblighd5669092007-08-27 19:01:05 +0000415 else:
mbligh0faf91f2007-10-18 03:10:48 +0000416 utils.run('scp -rpq %s %s' % (
417 " ".join(processed_source),
418 remote_dest))
mbligh7d2bde82007-08-02 16:26:10 +0000419
mblighdcd57a82007-07-11 23:06:47 +0000420 def get_tmp_dir(self):
mbligh7d2bde82007-08-02 16:26:10 +0000421 """
422 Return the pathname of a directory on the host suitable
mblighdcd57a82007-07-11 23:06:47 +0000423 for temporary file storage.
424
425 The directory and its content will be deleted automatically
426 on the destruction of the Host object that was used to obtain
427 it.
428 """
mbligha25b29e2007-08-26 13:58:04 +0000429 dir_name= self.run("mktemp -d /tmp/autoserv-XXXXXX").stdout.rstrip(" \n")
mblighdcd57a82007-07-11 23:06:47 +0000430 self.tmp_dirs.append(dir_name)
431 return dir_name
mbligh7d2bde82007-08-02 16:26:10 +0000432
433
mblighdcd57a82007-07-11 23:06:47 +0000434 def is_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000435 """
436 Check if the remote host is up.
mblighdcd57a82007-07-11 23:06:47 +0000437
438 Returns:
439 True if the remote host is up, False otherwise
440 """
441 try:
442 result= self.run("true", timeout=10)
443 except errors.AutoservRunError:
444 return False
445 else:
446 if result.exit_status == 0:
447 return True
448 else:
mbligh7d2bde82007-08-02 16:26:10 +0000449
mblighdcd57a82007-07-11 23:06:47 +0000450 return False
mbligh7d2bde82007-08-02 16:26:10 +0000451
mblighdcd57a82007-07-11 23:06:47 +0000452 def wait_up(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000453 """
454 Wait until the remote host is up or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000455
456 In fact, it will wait until an ssh connection to the remote
457 host can be established.
458
459 Args:
460 timeout: time limit in seconds before returning even
461 if the host is not up.
462
463 Returns:
464 True if the host was found to be up, False otherwise
465 """
466 if timeout:
467 end_time= time.time() + timeout
468
469 while not timeout or time.time() < end_time:
470 try:
mblighe9cf9d42007-08-31 08:56:00 +0000471 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000472 result= self.run("true", timeout=run_timeout)
473 except errors.AutoservRunError:
474 pass
475 else:
476 if result.exit_status == 0:
477 return True
478 time.sleep(1)
479
480 return False
mbligh7d2bde82007-08-02 16:26:10 +0000481
482
mblighdcd57a82007-07-11 23:06:47 +0000483 def wait_down(self, timeout=None):
mbligh7d2bde82007-08-02 16:26:10 +0000484 """
485 Wait until the remote host is down or the timeout expires.
mblighdcd57a82007-07-11 23:06:47 +0000486
487 In fact, it will wait until an ssh connection to the remote
488 host fails.
489
490 Args:
491 timeout: time limit in seconds before returning even
492 if the host is not up.
493
494 Returns:
495 True if the host was found to be down, False otherwise
496 """
497 if timeout:
498 end_time= time.time() + timeout
499
500 while not timeout or time.time() < end_time:
501 try:
mbligh7e1e9642007-07-31 18:00:45 +0000502 run_timeout= 10
mblighdcd57a82007-07-11 23:06:47 +0000503 result= self.run("true", timeout=run_timeout)
504 except errors.AutoservRunError:
505 return True
506 else:
507 if result.aborted:
508 return True
509 time.sleep(1)
510
511 return False
mbligh7d2bde82007-08-02 16:26:10 +0000512
513
mblighdbe4a382007-07-26 19:41:28 +0000514 def ensure_up(self):
mbligh7d2bde82007-08-02 16:26:10 +0000515 """
516 Ensure the host is up if it is not then do not proceed;
517 this prevents cacading failures of tests
518 """
mbligha0452c82007-08-08 20:24:57 +0000519 print 'Ensuring that %s is up before continuing' % self.hostname
520 if hasattr(self, 'hardreset') and not self.wait_up(300):
mblighdbe4a382007-07-26 19:41:28 +0000521 print "Performing a hardreset on %s" % self.hostname
522 self.hardreset()
mbligha9563b92007-10-25 14:45:56 +0000523 if not self.wait_up(60 * 30):
524 # 30 minutes should be more than enough
525 raise errors.AutoservHostError
mbligha0452c82007-08-08 20:24:57 +0000526 print 'Host up, continuing'
mbligh7d2bde82007-08-02 16:26:10 +0000527
528
mblighdcd57a82007-07-11 23:06:47 +0000529 def get_num_cpu(self):
mbligh7d2bde82007-08-02 16:26:10 +0000530 """
531 Get the number of CPUs in the host according to
mblighdcd57a82007-07-11 23:06:47 +0000532 /proc/cpuinfo.
533
534 Returns:
535 The number of CPUs
536 """
537
mbligh5f876ad2007-10-12 23:59:53 +0000538 proc_cpuinfo = self.run("cat /proc/cpuinfo").stdout
mblighdcd57a82007-07-11 23:06:47 +0000539 cpus = 0
540 for line in proc_cpuinfo.splitlines():
541 if line.startswith('processor'):
542 cpus += 1
543 return cpus
mbligh5f876ad2007-10-12 23:59:53 +0000544
545
546 def check_uptime(self):
547 """
548 Check that uptime is available and monotonically increasing.
549 """
550 if not self.ping():
551 raise "Client is not pingable"
552 result = self.run("/bin/cat /proc/uptime", 30)
553 return result.stdout.strip().split()[0]
554
555
556 def get_arch(self):
557 """
558 Get the hardware architecture of the remote machine
559 """
560 arch = self.run('/bin/uname -m').stdout.rstrip()
561 if re.match(r'i\d86$', arch):
562 arch = 'i386'
563 return arch
564
565
566 def get_kernel_ver(self):
567 """
568 Get the kernel version of the remote machine
569 """
570 return self.run('/bin/uname -r').stdout.rstrip()
571
572
573 def get_cmdline(self):
574 """
575 Get the kernel command line of the remote machine
576 """
577 return self.run('cat /proc/cmdline').stdout.rstrip()
578
579
580 def ping(self):
581 """
582 Ping the remote system, and return whether it's available
583 """
584 fpingcmd = "%s -q %s" % ('/usr/bin/fping', self.hostname)
585 rc = utils.system(fpingcmd, ignore_status = 1)
586 return (rc == 0)