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