Example #1
0
 def setUp(self):
     '''
     Setup the test case
     :return:
     '''
     self.archive_path = '/highway/to/hell'
     self.output_device = MagicMock()
     self.collector = SupportDataCollector(self.archive_path, self.output_device)
Example #2
0
    def run(self,
            profile='default',
            pillar=None,
            archive=None,
            output='nested'):
        '''
        Run Salt Support on the minion.

        profile
            Set available profile name. Default is "default".

        pillar
            Set available profile from the pillars.

        archive
            Override archive name. Default is "support". This results to "hostname-support-YYYYMMDD-hhmmss.bz2".

        output
            Change the default outputter. Default is "nested".

        CLI Example:

        .. code-block:: bash

            salt '*' support.run
            salt '*' support.run profile=network
            salt '*' support.run pillar=something_special
        '''
        class outputswitch(object):
            '''
            Output switcher on context
            '''
            def __init__(self, output_device):
                self._tmp_out = output_device
                self._orig_out = None

            def __enter__(self):
                self._orig_out = salt.cli.support.intfunc.out
                salt.cli.support.intfunc.out = self._tmp_out

            def __exit__(self, *args):
                salt.cli.support.intfunc.out = self._orig_out

        self.out = LogCollector()
        with outputswitch(self.out):
            self.collector = SupportDataCollector(
                archive or self._get_archive_name(archname=archive), output)
            self.collector.out = self.out
            self.collector.open()
            self.collect_local_data(profile=profile,
                                    profile_source=__pillar__.get(pillar))
            self.collect_internal_data()
            self.collector.close()

        return {
            'archive': self.collector.archive_path,
            'messages': self.out.messages
        }
Example #3
0
 def setUp(self):
     '''
     Set up test suite.
     :return:
     '''
     self.archive_path = '/dev/null'
     self.output_device = MagicMock()
     self.runner = SaltSupport()
     self.runner.collector = SupportDataCollector(self.archive_path, self.output_device)
