Implement plugin filtering

pull/35209/head
Toshio Kuratomi 7 years ago
parent 14c3b4d8e5
commit 340a7be7c3

@ -16,6 +16,7 @@
#module_utils = /usr/share/my_module_utils/ #module_utils = /usr/share/my_module_utils/
#remote_tmp = ~/.ansible/tmp #remote_tmp = ~/.ansible/tmp
#local_tmp = ~/.ansible/tmp #local_tmp = ~/.ansible/tmp
#plugin_filters_cfg = /etc/ansible/plugin_filters.yml
#forks = 5 #forks = 5
#poll_interval = 15 #poll_interval = 15
#sudo_user = root #sudo_user = root

@ -0,0 +1,6 @@
---
filter_version: '1.0'
module_blacklist:
# List the modules to blacklist here
#- easy_install
#- s3

@ -1387,6 +1387,16 @@ PLAYBOOK_VARS_ROOT:
ini: ini:
- {key: playbook_vars_root, section: defaults} - {key: playbook_vars_root, section: defaults}
choices: [ top, bottom, all ] choices: [ top, bottom, all ]
PLUGIN_FILTERS_CFG:
name: Config file for limiting valid plugins
default: null
version_added: "2.5.0"
description:
- "A path to configuration for filtering which plugins installed on the system are allowed to be used"
- " The default is /etc/ansible/plugin_filters.yml"
ini:
- key: plugin_filters_cfg
section: default
RETRY_FILES_ENABLED: RETRY_FILES_ENABLED:
name: Retry files name: Retry files
default: True default: True

@ -54,7 +54,7 @@ def _safe_load(stream, file_name=None, vault_secrets=None):
pass # older versions of yaml don't have dispose function, ignore pass # older versions of yaml don't have dispose function, ignore
def from_yaml(data, file_name='<string>', show_content=True): def from_yaml(data, file_name='<string>', show_content=True, vault_secrets=None):
''' '''
Creates a python datastructure from the given data, which can be either Creates a python datastructure from the given data, which can be either
a JSON or YAML string. a JSON or YAML string.
@ -80,7 +80,7 @@ def from_yaml(data, file_name='<string>', show_content=True):
except Exception: except Exception:
# must not be JSON, let the rest try # must not be JSON, let the rest try
try: try:
new_data = _safe_load(in_data, file_name=file_name) new_data = _safe_load(in_data, file_name=file_name, vault_secrets=vault_secrets)
except YAMLError as yaml_exc: except YAMLError as yaml_exc:
_handle_error(yaml_exc, file_name, show_content) _handle_error(yaml_exc, file_name, show_content)

