From 151f10b010651a49dfb4b46ca74e966be36b1279 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Wed, 4 Mar 2015 12:19:08 -0500 Subject: add vim vim modeline to ansible modules --- roles/openshift_node/library/openshift_register_node.py | 1 + 1 file changed, 1 insertion(+) (limited to 'roles') diff --git a/roles/openshift_node/library/openshift_register_node.py b/roles/openshift_node/library/openshift_register_node.py index 87290c209..981b818c8 100644 --- a/roles/openshift_node/library/openshift_register_node.py +++ b/roles/openshift_node/library/openshift_register_node.py @@ -1,5 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# vim: expandtab:tabstop=4:shiftwidth=4 import os import multiprocessing -- cgit v1.2.3 From 7c90cacef0f5cf61fb8ac3adb905507dd4247d84 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Tue, 3 Mar 2015 13:06:49 -0500 Subject: refactor firewall management into new role - Add os_firewall role - Remove firewall settings from base_os, add wait task to os_firewall - Added a iptables firewall module for maintaining the following (in a mostly naive manner): - ensure the OPENSHIFT_ALLOW chain is defined - ensure that there is a jump rule in the INPUT chain for OPENSHIFT_ALLOW - adds or removes entries from the OPENSHIFT_ALLOW chain - issues '/usr/libexec/iptables/iptables.init save' when rules are changed - Limitations of iptables firewall module - only allows setting of ports/protocols to open - no testing on ipv6 support - made os_firewall a dependency of openshift_common - Hardcoded openshift_common to use iptables (through the vars directory) until upstream support is in place for firewalld --- roles/base_os/tasks/main.yaml | 16 -- roles/openshift_common/meta/main.yml | 4 +- roles/openshift_common/tasks/firewall.yml | 34 --- roles/openshift_common/tasks/main.yml | 16 +- roles/openshift_common/vars/main.yml | 4 + roles/os_firewall/README.md | 66 ++++++ roles/os_firewall/defaults/main.yml | 2 + .../library/os_firewall_manage_iptables.py | 254 +++++++++++++++++++++ roles/os_firewall/meta/main.yml | 13 ++ roles/os_firewall/tasks/firewall/firewalld.yml | 68 ++++++ roles/os_firewall/tasks/firewall/iptables.yml | 53 +++++ roles/os_firewall/tasks/main.yml | 6 + 12 files changed, 477 insertions(+), 59 deletions(-) delete mode 100644 roles/openshift_common/tasks/firewall.yml create mode 100644 roles/os_firewall/README.md create mode 100644 roles/os_firewall/defaults/main.yml create mode 100644 roles/os_firewall/library/os_firewall_manage_iptables.py create mode 100644 roles/os_firewall/meta/main.yml create mode 100644 roles/os_firewall/tasks/firewall/firewalld.yml create mode 100644 roles/os_firewall/tasks/firewall/iptables.yml create mode 100644 roles/os_firewall/tasks/main.yml (limited to 'roles') diff --git a/roles/base_os/tasks/main.yaml b/roles/base_os/tasks/main.yaml index 51fe1e5b6..aad611f70 100644 --- a/roles/base_os/tasks/main.yaml +++ b/roles/base_os/tasks/main.yaml @@ -15,19 +15,3 @@ yum: pkg: bash-completion state: installed - -- name: Install firewalld - yum: - pkg: firewalld - state: installed - -- name: start and enable firewalld service - service: - name: firewalld - state: started - enabled: yes - register: result - -- name: need to pause here, otherwise the firewalld service starting can sometimes cause ssh to fail - pause: seconds=10 - when: result | changed diff --git a/roles/openshift_common/meta/main.yml b/roles/openshift_common/meta/main.yml index 128da25b4..7dc4603d0 100644 --- a/roles/openshift_common/meta/main.yml +++ b/roles/openshift_common/meta/main.yml @@ -1,3 +1,4 @@ +--- galaxy_info: author: Jason DeTiberus description: OpenShift Common @@ -10,4 +11,5 @@ galaxy_info: - 7 categories: - cloud -dependencies: [] +dependencies: +- { role: os_firewall } diff --git a/roles/openshift_common/tasks/firewall.yml b/roles/openshift_common/tasks/firewall.yml deleted file mode 100644 index 514466769..000000000 --- a/roles/openshift_common/tasks/firewall.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -# TODO: Ansible 1.9 will eliminate the need for separate firewalld tasks for -# enabling rules and making them permanent with the immediate flag -- name: "Add firewalld allow rules" - firewalld: - port: "{{ item.port }}" - permanent: false - state: enabled - with_items: allow - when: allow is defined - -- name: "Persist firewalld allow rules" - firewalld: - port: "{{ item.port }}" - permanent: true - state: enabled - with_items: allow - when: allow is defined - -- name: "Remove firewalld allow rules" - firewalld: - port: "{{ item.port }}" - permanent: false - state: disabled - with_items: deny - when: deny is defined - -- name: "Persist removal of firewalld allow rules" - firewalld: - port: "{{ item.port }}" - permanent: true - state: disabled - with_items: deny - when: deny is defined diff --git a/roles/openshift_common/tasks/main.yml b/roles/openshift_common/tasks/main.yml index b94fca690..723bdd9fa 100644 --- a/roles/openshift_common/tasks/main.yml +++ b/roles/openshift_common/tasks/main.yml @@ -7,6 +7,14 @@ - name: Configure local facts file file: path=/etc/ansible/facts.d/ state=directory mode=0750 +- name: Add KUBECONFIG to .bash_profile for user root + lineinfile: + dest: /root/.bash_profile + regexp: "KUBECONFIG=" + line: "export KUBECONFIG=/var/lib/openshift/openshift.local.certificates/admin/.kubeconfig" + state: present + insertafter: EOF + - name: Set common OpenShift facts include: set_facts.yml facts: @@ -19,11 +27,3 @@ - section: common option: debug_level value: "{{ openshift_debug_level }}" - -- name: Add KUBECONFIG to .bash_profile for user root - lineinfile: - dest: /root/.bash_profile - regexp: "KUBECONFIG=" - line: "export KUBECONFIG=/var/lib/openshift/openshift.local.certificates/admin/.kubeconfig" - state: present - insertafter: EOF diff --git a/roles/openshift_common/vars/main.yml b/roles/openshift_common/vars/main.yml index c93898665..0855c0cc5 100644 --- a/roles/openshift_common/vars/main.yml +++ b/roles/openshift_common/vars/main.yml @@ -1,2 +1,6 @@ --- openshift_master_credentials_dir: /var/lib/openshift/openshift.local.certificates/admin/ + +# TODO: Upstream kubernetes only supports iptables currently, if this changes, +# then these variable should be moved to defaults +openshift_use_firewalld: False diff --git a/roles/os_firewall/README.md b/roles/os_firewall/README.md new file mode 100644 index 000000000..fe6318184 --- /dev/null +++ b/roles/os_firewall/README.md @@ -0,0 +1,66 @@ +OS Firewall +=========== + +OS Firewall manages firewalld and iptables firewall settings for a minimal use +case (Adding/Removing rules based on protocol and port number). + +Requirements +------------ + +None. + +Role Variables +-------------- + +| Name | Default | | +|---------------------------|---------|----------------------------------------| +| os_firewall_use_firewalld | True | If false, use iptables | +| os_firewall_allow | [] | List of service,port mappings to allow | +| os_firewall_deny | [] | List of service, port mappings to deny | + +Dependencies +------------ + +None. + +Example Playbook +---------------- + +Use iptables and open tcp ports 80 and 443: +``` +--- +- hosts: servers + vars: + os_firewall_use_firewalld: false + os_firewall_allow: + - service: httpd + port: 80/tcp + - service: https + port: 443/tcp + roles: + - os_firewall +``` + +Use firewalld and open tcp port 443 and close previously open tcp port 80: +``` +--- +- hosts: servers + vars: + os_firewall_allow: + - service: https + port: 443/tcp + os_firewall_deny: + - service: httpd + port: 80/tcp + roles: + - os_firewall +``` + +License +------- + +ASL 2.0 + +Author Information +------------------ +Jason DeTiberus - jdetiber@redhat.com diff --git a/roles/os_firewall/defaults/main.yml b/roles/os_firewall/defaults/main.yml new file mode 100644 index 000000000..bcf1d9a34 --- /dev/null +++ b/roles/os_firewall/defaults/main.yml @@ -0,0 +1,2 @@ +--- +os_firewall_use_firewalld: True diff --git a/roles/os_firewall/library/os_firewall_manage_iptables.py b/roles/os_firewall/library/os_firewall_manage_iptables.py new file mode 100644 index 000000000..fef710055 --- /dev/null +++ b/roles/os_firewall/library/os_firewall_manage_iptables.py @@ -0,0 +1,254 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from subprocess import call, check_output + +DOCUMENTATION = ''' +--- +module: os_firewall_manage_iptables +short_description: This module manages iptables rules for a given chain +author: Jason DeTiberus +requirements: [ ] +''' +EXAMPLES = ''' +''' + + +class IpTablesError(Exception): + def __init__(self, msg, cmd, exit_code, output): + self.msg = msg + self.cmd = cmd + self.exit_code = exit_code + self.output = output + + +class IpTablesAddRuleError(IpTablesError): + pass + + +class IpTablesRemoveRuleError(IpTablesError): + pass + + +class IpTablesSaveError(IpTablesError): + pass + + +class IpTablesCreateChainError(IpTablesError): + def __init__(self, chain, msg, cmd, exit_code, output): + super(IpTablesCreateChainError, self).__init__(msg, cmd, exit_code, output) + self.chain = chain + + +class IpTablesCreateJumpRuleError(IpTablesError): + def __init__(self, chain, msg, cmd, exit_code, output): + super(IpTablesCreateJumpRuleError, self).__init__(msg, cmd, exit_code, + output) + self.chain = chain + + +# TODO: impliment rollbacks for any events that where successful and an +# exception was thrown later. for example, when the chain is created +# successfully, but the add/remove rule fails. +class IpTablesManager: + def __init__(self, module, ip_version, check_mode, chain): + self.module = module + self.ip_version = ip_version + self.check_mode = check_mode + self.chain = chain + self.cmd = self.gen_cmd() + self.save_cmd = self.gen_save_cmd() + self.output = [] + self.changed = False + + def save(self): + try: + self.output.append(check_output(self.save_cmd, + stderr=subprocess.STDOUT)) + except subprocess.CalledProcessError as e: + raise IpTablesSaveError( + msg="Failed to save iptables rules", + cmd=e.cmd, exit_code=e.returncode, output=e.output) + + def add_rule(self, port, proto): + rule = self.gen_rule(port, proto) + if not self.rule_exists(rule): + if not self.chain_exists(): + self.create_chain() + if not self.jump_rule_exists(): + self.create_jump_rule() + + if self.check_mode: + self.changed = True + self.output.append("Create rule for %s %s" % (proto, port)) + else: + cmd = self.cmd + ['-A'] + rule + try: + self.output.append(check_output(cmd)) + self.changed = True + self.save() + except subprocess.CalledProcessError as e: + raise IpTablesCreateChainError( + chain=self.chain, + msg="Failed to create rule for " + "%s %s" % (self.proto, self.port), + cmd=e.cmd, exit_code=e.returncode, + output=e.output) + + def remove_rule(self, port, proto): + rule = self.gen_rule(port, proto) + if self.rule_exists(rule): + if self.check_mode: + self.changed = True + self.output.append("Remove rule for %s %s" % (proto, port)) + else: + cmd = self.cmd + ['-D'] + rule + try: + self.output.append(check_output(cmd)) + self.changed = True + self.save() + except subprocess.CalledProcessError as e: + raise IpTablesRemoveChainError( + chain=self.chain, + msg="Failed to remove rule for %s %s" % (proto, port), + cmd=e.cmd, exit_code=e.returncode, output=e.output) + + def rule_exists(self, rule): + check_cmd = self.cmd + ['-C'] + rule + return True if subprocess.call(check_cmd) == 0 else False + + def gen_rule(self, port, proto): + return [self.chain, '-p', proto, '-m', 'state', '--state', 'NEW', + '-m', proto, '--dport', str(port), '-j', 'ACCEPT'] + + def create_jump_rule(self): + if self.check_mode: + self.changed = True + self.output.append("Create jump rule for chain %s" % self.chain) + else: + try: + cmd = self.cmd + ['-L', 'INPUT', '--line-numbers'] + output = check_output(cmd, stderr=subprocess.STDOUT) + + # break the input rules into rows and columns + input_rules = map(lambda s: s.split(), output.split('\n')) + + # Find the last numbered rule + last_rule_num = None + last_rule_target = None + for rule in input_rules[:-1]: + if rule: + try: + last_rule_num = int(rule[0]) + except ValueError: + continue + last_rule_target = rule[1] + + # Raise an exception if we do not find a valid INPUT rule + if not last_rule_num or not last_rule_target: + raise IpTablesCreateJumpRuleError( + chain=self.chain, + msg="Failed to find existing INPUT rules", + cmd=None, exit_code=None, output=None) + + # Naively assume that if the last row is a REJECT rule, then + # we can add insert our rule right before it, otherwise we + # assume that we can just append the rule. + if last_rule_target == 'REJECT': + # insert rule + cmd = self.cmd + ['-I', 'INPUT', str(last_rule_num)] + else: + # append rule + cmd = self.cmd + ['-A', 'INPUT'] + cmd += ['-j', self.chain] + output = check_output(cmd, stderr=subprocess.STDOUT) + changed = True + self.output.append(output) + except subprocess.CalledProcessError as e: + if '--line-numbers' in e.cmd: + raise IpTablesCreateJumpRuleError( + chain=self.chain, + msg="Failed to query existing INPUT rules to " + "determine jump rule location", + cmd=e.cmd, exit_code=e.returncode, + output=e.output) + else: + raise IpTablesCreateJumpRuleError( + chain=self.chain, + msg="Failed to create jump rule for chain %s" % + self.chain, + cmd=e.cmd, exit_code=e.returncode, + output=e.output) + + def create_chain(self): + if self.check_mode: + self.changed = True + self.output.append("Create chain %s" % self.chain) + else: + try: + cmd = self.cmd + ['-N', self.chain] + self.output.append(check_output(cmd, + stderr=subprocess.STDOUT)) + self.changed = True + self.output.append("Successfully created chain %s" % + self.chain) + except subprocess.CalledProcessError as e: + raise IpTablesCreateChainError( + chain=self.chain, + msg="Failed to create chain: %s" % self.chain, + cmd=e.cmd, exit_code=e.returncode, output=e.output + ) + + def jump_rule_exists(self): + cmd = self.cmd + ['-C', 'INPUT', '-j', self.chain] + return True if subprocess.call(cmd) == 0 else False + + def chain_exists(self): + cmd = self.cmd + ['-L', self.chain] + return True if subprocess.call(cmd) == 0 else False + + def gen_cmd(self): + cmd = 'iptables' if self.ip_version == 'ipv4' else 'ip6tables' + return ["/usr/sbin/%s" % cmd] + + def gen_save_cmd(self): + cmd = 'iptables' if self.ip_version == 'ipv4' else 'ip6tables' + return ['/usr/libexec/iptables/iptables.init', 'save'] + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + action=dict(required=True, choices=['add', 'remove']), + protocol=dict(required=True, choices=['tcp', 'udp']), + port=dict(required=True, type='int'), + ip_version=dict(required=False, default='ipv4', + choices=['ipv4', 'ipv6']), + ), + supports_check_mode=True + ) + + action = module.params['action'] + protocol = module.params['protocol'] + port = module.params['port'] + ip_version = module.params['ip_version'] + chain = 'OS_FIREWALL_ALLOW' + + iptables_manager = IpTablesManager(module, ip_version, module.check_mode, chain) + + try: + if action == 'add': + iptables_manager.add_rule(port, protocol) + elif action == 'remove': + iptables_manager.remove_rule(port, protocol) + except IpTablesError as e: + module.fail_json(msg=e.msg) + + return module.exit_json(changed=iptables_manager.changed, + output=iptables_manager.output) + + +# import module snippets +from ansible.module_utils.basic import * +main() diff --git a/roles/os_firewall/meta/main.yml b/roles/os_firewall/meta/main.yml new file mode 100644 index 000000000..e431f531c --- /dev/null +++ b/roles/os_firewall/meta/main.yml @@ -0,0 +1,13 @@ +galaxy_info: + author: Jason DeTiberus + description: os_firewall + company: Red Hat, Inc. + license: ASL 2.0 + min_ansible_version: 1.7 + platforms: + - name: EL + versions: + - 7 + categories: + - system +dependencies: [] diff --git a/roles/os_firewall/tasks/firewall/firewalld.yml b/roles/os_firewall/tasks/firewall/firewalld.yml new file mode 100644 index 000000000..f6d5fe2eb --- /dev/null +++ b/roles/os_firewall/tasks/firewall/firewalld.yml @@ -0,0 +1,68 @@ +--- +- name: Install firewalld packages + yum: + name: firewalld + state: present + +- name: Start and enable firewalld service + service: + name: firewalld + state: started + enabled: yes + register: result + +- name: need to pause here, otherwise the firewalld service starting can sometimes cause ssh to fail + pause: seconds=10 + when: result | changed + +- name: Ensure iptables services are not enabled + service: + name: "{{ item }}" + state: stopped + enabled: no + with_items: + - iptables + - ip6tables + +- name: Mask iptables services + command: systemctl mask "{{ item }}" + register: result + failed_when: result.rc != 0 + changed_when: False + with_items: + - iptables + - ip6tables + +# TODO: Ansible 1.9 will eliminate the need for separate firewalld tasks for +# enabling rules and making them permanent with the immediate flag +- name: Add firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: false + state: enabled + with_items: allow + when: allow is defined + +- name: Persist firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: true + state: enabled + with_items: allow + when: allow is defined + +- name: Remove firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: false + state: disabled + with_items: deny + when: deny is defined + +- name: Persist removal of firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: true + state: disabled + with_items: deny + when: deny is defined diff --git a/roles/os_firewall/tasks/firewall/iptables.yml b/roles/os_firewall/tasks/firewall/iptables.yml new file mode 100644 index 000000000..4f051c2bd --- /dev/null +++ b/roles/os_firewall/tasks/firewall/iptables.yml @@ -0,0 +1,53 @@ +--- +- name: Install iptables packages + yum: + name: "{{ item }}" + state: present + with_items: + - iptables + - iptables-services + +- name: Start and enable iptables services + service: + name: "{{ os_firewall_svc }}" + state: started + enabled: yes + with_items: + - iptables + - ip6tables + register: result + +- name: need to pause here, otherwise the iptables service starting can sometimes cause ssh to fail + pause: seconds=10 + when: result | changed + +- name: Ensure firewalld service is not enabled + service: + name: firewalld + state: stopped + enabled: no + +- name: Mask firewalld service + command: systemctl mask firewalld + register: result + failed_when: result.rc != 0 + changed_when: False + ignore_errors: yes + +- name: Add iptables allow rules + os_firewall_manage_iptables: + name: "{{ item.service }}" + action: add + protocol: "{{ item.port.split('/')[1] }}" + port: "{{ item.port.split('/')[0] }}" + with_items: allow + when: allow is defined + +- name: Remove iptables rules + os_firewall_manage_iptables: + name: "{{ item.service }}" + action: remove + protocol: "{{ item.port.split('/')[1] }}" + port: "{{ item.port.split('/')[0] }}" + with_items: deny + when: deny is defined diff --git a/roles/os_firewall/tasks/main.yml b/roles/os_firewall/tasks/main.yml new file mode 100644 index 000000000..ad89ef97c --- /dev/null +++ b/roles/os_firewall/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- include: firewall/firewalld.yml + when: os_firewall_use_firewalld + +- include: firewall/iptables.yml + when: not os_firewall_use_firewalld -- cgit v1.2.3 From b7008f070afe2629c9ebcbbdf0af3fa1f6ed9d34 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Wed, 4 Mar 2015 17:45:02 -0500 Subject: rename base_os role to os_env_extras, move application to end since it just sets environment configs for root user --- roles/base_os/files/irbrc | 2 -- roles/base_os/files/vimrc | 12 ------------ roles/base_os/tasks/main.yaml | 17 ----------------- roles/os_env_extras/files/irbrc | 2 ++ roles/os_env_extras/files/vimrc | 12 ++++++++++++ roles/os_env_extras/tasks/main.yaml | 17 +++++++++++++++++ 6 files changed, 31 insertions(+), 31 deletions(-) delete mode 100644 roles/base_os/files/irbrc delete mode 100644 roles/base_os/files/vimrc delete mode 100644 roles/base_os/tasks/main.yaml create mode 100644 roles/os_env_extras/files/irbrc create mode 100644 roles/os_env_extras/files/vimrc create mode 100644 roles/os_env_extras/tasks/main.yaml (limited to 'roles') diff --git a/roles/base_os/files/irbrc b/roles/base_os/files/irbrc deleted file mode 100644 index 47374e920..000000000 --- a/roles/base_os/files/irbrc +++ /dev/null @@ -1,2 +0,0 @@ -require 'irb/completion' -IRB.conf[:PROMPT_MODE] = :SIMPLE diff --git a/roles/base_os/files/vimrc b/roles/base_os/files/vimrc deleted file mode 100644 index 537b944ed..000000000 --- a/roles/base_os/files/vimrc +++ /dev/null @@ -1,12 +0,0 @@ -set tabstop=4 -set shiftwidth=4 -set expandtab -set list - -"flag problematic whitespace (trailing and spaces before tabs) -"Note you get the same by doing let c_space_errors=1 but -"this rule really applies to everything. -highlight RedundantSpaces term=standout ctermbg=red guibg=red -match RedundantSpaces /\s\+$\| \+\ze\t/ "\ze sets end of match so only spaces highlighted -"use :set list! to toggle visible whitespace on/off -set listchars=tab:>-,trail:.,extends:> diff --git a/roles/base_os/tasks/main.yaml b/roles/base_os/tasks/main.yaml deleted file mode 100644 index aad611f70..000000000 --- a/roles/base_os/tasks/main.yaml +++ /dev/null @@ -1,17 +0,0 @@ ---- -# basic role, configures irbrc, vimrc - -- name: Ensure irbrc is installed for user root - copy: - src: irbrc - dest: /root/.irbrc - -- name: Ensure vimrc is installed for user root - copy: - src: vimrc - dest: /root/.vimrc - -- name: Bash Completion - yum: - pkg: bash-completion - state: installed diff --git a/roles/os_env_extras/files/irbrc b/roles/os_env_extras/files/irbrc new file mode 100644 index 000000000..47374e920 --- /dev/null +++ b/roles/os_env_extras/files/irbrc @@ -0,0 +1,2 @@ +require 'irb/completion' +IRB.conf[:PROMPT_MODE] = :SIMPLE diff --git a/roles/os_env_extras/files/vimrc b/roles/os_env_extras/files/vimrc new file mode 100644 index 000000000..537b944ed --- /dev/null +++ b/roles/os_env_extras/files/vimrc @@ -0,0 +1,12 @@ +set tabstop=4 +set shiftwidth=4 +set expandtab +set list + +"flag problematic whitespace (trailing and spaces before tabs) +"Note you get the same by doing let c_space_errors=1 but +"this rule really applies to everything. +highlight RedundantSpaces term=standout ctermbg=red guibg=red +match RedundantSpaces /\s\+$\| \+\ze\t/ "\ze sets end of match so only spaces highlighted +"use :set list! to toggle visible whitespace on/off +set listchars=tab:>-,trail:.,extends:> diff --git a/roles/os_env_extras/tasks/main.yaml b/roles/os_env_extras/tasks/main.yaml new file mode 100644 index 000000000..96b12ad5b --- /dev/null +++ b/roles/os_env_extras/tasks/main.yaml @@ -0,0 +1,17 @@ +--- +# environment configuration role, configures irbrc, vimrc + +- name: Ensure irbrc is installed for user root + copy: + src: irbrc + dest: /root/.irbrc + +- name: Ensure vimrc is installed for user root + copy: + src: vimrc + dest: /root/.vimrc + +- name: Bash Completion + yum: + pkg: bash-completion + state: installed -- cgit v1.2.3