diff --git a/lib/ansible/modules/network/f5/bigip_log_destination.py b/lib/ansible/modules/network/f5/bigip_log_destination.py index 3434742c71..a897a567b4 100644 --- a/lib/ansible/modules/network/f5/bigip_log_destination.py +++ b/lib/ansible/modules/network/f5/bigip_log_destination.py @@ -24,9 +24,6 @@ options: description: - Specifies the name of the log destination. required: True - description: - description: - - The description of the log destination. type: description: - Specifies the type of log destination. @@ -34,10 +31,18 @@ options: choices: - remote-high-speed-log - remote-syslog + - arcsight + - splunk + - management-port + - ipfix required: True + description: + description: + - The description of the log destination. pool_settings: description: - This parameter is only available when C(type) is C(remote-high-speed-log). + - Deprecated. Use the equivalent top-level parameters instead. suboptions: pool: description: @@ -72,6 +77,7 @@ options: syslog_settings: description: - This parameter is only available when C(type) is C(remote-syslog). + - Deprecated. Use the equivalent top-level parameters instead. suboptions: syslog_format: description: @@ -92,6 +98,121 @@ options: single log server, or a remote high-speed log destination, which will be used to forward the logs to a pool of remote log servers. - When creating a new log destination (and C(type) is C(remote-syslog)), this parameter is required. + syslog_format: + description: + - Specifies the method to use to format the logs associated with the remote Syslog log destination. + - When creating a new log destination (and C(type) is C(remote-syslog)), if this parameter is + not specified, the default is C(bsd-syslog). + - The C(syslog) and C(rfc5424) choices are two ways of saying the same thing. + - The C(bsd-syslog) and C(rfc3164) choices are two ways of saying the same thing. + choices: + - bsd-syslog + - syslog + - legacy-bigip + - rfc5424 + - rfc3164 + version_added: 2.8 + forward_to: + description: + - When C(type) is C(remote-syslog), specifies the management port log destination, which will + be used to forward the logs to a single log server, or a remote high-speed log destination, + which will be used to forward the logs to a pool of remote log servers. + - When C(type) is C(splunk) or C(arcsight), specifies the log destination to which logs are + forwarded. This log destination may be a management port destination, a remote high-speed + log destination, or a remote Syslog destination which is configured to send logs to an + ArcSight or Splunk server. + - When creating a new log destination and C(type) is C(remote-syslog), C(splunk), or C(arcsight), + this parameter is required. + version_added: 2.8 + pool: + description: + - When C(type) is C(remote-high-speed-log), specifies the existing pool of remote high-speed + log servers where logs will be sent. + - When C(type) is C(ipfix), specifies the existing LTM pool of remote IPFIX collectors. Any + BIG-IP application that uses this log destination sends its IP-traffic logs to this pool + of collectors. + - When creating a new destination and C(type) is C(remote-high-speed-log) or C(ipfix), this + parameter is required. + version_added: 2.8 + protocol: + description: + - When C(type) is C(remote-high-speed-log), specifies the protocol for the system to use to + send logs to the pool of remote high-speed log servers, where the logs are stored. + - When C(type) is C(ipfix), can be IPFIX or Netflow v9, depending on the type of collectors + you have in the pool that you specify. + - When C(type) is C(management-port), specifies the protocol used to send messages to the + specified location. + - When C(type) is C(management-port), only C(tcp) and C(udp) are valid values. + choices: + - tcp + - udp + - ipfix + - netflow-9 + version_added: 2.8 + distribution: + description: + - Specifies the distribution method used by the Remote High Speed Log destination to send + messages to pool members. + - When C(adaptive), connections to pool members will be added as required to provide enough + logging bandwidth. This can have the undesirable effect of logs accumulating on only one + pool member when it provides sufficient logging bandwidth on its own. + - When C(balanced), sends each successive log to a new pool member, balancing the logs among + them according to the pool's load balancing method. + - When C(replicated), replicates each log to all pool members, for redundancy. + - When creating a new log destination and C(type) is C(remote-high-speed-log), if this + parameter is not specified, the default is C(adaptive). + choices: + - adaptive + - balanced + - replicated + version_added: 2.8 + address: + description: + - Specifies the IP address that will receive messages from the specified local Log Destination. + - This parameter is only available when C(type) is C(management-port). + - When creating a new log destination and C(type) is C(management-port), this parameter + is required. + version_added: 2.8 + port: + description: + - Specifies the port of the IP address that will receive messages from the specified local + Log Destination. + - This parameter is only available when C(type) is C(management-port). + - When creating a new log destination and C(type) is C(management-port), this parameter + is required. + version_added: 2.8 + transport_profile: + description: + - Is a transport profile based on either TCP or UDP. + - This profile defines the TCP or UDP options used to send IP-traffic logs + to the pool of collectors. + - This parameter is only available when C(type) is C(ipfix). + version_added: 2.8 + server_ssl_profile: + description: + - If the C(transport_profile) is a TCP profile, you can use this field to + choose a Secure Socket Layer (SSL) profile for sending logs to the IPFIX + collectors. + - An SSL server profile defines how to communicate securely over SSL or + Transport Layer Security (TLS). + - This parameter is only available when C(type) is C(ipfix). + version_added: 2.8 + template_retransmit_interval: + description: + - Enter the time (in seconds) between each transmission of IPFIX templates + to the pool of IPFIX collectors. + - The logging destination periodically retransmits all of its IPFIX templates + at the interval you set in this field. These retransmissions are helpful + for UDP, a lossy transport mechanism. + - This parameter is only available when C(type) is C(ipfix). + version_added: 2.8 + template_delete_delay: + description: + - Enter the time (in seconds) that the BIG-IP device should pause between + deleting an obsolete IPFIX template and reusing its template ID. + - This feature is useful for systems where you use iRules to create + customized IPFIX templates. + version_added: 2.8 partition: description: - Device partition to manage resources on. @@ -114,68 +235,109 @@ EXAMPLES = r''' bigip_log_destination: name: foo type: remote-high-speed-log - pool_settings: - pool: my-ltm-pool - password: secret - server: lb.mydomain.com - state: present - user: admin + pool: my-ltm-pool + provider: + password: secret + server: lb.mydomain.com + user: admin delegate_to: localhost - name: Create a remote-syslog logging destination bigip_log_destination: name: foo type: remote-syslog - syslog_settings: - syslog_format: rfc5424 - forward_to: my-destination - password: secret - server: lb.mydomain.com - state: present - user: admin + syslog_format: rfc5424 + forward_to: my-destination + provider: + password: secret + server: lb.mydomain.com + user: admin delegate_to: localhost ''' RETURN = r''' -param1: - description: The new param1 value of the resource. - returned: changed - type: bool - sample: true -param2: - description: The new param2 value of the resource. +forward_to: + description: The new Forward To value. returned: changed type: string - sample: Foo is bar + sample: /Common/dest1 +pool: + description: The new Pool value. + returned: changed + type: string + sample: /Common/pool1 +distribution: + description: The new Distribution Method value. + returned: changed + type: string + sample: balanced +protocol: + description: The new Protocol value. + returned: changed + type: string + sample: tcp +syslog_format: + description: The new Syslog format value. + returned: changed + type: string + sample: syslog +address: + description: The new Address value. + returned: changed + type: string + sample: 1.2.3.2 +port: + description: The new Port value. + returned: changed + type: int + sample: 2020 +template_delete_delay: + description: The new Template Delete Delay value. + returned: changed + type: int + sample: 20 +template_retransmit_interval: + description: The new Template Retransmit Interval value. + returned: changed + type: int + sample: 200 +transport_profile: + description: The new Transport Profile value. + returned: changed + type: string + sample: /Common/tcp +server_ssl_profile: + description: The new Server SSL Profile value. + returned: changed + type: string + sample: /Common/serverssl ''' from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback try: - from library.module_utils.network.f5.bigip import HAS_F5SDK - from library.module_utils.network.f5.bigip import F5Client + from library.module_utils.network.f5.bigip import F5RestClient from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec - try: - from library.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False + from library.module_utils.network.f5.common import transform_name + from library.module_utils.network.f5.common import exit_json + from library.module_utils.network.f5.common import fail_json + from library.module_utils.network.f5.compare import cmp_str_with_none except ImportError: - from ansible.module_utils.network.f5.bigip import HAS_F5SDK - from ansible.module_utils.network.f5.bigip import F5Client + from ansible.module_utils.network.f5.bigip import F5RestClient from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec - try: - from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False + from ansible.module_utils.network.f5.common import transform_name + from ansible.module_utils.network.f5.common import exit_json + from ansible.module_utils.network.f5.common import fail_json + from ansible.module_utils.network.f5.compare import cmp_str_with_none class V1Parameters(AnsibleF5Parameters): @@ -204,21 +366,30 @@ class V1Parameters(AnsibleF5Parameters): ] +# TODO(Remove in 2.12) class V1ModuleParameters(V1Parameters): @property def forward_to(self): - if self._values['syslog_settings'] is None: - return None - result = self._values['syslog_settings'].get('forward_to', None) + if self._values['forward_to']: + result = self._values['forward_to'] + else: + if self._values['syslog_settings'] is None: + return None + result = self._values['syslog_settings'].get('forward_to', None) + if result: result = fq_name(self.partition, result) return result @property def syslog_format(self): - if self._values['syslog_settings'] is None: - return None - result = self._values['syslog_settings'].get('syslog_format', None) + if self._values['syslog_format']: + result = self._values['syslog_format'] + else: + if self._values['syslog_settings'] is None: + return None + result = self._values['syslog_settings'].get('syslog_format', None) + if result == 'syslog': result = 'rfc5424' if result == 'bsd-syslog': @@ -226,12 +397,14 @@ class V1ModuleParameters(V1Parameters): return result +# TODO(Remove in 2.12) class V1ApiParameters(V1Parameters): @property def type(self): return 'remote-syslog' +# TODO(Remove in 2.12) class V1Changes(V1Parameters): def to_return(self): result = {} @@ -244,14 +417,17 @@ class V1Changes(V1Parameters): return result +# TODO(Remove in 2.12) class V1UsableChanges(V1Changes): pass +# TODO(Remove in 2.12) class V1ReportableChanges(V1Changes): pass +# TODO(Remove in 2.12) class V2Parameters(AnsibleF5Parameters): """Base Parameters for remote-high-speed-log @@ -280,35 +456,47 @@ class V2Parameters(AnsibleF5Parameters): ] +# TODO(Remove in 2.12) class V2ModuleParameters(V2Parameters): @property def pool(self): - if self._values['pool_settings'] is None: - return None - result = self._values['pool_settings'].get('pool', None) + if self._values['pool']: + result = self._values['pool'] + else: + if self._values['pool_settings'] is None: + return None + result = self._values['pool_settings'].get('pool', None) if result: result = fq_name(self.partition, result) return result @property def protocol(self): - if self._values['pool_settings'] is None: - return None - return self._values['pool_settings'].get('protocol', None) + if self._values['protocol']: + return self._values['protocol'] + else: + if self._values['pool_settings'] is None: + return None + return self._values['pool_settings'].get('protocol', None) @property def distribution(self): - if self._values['pool_settings'] is None: - return None - return self._values['pool_settings'].get('distribution', None) + if self._values['distribution']: + return self._values['distribution'] + else: + if self._values['pool_settings'] is None: + return None + return self._values['pool_settings'].get('distribution', None) +# TODO(Remove in 2.12) class V2ApiParameters(V2Parameters): @property def type(self): return 'remote-high-speed-log' +# TODO(Remove in 2.12) class V2Changes(V2Parameters): def to_return(self): result = {} @@ -321,14 +509,140 @@ class V2Changes(V2Parameters): return result +# TODO(Remove in 2.12) class V2UsableChanges(V2Changes): pass +# TODO(Remove in 2.12) class V2ReportableChanges(V2Changes): pass +class V3Parameters(AnsibleF5Parameters): + api_map = { + 'forwardTo': 'forward_to', + 'poolName': 'pool', + 'remoteHighSpeedLog': 'forward_to', + 'format': 'syslog_format', + 'ipAddress': 'address', + 'protocolVersion': 'protocol', + 'templateDeleteDelay': 'template_delete_delay', + 'templateRetransmitInterval': 'template_retransmit_interval', + 'transportProfile': 'transport_profile', + 'serversslProfile': 'server_ssl_profile', + } + + api_attributes = [ + 'forwardTo', + 'distribution', + 'poolName', + 'protocol', + 'remoteHighSpeedLog', + 'format', + 'ipAddress', + 'port', + 'serversslProfile', + 'transportProfile', + 'templateRetransmitInterval', + 'templateDeleteDelay', + 'protocolVersion', + ] + + returnables = [ + 'forward_to', + 'pool', + 'distribution', + 'protocol', + 'syslog_format', + 'address', + 'port', + 'template_delete_delay', + 'template_retransmit_interval', + 'transport_profile', + 'server_ssl_profile', + ] + + updatables = [ + 'forward_to', + 'type', + 'pool', + 'distribution', + 'protocol', + 'syslog_format', + 'address', + 'port', + 'template_delete_delay', + 'template_retransmit_interval', + 'transport_profile', + 'server_ssl_profile', + 'type', + ] + + +class V3ModuleParameters(V3Parameters): + @property + def forward_to(self): + if self._values['forward_to'] is None: + return None + return fq_name(self.partition, self._values['forward_to']) + + @property + def pool(self): + if self._values['pool'] is None: + return None + return fq_name(self.partition, self._values['pool']) + + @property + def syslog_format(self): + if self._values['syslog_format'] is None: + return None + result = self._values['syslog_format'] + if result == 'syslog': + result = 'rfc5424' + if result == 'bsd-syslog': + result = 'rfc3164' + return result + + @property + def server_ssl_profile(self): + if self._values['server_ssl_profile'] is None: + return None + elif self._values['server_ssl_profile'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['server_ssl_profile']) + + @property + def transport_profile(self): + if self._values['transport_profile'] is None: + return None + return fq_name(self.partition, self._values['transport_profile']) + + +class V3ApiParameters(V3Parameters): + pass + + +class V3Changes(V3Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + pass + return result + + +class V3UsableChanges(V3Changes): + pass + + +class V3ReportableChanges(V3Changes): + pass + + class Difference(object): def __init__(self, want, have=None): self.want = want @@ -357,6 +671,14 @@ class Difference(object): "'type' cannot be changed once it is set." ) + @property + def server_ssl_profile(self): + return cmp_str_with_none(self.want.server_ssl_profile, self.have.server_ssl_profile) + + @property + def transport_profile(self): + return cmp_str_with_none(self.want.transport_profile, self.have.transport_profile) + class BaseManager(object): def __init__(self, *args, **kwargs): @@ -400,13 +722,10 @@ class BaseManager(object): result = dict() state = self.want.state - try: - if state == "present": - changed = self.present() - elif state == "absent": - changed = self.absent() - except iControlUnexpectedHTTPError as e: - raise F5ModuleError(str(e)) + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() reportable = self.get_reportable_changes(params=self.changes.to_return()) changes = reportable.to_return() @@ -482,6 +801,7 @@ class V1Manager(BaseManager): "'forward_to' is required when creating a new remote-syslog destination." ) + # TODO(In 2.12, these get_* methods should no longer be needed) def get_reportable_changes(self, params=None): if params: return V1ReportableChanges(params=params) @@ -509,43 +829,88 @@ class V1Manager(BaseManager): return V1ApiParameters() def exists(self): - result = self.client.api.tm.sys.log_config.destination.remote_syslogs.remote_syslog.exists( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-syslog/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - return result + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True def create_on_device(self): params = self.changes.api_params() - self.client.api.tm.sys.log_config.destination.remote_syslogs.remote_syslog.create( - name=self.want.name, - partition=self.want.partition, - **params + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-syslog/".format( + self.client.provider['server'], + self.client.provider['server_port'] ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) def update_on_device(self): params = self.changes.api_params() - resource = self.client.api.tm.sys.log_config.destination.remote_syslogs.remote_syslog.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-syslog/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - resource.modify(**params) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) def remove_from_device(self): - resource = self.client.api.tm.sys.log_config.destination.remote_syslogs.remote_syslog.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-syslog/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - if resource: - resource.delete() + resp = self.client.api.delete(uri) + if resp.status == 200: + return True def read_current_from_device(self): - resource = self.client.api.tm.sys.log_config.destination.remote_syslogs.remote_syslog.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-syslog/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - result = resource.attrs - return V1ApiParameters(params=result) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + response['type'] = 'remote-syslog' + return V1ApiParameters(params=response) class V2Manager(BaseManager): @@ -595,43 +960,630 @@ class V2Manager(BaseManager): return V2ApiParameters() def exists(self): - result = self.client.api.tm.sys.log_config.destination.remote_high_speed_logs.remote_high_speed_log.exists( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-high-speed-log/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - return result + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True def create_on_device(self): params = self.changes.api_params() - self.client.api.tm.sys.log_config.destination.remote_high_speed_logs.remote_high_speed_log.create( - name=self.want.name, - partition=self.want.partition, - **params + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-high-speed-log/".format( + self.client.provider['server'], + self.client.provider['server_port'] ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) def update_on_device(self): params = self.changes.api_params() - resource = self.client.api.tm.sys.log_config.destination.remote_high_speed_logs.remote_high_speed_log.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-high-speed-log/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - resource.modify(**params) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) def remove_from_device(self): - resource = self.client.api.tm.sys.log_config.destination.remote_high_speed_logs.remote_high_speed_log.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-high-speed-log/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - if resource: - resource.delete() + resp = self.client.api.delete(uri) + if resp.status == 200: + return True def read_current_from_device(self): - resource = self.client.api.tm.sys.log_config.destination.remote_high_speed_logs.remote_high_speed_log.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/remote-high-speed-log/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - result = resource.attrs - return V2ApiParameters(params=result) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + response['type'] = 'remote-high-speed-log' + return V2ApiParameters(params=response) + + +class V3Manager(BaseManager): + def __init__(self, *args, **kwargs): + super(V3Manager, self).__init__(*args, **kwargs) + self.want = self.get_module_params(params=self.module.params) + self.have = self.get_api_params() + self.changes = self.get_usable_changes() + + def get_reportable_changes(self, params=None): + if params: + return V3ReportableChanges(params=params) + return V3ReportableChanges() + + def get_usable_changes(self, params=None): + if params: + return V3UsableChanges(params=params) + return V3UsableChanges() + + def _validate_creation_parameters(self): + if self.want.forward_to is None: + raise F5ModuleError( + "'forward_to' is required when creating a new arcsight destination." + ) + + def get_returnables(self): + return V3ApiParameters.returnables + + def get_updatables(self): + return V3ApiParameters.updatables + + def get_module_params(self, params=None): + if params: + return V3ModuleParameters(params=params) + return V3ModuleParameters() + + def get_api_params(self, params=None): + if params: + + return V3ApiParameters(params=params) + return V3ApiParameters() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/arcsight/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/arcsight/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/arcsight/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/arcsight/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.delete(uri) + if resp.status == 200: + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/arcsight/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + response['type'] = 'arcsight' + return V3ApiParameters(params=response) + + +class V4Manager(BaseManager): + """Manager for Splunk + + Do not worry about the usage of V3 classes in this V4 manager. + In Ansible 2.12, the Parameter classes will undergo a rename + because of parameters being deprecated. + + The correct Parameter classes to use in this class are the + V3 Parameter classes. + + """ + def __init__(self, *args, **kwargs): + super(V4Manager, self).__init__(*args, **kwargs) + self.want = self.get_module_params(params=self.module.params) + self.have = self.get_api_params() + self.changes = self.get_usable_changes() + + def get_reportable_changes(self, params=None): + if params: + return V3ReportableChanges(params=params) + return V3ReportableChanges() + + def get_usable_changes(self, params=None): + if params: + return V3UsableChanges(params=params) + return V3UsableChanges() + + def _validate_creation_parameters(self): + if self.want.forward_to is None: + raise F5ModuleError( + "'forward_to' is required when creating a new splunk destination." + ) + + def get_returnables(self): + return V3ApiParameters.returnables + + def get_updatables(self): + return V3ApiParameters.updatables + + def get_module_params(self, params=None): + if params: + return V3ModuleParameters(params=params) + return V3ModuleParameters() + + def get_api_params(self, params=None): + if params: + + return V3ApiParameters(params=params) + return V3ApiParameters() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/splunk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/splunk/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/splunk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/splunk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.delete(uri) + if resp.status == 200: + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/splunk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + response['type'] = 'splunk' + return V3ApiParameters(params=response) + + +class V5Manager(BaseManager): + """Manager for Management Port + + Do not worry about the usage of V3 classes in this V5 manager. + In Ansible 2.12, the Parameter classes will undergo a rename + because of parameters being deprecated. + + The correct Parameter classes to use in this class are the + V3 Parameter classes. + + """ + def __init__(self, *args, **kwargs): + super(V5Manager, self).__init__(*args, **kwargs) + self.want = self.get_module_params(params=self.module.params) + self.have = self.get_api_params() + self.changes = self.get_usable_changes() + + def get_reportable_changes(self, params=None): + if params: + return V3ReportableChanges(params=params) + return V3ReportableChanges() + + def get_usable_changes(self, params=None): + if params: + return V3UsableChanges(params=params) + return V3UsableChanges() + + def _validate_creation_parameters(self): + if self.want.address is None: + raise F5ModuleError( + "'address' is required when creating a new management-port destination." + ) + if self.want.port is None: + raise F5ModuleError( + "'port' is required when creating a new management-port destination." + ) + + def get_returnables(self): + return V3ApiParameters.returnables + + def get_updatables(self): + return V3ApiParameters.updatables + + def get_module_params(self, params=None): + if params: + return V3ModuleParameters(params=params) + return V3ModuleParameters() + + def get_api_params(self, params=None): + if params: + + return V3ApiParameters(params=params) + return V3ApiParameters() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/management-port/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/management-port/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/management-port/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/management-port/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.delete(uri) + if resp.status == 200: + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/management-port/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + response['type'] = 'management-port' + return V3ApiParameters(params=response) + + +class V6Manager(BaseManager): + """Manager for IPFIX + + Do not worry about the usage of V3 classes in this V6 manager. + In Ansible 2.12, the Parameter classes will undergo a rename + because of parameters being deprecated. + + The correct Parameter classes to use in this class are the + V3 Parameter classes. + + """ + def __init__(self, *args, **kwargs): + super(V6Manager, self).__init__(*args, **kwargs) + self.want = self.get_module_params(params=self.module.params) + self.have = self.get_api_params() + self.changes = self.get_usable_changes() + + def get_reportable_changes(self, params=None): + if params: + return V3ReportableChanges(params=params) + return V3ReportableChanges() + + def get_usable_changes(self, params=None): + if params: + return V3UsableChanges(params=params) + return V3UsableChanges() + + def _validate_creation_parameters(self): + if self.want.protocol is None: + raise F5ModuleError( + "'protocol' is required when creating a new ipfix destination." + ) + if self.want.pool is None: + raise F5ModuleError( + "'port' is required when creating a new ipfix destination." + ) + if self.want.transport_profile is None: + raise F5ModuleError( + "'transport_profile' is required when creating a new ipfix destination." + ) + + def get_returnables(self): + return V3ApiParameters.returnables + + def get_updatables(self): + return V3ApiParameters.updatables + + def get_module_params(self, params=None): + if params: + return V3ModuleParameters(params=params) + return V3ModuleParameters() + + def get_api_params(self, params=None): + if params: + + return V3ApiParameters(params=params) + return V3ApiParameters() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/ipfix/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/ipfix/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/ipfix/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/ipfix/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.delete(uri) + if resp.status == 200: + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/destination/ipfix/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + response['type'] = 'ipfix' + return V3ApiParameters(params=response) class ModuleManager(object): @@ -644,6 +1596,18 @@ class ModuleManager(object): manager = self.get_manager('v1') elif self.module.params['type'] == 'remote-high-speed-log': manager = self.get_manager('v2') + elif self.module.params['type'] == 'arcsight': + manager = self.get_manager('v3') + elif self.module.params['type'] == 'splunk': + manager = self.get_manager('v4') + elif self.module.params['type'] == 'management-port': + manager = self.get_manager('v5') + elif self.module.params['type'] == 'ipfix': + manager = self.get_manager('v6') + else: + raise F5ModuleError( + "Unknown type specified." + ) result = manager.exec_module() return result @@ -652,6 +1616,14 @@ class ModuleManager(object): return V1Manager(**self.kwargs) elif type == 'v2': return V2Manager(**self.kwargs) + elif type == 'v3': + return V3Manager(**self.kwargs) + elif type == 'v4': + return V4Manager(**self.kwargs) + elif type == 'v5': + return V5Manager(**self.kwargs) + elif type == 'v6': + return V6Manager(**self.kwargs) class ArgumentSpec(object): @@ -662,11 +1634,52 @@ class ArgumentSpec(object): type=dict( required=True, choices=[ + 'arcsight', 'remote-high-speed-log', - 'remote-syslog' + 'remote-syslog', + 'splunk', + 'management-port', + 'ipfix', ] ), description=dict(), + syslog_format=dict( + choices=[ + 'bsd-syslog', + 'syslog', + 'legacy-bigip', + 'rfc5424', + 'rfc3164' + ] + ), + forward_to=dict(), + pool=dict(), + protocol=dict( + choices=['tcp', 'udp', 'ipfix', 'netflow-9'] + ), + distribution=dict( + choices=[ + 'adaptive', + 'balanced', + 'replicated', + ] + ), + state=dict( + default='present', + choices=['present', 'absent'] + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + address=dict(), + port=dict(type='int'), + transport_profile=dict(), + server_ssl_profile=dict(), + template_retransmit_interval=dict(type='int'), + template_delete_delay=dict(type='int'), + + # Deprecated settings pool_settings=dict( type='dict', suboptions=dict( @@ -681,7 +1694,8 @@ class ArgumentSpec(object): 'replicated', ] ) - ) + ), + removed_in_version=2.12, ), syslog_settings=dict( type='dict', @@ -695,21 +1709,22 @@ class ArgumentSpec(object): 'rfc3164' ] ), - foward_to=dict() - ) + forward_to=dict() + ), + removed_in_version=2.12, ), - state=dict( - default='present', - choices=['present', 'absent'] - ), - partition=dict( - default='Common', - fallback=(env_fallback, ['F5_PARTITION']) - ) ) self.argument_spec = {} self.argument_spec.update(f5_argument_spec) self.argument_spec.update(argument_spec) + self.mutually_exclusive = [ + ['syslog_settings', 'syslog_format'], + ['syslog_settings', 'forward_to'], + + ['pool_settings', 'pool'], + ['pool_settings', 'protocol'], + ['pool_settings', 'distribution'], + ] def main(): @@ -719,18 +1734,17 @@ def main(): argument_spec=spec.argument_spec, supports_check_mode=spec.supports_check_mode ) - if not HAS_F5SDK: - module.fail_json(msg="The python f5-sdk module is required") + + client = F5RestClient(**module.params) try: - client = F5Client(**module.params) mm = ModuleManager(module=module, client=client) results = mm.exec_module() cleanup_tokens(client) - module.exit_json(**results) + exit_json(module, results, client) except F5ModuleError as ex: cleanup_tokens(client) - module.fail_json(msg=str(ex)) + fail_json(module, ex, client) if __name__ == '__main__': diff --git a/test/units/modules/network/f5/test_bigip_log_destination.py b/test/units/modules/network/f5/test_bigip_log_destination.py index add7b665ae..ee448811c1 100644 --- a/test/units/modules/network/f5/test_bigip_log_destination.py +++ b/test/units/modules/network/f5/test_bigip_log_destination.py @@ -8,16 +8,12 @@ __metaclass__ = type import os import json -import pytest import sys from nose.plugins.skip import SkipTest if sys.version_info < (2, 7): raise SkipTest("F5 Ansible modules require Python >= 2.7") -from units.compat import unittest -from units.compat.mock import Mock -from units.compat.mock import patch from ansible.module_utils.basic import AnsibleModule try: @@ -29,9 +25,13 @@ try: from library.modules.bigip_log_destination import V1Manager from library.modules.bigip_log_destination import V2Manager from library.modules.bigip_log_destination import ArgumentSpec - from library.module_utils.network.f5.common import F5ModuleError - from library.module_utils.network.f5.common import iControlUnexpectedHTTPError - from test.unit.modules.utils import set_module_args + + # In Ansible 2.8, Ansible changed import paths. + from test.units.compat import unittest + from test.units.compat.mock import Mock + from test.units.compat.mock import patch + + from test.units.modules.utils import set_module_args except ImportError: try: from ansible.modules.network.f5.bigip_log_destination import V1ApiParameters @@ -42,8 +42,12 @@ except ImportError: from ansible.modules.network.f5.bigip_log_destination import V1Manager from ansible.modules.network.f5.bigip_log_destination import V2Manager from ansible.modules.network.f5.bigip_log_destination import ArgumentSpec - from ansible.module_utils.network.f5.common import F5ModuleError - from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + + # Ansible 2.8 imports + from units.compat import unittest + from units.compat.mock import Mock + from units.compat.mock import patch + from units.modules.utils import set_module_args except ImportError: raise SkipTest("F5 Ansible modules require the f5-sdk Python library")