@ -17,8 +17,10 @@ import warnings
from collections import defaultdict from collections import defaultdict
from ansible import constants as C from ansible import constants as C
from ansible.plugins import get_plugin_class, MODULE_CACHE, PATH_CACHE, PLUGIN_PATH_CACHE from ansible.errors import AnsibleError
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.parsing.utils.yaml import from_yaml
from ansible.plugins import get_plugin_class, MODULE_CACHE, PATH_CACHE, PLUGIN_PATH_CACHE
from ansible.utils.plugin_docs import get_docstring from ansible.utils.plugin_docs import get_docstring
try: try:
@ -235,6 +237,10 @@ class PluginLoader:
def find_plugin(self, name, mod_type='', ignore_deprecated=False, check_aliases=False): def find_plugin(self, name, mod_type='', ignore_deprecated=False, check_aliases=False):
''' Find a plugin named name ''' ''' Find a plugin named name '''
global _PLUGIN_FILTERS
if name in _PLUGIN_FILTERS[self.package]:
return None
if mod_type: if mod_type:
suffix = mod_type suffix = mod_type
elif self.class_name: elif self.class_name:
@ -405,6 +411,8 @@ class PluginLoader:
def all(self, *args, **kwargs): def all(self, *args, **kwargs):
''' instantiates all plugins with the same arguments ''' ''' instantiates all plugins with the same arguments '''
global _PLUGIN_FILTERS
path_only = kwargs.pop('path_only', False) path_only = kwargs.pop('path_only', False)
class_only = kwargs.pop('class_only', False) class_only = kwargs.pop('class_only', False)
all_matches = [] all_matches = []
@ -416,7 +424,7 @@ class PluginLoader:
for path in sorted(all_matches, key=os.path.basename): for path in sorted(all_matches, key=os.path.basename):
name = os.path.basename(os.path.splitext(path)[0]) name = os.path.basename(os.path.splitext(path)[0])
if '__init__' in name: if '__init__' in name or name in _PLUGIN_FILTERS[self.package]:
continue continue
if path_only: if path_only:
@ -462,6 +470,63 @@ class PluginLoader:
self._update_object(obj, name, path) self._update_object(obj, name, path)
yield obj yield obj
def _load_plugin_filter():
filters = defaultdict(frozenset)
if C.PLUGIN_FILTERS_CFG is None:
filter_cfg = '/etc/ansible/plugin_filters.yml'
user_set = False
else:
filter_cfg = C.PLUGIN_FILTERS_CFG
user_set = True
if os.path.exists(filter_cfg):
with open(filter_cfg, 'rb') as f:
try:
filter_data = from_yaml(f.read())
except Exception as e:
display.warning(u'The plugin filter file, {0} was not parsable.'
u' Skipping: {1}'.format(filter_cfg, to_text(e)))
return filters
try:
version = filter_data['filter_version']
except KeyError:
display.warning(u'The plugin filter file, {0} was invalid.'
u' Skipping.'.format(filter_cfg))
return filters
# Try to convert for people specifying version as a float instead of string
version = to_text(version)
version = version.strip()
if version == u'1.0':
# Modules and action plugins share the same blacklist since the difference between the
# two isn't visible to the users
filters['ansible.modules'] = frozenset(filter_data['module_blacklist'])
filters['ansible.plugins.action'] = filters['ansible.modules']
else:
display.warning(u'The plugin filter file, {0} was a version not recognized by this'
u' version of Ansible. Skipping.')
else:
if user_set:
display.warning(u'The plugin filter file, {0} does not exist.'
u' Skipping.'.format(filter_cfg))
# Specialcase the stat module as Ansible can run very few things if stat is blacklisted.
if 'stat' in filters['ansible.modules']:
raise AnsibleError('The stat module was specified in the module blacklist file, {0}, but'
' Ansible will not function without the stat module. Please remove stat'
' from the blacklist.'.format(filter_cfg))
return filters
# TODO: All of the following is initialization code It should be moved inside of an initialization
# function which is called at some point early in the ansible and ansible-playbook CLI startup.
_PLUGIN_FILTERS = _load_plugin_filter()
# doc fragments first # doc fragments first
fragment_loader = PluginLoader( fragment_loader = PluginLoader(
'ModuleDocFragment', 'ModuleDocFragment',
@ -470,6 +535,7 @@ fragment_loader = PluginLoader(
'', '',
) )
action_loader = PluginLoader( action_loader = PluginLoader(
'ActionModule', 'ActionModule',
'ansible.plugins.action', 'ansible.plugins.action',

@ -0,0 +1,10 @@
---
- hosts: testhost
gather_facts: False
tasks:
- copy:
content: 'Testing 1... 2... 3...'
dest: ./testing.txt
- file:
state: absent
path: ./testing.txt

@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./filter_lookup.yml

@ -0,0 +1,6 @@
---
filter_version: 1.0
module_blacklist:
# Specify the name of a lookup plugin here. This should have no effect as
# this is only for filtering modules
- list

@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./filter_modules.yml

@ -0,0 +1,9 @@
---
filter_version: 1.0
module_blacklist:
# A pure action plugin
- pause
# A hybrid action plugin with module
- copy
# A pure module
- tempfile

@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./filter_ping.yml

@ -0,0 +1,5 @@
---
filter_version: 1.0
module_blacklist:
# Ping is special
- ping

@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./filter_stat.yml

@ -0,0 +1,5 @@
---
filter_version: 1.0
module_blacklist:
# Stat is special
- stat

@ -0,0 +1,14 @@
---
- hosts: testhost
gather_facts: False
vars:
data:
- one
- two
tasks:
- debug:
msg: '{{ lookup("list", data) }}'
- debug:
msg: '{{ item }}'
with_list: '{{ data }}'

@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./empty.yml

@ -0,0 +1,6 @@
---
- hosts: testhost
gather_facts: False
tasks:
- pause:
seconds: 1

@ -0,0 +1,6 @@
---
- hosts: testhost
gather_facts: False
tasks:
- ping:
data: 'Testing 1... 2... 3...'

@ -0,0 +1,128 @@
#!/usr/bin/env bash
set -ux
#
# Check that with no filters set, all of these modules run as expected
#
ANSIBLE_CONFIG=no_filters.ini ansible-playbook copy.yml -i ../../inventory -vvv "$@"
if test $? != 0 ; then
echo "### Failed to run copy with no filters applied"
exit 1
fi
ANSIBLE_CONFIG=no_filters.ini ansible-playbook pause.yml -i ../../inventory -vvv "$@"
if test $? != 0 ; then
echo "### Failed to run pause with no filters applied"
exit 1
fi
ANSIBLE_CONFIG=no_filters.ini ansible-playbook tempfile.yml -i ../../inventory -vvv "$@"
if test $? != 0 ; then
echo "### Failed to run tempfile with no filters applied"
exit 1
fi
#
# Check that with these modules filtered out, all of these modules fail to be found
#
ANSIBLE_CONFIG=filter_modules.ini ansible-playbook copy.yml -i ../../inventory -v "$@"
if test $? = 0 ; then
echo "### Failed to prevent copy from running"
exit 1
else
echo "### Copy was prevented from running as expected"
fi
ANSIBLE_CONFIG=filter_modules.ini ansible-playbook pause.yml -i ../../inventory -v "$@"
if test $? = 0 ; then
echo "### Failed to prevent pause from running"
exit 1
else
echo "### pause was prevented from running as expected"
fi
ANSIBLE_CONFIG=filter_modules.ini ansible-playbook tempfile.yml -i ../../inventory -v "$@"
if test $? = 0 ; then
echo "### Failed to prevent tempfile from running"
exit 1
else
echo "### tempfile was prevented from running as expected"
fi
#
# ping is a special module as we test for its existence. Check it specially
#
# Check that ping runs with no filter
ANSIBLE_CONFIG=no_filters.ini ansible-playbook ping.yml -i ../../inventory -vvv "$@"
if test $? != 0 ; then
echo "### Failed to run ping with no filters applied"
exit 1
fi
# Check that other modules run with ping filtered
ANSIBLE_CONFIG=filter_ping.ini ansible-playbook copy.yml -i ../../inventory -vvv "$@"
if test $? != 0 ; then
echo "### Failed to run copy when a filter was applied to ping"
exit 1
fi
# Check that ping fails to run when it is filtered
ANSIBLE_CONFIG=filter_ping.ini ansible-playbook ping.yml -i ../../inventory -v "$@"
if test $? = 0 ; then
echo "### Failed to prevent ping from running"
exit 1
else
echo "### Ping was prevented from running as expected"
fi
#
# Check that specifying a lookup plugin in the filter has no effect
#
ANSIBLE_CONFIG=filter_lookup.ini ansible-playbook lookup.yml -i ../../inventory -vvv "$@"
if test $? != 0 ; then
echo "### Failed to use a lookup plugin when it is incorrectly specified in the *module* blacklist"
exit 1
fi
#
# stat is a special module as we use it to run nearly every other module. Check it specially
#
# Check that stat runs with no filter
ANSIBLE_CONFIG=no_filters.ini ansible-playbook stat.yml -i ../../inventory -vvv "$@"
if test $? != 0 ; then
echo "### Failed to run stat with no filters applied"
exit 1
fi
# Check that running another module when stat is filtered gives us our custom error message
ANSIBLE_CONFIG=filter_stat.ini
export ANSIBLE_CONFIG
CAPTURE=$(ansible-playbook copy.yml -i ../../inventory -vvv "$@" 2>&1)
if test $? = 0 ; then
echo "### Copy ran even though stat is in the module blacklist"
exit 1
else
echo "$CAPTURE" | grep 'The stat module was specified in the module blacklist file,.*, but Ansible will not function without the stat module. Please remove stat from the blacklist.'
if test $? != 0 ; then
echo "### Stat did not give us our custom error message"
exit 1
fi
echo "### Filtering stat failed with our custom error message as expected"
fi
unset ANSIBLE_CONFIG
# Check that running stat when stat is filtered gives our custom error message
ANSIBLE_CONFIG=filter_stat.ini
export ANSIBLE_CONFIG
CAPTURE=$(ansible-playbook stat.yml -i ../../inventory -vvv "$@" 2>&1)
if test $? = 0 ; then
echo "### Stat ran even though it is in the module blacklist"
exit 1
else
echo "$CAPTURE" | grep 'The stat module was specified in the module blacklist file,.*, but Ansible will not function without the stat module. Please remove stat from the blacklist.'
if test $? != 0 ; then
echo "### Stat did not give us our custom error message"
exit 1
fi
echo "### Filtering stat failed with our custom error message as expected"
fi
unset ANSIBLE_CONFIG

@ -0,0 +1,6 @@
---
- hosts: testhost
gather_facts: False
tasks:
- stat:
path: '/'

@ -0,0 +1,9 @@
---
- hosts: testhost
gather_facts: False
tasks:
- tempfile:
register: temp_result
- file:
state: absent
path: '{{ temp_result["path"] }}'
Loading…
Cancel
Save