Implement plugin filtering
This commit is contained in:
parent
14c3b4d8e5
commit
340a7be7c3
22 changed files with 312 additions and 4 deletions
|
@ -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
|
||||||
|
|
6
examples/plugin_filters.yml
Normal file
6
examples/plugin_filters.yml
Normal file
|
@ -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',
|
||||||
|
|
1
test/integration/targets/plugin_filtering/aliases
Normal file
1
test/integration/targets/plugin_filtering/aliases
Normal file
|
@ -0,0 +1 @@
|
||||||
|
posix/ci/group3
|
10
test/integration/targets/plugin_filtering/copy.yml
Normal file
10
test/integration/targets/plugin_filtering/copy.yml
Normal file
|
@ -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
|
14
test/integration/targets/plugin_filtering/lookup.yml
Normal file
14
test/integration/targets/plugin_filtering/lookup.yml
Normal file
|
@ -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 }}'
|
4
test/integration/targets/plugin_filtering/no_filters.ini
Normal file
4
test/integration/targets/plugin_filtering/no_filters.ini
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[default]
|
||||||
|
retry_files_enabled = False
|
||||||
|
plugin_filters_cfg = ./empty.yml
|
||||||
|
|
6
test/integration/targets/plugin_filtering/pause.yml
Normal file
6
test/integration/targets/plugin_filtering/pause.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
- hosts: testhost
|
||||||
|
gather_facts: False
|
||||||
|
tasks:
|
||||||
|
- pause:
|
||||||
|
seconds: 1
|
6
test/integration/targets/plugin_filtering/ping.yml
Normal file
6
test/integration/targets/plugin_filtering/ping.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
- hosts: testhost
|
||||||
|
gather_facts: False
|
||||||
|
tasks:
|
||||||
|
- ping:
|
||||||
|
data: 'Testing 1... 2... 3...'
|
128
test/integration/targets/plugin_filtering/runme.sh
Executable file
128
test/integration/targets/plugin_filtering/runme.sh
Executable file
|
@ -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
|
6
test/integration/targets/plugin_filtering/stat.yml
Normal file
6
test/integration/targets/plugin_filtering/stat.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
- hosts: testhost
|
||||||
|
gather_facts: False
|
||||||
|
tasks:
|
||||||
|
- stat:
|
||||||
|
path: '/'
|
9
test/integration/targets/plugin_filtering/tempfile.yml
Normal file
9
test/integration/targets/plugin_filtering/tempfile.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
- hosts: testhost
|
||||||
|
gather_facts: False
|
||||||
|
tasks:
|
||||||
|
- tempfile:
|
||||||
|
register: temp_result
|
||||||
|
- file:
|
||||||
|
state: absent
|
||||||
|
path: '{{ temp_result["path"] }}'
|
Loading…
Reference in a new issue