Example #4
0
class SaltSupportModule(SaltSupport):
    '''
    Salt Support module class.
    '''
    def __init__(self):
        '''
        Constructor
        '''
        self.config = self.setup_config()

    def setup_config(self):
        '''
        Return current configuration
        :return:
        '''
        return __opts__

    def _get_archive_name(self, archname=None):
        '''
        Create default archive name.

        :return:
        '''
        archname = re.sub('[^a-z0-9]', '',
                          (archname or '').lower()) or 'support'
        for grain in ['fqdn', 'host', 'localhost', 'nodename']:
            host = __grains__.get(grain)
            if host:
                break
        if not host:
            host = 'localhost'

        return os.path.join(
            tempfile.gettempdir(),
            '{hostname}-{archname}-{date}-{time}.bz2'.format(
                archname=archname,
                hostname=host,
                date=time.strftime('%Y%m%d'),
                time=time.strftime('%H%M%S')))

    @salt.utils.decorators.external
    def profiles(self):
        '''
        Get list of profiles.

        :return:
        '''
        return {
            'standard': salt.cli.support.get_profiles(self.config),
            'custom': [],
        }

    @salt.utils.decorators.external
    def archives(self):
        '''
        Get list of existing archives.
        :return:
        '''
        arc_files = []
        tmpdir = tempfile.gettempdir()
        for filename in os.listdir(tmpdir):
            mtc = re.match(r'\w+-\w+-\d+-\d+\.bz2', filename)
            if mtc and len(filename) == mtc.span()[-1]:
                arc_files.append(os.path.join(tmpdir, filename))

        return arc_files

    @salt.utils.decorators.external
    def last_archive(self):
        '''
        Get the last available archive
        :return:
        '''
        archives = {}
        for archive in self.archives():
            archives[int(archive.split('.')[0].split('-')[-1])] = archive

        return archives and archives[max(archives)] or None

    @salt.utils.decorators.external
    def delete_archives(self, *archives):
        '''
        Delete archives
        :return:
        '''
        # Remove paths
        _archives = []
        for archive in archives:
            _archives.append(os.path.basename(archive))
        archives = _archives[:]

        ret = {'files': {}, 'errors': {}}
        for archive in self.archives():
            arc_dir = os.path.dirname(archive)
            archive = os.path.basename(archive)
            if archives and archive in archives or not archives:
                archive = os.path.join(arc_dir, archive)
                try:
                    os.unlink(archive)
                    ret['files'][archive] = 'removed'
                except Exception as err:
                    ret['errors'][archive] = str(err)
                    ret['files'][archive] = 'left'

        return ret

    def format_sync_stats(self, cnt):
        '''
        Format stats of the sync output.

        :param cnt:
        :return:
        '''
        stats = salt.utils.odict.OrderedDict()
        if cnt.get('retcode') == salt.defaults.exitcodes.EX_OK:
            for line in cnt.get('stdout', '').split(os.linesep):
                line = line.split(': ')
                if len(line) == 2:
                    stats[line[0].lower().replace(' ', '_')] = line[1]
            cnt['transfer'] = stats
            del cnt['stdout']

        # Remove empty
        empty_sections = []
        for section in cnt:
            if not cnt[section] and section != 'retcode':
                empty_sections.append(section)
        for section in empty_sections:
            del cnt[section]

        return cnt

    @salt.utils.decorators.depends('rsync')
    @salt.utils.decorators.external
    def sync(self,
             group,
             name=None,
             host=None,
             location=None,
             move=False,
             all=False):
        '''
        Sync the latest archive to the host on given location.

        CLI Example:

        .. code-block:: bash

            salt '*' support.sync group=test
            salt '*' support.sync group=test name=/tmp/myspecial-12345-67890.bz2
            salt '*' support.sync group=test name=/tmp/myspecial-12345-67890.bz2 host=allmystuff.lan
            salt '*' support.sync group=test name=/tmp/myspecial-12345-67890.bz2 host=allmystuff.lan location=/opt/

        :param group: name of the local directory to which sync is going to put the result files
        :param name: name of the archive. Latest, if not specified.
        :param host: name of the destination host for rsync. Default is master, if not specified.
        :param location: local destination directory, default temporary if not specified
        :param move: move archive file[s]. Default is False.
        :param all: work with all available archives. Default is False (i.e. latest available)

        :return:
        '''
        tfh, tfn = tempfile.mkstemp()
        processed_archives = []
        src_uri = uri = None

        last_arc = self.last_archive()
        if name:
            archives = [name]
        elif all:
            archives = self.archives()
        elif last_arc:
            archives = [last_arc]
        else:
            archives = []

        for name in archives:
            err = None
            if not name:
                err = 'No support archive has been defined.'
            elif not os.path.exists(name):
                err = 'Support archive "{}" was not found'.format(name)
            if err is not None:
                log.error(err)
                raise salt.exceptions.SaltInvocationError(err)

            if not uri:
                src_uri = os.path.dirname(name)
                uri = '{host}:{loc}'.format(
                    host=host or __opts__['master'],
                    loc=os.path.join(location or tempfile.gettempdir(), group))

            os.write(tfh,
                     salt.utils.stringutils.to_bytes(os.path.basename(name)))
            os.write(tfh, salt.utils.stringutils.to_bytes(os.linesep))
            processed_archives.append(name)
            log.debug('Syncing %s to %s', name, uri)
        os.close(tfh)

        if not processed_archives:
            raise salt.exceptions.SaltInvocationError(
                'No archives found to transfer.')

        ret = __salt__['rsync.rsync'](
            src=src_uri,
            dst=uri,
            additional_opts=['--stats', '--files-from={}'.format(tfn)])
        ret['files'] = {}
        for name in processed_archives:
            if move:
                salt.utils.dictupdate.update(ret, self.delete_archives(name))
                log.debug('Deleting %s', name)
                ret['files'][name] = 'moved'
            else:
                ret['files'][name] = 'copied'

        try:
            os.unlink(tfn)
        except (OSError, IOError) as err:
            log.error('Cannot remove temporary rsync file %s: %s', tfn, err)

        return self.format_sync_stats(ret)

    @salt.utils.decorators.external
    def run(self,
            profile='default',
            pillar=None,
            archive=None,
            output='nested'):
        '''
        Run Salt Support on the minion.

        profile
            Set available profile name. Default is "default".

        pillar
            Set available profile from the pillars.

        archive
            Override archive name. Default is "support". This results to "hostname-support-YYYYMMDD-hhmmss.bz2".

        output
            Change the default outputter. Default is "nested".

        CLI Example:

        .. code-block:: bash

            salt '*' support.run
            salt '*' support.run profile=network
            salt '*' support.run pillar=something_special
        '''
        class outputswitch(object):
            '''
            Output switcher on context
            '''
            def __init__(self, output_device):
                self._tmp_out = output_device
                self._orig_out = None

            def __enter__(self):
                self._orig_out = salt.cli.support.intfunc.out
                salt.cli.support.intfunc.out = self._tmp_out

            def __exit__(self, *args):
                salt.cli.support.intfunc.out = self._orig_out

        self.out = LogCollector()
        with outputswitch(self.out):
            self.collector = SupportDataCollector(
                archive or self._get_archive_name(archname=archive), output)
            self.collector.out = self.out
            self.collector.open()
            self.collect_local_data(profile=profile,
                                    profile_source=__pillar__.get(pillar))
            self.collect_internal_data()
            self.collector.close()

        return {
            'archive': self.collector.archive_path,
            'messages': self.out.messages
        }
