# Copyright Red Hat 2017, Jake Hunsaker <jhunsake@redhat.com>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import fnmatch
import inspect
import logging
import os
import pexpect
import re
import shlex
import shutil
import six

from distutils.version import LooseVersion
from pipes import quote
from soscollector.exceptions import *
from subprocess import Popen, PIPE


class SosNode():

    def __init__(self, address, config, force=False, load_facts=True):
        self.address = address.strip()
        self.local = False
        self.hostname = None
        self.config = config
        self.sos_path = None
        self.retrieved = False
        self.hash_retrieved = False
        self.sos_info = {
            'version': None,
            'enabled': [],
            'disabled': [],
            'options': [],
            'presets': []
        }
        filt = ['localhost', '127.0.0.1', self.config['hostname']]
        self.logger = logging.getLogger('sos_collector')
        self.console = logging.getLogger('sos_collector_console')
        self.control_path = ("%s/.sos-collector-%s"
                             % (self.config['tmp_dir'], self.address))
        self.ssh_cmd = self._create_ssh_command()
        if self.address not in filt or force:
            try:
                self.connected = self._create_ssh_session()
            except Exception as err:
                self.log_error('Unable to open SSH session: %s' % err)
                raise
        else:
            self.connected = True
            self.local = True
        if self.connected and load_facts:
            self.host = self.determine_host()
            self._set_sos_prefix(self.host.set_sos_prefix())
            if not self.host:
                self.connected = False
                self.close_ssh_session()
                return None
            self.log_debug("Host facts found to be %s" %
                           self.host.report_facts())
            self.get_hostname()
            self._load_sos_info()

    def _create_ssh_command(self):
        '''Build the complete ssh command for this node'''
        cmd = "ssh -oControlPath=%s " % self.control_path
        cmd += "%s@%s " % (self.config['ssh_user'], self.address)
        return cmd

    def _fmt_msg(self, msg):
        return '{:<{}} : {}'.format(self._hostname, self.config['hostlen'] + 1,
                                    msg)

    def file_exists(self, fname):
        '''Checks for the presence of fname on the remote node'''
        if not self.local:
            try:
                res = self.run_command("stat %s" % fname)
                return res['status'] == 0
            except Exception as err:
                return False
        else:
            try:
                os.stat(fname)
                return True
            except Exception:
                return False

    @property
    def _hostname(self):
        return self.hostname if self.hostname else self.address

    @property
    def control_socket_exists(self):
        '''Check if the SSH control socket exists

        The control socket is automatically removed by the SSH daemon in the
        event that the last connection to the node was greater than the timeout
        set by the ControlPersist option. This can happen for us if we are
        collecting from a large number of nodes, and the timeout expires before
        we start collection.
        '''
        return os.path.exists(self.control_path)

    def _sanitize_log_msg(self, msg):
        '''Attempts to obfuscate sensitive information in log messages such as
        passwords'''
        reg = r'(?P<var>(pass|key|secret|PASS|KEY|SECRET).*?=)(?P<value>.*?\s)'
        return re.sub(reg, r'\g<var>****** ', msg)

    def log_info(self, msg):
        '''Used to print and log info messages'''
        caller = inspect.stack()[1][3]
        lmsg = '[%s:%s] %s' % (self._hostname, caller, msg)
        self.logger.info(lmsg)
        self.console.info(self._fmt_msg(msg))

    def log_error(self, msg):
        '''Used to print and log error messages'''
        caller = inspect.stack()[1][3]
        lmsg = '[%s:%s] %s' % (self._hostname, caller, msg)
        self.logger.error(lmsg)
        self.console.error(self._fmt_msg(msg))

    def log_debug(self, msg):
        '''Used to print and log debug messages'''
        msg = self._sanitize_log_msg(msg)
        caller = inspect.stack()[1][3]
        msg = '[%s:%s] %s' % (self._hostname, caller, msg)
        self.logger.debug(msg)
        if self.config['verbose']:
            self.console.debug(msg)

    def get_hostname(self):
        '''Get the node's hostname'''
        sout = self.run_command('hostname')
        self.hostname = sout['stdout'].strip()
        self.log_debug(
            'Hostname set to %s' % self.hostname)

    def _format_cmd(self, cmd):
        '''If we need to provide a sudo or root password to a command, then
        here we prefix the command with the correct bits
        '''
        if self.config['become_root']:
            return "su -c '%s'" % cmd
        if self.config['need_sudo']:
            return "sudo -S %s" % cmd
        return cmd

    def _fmt_output(self, output=None, rc=0):
        '''Formats the returned output from a command into a dict'''
        if rc == 0:
            stdout = output
            stderr = ''
        else:
            stdout = ''
            stderr = output
        res = {'status': rc,
               'stdout': stdout,
               'stderr': stderr}
        return res

    def _load_sos_info(self):
        '''Queries the node for information about the installed version of sos
        '''
        cmd = self.host.prefix + self.host.pkg_query(self.host.sos_pkg_name)
        res = self.run_command(cmd)
        if res['status'] == 0:
            ver = res['stdout'].splitlines()[-1].split('-')[1]
            self.sos_info['version'] = ver
            self.log_debug('sos version is %s' % self.sos_info['version'])
        else:
            self.log_error('sos is not installed on this node')
            self.connected = False
            return False
        cmd = self.host.prefix + 'sosreport -l'
        sosinfo = self.run_command(cmd)
        if sosinfo['status'] == 0:
            self._load_sos_plugins(sosinfo['stdout'])
        if self.check_sos_version('3.6'):
            self._load_sos_presets()

    def _load_sos_presets(self):
        cmd = self.host.prefix + 'sosreport --list-presets'
        res = self.run_command(cmd)
        if res['status'] == 0:
            for line in res['stdout'].splitlines():
                if line.strip().startswith('name:'):
                    pname = line.split('name:')[1].strip()
                    self.sos_info['presets'].append(pname)

    def _load_sos_plugins(self, sosinfo):
        ENABLED = 'The following plugins are currently enabled:'
        DISABLED = 'The following plugins are currently disabled:'
        OPTIONS = 'The following plugin options are available:'
        PROFILES = 'Profiles:'

        enablereg = ENABLED + '(.*?)' + DISABLED
        disreg = DISABLED + '(.*?)' + OPTIONS
        optreg = OPTIONS + '(.*?)' + PROFILES
        proreg = PROFILES + '(.*?)' + '\n\n'

        self.sos_info['enabled'] = self._regex_sos_help(enablereg, sosinfo)
        self.sos_info['disabled'] = self._regex_sos_help(disreg, sosinfo)
        self.sos_info['options'] = self._regex_sos_help(optreg, sosinfo)
        self.sos_info['profiles'] = self._regex_sos_help(proreg, sosinfo, True)

    def _regex_sos_help(self, regex, sosinfo, is_list=False):
        res = []
        for result in re.findall(regex, sosinfo, re.S):
            for line in result.splitlines():
                if not is_list:
                    try:
                        res.append(line.split()[0])
                    except Exception:
                        pass
                else:
                    r = line.split(',')
                    res.extend(p.strip() for p in r if p.strip())
        return res

    def _set_sos_prefix(self, prefix):
        '''Applies any configuration settings to the sos prefix defined by a
        host type
        '''
        if self.host.containerized:
            prefix = prefix % {
                'image': self.config['image'] or self.host.container_image
            }
        self.host.prefix = prefix

    def read_file(self, to_read):
        '''Reads the specified file and returns the contents'''
        try:
            self.log_debug("Reading file %s" % to_read)
            if not self.local:
                res = self.run_command("cat %s" % to_read, timeout=5)
                if res['status'] == 0:
                    return res['stdout']
                else:
                    if 'No such file' in res['stdout']:
                        self.log_debug("File %s does not exist on node"
                                       % to_read)
                    else:
                        self.log_error("Error reading %s: %s" %
                                       (to_read, res['stdout'].split(':')[1:]))
                    return ''
            else:
                with open(to_read, 'r') as rfile:
                    return rfile.read()
        except Exception as err:
            self.log_error("Exception while reading %s: %s" % (to_read, err))
            return ''

    def determine_host(self):
        '''Attempts to identify the host installation against supported
        distributions
        '''
        for host_type in self.config['host_types']:
            host = self.config['host_types'][host_type](self.address)
            rel_string = self.read_file(host.release_file)
            if host._check_enabled(rel_string):
                self.log_debug("Host installation found to be %s" %
                               host.distribution)
                return host
        self.log_error('Unable to determine host installation. Ignoring node')
        raise UnsupportedHostException

    def check_sos_version(self, ver):
        '''Checks to see if the sos installation on the node is AT LEAST the
        given ver. This means that if the installed version is greater than
        ver, this will still return True
        '''
        return LooseVersion(self.sos_info['version']) >= ver

    def is_installed(self, pkg):
        '''Checks if a given package is installed on the node'''
        cmd = self.host.pkg_query(pkg)
        res = self.run_command(cmd)
        if res['status'] == 0:
            return True
        return False

    def run_command(self, cmd, timeout=180, get_pty=False, need_root=False,
                    force_local=False):
        '''Runs a given cmd, either via the SSH session or locally

        Arguments:
            cmd - the full command to be run
            timeout - time in seconds to wait for the command to complete
            get_pty - If a shell is absolutely needed to run a command, set
                      this to True
            need_root - if a command requires root privileges, setting this to
                        True tells sos-collector to format the command with
                        sudo or su - as appropriate and to input the password
            force_local - force a command to run locally. Mainly used for scp.
        '''
        if not self.control_socket_exists:
            self.log_debug('Control socket does not exist, attempting to '
                           're-create')
            try:
                _sock = self._create_ssh_session()
                if not _sock:
                    self.log_debug('Failed to re-create control socket')
                    raise ControlSocketMissingException
            except Exception as err:
                self.log_error('Cannot run command: control socket does not '
                               'exist')
                self.log_debug("Error while trying to create new SSH control "
                               "socket: %s" % err)
                raise
        if cmd.startswith('sosreport'):
            cmd = cmd.replace('sosreport', self.host.sos_bin_path)
            need_root = True
        if need_root:
            get_pty = True
            cmd = self._format_cmd(cmd)
        self.log_debug('Running command %s' % cmd)
        if 'atomic' in cmd:
            get_pty = True
        if not self.local and not force_local:
            cmd = "%s %s" % (self.ssh_cmd, quote(cmd))
            res = pexpect.spawn(cmd, encoding='utf-8')
            if need_root:
                if self.config['need_sudo']:
                    res.sendline(self.config['sudo_pw'])
                if self.config['become_root']:
                    res.sendline(self.config['root_password'])
            output = res.expect([pexpect.EOF, pexpect.TIMEOUT],
                                timeout=timeout)
            if output == 0:
                out = res.before
                res.close()
                rc = res.exitstatus
                return {'status': rc, 'stdout': out}
            elif output == 1:
                raise CommandTimeoutException(cmd)
        else:
            try:
                proc = Popen(shlex.split(cmd), shell=get_pty, stdin=PIPE,
                             stdout=PIPE, stderr=PIPE)
                if self.config['become_root'] and need_root:
                    stdout, stderr = proc.communicate(
                        input=self.config['root_password'] + '\n'
                    )
                elif self.config['need_sudo'] and need_root:
                    stdout, stderr = proc.communicate(
                        input=self.config['sudo_pw'] + '\n'
                    )
                else:
                    stdout, stderr = proc.communicate()
                proc.wait()
                rc = proc.returncode
                return {'status': rc, 'stdout': stdout or stderr}
            except Exception as err:
                self.log_error("Exception while running command %s: %s"
                               % (cmd, err))
                raise

    def sosreport(self):
        '''Run a sosreport on the node, then collect it'''
        self.finalize_sos_cmd()
        self.log_debug('Final sos command set to %s' % self.sos_cmd)
        try:
            path = self.execute_sos_command()
            if path:
                self.finalize_sos_path(path)
            else:
                self.log_error('Unable to determine path of sos archive')
            if self.sos_path:
                self.retrieved = self.retrieve_sosreport()
        except Exception:
            pass
        self.cleanup()

    def _create_ssh_session(self):
        '''
        Using ControlPersist, create the initial connection to the node.

        This will generate an OpenSSH ControlPersist socket within the tmp
        directory created or specified for sos-collector to use.

        At most, we will wait 30 seconds for a connection. This involves a 15
        second wait for the initial connection attempt, and a subsequent 15
        second wait for a response when we supply a password.

        Since we connect to nodes in parallel (using the --threads value), this
        means that the time between 'Connecting to nodes...' and 'Beginning
        collection of sosreports' that users see can be up to an amount of time
        equal to 30*(num_nodes/threads) seconds.

        Returns
            True if session is successfully opened, else raise Exception
        '''
        # Don't use self.ssh_cmd here as we need to add a few additional
        # parameters to establish the initial connection
        self.log_debug('Opening SSH session to create control socket')
        connected = False
        ssh_key = ''
        ssh_port = ''
        if self.config['ssh_port'] != 22:
            ssh_port = "-p%s " % self.config['ssh_port']
        if self.config['ssh_key']:
            ssh_key = "-i%s" % self.config['ssh_key']
        cmd = ("ssh %s %s -oControlPersist=600 -oControlMaster=auto "
               "-oStrictHostKeyChecking=no -oControlPath=%s %s@%s "
               "\"echo Connected\"" % (ssh_key,
                                       ssh_port,
                                       self.control_path,
                                       self.config['ssh_user'],
                                       self.address))
        res = pexpect.spawn(cmd, encoding='utf-8')

        connect_expects = [
            u'Connected',
            u'password:',
            u'.*Permission denied.*',
            u'.* port .*: No route to host',
            u'.*Could not resolve hostname.*',
            pexpect.TIMEOUT
        ]

        index = res.expect(connect_expects, timeout=15)
        if index == 0:
            connected = True
        elif index == 1:
            if self.config['password']:
                pass_expects = [
                    u'Connected',
                    u'Permission denied, please try again.',
                    pexpect.TIMEOUT
                ]
                res.sendline(self.config['password'])
                pass_index = res.expect(pass_expects, timeout=15)
                if pass_index == 0:
                    connected = True
                elif pass_index == 1:
                    # Note that we do not get an exitstatus here, so matching
                    # this line means an invalid password will be reported for
                    # both invalid passwords and invalid user names
                    raise InvalidPasswordException
                elif pass_index == 2:
                    raise TimeoutPasswordAuthException
            else:
                raise PasswordRequestException
        elif index == 2:
            raise AuthPermissionDeniedException
        elif index == 3:
            raise ConnectionException(self.address, self.config['ssh_port'])
        elif index == 4:
            raise ConnectionException(self.address)
        elif index == 5:
            raise ConnectionTimeoutException
        else:
            raise Exception("Unknown error, client returned %s" % res.before)
        if connected:
            self.log_debug("Successfully created control socket at %s"
                           % self.control_path)
            return True
        return False

    def close_ssh_session(self):
        '''Remove the control socket to effectively terminate the session'''
        if self.local:
            return True
        try:
            res = self.run_command("rm -f %s" % self.control_path,
                                   force_local=True)
            if res['status'] == 0:
                return True
            self.log_error("Could not remove ControlPath %s: %s"
                           % (self.control_path, res['stdout']))
            return False
        except Exception as e:
            self.log_error('Error closing SSH session: %s' % e)
            return False

    def _preset_exists(self, preset):
        '''Verifies if the given preset exists on the node'''
        return preset in self.sos_info['presets']

    def _plugin_exists(self, plugin):
        '''Verifies if the given plugin exists on the node'''
        return any(plugin in s for s in [self.sos_info['enabled'],
                                         self.sos_info['disabled']])

    def _check_enabled(self, plugin):
        '''Checks to see if the plugin is default enabled on node'''
        return plugin in self.sos_info['enabled']

    def _check_disabled(self, plugin):
        '''Checks to see if the plugin is default disabled on node'''
        return plugin in self.sos_info['disabled']

    def _plugin_option_exists(self, opt):
        '''Attempts to verify that the given option is available on the node.
        Note that we only get available options for enabled plugins, so if a
        plugin has been force-enabled we cannot validate if the plugin option
        is correct or not'''
        plug = opt.split('.')[0]
        if not self._plugin_exists(plug):
            return False
        if (self._check_disabled(plug) and
                plug not in self.config['enable_plugins']):
            return False
        if self._check_enabled(plug):
            return opt in self.sos_info['options']
        # plugin exists, but is normally disabled. Assume user knows option is
        # valid when enabling the plugin
        return True

    def _fmt_sos_opt_list(self, opts):
        '''Returns a comma delimited list for sos plugins that are confirmed
        to exist on the node'''
        return ','.join(o for o in opts if self._plugin_exists(o))

    def finalize_sos_cmd(self):
        '''Use host facts and compare to the cluster type to modify the sos
        command if needed'''
        self.sos_cmd = self.config['sos_cmd']
        self.sos_cmd = self.host.prefix + self.sos_cmd

        label = self.determine_sos_label()
        if label:
            self.sos_cmd = ' %s %s' % (self.sos_cmd, quote(label))

        if self.config['sos_opt_line']:
            return True

        if self.config['only_plugins']:
            plugs = [o for o in self.config['only_plugins']
                     if self._plugin_exists(o)]
            if len(plugs) != len(self.config['only_plugins']):
                not_only = list(set(self.config['only_plugins']) - set(plugs))
                self.log_debug('Requested plugins %s were requested to be '
                               'enabled but do not exist' % not_only)
            only = self._fmt_sos_opt_list(self.config['only_plugins'])
            if only:
                self.sos_cmd += ' --only-plugins=%s' % quote(only)
            return True

        if self.config['skip_plugins']:
            # only run skip-plugins for plugins that are enabled
            skip = [o for o in self.config['skip_plugins']
                    if self._check_enabled(o)]
            if len(skip) != len(self.config['skip_plugins']):
                not_skip = list(set(self.config['skip_plugins']) - set(skip))
                self.log_debug('Requested to skip plugins %s, but plugins are '
                               'already not enabled' % not_skip)
            skipln = self._fmt_sos_opt_list(skip)
            if skipln:
                self.sos_cmd += ' --skip-plugins=%s' % quote(skipln)

        if self.config['enable_plugins']:
            # only run enable for plugins that are disabled
            opts = [o for o in self.config['enable_plugins']
                    if o not in self.config['skip_plugins']
                    and self._check_disabled(o) and self._plugin_exists(o)]
            if len(opts) != len(self.config['enable_plugins']):
                not_on = list(set(self.config['enable_plugins']) - set(opts))
                self.log_debug('Requested to enable plugins %s, but plugins '
                               'are already enabled or do not exist' % not_on)
            enable = self._fmt_sos_opt_list(opts)
            if enable:
                self.sos_cmd += ' --enable-plugins=%s' % quote(enable)

        if self.config['plugin_options']:
            opts = [o for o in self.config['plugin_options']
                    if self._plugin_exists(o.split('.')[0])
                    and self._plugin_option_exists(o.split('=')[0])]
            if opts:
                self.sos_cmd += ' -k %s' % quote(','.join(o for o in opts))

        if self.config['preset']:
            if self._preset_exists(self.config['preset']):
                self.sos_cmd += ' --preset=%s' % quote(self.config['preset'])
            else:
                self.log_debug('Requested to enable preset %s but preset does '
                               'not exist on node' % self.config['preset'])

    def determine_sos_label(self):
        '''Determine what, if any, label should be added to the sosreport'''
        label = ''
        label += self.config['cluster'].get_node_label(self)

        if self.config['label']:
            label += ('%s' % self.config['label'] if not label
                      else '-%s' % self.config['label'])

        if not label:
            return None

        self.log_debug('Label for sosreport set to %s' % label)
        if self.check_sos_version('3.6'):
            lcmd = '--label'
        else:
            lcmd = '--name'
            label = '%s-%s' % (self.address.split('.')[0], label)
        return '%s=%s' % (lcmd, label)

    def finalize_sos_path(self, path):
        '''Use host facts to determine if we need to change the sos path
        we are retrieving from'''
        pstrip = self.host.sos_path_strip
        if pstrip:
            path = path.replace(pstrip, '')
        path = path.split()[0]
        self.log_debug('Final sos path: %s' % path)
        self.sos_path = path
        self.archive = path.split('/')[-1]

    def determine_sos_error(self, rc, stdout):
        if rc == -1:
            return 'sosreport process received SIGKILL on node'
        if rc == 1:
            if 'sudo' in stdout:
                return 'sudo attempt failed'
        if rc == 127:
            return 'sosreport terminated unexpectedly. Check disk space'
        if len(stdout) > 0:
            return stdout.split('\n')[0:1]
        else:
            return 'sos exited with code %s' % rc

    def execute_sos_command(self):
        '''Run sosreport and capture the resulting file path'''
        self.log_info("Generating sosreport...")
        try:
            path = False
            res = self.run_command(self.sos_cmd,
                                   timeout=self.config['timeout'],
                                   get_pty=True, need_root=True)
            if res['status'] == 0:
                for line in res['stdout'].splitlines():
                    if fnmatch.fnmatch(line, '*sosreport-*tar*'):
                        path = line.strip()
            else:
                err = self.determine_sos_error(res['status'], res['stdout'])
                self.log_debug("Error running sosreport. rc = %s msg = %s"
                               % (res['status'], res['stdout'] or
                                  res['stderr']))
                raise Exception(err)
            return path
        except CommandTimeoutException:
            self.log_error('Timeout exceeded')
            raise
        except Exception as e:
            self.log_error('Error running sosreport: %s' % e)
            raise

    def retrieve_file(self, path):
        '''Copies the specified file from the host to our temp dir'''
        destdir = self.config['tmp_dir'] + '/'
        dest = destdir + path.split('/')[-1]
        try:
            if not self.local:
                if self.file_exists(path):
                    self.log_debug("Copying remote %s to local %s" %
                                   (path, destdir))
                    cmd = "/usr/bin/scp -oControlPath=%s %s@%s:%s %s" % (
                        self.control_path,
                        self.config['ssh_user'],
                        self.address,
                        path,
                        destdir
                    )
                    res = self.run_command(cmd, force_local=True)
                    return res['status'] == 0
                else:
                    self.log_debug("Attempting to copy remote file %s, but it "
                                   "does not exist on filesystem" % path)
                    return False
            else:
                self.log_debug("Moving %s to %s" % (path, destdir))
                shutil.copy(path, dest)
            return True
        except Exception as err:
            self.log_debug("Failed to retrieve %s: %s" % (path, err))
            return False

    def remove_file(self, path):
        '''Removes the spciefied file from the host. This should only be used
        after we have retrieved the file already
        '''
        path = ''.join(path.split())
        try:
            if len(path) <= 2:  # ensure we have a non '/' path
                self.log_debug("Refusing to remove path %s: appears to be "
                               "incorrect and possibly dangerous" % path)
                return False
            if self.file_exists(path):
                self.log_debug("Removing file %s" % path)
                cmd = "rm -f %s" % path
                res = self.run_command(cmd, need_root=True)
                return True
            else:
                self.log_debug("Attempting to remove remote file %s, but it "
                               "does not exist on filesystem" % path)
                return False
        except Exception as e:
            self.log_debug('Failed to remove %s: %s' % (path, e))
            return False

    def retrieve_sosreport(self):
        '''Collect the sosreport archive from the node'''
        if self.sos_path:
            if self.config['need_sudo'] or self.config['become_root']:
                try:
                    self.make_archive_readable(self.sos_path)
                except Exception:
                    self.log_error('Failed to make archive readable')
                    return False
                try:
                    self.make_archive_readable(self.sos_path + '.md5')
                except Exception:
                    self.log_debug('Failed to make md5 readable')
            self.logger.info('Retrieving sosreport from %s' % self.address)
            self.log_info('Retrieving sosreport...')
            ret = self.retrieve_file(self.sos_path)
            if ret:
                self.log_info('Successfully collected sosreport')
            else:
                self.log_error('Failed to retrieve sosreport')
                raise SystemExit
                return False
            self.hash_retrieved = self.retrieve_file(self.sos_path + '.md5')
            return True
        else:
            # sos sometimes fails but still returns a 0 exit code
            if self.stderr.read():
                e = self.stderr.read()
            else:
                e = [x.strip() for x in self.stdout.readlines() if x.strip][-1]
            self.logger.error(
                'Failed to run sosreport on %s: %s' % (self.address, e))
            self.log_error('Failed to run sosreport. %s' % e)
            return False

    def remove_sos_archive(self):
        '''Remove the sosreport archive from the node, since we have
        collected it and it would be wasted space otherwise'''
        if self.sos_path is None:
            return
        if 'sosreport' not in self.sos_path:
            self.log_debug("Node sosreport path %s looks incorrect. Not "
                           "attempting to remove path" % self.sos_path)
            return
        removed = self.remove_file(self.sos_path)
        if not removed:
            self.log_error('Failed to remove sosreport')

    def cleanup(self):
        '''Remove the sos archive from the node once we have it locally'''
        self.remove_sos_archive()
        if self.hash_retrieved:
            self.remove_file(self.sos_path + '.md5')
        cleanup = self.host.set_cleanup_cmd()
        if cleanup:
            self.run_command(cleanup)

    def collect_extra_cmd(self, filenames):
        '''Collect the file created by a cluster outside of sos'''
        for filename in filenames:
            try:
                if self.config['need_sudo'] or self.config['become_root']:
                    try:
                        self.make_archive_readable(filename)
                    except Exception as err:
                        self.log_error("Unable to retrieve file %s" % filename)
                        self.log_debug("Failed to make file %s readable: %s"
                                       % (filename, err))
                        continue
                ret = self.retrieve_file(filename)
                if ret:
                    self.remove_file(filename)
                else:
                    self.log_error("Unable to retrieve file %s" % filename)
            except Exception as e:
                msg = 'Error collecting additional data from master: %s' % e
                self.log_error(msg)

    def make_archive_readable(self, filepath):
        '''Used to make the given archive world-readable, which is slightly
        better than changing the ownership outright.

        This is only used when we're not connecting as root.
        '''
        cmd = 'chmod o+r %s' % filepath
        res = self.run_command(cmd, timeout=10, need_root=True)
        if res['status'] == 0:
            return True
        else:
            msg = "Exception while making %s readable. Return code was %s"
            self.log_error(msg % (filepath, res['status']))
            raise Exception
