1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10ANSIBLE_METADATA = { 11 'metadata_version': '1.1', 12 'status': ['preview'], 13 'supported_by': 'community' 14} 15 16DOCUMENTATION = r''' 17--- 18module: vmware_host_firewall_manager 19short_description: Manage firewall configurations about an ESXi host 20description: 21- This module can be used to manage firewall configurations about an ESXi host when ESXi hostname or Cluster name is given. 22version_added: '2.5' 23author: 24- Abhijeet Kasurde (@Akasurde) 25- Aaron Longchamps (@alongchamps) 26notes: 27- Tested on vSphere 6.0, vSphere 6.5 28requirements: 29- python >= 2.6 30- PyVmomi 31options: 32 cluster_name: 33 description: 34 - Name of the cluster. 35 - Firewall settings are applied to every ESXi host system in given cluster. 36 - If C(esxi_hostname) is not given, this parameter is required. 37 type: str 38 esxi_hostname: 39 description: 40 - ESXi hostname. 41 - Firewall settings are applied to this ESXi host system. 42 - If C(cluster_name) is not given, this parameter is required. 43 type: str 44 rules: 45 description: 46 - A list of Rule set which needs to be managed. 47 - Each member of list is rule set name and state to be set the rule. 48 - Both rule name and rule state are required parameters. 49 - Additional IPs and networks can also be specified 50 - Please see examples for more information. 51 default: [] 52 type: list 53extends_documentation_fragment: vmware.documentation 54''' 55 56EXAMPLES = r''' 57- name: Enable vvold rule set for all ESXi Host in given Cluster 58 vmware_host_firewall_manager: 59 hostname: '{{ vcenter_hostname }}' 60 username: '{{ vcenter_username }}' 61 password: '{{ vcenter_password }}' 62 cluster_name: cluster_name 63 rules: 64 - name: vvold 65 enabled: True 66 delegate_to: localhost 67 68- name: Enable vvold rule set for an ESXi Host 69 vmware_host_firewall_manager: 70 hostname: '{{ vcenter_hostname }}' 71 username: '{{ vcenter_username }}' 72 password: '{{ vcenter_password }}' 73 esxi_hostname: '{{ esxi_hostname }}' 74 rules: 75 - name: vvold 76 enabled: True 77 delegate_to: localhost 78 79- name: Manage multiple rule set for an ESXi Host 80 vmware_host_firewall_manager: 81 hostname: '{{ vcenter_hostname }}' 82 username: '{{ vcenter_username }}' 83 password: '{{ vcenter_password }}' 84 esxi_hostname: '{{ esxi_hostname }}' 85 rules: 86 - name: vvold 87 enabled: True 88 - name: CIMHttpServer 89 enabled: False 90 delegate_to: localhost 91 92- name: Manage IP and network based firewall permissions for ESXi 93 vmware_host_firewall_manager: 94 hostname: '{{ vcenter_hostname }}' 95 username: '{{ vcenter_username }}' 96 password: '{{ vcenter_password }}' 97 esxi_hostname: '{{ esxi_hostname }}' 98 rules: 99 - name: gdbserver 100 enabled: True 101 allowed_hosts: 102 all_ip: False 103 ip_address: 104 - 192.168.20.10 105 - 192.168.20.11 106 - name: CIMHttpServer 107 enabled: True 108 allowed_hosts: 109 all_ip: False 110 ip_network: 111 - 192.168.100.0/24 112 - name: remoteSerialPort 113 enabled: True 114 allowed_hosts: 115 all_ip: False 116 ip_address: 117 - 192.168.100.11 118 ip_network: 119 - 192.168.200.0/24 120 delegate_to: localhost 121''' 122 123RETURN = r''' 124rule_set_state: 125 description: 126 - dict with hostname as key and dict with firewall rule set facts as value 127 returned: success 128 type: dict 129 sample: { 130 "rule_set_state": { 131 "localhost.localdomain": { 132 "CIMHttpServer": { 133 "current_state": False, 134 "desired_state": False, 135 "previous_state": True, 136 "allowed_hosts": { 137 "current_allowed_all": True, 138 "previous_allowed_all": True, 139 "desired_allowed_all": True, 140 "current_allowed_ip": [], 141 "previous_allowed_ip": [], 142 "desired_allowed_ip": [], 143 "current_allowed_networks": [], 144 "previous_allowed_networks": [], 145 "desired_allowed_networks": [], 146 } 147 }, 148 "remoteSerialPort": { 149 "current_state": True, 150 "desired_state": True, 151 "previous_state": True, 152 "allowed_hosts": { 153 "current_allowed_all": False, 154 "previous_allowed_all": True, 155 "desired_allowed_all": False, 156 "current_allowed_ip": ["192.168.100.11"], 157 "previous_allowed_ip": [], 158 "desired_allowed_ip": ["192.168.100.11"], 159 "current_allowed_networks": ["192.168.200.0/24"], 160 "previous_allowed_networks": [], 161 "desired_allowed_networks": ["192.168.200.0/24"], 162 } 163 } 164 } 165 } 166 } 167''' 168 169try: 170 from pyVmomi import vim 171except ImportError: 172 pass 173 174from ansible.module_utils.basic import AnsibleModule 175from ansible.module_utils.vmware import vmware_argument_spec, PyVmomi 176from ansible.module_utils._text import to_native, to_text 177from ansible.module_utils.compat import ipaddress 178 179 180class VmwareFirewallManager(PyVmomi): 181 def __init__(self, module): 182 super(VmwareFirewallManager, self).__init__(module) 183 cluster_name = self.params.get('cluster_name', None) 184 esxi_host_name = self.params.get('esxi_hostname', None) 185 self.options = self.params.get('options', dict()) 186 self.hosts = self.get_all_host_objs(cluster_name=cluster_name, esxi_host_name=esxi_host_name) 187 self.firewall_facts = dict() 188 self.rule_options = self.module.params.get("rules") 189 self.gather_rule_set() 190 191 def gather_rule_set(self): 192 for host in self.hosts: 193 self.firewall_facts[host.name] = {} 194 firewall_system = host.configManager.firewallSystem 195 if firewall_system: 196 for rule_set_obj in firewall_system.firewallInfo.ruleset: 197 temp_rule_dict = dict() 198 temp_rule_dict['enabled'] = rule_set_obj.enabled 199 allowed_host = rule_set_obj.allowedHosts 200 rule_allow_host = dict() 201 rule_allow_host['ip_address'] = allowed_host.ipAddress 202 rule_allow_host['ip_network'] = [ip.network + "/" + str(ip.prefixLength) for ip in allowed_host.ipNetwork] 203 rule_allow_host['all_ip'] = allowed_host.allIp 204 temp_rule_dict['allowed_hosts'] = rule_allow_host 205 self.firewall_facts[host.name][rule_set_obj.key] = temp_rule_dict 206 207 def check_params(self): 208 rules_by_host = {} 209 for host in self.hosts: 210 rules_by_host[host.name] = self.firewall_facts[host.name].keys() 211 212 for rule_option in self.rule_options: 213 rule_name = rule_option.get('name') 214 if rule_name is None: 215 self.module.fail_json(msg="Please specify rule.name for rule set" 216 " as it is required parameter.") 217 hosts_with_rule_name = [h for h, r in rules_by_host.items() if rule_name in r] 218 hosts_without_rule_name = set([i.name for i in self.hosts]) - set(hosts_with_rule_name) 219 if hosts_without_rule_name: 220 self.module.fail_json(msg="rule named '%s' wasn't found on hosts: %s" % ( 221 rule_name, hosts_without_rule_name)) 222 223 if 'enabled' not in rule_option: 224 self.module.fail_json(msg="Please specify rules.enabled for rule set" 225 " %s as it is required parameter." % rule_name) 226 227 allowed_hosts = rule_option.get('allowed_hosts', {}) 228 ip_addresses = allowed_hosts.get('ip_address', []) 229 ip_networks = allowed_hosts.get('ip_network', []) 230 for ip_address in ip_addresses: 231 try: 232 ipaddress.ip_address(to_text(ip_address)) 233 except ValueError: 234 self.module.fail_json(msg="The provided IP address %s is not a valid IP" 235 " for the rule %s" % (ip_address, rule_name)) 236 237 for ip_network in ip_networks: 238 try: 239 ipaddress.ip_network(ip_network) 240 except ValueError: 241 self.module.fail_json(msg="The provided IP network %s is not a valid network" 242 " for the rule %s" % (ip_network, rule_name)) 243 244 def ensure(self): 245 """ 246 Function to ensure rule set configuration 247 248 """ 249 fw_change_list = [] 250 enable_disable_changed = False 251 allowed_ip_changed = False 252 results = dict(changed=False, rule_set_state=dict()) 253 for host in self.hosts: 254 firewall_system = host.configManager.firewallSystem 255 if firewall_system is None: 256 continue 257 results['rule_set_state'][host.name] = {} 258 for rule_option in self.rule_options: 259 rule_name = rule_option.get('name', None) 260 261 current_rule_state = self.firewall_facts[host.name][rule_name]['enabled'] 262 if current_rule_state != rule_option['enabled']: 263 try: 264 if not self.module.check_mode: 265 if rule_option['enabled']: 266 firewall_system.EnableRuleset(id=rule_name) 267 else: 268 firewall_system.DisableRuleset(id=rule_name) 269 # keep track of changes as we go 270 enable_disable_changed = True 271 except vim.fault.NotFound as not_found: 272 self.module.fail_json(msg="Failed to enable rule set %s as" 273 " rule set id is unknown : %s" % ( 274 rule_name, 275 to_native(not_found.msg))) 276 except vim.fault.HostConfigFault as host_config_fault: 277 self.module.fail_json(msg="Failed to enabled rule set %s as an internal" 278 " error happened while reconfiguring" 279 " rule set : %s" % ( 280 rule_name, 281 to_native(host_config_fault.msg))) 282 283 # save variables here for comparison later and change tracking 284 # also covers cases where inputs may be null 285 permitted_networking = self.firewall_facts[host.name][rule_name] 286 rule_allows_all = permitted_networking['allowed_hosts']['all_ip'] 287 rule_allowed_ips = set(permitted_networking['allowed_hosts']['ip_address']) 288 rule_allowed_networks = set(permitted_networking['allowed_hosts']['ip_network']) 289 290 allowed_hosts = rule_option.get('allowed_hosts', {}) 291 playbook_allows_all = allowed_hosts.get('all_ip', False) 292 playbook_allowed_ips = set(allowed_hosts.get('ip_address', [])) 293 playbook_allowed_networks = set(allowed_hosts.get('ip_network', [])) 294 295 # compare what is configured on the firewall rule with what the playbook provides 296 allowed_all_ips_different = bool(rule_allows_all != playbook_allows_all) 297 ip_list_different = bool(rule_allowed_ips != playbook_allowed_ips) 298 ip_network_different = bool(rule_allowed_networks != playbook_allowed_networks) 299 300 # apply everything here in one function call 301 if allowed_all_ips_different is True or ip_list_different is True or ip_network_different is True: 302 try: 303 allowed_ip_changed = True 304 if not self.module.check_mode: 305 # setup spec 306 firewall_spec = vim.host.Ruleset.RulesetSpec() 307 firewall_spec.allowedHosts = vim.host.Ruleset.IpList() 308 firewall_spec.allowedHosts.allIp = playbook_allows_all 309 firewall_spec.allowedHosts.ipAddress = list(playbook_allowed_ips) 310 firewall_spec.allowedHosts.ipNetwork = [] 311 312 for i in playbook_allowed_networks: 313 address, mask = i.split('/') 314 tmp_ip_network_spec = vim.host.Ruleset.IpNetwork() 315 tmp_ip_network_spec.network = address 316 tmp_ip_network_spec.prefixLength = int(mask) 317 firewall_spec.allowedHosts.ipNetwork.append(tmp_ip_network_spec) 318 319 firewall_system.UpdateRuleset(id=rule_name, spec=firewall_spec) 320 except vim.fault.NotFound as not_found: 321 self.module.fail_json(msg="Failed to configure rule set %s as" 322 " rule set id is unknown : %s" % (rule_name, 323 to_native(not_found.msg))) 324 except vim.fault.HostConfigFault as host_config_fault: 325 self.module.fail_json(msg="Failed to configure rule set %s as an internal" 326 " error happened while reconfiguring" 327 " rule set : %s" % (rule_name, 328 to_native(host_config_fault.msg))) 329 except vim.fault.RuntimeFault as runtime_fault: 330 self.module.fail_json(msg="Failed to configure the rule set %s as a runtime" 331 " error happened while applying the reconfiguration:" 332 " %s" % (rule_name, to_native(runtime_fault.msg))) 333 334 results['rule_set_state'][host.name][rule_name] = { 335 'current_state': rule_option['enabled'], 336 'previous_state': current_rule_state, 337 'desired_state': rule_option['enabled'], 338 'allowed_hosts': { 339 'current_allowed_all': playbook_allows_all, 340 'previous_allowed_all': permitted_networking['allowed_hosts']['all_ip'], 341 'desired_allowed_all': playbook_allows_all, 342 'current_allowed_ip': playbook_allowed_ips, 343 'previous_allowed_ip': set(permitted_networking['allowed_hosts']['ip_address']), 344 'desired_allowed_ip': playbook_allowed_ips, 345 'current_allowed_networks': playbook_allowed_networks, 346 'previous_allowed_networks': set(permitted_networking['allowed_hosts']['ip_network']), 347 'desired_allowed_networks': playbook_allowed_networks, 348 } 349 } 350 351 if enable_disable_changed or allowed_ip_changed: 352 fw_change_list.append(True) 353 354 if any(fw_change_list): 355 results['changed'] = True 356 self.module.exit_json(**results) 357 358 359def main(): 360 argument_spec = vmware_argument_spec() 361 argument_spec.update( 362 cluster_name=dict(type='str', required=False), 363 esxi_hostname=dict(type='str', required=False), 364 rules=dict(type='list', default=list(), required=False), 365 ) 366 367 module = AnsibleModule( 368 argument_spec=argument_spec, 369 required_one_of=[ 370 ['cluster_name', 'esxi_hostname'], 371 ], 372 supports_check_mode=True 373 ) 374 375 for rule_option in module.params.get("rules", []): 376 if 'allowed_hosts' in rule_option: 377 if isinstance(rule_option['allowed_hosts'], list): 378 if len(rule_option['allowed_hosts']) == 1: 379 allowed_hosts = rule_option['allowed_hosts'][0] 380 rule_option['allowed_hosts'] = allowed_hosts 381 module.deprecate('allowed_hosts should be a dict, not a list', '2.13') 382 383 vmware_firewall_manager = VmwareFirewallManager(module) 384 vmware_firewall_manager.check_params() 385 vmware_firewall_manager.ensure() 386 387 388if __name__ == "__main__": 389 main() 390