Example #5
0
class SaltSupportCollectorTestCase(TestCase):
    '''
    Collector tests.
    '''
    def setUp(self):
        '''
        Setup the test case
        :return:
        '''
        self.archive_path = '/highway/to/hell'
        self.output_device = MagicMock()
        self.collector = SupportDataCollector(self.archive_path,
                                              self.output_device)

    def tearDown(self):
        '''
        Tear down the test case elements
        :return:
        '''
        del self.collector
        del self.archive_path
        del self.output_device

    @patch('salt.cli.support.collector.tarfile.TarFile', MagicMock())
    def test_archive_open(self):
        '''
        Test archive is opened.

        :return:
        '''
        self.collector.open()
        assert self.collector.archive_path == self.archive_path
        with pytest.raises(salt.exceptions.SaltException) as err:
            self.collector.open()
        assert 'Archive already opened' in str(err)

    @patch('salt.cli.support.collector.tarfile.TarFile', MagicMock())
    def test_archive_close(self):
        '''
        Test archive is opened.

        :return:
        '''
        self.collector.open()
        self.collector._flush_content = lambda: None
        self.collector.close()
        assert self.collector.archive_path == self.archive_path
        with pytest.raises(salt.exceptions.SaltException) as err:
            self.collector.close()
        assert 'Archive already closed' in str(err)

    def test_archive_addwrite(self):
        '''
        Test add to the archive a section and write to it.

        :return:
        '''
        archive = MagicMock()
        with patch('salt.cli.support.collector.tarfile.TarFile', archive):
            self.collector.open()
            self.collector.add('foo')
            self.collector.write(title='title', data='data', output='null')
            self.collector._flush_content()

            assert (archive.bz2open().addfile.call_args[1]['fileobj'].read() ==
                    to_bytes('title\n-----\n\nraw-content: data\n\n\n\n'))

    @patch('salt.utils.files.fopen', MagicMock(return_value='path=/dev/null'))
    def test_archive_addlink(self):
        '''
        Test add to the archive a section and link an external file or directory to it.

        :return:
        '''
        archive = MagicMock()
        with patch('salt.cli.support.collector.tarfile.TarFile', archive):
            self.collector.open()
            self.collector.add('foo')
            self.collector.link(title='Backup Path',
                                path='/path/to/backup.config')
            self.collector._flush_content()

            assert archive.bz2open().addfile.call_count == 1
            assert (archive.bz2open().addfile.call_args[1]['fileobj'].read(
            ) == to_bytes('Backup Path\n-----------\n\npath=/dev/null\n\n\n'))

    @patch('salt.utils.files.fopen', MagicMock(return_value='path=/dev/null'))
    def test_archive_discard_section(self):
        '''
        Test discard a section from the archive.

        :return:
        '''
        archive = MagicMock()
        with patch('salt.cli.support.collector.tarfile.TarFile', archive):
            self.collector.open()
            self.collector.add('solar-interference')
            self.collector.link(title='Thermal anomaly',
                                path='/path/to/another/great.config')
            self.collector.add('foo')
            self.collector.link(title='Backup Path',
                                path='/path/to/backup.config')
            self.collector._flush_content()
            assert archive.bz2open().addfile.call_count == 2
            assert (archive.bz2open().addfile.mock_calls[0][2]['fileobj'].read(
            ) == to_bytes(
                'Thermal anomaly\n---------------\n\npath=/dev/null\n\n\n'))
            self.collector.close()

        archive = MagicMock()
        with patch('salt.cli.support.collector.tarfile.TarFile', archive):
            self.collector.open()
            self.collector.add('solar-interference')
            self.collector.link(title='Thermal anomaly',
                                path='/path/to/another/great.config')
            self.collector.discard_current()
            self.collector.add('foo')
            self.collector.link(title='Backup Path',
                                path='/path/to/backup.config')
            self.collector._flush_content()
            assert archive.bz2open().addfile.call_count == 2
            assert (archive.bz2open().addfile.mock_calls[0][2]['fileobj'].read(
            ) == to_bytes('Backup Path\n-----------\n\npath=/dev/null\n\n\n'))
            self.collector.close()