diff --git a/.gitignore b/.gitignore index 41b24424f..710efee9b 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ examples/hosts.ini .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf +.venv/* # Sensitive or high-churn files .idea/**/dataSources/ diff --git a/ansible_collections/f5networks/f5_modules/changelogs/fragments/issue_2419_2421.yaml b/ansible_collections/f5networks/f5_modules/changelogs/fragments/issue_2419_2421.yaml new file mode 100644 index 000000000..acf2a643c --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/changelogs/fragments/issue_2419_2421.yaml @@ -0,0 +1,5 @@ +--- +minor_changes: + - bigip_device_info - virtual-servers - return per_flow_request_access_policy if defined. + - bigip_virtual_server - set per_flow_request_access_policy and stay idempotent. + - bigip_asm_dos_application - add support for creating dos profile. \ No newline at end of file diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/icontrol.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/icontrol.py index 723c53645..ad21e5654 100644 --- a/ansible_collections/f5networks/f5_modules/plugins/module_utils/icontrol.py +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/icontrol.py @@ -182,7 +182,7 @@ def send(self, method, url, **kwargs): if not data and json is not None: self.request.headers.update(BASE_HEADERS) - body = _json.dumps(json) + body = _json.dumps(json, ensure_ascii=False) if not isinstance(body, bytes): body = body.encode('utf-8') if data: diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_dos_application.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_dos_application.py index 5ee49e6e9..4519104dc 100644 --- a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_dos_application.py +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_dos_application.py @@ -1136,7 +1136,21 @@ def profile_exists(self): raise F5ModuleError(str(ex)) if resp.status == 404 or 'code' in response and response['code'] == 404: - return False + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + payload = {'name': self.want.profile, 'partition': self.want.partition} + resp = self.client.api.post(uri, json=payload) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + # response = resp.json() + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: return True @@ -1148,16 +1162,11 @@ def profile_exists(self): def exists(self): errors = [401, 403, 409, 500, 501, 502, 503, 504] - if not self.profile_exists(): - raise F5ModuleError( - 'Specified DOS profile: {0} on partition: {1} does not exist.'.format( - self.want.profile, self.want.partition) - ) - uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format( + + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(self.want.partition, self.want.profile), - self.want.profile ) resp = self.client.api.get(uri) try: @@ -1169,7 +1178,6 @@ def exists(self): return False if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: return True - if resp.status in errors or 'code' in response and response['code'] in errors: if 'message' in response: raise F5ModuleError(response['message']) @@ -1179,6 +1187,19 @@ def exists(self): def create_on_device(self): params = self.changes.api_params() params['name'] = self.want.profile + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + payload = {'name': self.want.profile, 'partition': self.want.partition} + + resp = self.client.api.post(uri, json=payload) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 404 or 'code' in response and response['code'] == 404: + raise F5ModuleError(resp.content) uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/".format( self.client.provider['server'], self.client.provider['server_port'], @@ -1213,11 +1234,10 @@ def update_on_device(self): raise F5ModuleError(resp.content) def remove_from_device(self): - uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format( + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(self.want.partition, self.want.profile), - self.want.profile ) response = self.client.api.delete(uri) if response.status == 200: diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_info.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_info.py index 6753634a7..6b07d4872 100644 --- a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_info.py +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_info.py @@ -7454,6 +7454,12 @@ returned: queried type: str sample: tcp + per_flow_request_access_policy: + description: + - per request policy. + returned: queried + type: str + sample: /Common/my-custom-per-request-policy total_requests: description: - Total requests. @@ -17114,6 +17120,7 @@ class VirtualServersParameters(BaseParameters): 'securityLogProfiles': 'security_log_profiles', 'profilesReference': 'profiles', 'policiesReference': 'policies', + 'perFlowRequestAccessPolicy': 'per_flow_request_access_policy', } returnables = [ @@ -17153,6 +17160,7 @@ class VirtualServersParameters(BaseParameters): 'type', 'policies', 'profiles', + 'per_flow_request_access_policy', 'destination_address', 'destination_port', 'availability_status', diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_server.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_server.py index 7994c15e4..82e029011 100644 --- a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_server.py +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_server.py @@ -227,6 +227,10 @@ elements: str aliases: - all_policies + per_flow_request_access_policy: + description: + - Specifies the Per-Request access policy for the virtual server. + type: str snat: description: - Source network address policy. @@ -811,6 +815,11 @@ returned: changed type: list sample: ['/Common/policy1', '/Common/policy2'] +per_flow_request_access_policy: + description: Per-request policy attached to the virtual. + returned: changed + type: str + sample: '/Common/sample_per-request_policy' port: description: Port the virtual server is configured to listen on. returned: changed @@ -981,7 +990,8 @@ class Parameters(AnsibleF5Parameters): 'rateLimitSrcMask': 'rate_limit_src_mask', 'clonePools': 'clone_pools', 'autoLasthop': 'auto_last_hop', - 'serviceDownImmediateAction': 'service_down_immediate_action' + 'serviceDownImmediateAction': 'service_down_immediate_action', + 'perFlowRequestAccessPolicy': 'per_flow_request_access_policy', } api_attributes = [ @@ -1025,6 +1035,7 @@ class Parameters(AnsibleF5Parameters): 'rateLimitSrcMask', 'clonePools', 'autoLasthop', + 'perFlowRequestAccessPolicy', ] updatables = [ @@ -1062,6 +1073,7 @@ class Parameters(AnsibleF5Parameters): 'rate_limit_dst_mask', 'clone_pools', 'auto_last_hop', + 'per_flow_request_access_policy', ] returnables = [ @@ -1103,6 +1115,7 @@ class Parameters(AnsibleF5Parameters): 'rate_limit_dst_mask', 'clone_pools', 'auto_last_hop', + 'per_flow_request_access_policy', ] profiles_mutex = [ @@ -1627,6 +1640,7 @@ def profiles(self): return None result = [] prof_path = 'https://localhost/mgmt/tm/ltm/profile/' + accprof_path = 'https://localhost/mgmt/tm/apm/profile/access' for item in self._values['profiles']['items']: context = item['context'] name = item['name'] @@ -1634,6 +1648,8 @@ def profiles(self): if context in ['all', 'serverside', 'clientside']: if path.startswith(prof_path): result.append(dict(name=name, context=context, fullPath=item['fullPath'])) + if path.startswith(accprof_path): + result.append(dict(name=name, context=context, fullPath=item['fullPath'])) else: raise F5ModuleError( "Unknown profile context found: '{0}'".format(context) @@ -2631,6 +2647,7 @@ def check_update(self): def check_create(self): # Regular checks + self._verify_virtual_has_required_parameters() self._set_default_ip_protocol() self._set_default_profiles() self._override_port_by_type() @@ -2639,7 +2656,6 @@ def check_create(self): self._verify_default_persistence_profile_for_type() self._verify_fallback_persistence_profile_for_type() self._update_persistence_profile() - self._verify_virtual_has_required_parameters() self._ensure_server_type_supports_vlans() self._override_vlans_if_all_specified() self._check_source_and_destination_match() @@ -3629,6 +3645,9 @@ def create_on_device(self): params = self.changes.api_params() params['name'] = self.want.name params['partition'] = self.want.partition + if 'destination' not in params: + params['destination'] = f"/Common/0.0.0.0:{self.want.port}" + params['mask'] = "255.255.255.255" if self.want.insert_metadata: # Mark the resource as managed by Ansible, this is default behavior params = mark_managed_by(self.module.ansible_version, params) @@ -3743,6 +3762,7 @@ def __init__(self): service_down_immediate_action=dict( choices=['drop', 'none', 'reset'] ), + per_flow_request_access_policy=dict(), security_log_profiles=dict( type='list', elements='str', diff --git a/requirements.txt b/requirements.txt index fd2a4a164..4b97da8df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,20 @@ ansible>=8.5.0 -q -invoke~=1.7.3 -Jinja2~=3.1.2 +invoke==2.2.0 +pycodestyle==2.12.1 +flake8==7.1.1 +nose +mock +semver +pytest +pytest-cov +coverage packaging -semver~=2.13.0 -pytest~=7.2.0 -PyYAML~=6.0 -netaddr~=0.8.0 -packaging~=21.3 -ordereddict~=1.1 -cryptography~=42.0.8 -objectpath~=0.6.1 \ No newline at end of file +antsibull-changelog +ansible-lint +paramiko +jinja2 +netaddr +objectpath +PyYAML +ordereddict +cryptography \ No newline at end of file