Skip to content

Commit

Permalink
Implement plugin filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
abadger committed Jan 23, 2018
1 parent 14c3b4d commit 340a7be
Show file tree
Hide file tree
Showing 22 changed files with 312 additions and 4 deletions.
1 change: 1 addition & 0 deletions examples/ansible.cfg
Expand Up @@ -16,6 +16,7 @@
#module_utils = /usr/share/my_module_utils/
#remote_tmp = ~/.ansible/tmp
#local_tmp = ~/.ansible/tmp
#plugin_filters_cfg = /etc/ansible/plugin_filters.yml
#forks = 5
#poll_interval = 15
#sudo_user = root
Expand Down
6 changes: 6 additions & 0 deletions examples/plugin_filters.yml
@@ -0,0 +1,6 @@
---
filter_version: '1.0'
module_blacklist:
# List the modules to blacklist here
#- easy_install
#- s3
10 changes: 10 additions & 0 deletions lib/ansible/config/base.yml
Expand Up @@ -1387,6 +1387,16 @@ PLAYBOOK_VARS_ROOT:
ini:
- {key: playbook_vars_root, section: defaults}
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:
name: Retry files
default: True
Expand Down
4 changes: 2 additions & 2 deletions lib/ansible/parsing/utils/yaml.py
Expand Up @@ -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


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
a JSON or YAML string.
Expand All @@ -80,7 +80,7 @@ def from_yaml(data, file_name='<string>', show_content=True):
except Exception:
# must not be JSON, let the rest 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:
_handle_error(yaml_exc, file_name, show_content)

Expand Down
70 changes: 68 additions & 2 deletions lib/ansible/plugins/loader.py
Expand Up @@ -17,8 +17,10 @@
from collections import defaultdict

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.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

try:
Expand Down Expand Up @@ -235,6 +237,10 @@ def add_directory(self, directory, with_subdir=False):
def find_plugin(self, name, mod_type='', ignore_deprecated=False, check_aliases=False):
''' Find a plugin named name '''

global _PLUGIN_FILTERS
if name in _PLUGIN_FILTERS[self.package]:
return None

if mod_type:
suffix = mod_type
elif self.class_name:
Expand Down Expand Up @@ -405,6 +411,8 @@ def _display_plugin_load(self, class_name, name, searched_paths, path, found_in_
def all(self, *args, **kwargs):
''' instantiates all plugins with the same arguments '''

global _PLUGIN_FILTERS

path_only = kwargs.pop('path_only', False)
class_only = kwargs.pop('class_only', False)
all_matches = []
Expand All @@ -416,7 +424,7 @@ def all(self, *args, **kwargs):
for path in sorted(all_matches, key=os.path.basename):
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

if path_only:
Expand Down Expand Up @@ -462,6 +470,63 @@ def all(self, *args, **kwargs):
self._update_object(obj, name, path)
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
fragment_loader = PluginLoader(
'ModuleDocFragment',
Expand All @@ -470,6 +535,7 @@ def all(self, *args, **kwargs):
'',
)


action_loader = PluginLoader(
'ActionModule',
'ansible.plugins.action',
Expand Down
1 change: 1 addition & 0 deletions test/integration/targets/plugin_filtering/aliases
@@ -0,0 +1 @@
posix/ci/group3
10 changes: 10 additions & 0 deletions test/integration/targets/plugin_filtering/copy.yml
@@ -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
4 changes: 4 additions & 0 deletions test/integration/targets/plugin_filtering/filter_lookup.ini
@@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./filter_lookup.yml

6 changes: 6 additions & 0 deletions test/integration/targets/plugin_filtering/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
4 changes: 4 additions & 0 deletions test/integration/targets/plugin_filtering/filter_modules.ini
@@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./filter_modules.yml

9 changes: 9 additions & 0 deletions test/integration/targets/plugin_filtering/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
4 changes: 4 additions & 0 deletions test/integration/targets/plugin_filtering/filter_ping.ini
@@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./filter_ping.yml

5 changes: 5 additions & 0 deletions test/integration/targets/plugin_filtering/filter_ping.yml
@@ -0,0 +1,5 @@
---
filter_version: 1.0
module_blacklist:
# Ping is special
- ping
4 changes: 4 additions & 0 deletions test/integration/targets/plugin_filtering/filter_stat.ini
@@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./filter_stat.yml

5 changes: 5 additions & 0 deletions test/integration/targets/plugin_filtering/filter_stat.yml
@@ -0,0 +1,5 @@
---
filter_version: 1.0
module_blacklist:
# Stat is special
- stat
14 changes: 14 additions & 0 deletions test/integration/targets/plugin_filtering/lookup.yml
@@ -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 changes: 4 additions & 0 deletions test/integration/targets/plugin_filtering/no_filters.ini
@@ -0,0 +1,4 @@
[default]
retry_files_enabled = False
plugin_filters_cfg = ./empty.yml

6 changes: 6 additions & 0 deletions test/integration/targets/plugin_filtering/pause.yml
@@ -0,0 +1,6 @@
---
- hosts: testhost
gather_facts: False
tasks:
- pause:
seconds: 1
6 changes: 6 additions & 0 deletions test/integration/targets/plugin_filtering/ping.yml
@@ -0,0 +1,6 @@
---
- hosts: testhost
gather_facts: False
tasks:
- ping:
data: 'Testing 1... 2... 3...'
128 changes: 128 additions & 0 deletions test/integration/targets/plugin_filtering/runme.sh
@@ -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 changes: 6 additions & 0 deletions test/integration/targets/plugin_filtering/stat.yml
@@ -0,0 +1,6 @@
---
- hosts: testhost
gather_facts: False
tasks:
- stat:
path: '/'

0 comments on commit 340a7be

Please sign in to comment.