diff --git a/lib/ansible/modules/cloud/vmware/vmware_host_firewall_manager.py b/lib/ansible/modules/cloud/vmware/vmware_host_firewall_manager.py index f167fa2067c..68f1437a73b 100644 --- a/lib/ansible/modules/cloud/vmware/vmware_host_firewall_manager.py +++ b/lib/ansible/modules/cloud/vmware/vmware_host_firewall_manager.py @@ -13,7 +13,7 @@ ANSIBLE_METADATA = { 'supported_by': 'community' } -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: vmware_host_firewall_manager short_description: Manage firewall configurations about an ESXi host @@ -22,8 +22,9 @@ description: version_added: '2.5' author: - Abhijeet Kasurde (@Akasurde) +- Aaron Longchamps (@alongchamps) notes: -- Tested on vSphere 6.5 +- Tested on vSphere 6.0, vSphere 6.5 requirements: - python >= 2.6 - PyVmomi @@ -43,6 +44,7 @@ options: - A list of Rule set which needs to be managed. - Each member of list is rule set name and state to be set the rule. - Both rule name and rule state are required parameters. + - Additional IPs and networks can also be specified - Please see examples for more information. default: [] extends_documentation_fragment: vmware.documentation @@ -83,6 +85,35 @@ EXAMPLES = r''' - name: CIMHttpServer enabled: False delegate_to: localhost + +- name: Manage IP and network based firewall permissions for ESXi + vmware_host_firewall_manager: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + esxi_hostname: '{{ esxi_hostname }}' + rules: + - name: gdbserver + enabled: True + allowed_hosts: + - all_ip: False + ip_address: + 192.168.20.10 + - name: CIMHttpServer + enabled: True + allowed_hosts: + - all_ip: False + ip_network: + 192.168.100.0/24 + - name: remoteSerialPort + enabled: True + allowed_hosts: + - all_ip: False + ip_address: + 192.168.100.11 + ip_network: + 192.168.200.0/24 + delegate_to: localhost ''' RETURN = r''' @@ -95,14 +126,36 @@ rule_set_state: "rule_set_state": { "localhost.localdomain": { "CIMHttpServer": { - "current_state": true, - "desired_state": true, - "previous_state": true + "current_state": False, + "desired_state": False, + "previous_state": True, + "allowed_hosts": { + "current_allowed_all": True, + "previous_allowed_all": True, + "desired_allowed_all": True, + "current_allowed_ip": [], + "previous_allowed_ip": [], + "desired_allowed_ip": [], + "current_allowed_networks": [], + "previous_allowed_networks": [], + "desired_allowed_networks": [], + } }, - "vvold": { - "current_state": true, - "desired_state": true, - "previous_state": true + "remoteSerialPort": { + "current_state": True, + "desired_state": True, + "previous_state": True, + "allowed_hosts": { + "current_allowed_all": False, + "previous_allowed_all": True, + "desired_allowed_all": False, + "current_allowed_ip": ["192.168.100.11"], + "previous_allowed_ip": [], + "desired_allowed_ip": ["192.168.100.11"], + "current_allowed_networks": ["192.168.200.0/24"], + "previous_allowed_networks": [], + "desired_allowed_networks": ["192.168.200.0/24"], + } } } } @@ -117,6 +170,7 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.vmware import vmware_argument_spec, PyVmomi from ansible.module_utils._text import to_native +from ansible.module_utils.compat import ipaddress class VmwareFirewallManager(PyVmomi): @@ -138,6 +192,12 @@ class VmwareFirewallManager(PyVmomi): for rule_set_obj in firewall_system.firewallInfo.ruleset: temp_rule_dict = dict() temp_rule_dict['enabled'] = rule_set_obj.enabled + allowed_host = rule_set_obj.allowedHosts + rule_allow_host = dict() + rule_allow_host['ip_address'] = allowed_host.ipAddress + rule_allow_host['ip_network'] = [ip.network + "/" + str(ip.prefixLength) for ip in allowed_host.ipNetwork] + rule_allow_host['all_ip'] = allowed_host.allIp + temp_rule_dict['allowed_hosts'] = rule_allow_host self.firewall_facts[host.name][rule_set_obj.key] = temp_rule_dict def ensure(self): @@ -146,6 +206,8 @@ class VmwareFirewallManager(PyVmomi): """ fw_change_list = [] + enable_disable_changed = False + allowed_ip_changed = False results = dict(changed=False, rule_set_state=dict()) for host in self.hosts: firewall_system = host.configManager.firewallSystem @@ -167,6 +229,26 @@ class VmwareFirewallManager(PyVmomi): self.module.fail_json(msg="Please specify rules.enabled for rule set" " %s as it is required parameter." % rule_name) + # validate IP addresses are valid + rule_config = rule_option.get('allowed_hosts', None) + + if 'ip_address' in rule_config[0].keys(): + for ip_addr in rule_config[0]['ip_address']: + try: + ip = ipaddress.ip_address(ip_addr) + except ValueError: + self.module.fail_json(msg="The provided IP address %s is not a valid IP" + " for the rule %s" % (ip_addr, rule_name)) + + # validate provided subnets are valid networks + if 'ip_network' in rule_config[0].keys(): + for ip_net in rule_config[0]['ip_network']: + try: + network_validation = ipaddress.ip_network(ip_net) + except ValueError: + self.module.fail_json(msg="The provided network %s is not a valid network" + " for the rule %s" % (ip_net, rule_name)) + current_rule_state = self.firewall_facts[host.name][rule_name]['enabled'] if current_rule_state != rule_enabled: try: @@ -175,7 +257,8 @@ class VmwareFirewallManager(PyVmomi): firewall_system.EnableRuleset(id=rule_name) else: firewall_system.DisableRuleset(id=rule_name) - fw_change_list.append(True) + # keep track of changes as we go + enable_disable_changed = True except vim.fault.NotFound as not_found: self.module.fail_json(msg="Failed to enable rule set %s as" " rule set id is unknown : %s" % (rule_name, @@ -185,11 +268,73 @@ class VmwareFirewallManager(PyVmomi): " error happened while reconfiguring" " rule set : %s" % (rule_name, to_native(host_config_fault.msg))) + + # save variables here for comparison later and change tracking + # also covers cases where inputs may be null + permitted_networking = self.firewall_facts[host.name][rule_name] + rule_allows_all = permitted_networking['allowed_hosts']['all_ip'] + playbook_allows_all = rule_config[0]['all_ip'] + rule_allowed_ip = set(permitted_networking['allowed_hosts']['ip_address']) + playbook_allowed_ip = set(rule_config[0].get('ip_address', '')) + rule_allowed_networks = set(permitted_networking['allowed_hosts']['ip_network']) + playbook_allowed_networks = set(rule_config[0].get('ip_network', '')) + + # compare what is configured on the firewall rule with what the playbook provides + allowed_all_ips_different = bool(rule_allows_all != playbook_allows_all) + ip_list_different = bool(rule_allowed_ip != playbook_allowed_ip) + ip_network_different = bool(rule_allowed_networks != playbook_allowed_networks) + + # apply everything here in one function call + if allowed_all_ips_different is True or ip_list_different is True or ip_network_different is True: + try: + allowed_ip_changed = True + if not self.module.check_mode: + # setup spec + firewall_spec = vim.host.Ruleset.RulesetSpec() + firewall_spec.allowedHosts = vim.host.Ruleset.IpList() + firewall_spec.allowedHosts.allIp = rule_config[0].get('all_ip', True) + firewall_spec.allowedHosts.ipAddress = rule_config[0].get('ip_address', None) + firewall_spec.allowedHosts.ipNetwork = [] + + if 'ip_network' in rule_config[0].keys(): + for allowed_network in rule_config[0].get('ip_network', None): + tmp_ip_network_spec = vim.host.Ruleset.IpNetwork() + tmp_ip_network_spec.network = allowed_network.split("/")[0] + tmp_ip_network_spec.prefixLength = int(allowed_network.split("/")[1]) + firewall_spec.allowedHosts.ipNetwork.append(tmp_ip_network_spec) + + firewall_system.UpdateRuleset(id=rule_name, spec=firewall_spec) + except vim.fault.NotFound as not_found: + self.module.fail_json(msg="Failed to configure rule set %s as" + " rule set id is unknown : %s" % (rule_name, + to_native(not_found.msg))) + except vim.fault.HostConfigFault as host_config_fault: + self.module.fail_json(msg="Failed to configure rule set %s as an internal" + " error happened while reconfiguring" + " rule set : %s" % (rule_name, + to_native(host_config_fault.msg))) + except vim.fault.RuntimeFault as runtime_fault: + self.module.fail_json(msg="Failed to conifgure the rule set %s as a runtime" + " error happened while applying the reconfiguration:" + " %s" % (rule_name, to_native(runtime_fault.msg))) + results['rule_set_state'][host.name][rule_name] = dict(current_state=rule_enabled, previous_state=current_rule_state, desired_state=rule_enabled, + current_allowed_all=playbook_allows_all, + previous_allowed_all=permitted_networking['allowed_hosts']['all_ip'], + desired_allowed_all=playbook_allows_all, + current_allowed_ip=playbook_allowed_ip, + previous_allowed_ip=set(permitted_networking['allowed_hosts']['ip_address']), + desired_allowed_ip=playbook_allowed_ip, + current_allowed_networks=playbook_allowed_networks, + previous_allowed_networks=set(permitted_networking['allowed_hosts']['ip_network']), + desired_allowed_networks=playbook_allowed_networks ) + if enable_disable_changed or allowed_ip_changed: + fw_change_list.append(True) + if any(fw_change_list): results['changed'] = True self.module.exit_json(**results) diff --git a/test/integration/targets/vmware_host_firewall_manager/tasks/main.yml b/test/integration/targets/vmware_host_firewall_manager/tasks/main.yml index b12c81a6dc1..07b4d33d848 100644 --- a/test/integration/targets/vmware_host_firewall_manager/tasks/main.yml +++ b/test/integration/targets/vmware_host_firewall_manager/tasks/main.yml @@ -127,3 +127,78 @@ - host_result_check_mode.rule_set_state[item]['vvold']['previous_state'] == False with_items: - '{{ esxi1 }}' + + - name: Configure CIMHttpServer rule set on all hosts of {{ ccr1 }} + vmware_host_firewall_manager: + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + validate_certs: no + cluster_name: "{{ ccr1 }}" + rules: + - name: CIMHttpServer + enabled: True + allowed_hosts: + - all_ip: False + ip_address: + - "192.168.100.11" + ip_network: + - "192.168.200.0/24" + register: all_hosts_ip_specific + - debug: var=all_hosts_ip_specific + - name: ensure everything is changed for all hosts of {{ ccr1 }} + assert: + that: + - all_hosts_ip_specific.changed + - all_hosts_ip_specific.rule_set_state is defined + + - name: ensure CIMHttpServer is configured for all hosts in {{ ccr1 }} + assert: + that: + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['current_state'] == True + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['desired_state'] == True + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['previous_state'] == True + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['allowed_hosts']['current_allowed_all'] == False + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['allowed_hosts']['previous_allowed_all'] == True + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['allowed_hosts']['desired_allowed_all'] == False + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['allowed_hosts']['current_allowed_ip'] == ["192.168.100.11"] + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['allowed_hosts']['previous_allowed_ip'] == [] + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['allowed_hosts']['desired_allowed_ip'] == ["192.168.100.11"] + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['allowed_hosts']['current_allowed_networks'] == ["192.168.200.0/24"] + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['allowed_hosts']['previous_allowed_networks'] == [] + - all_hosts_ip_specific.rule_set_state[item]['CIMHttpServer']['allowed_hosts']['desired_allowed_networks'] == ["192.168.200.0/24"] + with_items: + - '{{ esxi1 }}' + - '{{ esxi2 }}' + + - name: Configure the NFC firewall rule to only allow traffic from one IP on one ESXi host + vmware_host_firewall_manager: + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + validate_certs: no + esxi_hostname: "{{ esxi1 }}" + rules: + - name: NFC + enabled: True + allowed_hosts: + - all_ip: False + ip_address: + - "192.168.100.11" + register: single_host_ip_specific + - debug: var=single_host_ip_specific + - name: ensure NFC is configured on that host + assert: + that: + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['current_state'] == True + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['desired_state'] == True + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['previous_state'] == True + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['allowed_hosts']['current_allowed_all'] == False + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['allowed_hosts']['previous_allowed_all'] == True + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['allowed_hosts']['desired_allowed_all'] == False + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['allowed_hosts']['current_allowed_ip'] == ["192.168.100.11"] + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['allowed_hosts']['previous_allowed_ip'] == None + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['allowed_hosts']['desired_allowed_ip'] == ["192.168.100.11"] + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['allowed_hosts']['current_allowed_networks'] == [] + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['allowed_hosts']['previous_allowed_networks'] == [] + - single_host_ip_specific.rule_set_state[{{ esxi1 }}]['NFC']['allowed_hosts']['desired_allowed_networks'] == []