From 66cec45960ce1d9c794e9399de15c138acb18aed Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 18:03:42 +0200 Subject: Adding upstream version 7.3.0+dfsg. Signed-off-by: Daniel Baumann --- .../f5_modules/plugins/action/__init__.py | 0 .../f5networks/f5_modules/plugins/action/bigip.py | 98 + .../plugins/action/bigip_imish_config.py | 108 + .../f5networks/f5_modules/plugins/action/bigiq.py | 90 + .../f5_modules/plugins/doc_fragments/f5.py | 84 + .../plugins/doc_fragments/f5_rest_cli.py | 90 + .../f5_modules/plugins/doc_fragments/f5ssh.py | 90 + .../f5_modules/plugins/filter/__init__.py | 0 .../f5_modules/plugins/filter/abspath.py | 15 + .../f5_modules/plugins/filter/markdev.py | 13 + .../f5_modules/plugins/lookup/__init__.py | 0 .../f5_modules/plugins/lookup/bigiq_license.py | 154 + .../f5_modules/plugins/lookup/license_hopper.py | 73 + .../f5_modules/plugins/module_utils/__init__.py | 0 .../f5_modules/plugins/module_utils/bigip.py | 113 + .../f5_modules/plugins/module_utils/bigiq.py | 162 + .../f5_modules/plugins/module_utils/common.py | 672 + .../f5_modules/plugins/module_utils/compare.py | 84 + .../f5_modules/plugins/module_utils/constants.py | 48 + .../f5_modules/plugins/module_utils/icontrol.py | 681 + .../f5_modules/plugins/module_utils/ipaddress.py | 85 + .../f5_modules/plugins/module_utils/teem.py | 165 + .../f5_modules/plugins/module_utils/urls.py | 119 + .../f5_modules/plugins/module_utils/version.py | 7 + .../f5_modules/plugins/modules/__init__.py | 0 .../f5_modules/plugins/modules/bigip_apm_acl.py | 998 + .../plugins/modules/bigip_apm_network_access.py | 1031 + .../plugins/modules/bigip_apm_policy_fetch.py | 507 + .../plugins/modules/bigip_apm_policy_import.py | 453 + .../plugins/modules/bigip_asm_advanced_settings.py | 432 + .../plugins/modules/bigip_asm_dos_application.py | 1346 ++ .../plugins/modules/bigip_asm_policy_fetch.py | 703 + .../plugins/modules/bigip_asm_policy_import.py | 640 + .../plugins/modules/bigip_asm_policy_manage.py | 989 + .../modules/bigip_asm_policy_server_technology.py | 496 + .../modules/bigip_asm_policy_signature_set.py | 722 + .../plugins/modules/bigip_cgnat_lsn_pool.py | 1147 ++ .../f5_modules/plugins/modules/bigip_cli_alias.py | 416 + .../f5_modules/plugins/modules/bigip_cli_script.py | 457 + .../f5_modules/plugins/modules/bigip_command.py | 745 + .../f5_modules/plugins/modules/bigip_config.py | 412 + .../plugins/modules/bigip_configsync_action.py | 432 + .../f5_modules/plugins/modules/bigip_data_group.py | 1502 ++ .../plugins/modules/bigip_device_auth.py | 826 + .../plugins/modules/bigip_device_auth_ldap.py | 911 + .../plugins/modules/bigip_device_auth_radius.py | 622 + .../modules/bigip_device_auth_radius_server.py | 532 + .../plugins/modules/bigip_device_certificate.py | 622 + .../plugins/modules/bigip_device_connectivity.py | 698 + .../f5_modules/plugins/modules/bigip_device_dns.py | 536 + .../plugins/modules/bigip_device_group.py | 620 + .../plugins/modules/bigip_device_group_member.py | 294 + .../plugins/modules/bigip_device_ha_group.py | 820 + .../plugins/modules/bigip_device_httpd.py | 713 + .../plugins/modules/bigip_device_info.py | 18661 +++++++++++++++++++ .../plugins/modules/bigip_device_license.py | 950 + .../f5_modules/plugins/modules/bigip_device_ntp.py | 401 + .../plugins/modules/bigip_device_sshd.py | 436 + .../plugins/modules/bigip_device_syslog.py | 721 + .../plugins/modules/bigip_device_traffic_group.py | 661 + .../plugins/modules/bigip_device_trust.py | 373 + .../plugins/modules/bigip_dns_cache_resolver.py | 538 + .../plugins/modules/bigip_dns_nameserver.py | 461 + .../plugins/modules/bigip_dns_resolver.py | 528 + .../f5_modules/plugins/modules/bigip_dns_zone.py | 697 + .../f5_modules/plugins/modules/bigip_file_copy.py | 695 + .../plugins/modules/bigip_firewall_address_list.py | 1016 + .../plugins/modules/bigip_firewall_dos_profile.py | 421 + .../plugins/modules/bigip_firewall_dos_vector.py | 1627 ++ .../plugins/modules/bigip_firewall_global_rules.py | 368 + .../plugins/modules/bigip_firewall_log_profile.py | 870 + .../modules/bigip_firewall_log_profile_network.py | 1271 ++ .../plugins/modules/bigip_firewall_policy.py | 535 + .../plugins/modules/bigip_firewall_port_list.py | 655 + .../plugins/modules/bigip_firewall_rule.py | 1321 ++ .../plugins/modules/bigip_firewall_rule_list.py | 536 + .../plugins/modules/bigip_firewall_schedule.py | 664 + .../plugins/modules/bigip_gtm_datacenter.py | 495 + .../plugins/modules/bigip_gtm_dns_listener.py | 875 + .../f5_modules/plugins/modules/bigip_gtm_global.py | 342 + .../plugins/modules/bigip_gtm_monitor_bigip.py | 680 + .../plugins/modules/bigip_gtm_monitor_external.py | 697 + .../plugins/modules/bigip_gtm_monitor_firepass.py | 808 + .../plugins/modules/bigip_gtm_monitor_http.py | 851 + .../plugins/modules/bigip_gtm_monitor_https.py | 974 + .../plugins/modules/bigip_gtm_monitor_tcp.py | 809 + .../modules/bigip_gtm_monitor_tcp_half_open.py | 707 + .../f5_modules/plugins/modules/bigip_gtm_pool.py | 1125 ++ .../plugins/modules/bigip_gtm_pool_member.py | 1080 ++ .../f5_modules/plugins/modules/bigip_gtm_server.py | 1803 ++ .../plugins/modules/bigip_gtm_topology_record.py | 1069 ++ .../plugins/modules/bigip_gtm_topology_region.py | 880 + .../plugins/modules/bigip_gtm_virtual_server.py | 1189 ++ .../plugins/modules/bigip_gtm_wide_ip.py | 945 + .../f5_modules/plugins/modules/bigip_hostname.py | 293 + .../plugins/modules/bigip_iapp_service.py | 987 + .../plugins/modules/bigip_iapp_template.py | 566 + .../f5_modules/plugins/modules/bigip_ike_peer.py | 824 + .../plugins/modules/bigip_imish_config.py | 847 + .../f5_modules/plugins/modules/bigip_interface.py | 932 + .../plugins/modules/bigip_ipsec_policy.py | 771 + .../f5_modules/plugins/modules/bigip_irule.py | 568 + .../plugins/modules/bigip_log_destination.py | 1254 ++ .../plugins/modules/bigip_log_publisher.py | 422 + .../f5_modules/plugins/modules/bigip_ltm_global.py | 332 + .../f5_modules/plugins/modules/bigip_lx_package.py | 522 + .../plugins/modules/bigip_management_route.py | 453 + .../plugins/modules/bigip_message_routing_peer.py | 663 + .../modules/bigip_message_routing_protocol.py | 569 + .../plugins/modules/bigip_message_routing_route.py | 558 + .../modules/bigip_message_routing_router.py | 759 + .../bigip_message_routing_transport_config.py | 679 + .../plugins/modules/bigip_monitor_dns.py | 1020 + .../plugins/modules/bigip_monitor_external.py | 734 + .../plugins/modules/bigip_monitor_ftp.py | 817 + .../plugins/modules/bigip_monitor_gateway_icmp.py | 792 + .../plugins/modules/bigip_monitor_http.py | 766 + .../plugins/modules/bigip_monitor_https.py | 800 + .../plugins/modules/bigip_monitor_icmp.py | 787 + .../plugins/modules/bigip_monitor_ldap.py | 829 + .../plugins/modules/bigip_monitor_mysql.py | 892 + .../plugins/modules/bigip_monitor_oracle.py | 880 + .../plugins/modules/bigip_monitor_smtp.py | 765 + .../plugins/modules/bigip_monitor_snmp_dca.py | 757 + .../plugins/modules/bigip_monitor_tcp.py | 684 + .../plugins/modules/bigip_monitor_tcp_echo.py | 590 + .../plugins/modules/bigip_monitor_tcp_half_open.py | 641 + .../plugins/modules/bigip_monitor_udp.py | 651 + .../plugins/modules/bigip_network_globals.py | 1508 ++ .../f5_modules/plugins/modules/bigip_node.py | 1113 ++ .../f5_modules/plugins/modules/bigip_partition.py | 500 + .../plugins/modules/bigip_password_policy.py | 428 + .../f5_modules/plugins/modules/bigip_policy.py | 1154 ++ .../plugins/modules/bigip_policy_rule.py | 2725 +++ .../f5_modules/plugins/modules/bigip_pool.py | 1359 ++ .../plugins/modules/bigip_pool_member.py | 1672 ++ .../plugins/modules/bigip_profile_analytics.py | 756 + .../plugins/modules/bigip_profile_client_ssl.py | 1244 ++ .../plugins/modules/bigip_profile_dns.py | 749 + .../plugins/modules/bigip_profile_fastl4.py | 1457 ++ .../plugins/modules/bigip_profile_ftp.py | 653 + .../plugins/modules/bigip_profile_http.py | 1797 ++ .../plugins/modules/bigip_profile_http2.py | 671 + .../modules/bigip_profile_http_compression.py | 551 + .../plugins/modules/bigip_profile_oneconnect.py | 612 + .../modules/bigip_profile_persistence_cookie.py | 964 + .../modules/bigip_profile_persistence_src_addr.py | 630 + .../modules/bigip_profile_persistence_universal.py | 600 + .../plugins/modules/bigip_profile_server_ssl.py | 853 + .../plugins/modules/bigip_profile_sip.py | 786 + .../plugins/modules/bigip_profile_tcp.py | 808 + .../plugins/modules/bigip_profile_udp.py | 463 + .../f5_modules/plugins/modules/bigip_provision.py | 1153 ++ .../f5_modules/plugins/modules/bigip_qkview.py | 623 + .../plugins/modules/bigip_remote_role.py | 553 + .../plugins/modules/bigip_remote_syslog.py | 458 + .../plugins/modules/bigip_remote_user.py | 383 + .../plugins/modules/bigip_routedomain.py | 741 + .../f5_modules/plugins/modules/bigip_selfip.py | 919 + .../plugins/modules/bigip_service_policy.py | 444 + .../f5_modules/plugins/modules/bigip_smtp.py | 569 + .../f5_modules/plugins/modules/bigip_snat_pool.py | 528 + .../plugins/modules/bigip_snat_translation.py | 774 + .../f5_modules/plugins/modules/bigip_snmp.py | 415 + .../plugins/modules/bigip_snmp_community.py | 924 + .../f5_modules/plugins/modules/bigip_snmp_trap.py | 829 + .../plugins/modules/bigip_software_image.py | 505 + .../plugins/modules/bigip_software_install.py | 715 + .../plugins/modules/bigip_software_update.py | 330 + .../plugins/modules/bigip_ssl_certificate.py | 592 + .../f5_modules/plugins/modules/bigip_ssl_csr.py | 477 + .../f5_modules/plugins/modules/bigip_ssl_key.py | 540 + .../plugins/modules/bigip_ssl_key_cert.py | 803 + .../f5_modules/plugins/modules/bigip_ssl_ocsp.py | 787 + .../plugins/modules/bigip_static_route.py | 705 + .../plugins/modules/bigip_sys_daemon_log_tmm.py | 488 + .../f5_modules/plugins/modules/bigip_sys_db.py | 398 + .../f5_modules/plugins/modules/bigip_sys_global.py | 498 + .../plugins/modules/bigip_timer_policy.py | 643 + .../plugins/modules/bigip_traffic_selector.py | 509 + .../f5_modules/plugins/modules/bigip_trunk.py | 610 + .../f5_modules/plugins/modules/bigip_tunnel.py | 619 + .../f5_modules/plugins/modules/bigip_ucs.py | 753 + .../f5_modules/plugins/modules/bigip_ucs_fetch.py | 755 + .../f5_modules/plugins/modules/bigip_user.py | 1128 ++ .../f5_modules/plugins/modules/bigip_vcmp_guest.py | 1025 + .../plugins/modules/bigip_virtual_address.py | 902 + .../plugins/modules/bigip_virtual_server.py | 3822 ++++ .../f5_modules/plugins/modules/bigip_vlan.py | 983 + .../f5_modules/plugins/modules/bigip_wait.py | 536 + .../plugins/modules/bigiq_application_fasthttp.py | 762 + .../modules/bigiq_application_fastl4_tcp.py | 710 + .../modules/bigiq_application_fastl4_udp.py | 707 + .../plugins/modules/bigiq_application_http.py | 760 + .../modules/bigiq_application_https_offload.py | 1020 + .../plugins/modules/bigiq_application_https_waf.py | 1049 ++ .../plugins/modules/bigiq_device_discovery.py | 1256 ++ .../plugins/modules/bigiq_device_info.py | 2327 +++ .../plugins/modules/bigiq_regkey_license.py | 500 + .../modules/bigiq_regkey_license_assignment.py | 624 + .../plugins/modules/bigiq_regkey_pool.py | 412 + .../plugins/modules/bigiq_utility_license.py | 455 + .../modules/bigiq_utility_license_assignment.py | 639 + .../f5_modules/plugins/terminal/bigip.py | 62 + 204 files changed, 161484 insertions(+) create mode 100644 ansible_collections/f5networks/f5_modules/plugins/action/__init__.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/action/bigip.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/action/bigip_imish_config.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/action/bigiq.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5_rest_cli.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5ssh.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/filter/__init__.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/filter/abspath.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/filter/markdev.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/lookup/__init__.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/lookup/bigiq_license.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/lookup/license_hopper.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/__init__.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/bigip.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/bigiq.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/common.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/compare.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/constants.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/icontrol.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/ipaddress.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/teem.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/urls.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/module_utils/version.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/__init__.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_acl.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_network_access.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_policy_fetch.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_policy_import.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_advanced_settings.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_dos_application.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_fetch.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_import.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_manage.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_server_technology.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_signature_set.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cgnat_lsn_pool.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cli_alias.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cli_script.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_command.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_config.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_configsync_action.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_data_group.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_ldap.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_radius.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_radius_server.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_certificate.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_connectivity.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_dns.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_group.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_group_member.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_ha_group.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_httpd.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_info.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_license.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_ntp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_sshd.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_syslog.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_traffic_group.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_trust.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_cache_resolver.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_nameserver.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_resolver.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_zone.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_file_copy.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_address_list.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_dos_profile.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_dos_vector.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_global_rules.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_log_profile.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_log_profile_network.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_policy.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_port_list.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_rule.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_rule_list.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_schedule.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_datacenter.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_dns_listener.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_global.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_bigip.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_external.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_firepass.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_http.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_https.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_tcp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_tcp_half_open.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_pool.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_pool_member.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_server.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_topology_record.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_topology_region.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_virtual_server.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_wide_ip.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_hostname.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_iapp_service.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_iapp_template.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ike_peer.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_imish_config.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_interface.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ipsec_policy.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_irule.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_log_destination.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_log_publisher.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ltm_global.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_lx_package.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_management_route.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_peer.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_protocol.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_route.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_router.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_transport_config.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_dns.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_external.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_ftp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_gateway_icmp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_http.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_https.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_icmp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_ldap.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_mysql.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_oracle.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_smtp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_snmp_dca.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp_echo.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp_half_open.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_udp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_network_globals.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_node.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_partition.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_password_policy.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_policy.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_policy_rule.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_pool.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_pool_member.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_analytics.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_client_ssl.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_dns.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_fastl4.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_ftp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http2.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http_compression.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_oneconnect.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_cookie.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_src_addr.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_universal.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_server_ssl.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_sip.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_tcp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_udp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_provision.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_qkview.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_role.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_syslog.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_user.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_routedomain.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_selfip.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_service_policy.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_smtp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snat_pool.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snat_translation.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp_community.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp_trap.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_image.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_install.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_update.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_certificate.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_csr.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_key.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_key_cert.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_ocsp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_static_route.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_daemon_log_tmm.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_db.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_global.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_timer_policy.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_traffic_selector.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_trunk.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_tunnel.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ucs.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ucs_fetch.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_user.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_vcmp_guest.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_address.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_server.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_vlan.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigip_wait.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fasthttp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fastl4_tcp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fastl4_udp.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_http.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_https_offload.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_https_waf.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_device_discovery.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_device_info.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_license.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_license_assignment.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_pool.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_utility_license.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_utility_license_assignment.py create mode 100644 ansible_collections/f5networks/f5_modules/plugins/terminal/bigip.py (limited to 'ansible_collections/f5networks/f5_modules/plugins') diff --git a/ansible_collections/f5networks/f5_modules/plugins/action/__init__.py b/ansible_collections/f5networks/f5_modules/plugins/action/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ansible_collections/f5networks/f5_modules/plugins/action/bigip.py b/ansible_collections/f5networks/f5_modules/plugins/action/bigip.py new file mode 100644 index 00000000..2e79bcae --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/action/bigip.py @@ -0,0 +1,98 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import copy + +from ansible import constants as C +from ansible.module_utils._text import to_text +from ansible.module_utils.connection import Connection +from ansible.utils.display import Display + + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import load_provider +from ansible_collections.ansible.netcommon.plugins.action.network import ActionModule as ActionNetworkModule + + +from ansible_collections.f5networks.f5_modules.plugins.module_utils.common import f5_provider_spec + +display = Display() + + +class ActionModule(ActionNetworkModule): + + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + self._config_module = True if self._task.action == 'bigip_imish_config' else False + socket_path = None + transport = 'rest' + + if self._play_context.connection == 'network_cli': + provider = self._task.args.get('provider', {}) + if any(provider.values()): + display.warning("'provider' is unnecessary when using 'network_cli' and will be ignored") + elif self._play_context.connection == 'local': + provider = load_provider(f5_provider_spec, self._task.args) + transport = provider['transport'] or transport + + display.vvvv('connection transport is %s' % transport, self._play_context.remote_addr) + + if transport == 'cli': + pc = copy.deepcopy(self._play_context) + pc.connection = 'network_cli' + pc.network_os = 'bigip' + pc.remote_addr = provider['server'] or self._play_context.remote_addr + pc.port = int(provider['server_port'] or self._play_context.port or 22) + pc.remote_user = provider['user'] or self._play_context.connection_user + pc.password = provider['password'] or self._play_context.password + pc.private_key_file = provider.get('ssh_keyfile', None) or self._play_context.private_key_file + command_timeout = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT) + + display.vvv('using connection plugin %s' % pc.connection, pc.remote_addr) + connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin) + connection.set_options(direct={'persistent_command_timeout': command_timeout}) + + socket_path = connection.run() + display.vvvv('socket_path: %s' % socket_path, pc.remote_addr) + if not socket_path: + return { + 'failed': True, + 'msg': 'Unable to open shell. Please see: ' + 'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell' + } + + task_vars['ansible_socket'] = socket_path + + if (self._play_context.connection == 'local' and transport == 'cli') or self._play_context.connection == 'network_cli': + # make sure we are in the right cli context which should be + # enable mode and not config module + if socket_path is None: + socket_path = self._connection.socket_path + conn = Connection(socket_path) + out = conn.get_prompt() + while '(config' in to_text(out, errors='surrogate_then_replace').strip(): + display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr) + conn.send_command('exit') + out = conn.get_prompt() + + result = super(ActionModule, self).run(task_vars=task_vars) + return result diff --git a/ansible_collections/f5networks/f5_modules/plugins/action/bigip_imish_config.py b/ansible_collections/f5networks/f5_modules/plugins/action/bigip_imish_config.py new file mode 100644 index 00000000..54535021 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/action/bigip_imish_config.py @@ -0,0 +1,108 @@ +# +# (c) 2017, Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import re +import time +import glob + +from ansible.module_utils._text import to_text +from ansible.module_utils.six.moves.urllib.parse import urlsplit +from ansible_collections.ansible.netcommon.plugins.action.network import ActionModule as ActionNetworkModule +from ansible.utils.display import Display + +display = Display() + + +PRIVATE_KEYS_RE = re.compile('__.+__') + + +class ActionModule(ActionNetworkModule): + + def run(self, tmp=None, task_vars=None): + if self._task.args.get('src'): + try: + self._handle_template() + except ValueError as exc: + return dict(failed=True, msg=to_text(exc)) + + result = super(ActionModule, self).run(task_vars=task_vars) + del tmp # tmp no longer has any effect + + if self._task.args.get('backup') and result.get('__backup__'): + # User requested backup and no error occurred in module. + # NOTE: If there is a parameter error, _backup key may not be in results. + filepath = self._write_backup(task_vars['inventory_hostname'], result['__backup__']) + result['backup_path'] = filepath + + # strip out any keys that have two leading and two trailing + # underscore characters + for key in list(result.keys()): + if PRIVATE_KEYS_RE.match(key): + del result[key] + return result + + def _write_backup(self, host, contents): + backup_path = self._get_working_path() + '/backup' + if not os.path.exists(backup_path): + os.mkdir(backup_path) + for fn in glob.glob('%s/%s*' % (backup_path, host)): + os.remove(fn) + tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time())) + filename = '%s/%s_config.%s' % (backup_path, host, tstamp) + fh = open(filename, 'w') + fh.write(contents) + fh.close() + return filename + + def _handle_template(self): + src = self._task.args.get('src') + working_path = self._get_working_path() + + if os.path.isabs(src) or urlsplit('src').scheme: + source = src + else: + source = self._loader.path_dwim_relative(working_path, 'templates', src) + if not source: + source = self._loader.path_dwim_relative(working_path, src) + + if not os.path.exists(source): + raise ValueError('path specified in src not found') + + try: + with open(source, 'r') as f: + template_data = to_text(f.read()) + except IOError: + return dict(failed=True, msg='unable to load src file') + + # Create a template search path in the following order: + # [working_path, self_role_path, dependent_role_paths, dirname(source)] + searchpath = [working_path] + if self._task._role is not None: + searchpath.append(self._task._role._role_path) + if hasattr(self._task, "_block:"): + dep_chain = self._task._block.get_dep_chain() + if dep_chain is not None: + for role in dep_chain: + searchpath.append(role._role_path) + searchpath.append(os.path.dirname(source)) + self._templar.environment.loader.searchpath = searchpath + self._task.args['src'] = self._templar.template(template_data) diff --git a/ansible_collections/f5networks/f5_modules/plugins/action/bigiq.py b/ansible_collections/f5networks/f5_modules/plugins/action/bigiq.py new file mode 100644 index 00000000..23e9d8d0 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/action/bigiq.py @@ -0,0 +1,90 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import copy + +from ansible import constants as C +from ansible.module_utils._text import to_text +from ansible.module_utils.connection import Connection +from ansible.utils.display import Display + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import load_provider +from ansible_collections.ansible.netcommon.plugins.action.network import ActionModule as ActionNetworkModule + +from ansible_collections.f5networks.f5_modules.plugins.module_utils.common import f5_provider_spec + +display = Display() + + +class ActionModule(ActionNetworkModule): + + def run(self, tmp=None, task_vars=None): + socket_path = None + transport = 'rest' + + if self._play_context.connection == 'network_cli': + provider = self._task.args.get('provider', {}) + if any(provider.values()): + display.warning("'provider' is unnecessary when using 'network_cli' and will be ignored") + elif self._play_context.connection == 'local': + provider = load_provider(f5_provider_spec, self._task.args) + transport = provider['transport'] or transport + + display.vvvv('connection transport is %s' % transport, self._play_context.remote_addr) + + if transport == 'cli': + pc = copy.deepcopy(self._play_context) + pc.connection = 'network_cli' + pc.network_os = 'bigiq' + pc.remote_addr = provider.get('server', self._play_context.remote_addr) + pc.port = int(provider['server_port'] or self._play_context.port or 22) + pc.remote_user = provider.get('user', self._play_context.connection_user) + pc.password = provider.get('password', self._play_context.password) + pc.private_key_file = provider['ssh_keyfile'] or self._play_context.private_key_file + command_timeout = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT) + + display.vvv('using connection plugin %s' % pc.connection, pc.remote_addr) + connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin) + connection.set_options(direct={'persistent_command_timeout': command_timeout}) + socket_path = connection.run() + display.vvvv('socket_path: %s' % socket_path, pc.remote_addr) + if not socket_path: + return {'failed': True, + 'msg': 'Unable to open shell. Please see: ' + 'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell'} + + task_vars['ansible_socket'] = socket_path + + if (self._play_context.connection == 'local' and transport == 'cli') or self._play_context.connection == 'network_cli': + # make sure we are in the right cli context which should be + # enable mode and not config module + if socket_path is None: + socket_path = self._connection.socket_path + conn = Connection(socket_path) + out = conn.get_prompt() + while '(config' in to_text(out, errors='surrogate_then_replace').strip(): + display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr) + conn.send_command('exit') + out = conn.get_prompt() + + result = super(ActionModule, self).run(task_vars=task_vars) + return result diff --git a/ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5.py b/ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5.py new file mode 100644 index 00000000..2fdd53d4 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + # Standard F5 documentation fragment + DOCUMENTATION = r''' +options: + provider: + description: + - A dict object containing connection details. + type: dict + version_added: "1.0.0" + suboptions: + password: + description: + - The password for the user account used to connect to the BIG-IP. + - You may omit this option by setting the environment variable C(F5_PASSWORD). + type: str + required: true + aliases: [ pass, pwd ] + server: + description: + - The BIG-IP host. + - You may omit this option by setting the environment variable C(F5_SERVER). + type: str + required: true + server_port: + description: + - The BIG-IP server port. + - You may omit this option by setting the environment variable C(F5_SERVER_PORT). + type: int + default: 443 + user: + description: + - The username to connect to the BIG-IP with. This user must have + administrative privileges on the device. + - You may omit this option by setting the environment variable C(F5_USER). + type: str + required: true + validate_certs: + description: + - If C(no), SSL certificates are not validated. Use this only + on personally controlled sites using self-signed certificates. + - You may omit this option by setting the environment variable C(F5_VALIDATE_CERTS). + type: bool + default: yes + timeout: + description: + - Specifies the timeout in seconds for communicating with the network device + for either connecting or sending commands. If the timeout is + exceeded before the operation is completed, the module will error. + type: int + transport: + description: + - Configures the transport connection to use when connecting to the + remote device. + type: str + choices: [ rest ] + default: rest + no_f5_teem: + description: + - If C(yes), TEEM telemetry data is not sent to F5. + - You may omit this option by setting the environment variable C(F5_TELEMETRY_OFF). + - Previously used variable C(F5_TEEM) is deprecated as its name was confusing. + default: no + type: bool + auth_provider: + description: + - Configures the auth provider for to obtain authentication tokens from the remote device. + - This option is really used when working with BIG-IQ devices. + type: str +notes: + - For more information on using Ansible to manage F5 Networks devices see U(https://www.ansible.com/integrations/networks/f5). + - Requires BIG-IP software version >= 12. + - The F5 modules only manipulate the running configuration of the F5 product. To ensure that BIG-IP + specific configuration persists to disk, be sure to include at least one task that uses the + M(f5networks.f5_modules.bigip_config) module to save the running configuration. Refer to the module's documentation for + the correct usage of the module to save your running configuration. +''' diff --git a/ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5_rest_cli.py b/ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5_rest_cli.py new file mode 100644 index 00000000..16f7b4f1 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5_rest_cli.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + # Standard F5 documentation fragment + DOCUMENTATION = r''' +options: + provider: + description: + - A dict object containing connection details. + type: dict + version_added: "1.0.0" + suboptions: + password: + description: + - The password for the user account used to connect to the BIG-IP. + - You may omit this option by setting the environment variable C(F5_PASSWORD). + type: str + required: true + aliases: [ pass, pwd ] + server: + description: + - The BIG-IP host. + - You may omit this option by setting the environment variable C(F5_SERVER). + type: str + required: true + server_port: + description: + - The BIG-IP server port. + - You may omit this option by setting the environment variable C(F5_SERVER_PORT). + type: int + default: 443 + user: + description: + - The username to connect to the BIG-IP with. This user must have + administrative privileges on the device. + - You may omit this option by setting the environment variable C(F5_USER). + type: str + required: true + validate_certs: + description: + - If C(no), SSL certificates are not validated. Use this only + on personally controlled sites using self-signed certificates. + - You may omit this option by setting the environment variable C(F5_VALIDATE_CERTS). + type: bool + default: yes + timeout: + description: + - Specifies the timeout in seconds for communicating with the network device + for either connecting or sending commands. If the timeout is + exceeded before the operation is completed, the module will error. + type: int + ssh_keyfile: + description: + - Specifies the SSH keyfile to use to authenticate the connection to + the remote device. This argument is only used for I(cli) transports. + - You may omit this option by setting the environment variable C(ANSIBLE_NET_SSH_KEYFILE). + type: path + transport: + description: + - Configures the transport connection to use when connecting to the + remote device. + type: str + choices: [ cli, rest ] + default: rest + no_f5_teem: + description: + - If C(yes), TEEM telemetry data is not sent to F5. + - You may omit this option by setting the environment variable C(F5_TELEMETRY_OFF). + - Previously used variable C(F5_TEEM) is deprecated as its name was confusing. + default: no + type: bool + auth_provider: + description: + - Configures the auth provider for to obtain authentication tokens from the remote device. + - This option is really used when working with BIG-IQ devices. + type: str +notes: + - For more information on using Ansible to manage F5 Networks devices see U(https://www.ansible.com/integrations/networks/f5). + - Requires BIG-IP software version >= 12. + - The F5 modules only manipulate the running configuration of the F5 product. To ensure that BIG-IP + specific configuration persists to disk, be sure to include at least one task that uses the + M(f5networks.f5_modules.bigip_config) module to save the running configuration. Refer to the module's documentation for + the correct usage of the module to save your running configuration. +''' diff --git a/ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5ssh.py b/ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5ssh.py new file mode 100644 index 00000000..9aa4f6ec --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/doc_fragments/f5ssh.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + # Standard F5 documentation fragment + DOCUMENTATION = r''' +options: + provider: + description: + - A dict object containing connection details. + type: dict + version_added: "1.0.0" + suboptions: + password: + description: + - The password for the user account used to connect to the BIG-IP. + - You may omit this option by setting the environment variable C(F5_PASSWORD). + type: str + required: true + aliases: [ pass, pwd ] + server: + description: + - The BIG-IP host. + - You may omit this option by setting the environment variable C(F5_SERVER). + type: str + required: true + server_port: + description: + - The BIG-IP server port. + - You may omit this option by setting the environment variable C(F5_SERVER_PORT). + type: int + default: 22 + user: + description: + - The username to connect to the BIG-IP with. This user must have + administrative privileges on the device. + - You may omit this option by setting the environment variable C(F5_USER). + type: str + required: true + validate_certs: + description: + - If C(no), SSL certificates are not validated. Use this only + on personally controlled sites using self-signed certificates. + - You may omit this option by setting the environment variable C(F5_VALIDATE_CERTS). + type: bool + default: yes + timeout: + description: + - Specifies the timeout in seconds for communicating with the network device + for either connecting or sending commands. If the timeout is + exceeded before the operation is completed, the module will error. + type: int + ssh_keyfile: + description: + - Specifies the SSH keyfile to use to authenticate the connection to + the remote device. This argument is only used for I(cli) transports. + - You may omit this option by setting the environment variable C(ANSIBLE_NET_SSH_KEYFILE). + type: path + transport: + description: + - Configures the transport connection to use when connecting to the + remote device. + type: str + choices: ['cli'] + default: cli + no_f5_teem: + description: + - If C(yes), TEEM telemetry data is not sent to F5. + - You may omit this option by setting the environment variable C(F5_TELEMETRY_OFF). + - Previously used variable C(F5_TEEM) is deprecated as its name was confusing. + default: no + type: bool + auth_provider: + description: + - Configures the auth provider for to obtain authentication tokens from the remote device. + - This option is really used when working with BIG-IQ devices. + type: str +notes: + - For more information on using Ansible to manage F5 Networks devices see U(https://www.ansible.com/integrations/networks/f5). + - Requires BIG-IP software version >= 12. + - The F5 modules only manipulate the running configuration of the F5 product. To ensure that BIG-IP + specific configuration persists to disk, be sure to include at least one task that uses the + M(f5networks.f5_modules.bigip_config) module to save the running configuration. Refer to the module's documentation for + the correct usage of the module to save your running configuration. +''' diff --git a/ansible_collections/f5networks/f5_modules/plugins/filter/__init__.py b/ansible_collections/f5networks/f5_modules/plugins/filter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ansible_collections/f5networks/f5_modules/plugins/filter/abspath.py b/ansible_collections/f5networks/f5_modules/plugins/filter/abspath.py new file mode 100644 index 00000000..49db0cfc --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/filter/abspath.py @@ -0,0 +1,15 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os + + +def abspath(file): + return os.path.abspath(file) + + +class FilterModule(object): + def filters(self): + return { + 'abspath': abspath + } diff --git a/ansible_collections/f5networks/f5_modules/plugins/filter/markdev.py b/ansible_collections/f5networks/f5_modules/plugins/filter/markdev.py new file mode 100644 index 00000000..e2e90275 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/filter/markdev.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class FilterModule(object): + def filters(self): + return { + 'verchg': self.mark_devel + } + + def mark_devel(self, var): + result = var.split('-')[0] + '-devel' + return result diff --git a/ansible_collections/f5networks/f5_modules/plugins/lookup/__init__.py b/ansible_collections/f5networks/f5_modules/plugins/lookup/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ansible_collections/f5networks/f5_modules/plugins/lookup/bigiq_license.py b/ansible_collections/f5networks/f5_modules/plugins/lookup/bigiq_license.py new file mode 100644 index 00000000..74e5b07e --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/lookup/bigiq_license.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2020, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + lookup: bigiq_license + author: Wojciech Wypior + version_added: "1.0" + short_description: Select a random license key from a pool of biqiq available licenses + description: + - Select a random license key from a pool of biqiq available licenses + ,Requires specifying BIGIQ license pool name and connection parameters +""" + +EXAMPLES = """ +- name: Get a regkey license from a license pool + bigiq_regkey_license: + key: "{{ lookup('f5networks.f5_modules.bigiq_license', pool_name='foo_pool', username=baz, password=bar, host=192.168.1.1, port=10443}}" + state: present + pool: foo_pool + +- name: Get a regkey license from a license pool, use default credentials and port, disable SSL verification + bigiq_regkey_license: + key: "{{ lookup('f5networks.f5_modules.bigiq_license', pool_name='foo_pool', host=192.168.1.1, validate_certs=false}}" + state: present + pool: foo_pool +""" + +RETURN = """ + _raw: + description: + - random item +""" + +import random + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible_collections.f5networks.f5_modules.plugins.module_utils.bigiq import F5RestClient + + +class LookupModule(LookupBase): + def __init__(self, loader=None, templar=None, **kwargs): + super(LookupModule, self).__init__(loader, templar, **kwargs) + self.username = None + self.password = None + self.validate_certs = False + self.host = None + self.pool_name = None + self.port = 443 + self.client = None + self.params = None + + def _validate_and_merge_params(self, **kwargs): + self.username = kwargs.pop('username', 'admin') + self.password = kwargs.pop('password', 'admin') + self.validate_certs = kwargs.pop('validate_certs', False) + self.host = kwargs.pop('host', None) + self.port = kwargs.pop('port', 443) + self.pool_name = kwargs.pop('pool_name', None) + + if self.host is None: + raise AnsibleError('A valid hostname or IP for BIGIQ needs to be provided') + if self.pool_name is None: + raise AnsibleError('License pool name needs to be specified') + self.params = dict( + provider=dict( + server=self.host, + server_port=self.port, + validate_certs=self.validate_certs, + user=self.username, + password=self.password + ) + ) + + def _get_pool_uuid(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses".format(self.host, self.port) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise AnsibleError(str(ex)) + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise AnsibleError(response['message']) + else: + raise AnsibleError(resp.content) + if 'items' not in response: + raise AnsibleError('No license pools configured on BIGIQ') + + resource = next((x for x in response['items'] if x['name'] == self.pool_name), None) + if resource is None: + raise AnsibleError("Could not find the specified license pool.") + return resource['id'] + + def _get_registation_keys(self, pool_id): + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/'.format( + self.host, + self.port, + pool_id, + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise AnsibleError(str(ex)) + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise AnsibleError(response['message']) + else: + raise AnsibleError(resp.content) + regkeys = [x['regKey'] for x in response['items']] + + if not regkeys: + raise AnsibleError('Failed to obtain registration keys') + + return regkeys + + def run(self, terms, variables=None, **kwargs): + self._validate_and_merge_params(**kwargs) + self.client = F5RestClient(**self.params) + pool_id = self._get_pool_uuid() + regkeys = self._get_registation_keys(pool_id) + keys = [] + regkeypool = [] + for key in regkeys: + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}/members'.format( + self.host, + self.port, + pool_id, + key + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise AnsibleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise AnsibleError(response['message']) + else: + raise AnsibleError(resp.content) + + if not response['items']: + keys.append(key) + + result = random.choice(keys) + regkeypool.append(result) + return regkeypool diff --git a/ansible_collections/f5networks/f5_modules/plugins/lookup/license_hopper.py b/ansible_collections/f5networks/f5_modules/plugins/lookup/license_hopper.py new file mode 100644 index 00000000..e0899239 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/lookup/license_hopper.py @@ -0,0 +1,73 @@ +# (c) 2013, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + lookup: Select a random license key from a file and remove it from future lookups + author: Tim Rupp + version_added: "1.0" + short_description: Return random license from list + description: + - Select a random license key from a file and remove it from future lookups + - Can optionally remove the key if C(remove=True) is specified +""" + +EXAMPLES = """ +- name: Get a regkey license from a stash without deleting it + bigiq_regkey_license: + key: "{{ lookup('license_hopper', 'filename=/path/to/licenses.txt') }}" + state: present + pool: regkey1 + +- name: Get a regkey license from a stash and delete the key from the file + bigiq_regkey_license: + key: "{{ lookup('license_hopper', 'filename=/path/to/licenses.txt', remove=True) }}" + state: present + pool: regkey1 +""" + +RETURN = """ + _raw: + description: + - random item +""" + +import random + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native +from ansible.plugins.lookup import LookupBase + +BOOLEANS_TRUE = frozenset(('y', 'yes', 'on', '1', 'true', 'True', 't', 1, 1.0, True)) + + +class LookupModule(LookupBase): + def __init__(self, loader=None, templar=None, **kwargs): + + super(LookupModule, self).__init__(loader, templar, **kwargs) + + self.filename = None + self.remove = False + + def run(self, terms, variables=None, **kwargs): + self.filename = kwargs.pop('filename', None) + self.remove = kwargs.pop('remove', False) + if self.filename is None: + raise AnsibleError("No 'filename' was specified") + lookupfile = self.find_file_in_search_path(variables, 'files', self.filename) + if lookupfile is None: + raise AnsibleError("Could not find the specified 'filename'") + fh = open(lookupfile, 'r') + lines = [x.strip() for x in fh.readlines()] + fh.close() + try: + ret = [random.choice(lines)] + except Exception as e: + raise AnsibleError("Unable to choose random license: %s" % to_native(e)) + if self.remove: + to_write = [x + "\n" for x in lines if x != ret[0]] + fh = open(lookupfile, 'w') + fh.writelines(to_write) + return ret diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/__init__.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/bigip.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/bigip.py new file mode 100644 index 00000000..5a70e0c5 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/bigip.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import time + +from .common import ( + F5BaseClient, F5ModuleError +) +from .constants import ( + LOGIN, BASE_HEADERS +) +from .icontrol import iControlRestSession + + +class F5RestClient(F5BaseClient): + def __init__(self, *args, **kwargs): + super(F5RestClient, self).__init__(*args, **kwargs) + self.provider = self.merge_provider_params() + self.headers = BASE_HEADERS + self.retries = 0 + + @property + def api(self): + if self._client: + return self._client + session, err = self.connect_via_token_auth() + if err or session is None: + session, err = self.connect_via_basic_auth() + if err or session is None: + raise F5ModuleError(err) + self._client = session + return session + + def connect_via_token_auth(self): + url = "https://{0}:{1}{2}".format( + self.provider['server'], self.provider['server_port'], LOGIN + ) + payload = { + 'username': self.provider['user'], + 'password': self.provider['password'], + 'loginProviderName': self.provider['auth_provider'] or 'tmos' + } + session = iControlRestSession( + validate_certs=self.provider['validate_certs'] + ) + + response = session.post( + url, + json=payload, + headers=self.headers + ) + + if response.status not in [200]: + if b'Configuration Utility restarting...' in response.content and self.retries < 3: + time.sleep(30) + self.retries += 1 + return self.connect_via_token_auth() + else: + self.retries = 0 + return None, response.content + + self.retries = 0 + session.request.headers['X-F5-Auth-Token'] = response.json()['token']['token'] + if 'timeout' in self.provider and self.provider['timeout'] is not None: + token_value = response.json()['token']['token'] + self.modify_token_timeout(session, token_value, self.provider['timeout']) + return session, None + + def connect_via_basic_auth(self): + url = "https://{0}:{1}/mgmt/tm/sys".format( + self.provider['server'], self.provider['server_port'] + ) + session = iControlRestSession( + url_username=self.provider['user'], + url_password=self.provider['password'], + validate_certs=self.provider['validate_certs'], + ) + + response = session.get( + url, + headers=self.headers + ) + + if response.status not in [200]: + if b'Configuration Utility restarting...' in response.content and self.retries < 3: + time.sleep(30) + self.retries += 1 + return self.connect_via_basic_auth() + else: + self.retries = 0 + return None, response.content + self.retries = 0 + return session, None + + def modify_token_timeout(self, client_session, token_value, token_timeout): + url = "https://{0}:{1}/mgmt/shared/authz/tokens/{2}".format( + self.provider['server'], self.provider['server_port'], token_value + ) + payload = { + 'timeout': token_timeout + } + response = client_session.patch( + url, + json=payload + ) + if response.status not in [200]: + raise F5ModuleError(response.content) + return None diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/bigiq.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/bigiq.py new file mode 100644 index 00000000..ab0a37c5 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/bigiq.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import os + +from .common import ( + F5BaseClient, F5ModuleError +) +from .constants import ( + LOGIN, BASE_HEADERS +) +from .icontrol import iControlRestSession + + +class F5RestClient(F5BaseClient): + def __init__(self, *args, **kwargs): + super(F5RestClient, self).__init__(*args, **kwargs) + self.provider = self.merge_provider_params() + self.headers = BASE_HEADERS + self.access_token = None + self.refresh_token = None + + @property + def api(self): + if self._client: + return self._client + session, err = self.connect_via_token_auth() + if err: + raise F5ModuleError(err) + self._client = session + return session + + def connect_via_token_auth(self): + provider = self.provider['auth_provider'] or 'local' + + url = "https://{0}:{1}{2}".format( + self.provider['server'], self.provider['server_port'], LOGIN + ) + payload = { + 'username': self.provider['user'], + 'password': self.provider['password'], + } + + # - local is a special provider that is baked into the system and + # has no loginReference + if provider != 'local': + login_ref = self.get_login_ref(provider) + payload.update(login_ref) + + session = iControlRestSession( + validate_certs=self.provider['validate_certs'] + ) + + response = session.post( + url, + json=payload, + headers=self.headers + ) + + if response.status not in [200]: + return None, response.content + self.access_token = response.json()['token']['token'] + self.refresh_token = response.json()['refreshToken']['token'] + session.request.headers['X-F5-Auth-Token'] = self.access_token + return session, None + + def get_login_ref(self, provider): + info = self.read_provider_info_from_device() + uuids = [os.path.basename(os.path.dirname(x['link'])) for x in info['providers'] if '-' in x['link']] + if provider in uuids: + link = self._get_login_ref_by_id(info, provider) + if not link: + raise F5ModuleError( + "Provider with the UUID {0} was not found.".format(provider) + ) + return dict( + loginReference=dict( + link=link + ) + ) + names = [os.path.basename(os.path.dirname(x['link'])) for x in info['providers'] if '-' in x['link']] + if names.count(provider) > 1: + raise F5ModuleError( + "Ambiguous auth_provider name provided. Please specify a specific provider name or UUID." + ) + link = self._get_login_ref_by_name(info, provider) + if not link: + raise F5ModuleError( + "Provider with the name '{0}' was not found.".format(provider) + ) + return dict( + loginReference=dict( + link=link + ) + ) + + @staticmethod + def _get_login_ref_by_id(info, provider): + provider = '/' + provider + '/' + for x in info['providers']: + if x['link'].find(provider) > -1: + return x['link'] + + @staticmethod + def _get_login_ref_by_name(info, provider): + for x in info['providers']: + if x['name'] == provider: + return x['link'] + return None + + def read_provider_info_from_device(self): + uri = "https://{0}:{1}/info/system".format( + self.provider['server'], self.provider['server_port'] + ) + session = iControlRestSession() + session.verify = self.provider['validate_certs'] + + resp = session.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) + return response + + def reconnect(self): + url = "https://{0}:{1}/mgmt/shared/authn/exchange".format( + self.provider['server'], self.provider['server_port'] + ) + payload = { + 'refreshToken': { + 'token': self.refresh_token + } + } + + session = iControlRestSession( + validate_certs=self.provider['validate_certs'] + ) + + response = session.post( + url, + json=payload, + headers=BASE_HEADERS + ) + + if response.status not in [200]: + raise F5ModuleError('Failed to refresh token, server returned: {0}'.format(response.content)) + self.access_token = response.json()['token']['token'] + self.refresh_token = response.json()['refreshToken']['token'] + session.request.headers['X-F5-Auth-Token'] = self.access_token + self._client = session diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/common.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/common.py new file mode 100644 index 00000000..365a2f92 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/common.py @@ -0,0 +1,672 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import copy +import os +import re +import datetime + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.connection import exec_command +from ansible.module_utils.six import iteritems +from ansible.module_utils.parsing.convert_bool import ( + BOOLEANS_TRUE, BOOLEANS_FALSE +) +from collections import defaultdict + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + NetworkConfig, ConfigLine, ignore_line +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, ComplexList +) +from .constants import ( + MANAGED_BY_ANNOTATION_MODIFIED, MANAGED_BY_ANNOTATION_VERSION +) + +f5_provider_spec = { + 'server': dict( + required=True, + fallback=(env_fallback, ['F5_SERVER']) + ), + 'server_port': dict( + type='int', + default=443, + fallback=(env_fallback, ['F5_SERVER_PORT']) + ), + 'user': dict( + required=True, + fallback=(env_fallback, ['F5_USER', 'ANSIBLE_NET_USERNAME']) + ), + 'password': dict( + required=True, + no_log=True, + aliases=['pass', 'pwd'], + fallback=(env_fallback, ['F5_PASSWORD', 'ANSIBLE_NET_PASSWORD']), + ), + 'validate_certs': dict( + type='bool', + default='yes', + fallback=(env_fallback, ['F5_VALIDATE_CERTS']) + ), + 'transport': dict( + choices=['rest'], + default='rest' + ), + 'timeout': dict(type='int'), + 'no_f5_teem': dict( + type='bool', + default='no', + fallback=(env_fallback, ['F5_TEEM', 'F5_TELEMETRY_OFF']) + ), + 'auth_provider': dict(), +} + +f5_argument_spec = { + 'provider': dict(type='dict', options=f5_provider_spec), +} + + +def get_provider_argspec(): + return f5_provider_spec + + +def load_params(params): + provider = params.get('provider') or dict() + for key, value in iteritems(provider): + if key in f5_argument_spec: + if params.get(key) is None and value is not None: + params[key] = value + + +def is_empty_list(seq): + if len(seq) == 1: + if seq[0] == '' or seq[0] == 'none': + return True + return False + + +def fq_name(partition, value, sub_path=''): + """Returns a 'Fully Qualified' name + + A BIG-IP expects most names of resources to be in a fully-qualified + form. This means that both the simple name, and the partition need + to be combined. + + The Ansible modules, however, can accept (as names for several + resources) their name in the FQ format. This becomes an issue when + the FQ name and the partition are both specified as separate values. + + Consider the following examples. + + # Name not FQ + name: foo + partition: Common + + # Name FQ + name: /Common/foo + partition: Common + + This method will rectify the above situation and will, in both cases, + return the following for name. + + /Common/foo + + Args: + partition (string): The partition that you would want attached to + the name if the name has no partition. + value (string): The name that you want to attach a partition to. + This value will be returned unchanged if it has a partition + attached to it already. + sub_path (string): The sub path element. If defined the sub_path + will be inserted between partition and value. + This will also work on FQ names. + Returns: + string: The fully qualified name, given the input parameters. + """ + if value is not None and sub_path == '': + try: + int(value) + return '/{0}/{1}'.format(partition, value) + except (ValueError, TypeError): + if not value.startswith('/'): + return '/{0}/{1}'.format(partition, value) + if value is not None and sub_path != '': + try: + int(value) + return '/{0}/{1}/{2}'.format(partition, sub_path, value) + except (ValueError, TypeError): + if value.startswith('/'): + dummy, partition, name = value.split('/') + return '/{0}/{1}/{2}'.format(partition, sub_path, name) + if not value.startswith('/'): + return '/{0}/{1}/{2}'.format(partition, sub_path, value) + return value + + +# Fully Qualified name (with partition) for a list +def fq_list_names(partition, list_names): + if list_names is None: + return None + return map(lambda x: fq_name(partition, x), list_names) + + +def to_commands(module, commands): + spec = { + 'command': dict(key=True), + 'prompt': dict(), + 'answer': dict() + } + transform = ComplexList(spec, module) + return transform(commands) + + +def run_commands(module, commands, check_rc=True): + responses = list() + commands = to_commands(module, to_list(commands)) + for cmd in commands: + cmd = module.jsonify(cmd) + rc, out, err = exec_command(module, cmd) + if check_rc and rc != 0: + raise F5ModuleError(to_text(err, errors='surrogate_then_replace')) + result = to_text(out, errors='surrogate_then_replace') + responses.append(result) + return responses + + +def flatten_boolean(value): + truthy = list(BOOLEANS_TRUE) + ['enabled', 'True', 'true'] + falsey = list(BOOLEANS_FALSE) + ['disabled', 'False', 'false'] + if value is None: + return None + elif value in truthy: + return 'yes' + elif value in falsey: + return 'no' + + +def is_cli(module): + transport = module.params.get('transport', None) + provider_transport = (module.params['provider'] or {}).get('transport') + result = 'cli' in (transport, provider_transport) + return result + + +def is_valid_hostname(host): + """Reasonable attempt at validating a hostname + + Compiled from various paragraphs outlined here + https://tools.ietf.org/html/rfc3696#section-2 + https://tools.ietf.org/html/rfc1123 + + Notably, + * Host software MUST handle host names of up to 63 characters and + SHOULD handle host names of up to 255 characters. + * The "LDH rule", after the characters that it permits. (letters, digits, hyphen) + * If the hyphen is used, it is not permitted to appear at + either the beginning or end of a label + + :param host: + :return: + """ + if len(host) > 255: + return False + host = host.rstrip(".") + allowed = re.compile(r'(?!-)[A-Z0-9-]{1,63}(? 255: + return False + host = host.rstrip(".") + allowed = re.compile(r'(?!-)[A-Z0-9-*]{1,63}(? 1: + return True + return False + + +def transform_name(partition='', name='', sub_path=''): + if partition != '': + if name.startswith(partition + '/'): + name = name.replace(partition + '/', '') + if name.startswith('/' + partition + '/'): + name = name.replace('/' + partition + '/', '') + + if name: + name = name.replace('/', '~') + name = name.replace('%', '%25') + + if partition: + partition = partition.replace('/', '~') + if not partition.startswith('~'): + partition = '~' + partition + else: + if sub_path: + raise F5ModuleError( + 'When giving the subPath component include partition as well.' + ) + + if sub_path and partition: + sub_path = '~' + sub_path + + if name and partition: + name = '~' + name + + result = partition + sub_path + name + return result + + +def is_ansible_debug(module): + if module._debug and module._verbosity >= 4: + return True + return False + + +def is_uuid(uuid=None): + """Check to see if value is an F5 UUID + + UUIDs are used in BIG-IQ and in select areas of BIG-IP (notably ASM). This method + will check to see if the provided value matches a UUID as known by these products. + + Args: + uuid (string): The value to check for UUID-ness + + Returns: + bool: + """ + if uuid is None: + return False + pattern = r'[A-Za-z0-9]{8}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{12}' + if re.match(pattern, uuid): + return True + return False + + +def on_bigip(): + if os.path.exists('/usr/bin/tmsh'): + return True + return False + + +def mark_managed_by(ansible_version, params): + metadata = [] + result = copy.deepcopy(params) + found1 = False + found2 = False + mark1 = dict( + name=MANAGED_BY_ANNOTATION_VERSION, + value=ansible_version, + persist='true' + ) + mark2 = dict( + name=MANAGED_BY_ANNOTATION_MODIFIED, + value=str(datetime.datetime.utcnow()), + persist='true' + ) + + if 'metadata' not in result: + result['metadata'] = [mark1, mark2] + return result + + for x in params['metadata']: + if x['name'] == MANAGED_BY_ANNOTATION_VERSION: + found1 = True + metadata.append(mark1) + if x['name'] == MANAGED_BY_ANNOTATION_MODIFIED: + found2 = True + metadata.append(mark1) + else: + metadata.append(x) + if not found1: + metadata.append(mark1) + if not found2: + metadata.append(mark2) + + result['metadata'] = metadata + return result + + +def only_has_managed_metadata(metadata): + managed = [ + MANAGED_BY_ANNOTATION_MODIFIED, + MANAGED_BY_ANNOTATION_VERSION, + ] + + for x in metadata: + if x['name'] not in managed: + return False + return True + + +def merge_two_dicts(x, y): + """ Merge any two dicts passed to the function + This does not do a deep copy, just a shallow + copy. However, it does create a new object, + so there's that. + """ + z = x.copy() + z.update(y) + return z + + +class Noop(object): + """Represent no-operation required + + This class is used in the Difference engine to specify when an attribute + has not changed. Difference attributes may return an instance of this + class as a means to indicate when the attribute has not changed. + + The Noop object allows attributes to be set to None when sending updates + to the API. `None` is technically a valid value in some cases (it indicates + that the attribute should be removed from the resource). + """ + pass + + +class F5BaseClient(object): + def __init__(self, *args, **kwargs): + self.params = kwargs + self.module = kwargs.get('module', None) + load_params(self.params) + self._client = None + + @property + def api(self): + raise F5ModuleError("Management root must be used from the concrete product classes.") + + def reconnect(self): + """Attempts to reconnect to a device + + The existing token from a ManagementRoot can become invalid if you, + for example, upgrade the device (such as is done in the *_software + module. + + This method can be used to reconnect to a remote device without + having to re-instantiate the ArgumentSpec and AnsibleF5Client classes + it will use the same values that were initially provided to those + classes + + :return: + :raises iControlUnexpectedHTTPError + """ + self._client = None + + @staticmethod + def validate_params(key, store): + if key in store and store[key] is not None: + return True + else: + return False + + def merge_provider_params(self): + result = dict() + provider = self.params.get('provider', None) + if not provider: + provider = {} + + self.merge_provider_server_param(result, provider) + self.merge_provider_server_port_param(result, provider) + self.merge_provider_validate_certs_param(result, provider) + self.merge_provider_auth_provider_param(result, provider) + self.merge_provider_user_param(result, provider) + self.merge_provider_timeout_param(result, provider) + self.merge_provider_password_param(result, provider) + self.merge_provider_no_f5_teem_param(result, provider) + return result + + def merge_provider_server_param(self, result, provider): + if self.validate_params('server', provider): + result['server'] = provider['server'] + elif self.validate_params('F5_SERVER', os.environ): + result['server'] = os.environ['F5_SERVER'] + else: + raise F5ModuleError('Server parameter cannot be None or missing, please provide a valid value') + + def merge_provider_server_port_param(self, result, provider): + if self.validate_params('server_port', provider): + result['server_port'] = provider['server_port'] + elif self.validate_params('F5_SERVER_PORT', os.environ): + result['server_port'] = os.environ['F5_SERVER_PORT'] + else: + result['server_port'] = 443 + + def merge_provider_validate_certs_param(self, result, provider): + if self.validate_params('validate_certs', provider): + result['validate_certs'] = provider['validate_certs'] + elif self.validate_params('F5_VALIDATE_CERTS', os.environ): + result['validate_certs'] = os.environ['F5_VALIDATE_CERTS'] + else: + result['validate_certs'] = True + if result['validate_certs'] in BOOLEANS_TRUE: + result['validate_certs'] = True + else: + result['validate_certs'] = False + + def merge_provider_auth_provider_param(self, result, provider): + if self.validate_params('auth_provider', provider): + result['auth_provider'] = provider['auth_provider'] + elif self.validate_params('F5_AUTH_PROVIDER', os.environ): + result['auth_provider'] = os.environ['F5_AUTH_PROVIDER'] + else: + result['auth_provider'] = None + + # Handle a specific case of the user specifying ``|default(omit)`` + # as the value to the auth_provider. + # + # In this case, Ansible will inject the omit-placeholder value + # and the module params incorrectly interpret this. This case + # can occur when specifying ``|default(omit)`` for a variable + # value defined in the ``environment`` section of a Play. + # + # An example of the omit placeholder is shown below. + # + # __omit_place_holder__11bd71a2840bff144594b9cc2149db814256f253 + # + if result['auth_provider'] is not None and '__omit_place_holder__' in result['auth_provider']: + result['auth_provider'] = None + + def merge_provider_user_param(self, result, provider): + if self.validate_params('user', provider): + result['user'] = provider['user'] + elif self.validate_params('F5_USER', os.environ): + result['user'] = os.environ.get('F5_USER') + elif self.validate_params('ANSIBLE_NET_USERNAME', os.environ): + result['user'] = os.environ.get('ANSIBLE_NET_USERNAME') + else: + result['user'] = None + + def merge_provider_timeout_param(self, result, provider): + if self.validate_params('timeout', provider): + result['timeout'] = provider['timeout'] + elif self.validate_params('F5_TIMEOUT', os.environ): + result['timeout'] = os.environ.get('F5_TIMEOUT') + else: + result['timeout'] = None + + def merge_provider_password_param(self, result, provider): + if self.validate_params('password', provider): + result['password'] = provider['password'] + elif self.validate_params('F5_PASSWORD', os.environ): + result['password'] = os.environ.get('F5_PASSWORD') + elif self.validate_params('ANSIBLE_NET_PASSWORD', os.environ): + result['password'] = os.environ.get('ANSIBLE_NET_PASSWORD') + else: + result['password'] = None + + def merge_provider_no_f5_teem_param(self, result, provider): + if self.validate_params('no_f5_teem', provider): + result['no_f5_teem'] = provider['no_f5_teem'] + elif self.validate_params('F5_TEEM', os.environ): + result['no_f5_teem'] = os.environ['F5_TEEM'] + elif self.validate_params('F5_TELEMETRY_OFF', os.environ): + result['no_f5_teem'] = os.environ['F5_TELEMETRY_OFF'] + else: + result['no_f5_teem'] = False + + if result['no_f5_teem'] in BOOLEANS_TRUE: + result['no_f5_teem'] = True + else: + result['no_f5_teem'] = False + + +class AnsibleF5Parameters(object): + def __init__(self, *args, **kwargs): + self._values = defaultdict(lambda: None) + self._values['__warnings'] = [] + self.client = kwargs.pop('client', None) + self._module = kwargs.pop('module', None) + self._params = {} + + params = kwargs.pop('params', None) + if params: + self.update(params=params) + self._params.update(params) + + def update(self, params=None): + if params: + self._params.update(params) + for k, v in iteritems(params): + # Adding this here because ``username`` is a connection parameter + # and in cases where it is also an API parameter, we run the risk + # of overriding the specified parameter with the connection parameter. + # + # Since this is a problem, and since "username" is never a valid + # parameter outside its usage in connection params (where we do not + # use the ApiParameter or ModuleParameters classes) it is safe to + # skip over it if it is provided. + if k == 'password': + continue + if self.api_map is not None and k in self.api_map: + map_key = self.api_map[k] + else: + map_key = k + + # Handle weird API parameters like `dns.proxy.__iter__` by + # using a map provided by the module developer + class_attr = getattr(type(self), map_key, None) + if isinstance(class_attr, property): + # There is a mapped value for the api_map key + if class_attr.fset is None: + # If the mapped value does not have + # an associated setter + self._values[map_key] = v + else: + # The mapped value has a setter + setattr(self, map_key, v) + else: + # If the mapped value is not a @property + self._values[map_key] = v + + def api_params(self): + result = {} + for api_attribute in self.api_attributes: + if self.api_map is not None and api_attribute in self.api_map: + result[api_attribute] = getattr(self, self.api_map[api_attribute]) + else: + result[api_attribute] = getattr(self, api_attribute) + result = self._filter_params(result) + return result + + def __getattr__(self, item): + # Ensures that properties that weren't defined, and therefore stashed + # in the `_values` dict, will be retrievable. + return self._values[item] + + @property + def partition(self): + if self._values['partition'] is None: + return 'Common' + return self._values['partition'].strip('/') + + @partition.setter + def partition(self, value): + self._values['partition'] = value + + def _filter_params(self, params): + return dict((k, v) for k, v in iteritems(params) if v is not None) + + +class ImishConfig(NetworkConfig): + def add(self, lines, parents=None, duplicates=False): + ancestors = list() + offset = 0 + obj = None + + # global config command + if not parents: + for line in lines: + # handle ignore lines + if ignore_line(line): + continue + + item = ConfigLine(line) + item.raw = line + if item not in self.items: + self.items.append(item) + + else: + for index, p in enumerate(parents): + try: + i = index + 1 + obj = self.get_block(parents[:i])[0] + ancestors.append(obj) + + except ValueError: + # add parent to config + offset = index * self._indent + obj = ConfigLine(p) + obj.raw = p.rjust(len(p) + offset) + if ancestors: + obj._parents = list(ancestors) + ancestors[-1]._children.append(obj) + self.items.append(obj) + ancestors.append(obj) + + # add child objects + for line in lines: + # handle ignore lines + if ignore_line(line): + continue + + # check if child already exists + for child in ancestors[-1]._children: + if child.text == line and not duplicates: + break + else: + offset = len(parents) * self._indent + item = ConfigLine(line) + item.raw = line.rjust(len(line) + offset) + item._parents = ancestors + ancestors[-1]._children.append(item) + self.items.append(item) + + +class F5ModuleError(Exception): + pass diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/compare.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/compare.py new file mode 100644 index 00000000..ca987645 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/compare.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.six import iteritems + + +def cmp_simple_list(want, have): + if want is None: + return None + if have is None and want in ['', 'none']: + return None + if have is not None and want in ['', 'none']: + return [] + if have is None: + return want + if set(want) != set(have): + return want + return None + + +def cmp_str_with_none(want, have): + if want is None: + return None + if have is None and want == '': + return None + if want != have: + return want + + +def compare_complex_list(want, have): + """Performs a complex list comparison + + A complex list is a list of dictionaries + + Args: + want (list): List of dictionaries to compare with second parameter. + have (list): List of dictionaries to compare with first parameter. + + Returns: + bool: + """ + if want == [] and have is None: + return None + if want is None: + return None + w = [] + h = [] + for x in want: + tmp = [(str(k), str(v)) for k, v in iteritems(x)] + w += tmp + for x in have: + tmp = [(str(k), str(v)) for k, v in iteritems(x)] + h += tmp + if set(w) == set(h): + return None + else: + return want + + +def compare_dictionary(want, have): + """Performs a dictionary comparison + + Args: + want (dict): Dictionary to compare with second parameter. + have (dict): Dictionary to compare with first parameter. + + Returns: + bool: + """ + if want == {} and have is None: + return None + if want is None: + return None + w = [(str(k), str(v)) for k, v in iteritems(want)] + h = [(str(k), str(v)) for k, v in iteritems(have)] + if set(w) == set(h): + return None + else: + return want diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/constants.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/constants.py new file mode 100644 index 00000000..c5c0e3b2 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/constants.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2020, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +BASE_HEADERS = {'Content-Type': 'application/json'} + +MANAGED_BY_ANNOTATION_VERSION = 'f5-ansible.version' +MANAGED_BY_ANNOTATION_MODIFIED = 'f5-ansible.last_modified' + +LOGIN = '/mgmt/shared/authn/login' +LOGOUT = '/mgmt/shared/authz/tokens/' + +PLATFORM = { + 'bigip': 'BIG-IP', + 'bigiq': 'BIG-IQ' +} + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + + +TEEM_ENDPOINT = 'product.apis.f5.com', +TEEM_KEY = 'mmhJU2sCd63BznXAXDh4kxLIyfIMm3Ar' +TEEM_TIMEOUT = 10 +TEEM_VERIFY = False + +CICD_ENV = { + 'bamboo.buildKey': 'Bamboo', + 'DRONE': 'Drone CI', + 'BUILDKITE': 'Buildkite', + 'CIRCLECI': 'Circle CI', + 'CIRRUS_CI': 'Cirrus CI', + 'CODEBUILD_BUILD_ID': 'AWS CodeBuild', + 'GITHUB_ACTIONS': 'GitHub Actions', + 'GITLAB_CI': 'GitLab CI', + 'HUDSON_URL': 'Hudson CI', + 'JENKINS_URL': 'Jenkins CI', + 'TF_BUILD': 'Azure Pipelines', + 'HEROKU_TEST_RUN_ID': 'Heroku CI', + 'TEAMCITY_VERSION': 'TeamCity', + 'TRAVIS': 'Travis CI', + 'CI_NAME': 'CodeShip CI' +} diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/icontrol.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/icontrol.py new file mode 100644 index 00000000..723c5364 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/icontrol.py @@ -0,0 +1,681 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import os + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +try: + from BytesIO import BytesIO +except ImportError: + from io import BytesIO + +from ansible.module_utils.urls import urlparse +from ansible.module_utils.urls import generic_urlparse +from ansible.module_utils.urls import Request + +try: + import json as _json +except ImportError: + import simplejson as _json + +from .common import F5ModuleError + + +"""An F5 REST API URI handler. + +Use this module to make calls to an F5 REST server. It is influenced by the same +API that the Python ``requests`` tool uses, but the two are not the same, as the +library here is **much** more simple and targeted specifically to F5's needs. + +The ``requests`` design was chosen due to familiarity with the tool. Internally, +the classes contained herein use Ansible native libraries. + +The means by which you should use it are similar to ``requests`` basic usage. + +Authentication is not handled for you automatically by this library, however it *is* +handled automatically for you in the supporting F5 module_utils code; specifically the +different product module_util files (bigip.py, bigiq.py, etc). + +Internal (non-module) usage of this library looks like this. + +``` +# Create a session instance +mgmt = iControlRestSession() +mgmt.verify = False + +server = '1.1.1.1' +port = 443 + +# Payload used for getting an initial authentication token +payload = { + 'username': 'admin', + 'password': 'secret', + 'loginProviderName': 'tmos' +} + +# Create URL to call, injecting server and port +url = f"https://{server}:{port}/mgmt/shared/authn/login" + +# Call the API +resp = session.post(url, json=payload) + +# View the response +print(resp.json()) + +# Update the session with the authentication token +session.headers['X-F5-Auth-Token'] = resp.json()['token']['token'] + +# Create another URL to call, injecting server and port +url = f"https://{server}:{port}/mgmt/tm/ltm/virtual/~Common~virtual1" + +# Call the API +resp = session.get(url) + +# View the details of a virtual payload +print(resp.json()) +``` +""" + +from ansible.module_utils.six.moves.urllib.error import HTTPError + +from .constants import ( + LOGOUT, BASE_HEADERS +) + + +class Response(object): + def __init__(self): + self._content = None + self.status = None + self.headers = dict() + self.url = None + self.reason = None + self.request = None + self.msg = None + + @property + def content(self): + return self._content + + @property + def raw_content(self): + return self._content + + def json(self): + return _json.loads(self._content or 'null') + + @property + def ok(self): + if self.status is not None and int(self.status) > 400: + return False + try: + response = self.json() + if 'code' in response and response['code'] > 400: + return False + except ValueError: + pass + return True + + +class iControlRestSession(object): + """Represents a session that communicates with a BigIP. + + This acts as a loose wrapper around Ansible's ``Request`` class. We're doing + this as interim work until we move to the httpapi connector. + """ + def __init__(self, headers=None, use_proxy=True, force=False, timeout=120, + validate_certs=True, url_username=None, url_password=None, + http_agent=None, force_basic_auth=False, follow_redirects='urllib2', + client_cert=None, client_key=None, cookies=None): + self.request = Request( + headers=headers, + use_proxy=use_proxy, + force=force, + timeout=timeout, + validate_certs=validate_certs, + url_username=url_username, + url_password=url_password, + http_agent=http_agent, + force_basic_auth=force_basic_auth, + follow_redirects=follow_redirects, + client_cert=client_cert, + client_key=client_key, + cookies=cookies + ) + self.last_url = None + + def get_headers(self, result): + try: + return dict(result.getheaders()) + except AttributeError: + return result.headers + + def update_response(self, response, result): + response.headers = self.get_headers(result) + response._content = result.read() + response.status = result.getcode() + response.url = result.geturl() + response.msg = "OK (%s bytes)" % response.headers.get('Content-Length', 'unknown') + + def send(self, method, url, **kwargs): + response = Response() + + # Set the last_url called + # + # This is used by the object destructor to erase the token when the + # ModuleManager exits and destroys the iControlRestSession object + self.last_url = url + + body = None + data = kwargs.pop('data', None) + json = kwargs.pop('json', None) + + if not data and json is not None: + self.request.headers.update(BASE_HEADERS) + body = _json.dumps(json) + if not isinstance(body, bytes): + body = body.encode('utf-8') + if data: + body = data + if body: + kwargs['data'] = body + + try: + result = self.request.open(method, url, **kwargs) + except HTTPError as e: + # Catch HTTPError delivered from Ansible + # + # The structure of this object, in Ansible 2.8 is + # + # HttpError { + # args + # characters_written + # close + # code + # delete + # errno + # file + # filename + # filename2 + # fp + # getcode + # geturl + # hdrs + # headers + # info + # msg + # name + # reason + # strerror + # url + # with_traceback + # } + self.update_response(response, e) + return response + + self.update_response(response, result) + return response + + def delete(self, url, **kwargs): + return self.send('DELETE', url, **kwargs) + + def get(self, url, **kwargs): + return self.send('GET', url, **kwargs) + + def patch(self, url, data=None, **kwargs): + return self.send('PATCH', url, data=data, **kwargs) + + def post(self, url, data=None, **kwargs): + return self.send('POST', url, data=data, **kwargs) + + def put(self, url, data=None, **kwargs): + return self.send('PUT', url, data=data, **kwargs) + + def __del__(self): + if self.last_url is None: + return + token = self.request.headers.get('X-F5-Auth-Token', None) + if not token: + return + try: + p = generic_urlparse(urlparse(self.last_url)) + uri = "https://{0}:{1}{2}{3}".format( + p['hostname'], p['port'], LOGOUT, token + ) + self.delete(uri) + except ValueError: + pass + + +class TransactionContextManager(object): + def __init__(self, client, validate_only=False): + self.client = client + self.validate_only = validate_only + self.transid = None + + def __enter__(self): + uri = "https://{0}:{1}/mgmt/tm/transaction/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json={}) + if resp.status not in [200]: + raise Exception + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + self.transid = response['transId'] + self.client.api.request.headers['X-F5-REST-Coordination-Id'] = self.transid + return self.client + + def __exit__(self, exc_type, exc_value, exc_tb): + self.client.api.request.headers.pop('X-F5-REST-Coordination-Id') + if exc_tb is None: + uri = "https://{0}:{1}/mgmt/tm/transaction/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.transid + ) + params = dict( + state="VALIDATING", + validateOnly=self.validate_only + ) + resp = self.client.api.patch(uri, json=params) + if resp.status not in [200]: + raise Exception + + +def download_asm_file(client, url, dest, file_size): + """Download a large ASM file from the remote device + + This method handles issues with ASM file endpoints that allow + downloads of ASM objects on the BIG-IP, as well as handles + chunking of large files. + + Arguments: + client (object): The F5RestClient connection object. + url (string): The URL to download. + dest (string): The location on (Ansible controller) disk to store the file. + file_size (integer): The size of the remote file. + + Returns: + bool: No response on success. Fail otherwise. + """ + + with open(dest, 'wb') as fileobj: + chunk_size = 512 * 1024 + start = 0 + end = chunk_size - 1 + size = file_size + # current_bytes = 0 + + while True: + content_range = "%s-%s/%s" % (start, end, size) + headers = { + 'Content-Range': content_range, + 'Content-Type': 'application/json' + } + data = { + 'headers': headers, + 'verify': False, + 'stream': False + } + + response = client.api.get(url, headers=headers, json=data) + if response.status == 200: + if 'Content-Length' not in response.headers: + error_message = "The Content-Length header is not present." + raise F5ModuleError(error_message) + length = response.headers['Content-Length'] + if int(length) > 0: + fileobj.write(response.content) + else: + error = "Invalid Content-Length value returned: %s ," \ + "the value should be greater than 0" % length + raise F5ModuleError(error) + # fileobj.write(response.raw_content) + if end == size: + break + start += chunk_size + if start >= size: + break + if (end + chunk_size) > size: + end = size - 1 + else: + end = start + chunk_size - 1 + + +def download_file(client, url, dest): + """Download a file from the remote device + + This method handles the chunking needed to download a file from + a given URL on the BIG-IP. + + Arguments: + client (object): The F5RestClient connection object. + url (string): The URL to download. + dest (string): The location on (Ansible controller) disk to store the file. + + Returns: + bool: True on success. False otherwise. + """ + with open(dest, 'wb') as fileobj: + chunk_size = 512 * 1024 + start = 0 + end = chunk_size - 1 + size = 0 + current_bytes = 0 + + while True: + content_range = "%s-%s/%s" % (start, end, size) + headers = { + 'Content-Range': content_range, + 'Content-Type': 'application/octet-stream' + } + data = { + 'headers': headers, + 'verify': False, + 'stream': False + } + response = client.api.get(url, headers=headers, json=data) + if response.status == 200: + # If the size is zero, then this is the first time through + # the loop and we don't want to write data because we + # haven't yet figured out the total size of the file. + if size > 0: + current_bytes += chunk_size + fileobj.write(response.raw_content) + # Once we've downloaded the entire file, we can break out of + # the loop + if end == size: + break + crange = response.headers['Content-Range'] + # Determine the total number of bytes to read. + if size == 0: + size = int(crange.split('/')[-1]) - 1 + # If the file is smaller than the chunk_size, the BigIP + # will return an HTTP 400. Adjust the chunk_size down to + # the total file size... + if chunk_size > size: + end = size + # ...and pass on the rest of the code. + continue + start += chunk_size + if (current_bytes + chunk_size) > size: + end = size + else: + end = start + chunk_size - 1 + return True + + +def upload_file(client, url, src, dest=None): + """Upload a file to an arbitrary URL. + + This method is responsible for correctly chunking an upload request to an + arbitrary file worker URL. + + Arguments: + client (object): The F5RestClient connection object. + url (string): The URL to upload a file to. + src (string): The file to be uploaded. + dest (string): The file name to create on the remote device. + + Examples: + The ``dest`` may be either an absolute or relative path. The basename + of the path is used as the remote file name upon upload. For instance, + in the example below, ``BIGIP-13.1.0.8-0.0.3.iso`` would be the name + of the remote file. + + The specified URL should be the full URL to where you want to upload a + file. BIG-IP has many different URLs that can be used to handle different + types of files. This is why a full URL is required. + + >>> from ansible_collections.f5networks.f5_modules.plugins.module_utils.icontrol import upload_client + >>> url = 'https://{0}:{1}/mgmt/cm/autodeploy/software-image-uploads'.format( + ... self.client.provider['server'], + ... self.client.provider['server_port'] + ... ) + >>> dest = '/path/to/BIGIP-13.1.0.8-0.0.3.iso' + >>> upload_file(self.client, url, dest) + True + + Returns: + bool: True on success. False otherwise. + + Raises: + F5ModuleError: Raised if ``retries`` limit is exceeded. + """ + if isinstance(src, StringIO) or isinstance(src, BytesIO): + fileobj = src + else: + fileobj = open(src, 'rb') + + try: + size = os.stat(src).st_size + is_file = True + except TypeError: + src.seek(0, os.SEEK_END) + size = src.tell() + src.seek(0) + is_file = False + + # This appears to be the largest chunk size that iControlREST can handle. + # + # The trade-off you are making by choosing a chunk size is speed, over size of + # transmission. A lower chunk size will be slower because a smaller amount of + # data is read from disk and sent via HTTP. Lots of disk reads are slower and + # There is overhead in sending the request to the BIG-IP. + # + # Larger chunk sizes are faster because more data is read from disk in one + # go, and therefore more data is transmitted to the BIG-IP in one HTTP request. + # + # If you are transmitting over a slow link though, it may be more reliable to + # transmit many small chunks that fewer large chunks. It will clearly take + # longer, but it may be more robust. + chunk_size = 1024 * 7168 + start = 0 + retries = 0 + if dest is None and is_file: + basename = os.path.basename(src) + else: + basename = dest + url = '{0}/{1}'.format(url.rstrip('/'), basename) + + while True: + if retries == 3: + # Retries are used here to allow the REST API to recover if you kill + # an upload mid-transfer. + # + # There exists a case where retrying a new upload will result in the + # API returning the POSTed payload (in bytes) with a non-200 response + # code. + # + # Retrying (after seeking back to 0) seems to resolve this problem. + raise F5ModuleError( + "Failed to upload file too many times." + ) + try: + file_slice = fileobj.read(chunk_size) + if not file_slice: + break + + current_bytes = len(file_slice) + if current_bytes < chunk_size: + end = size + else: + end = start + current_bytes + headers = { + 'Content-Range': '%s-%s/%s' % (start, end - 1, size), + 'Content-Type': 'application/octet-stream' + } + + # Data should always be sent using the ``data`` keyword and not the + # ``json`` keyword. This allows bytes to be sent (such as in the case + # of uploading ISO files. + response = client.api.post(url, headers=headers, data=file_slice) + + if response.status != 200: + # When this fails, the output is usually the body of whatever you + # POSTed. This is almost always unreadable because it is a series + # of bytes. + # + # Therefore, we only inform on the returned HTTP error code. + raise F5ModuleError('Error during upload, http error code: {0}'.format(str(response.status))) + start += current_bytes + except F5ModuleError: + # You must seek back to the beginning of the file upon exception. + # + # If this is not done, then you risk uploading a partial file. + fileobj.seek(0) + retries += 1 + return True + + +def tmos_version(client): + uri = "https://{0}:{1}/mgmt/tm/sys/".format( + client.provider['server'], + client.provider['server_port'], + ) + resp = client.api.get(uri) + + 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) + + to_parse = urlparse(response['selfLink']) + query = to_parse.query + version = query.split('=')[1] + return version + + +def bigiq_version(client): + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-shared-all-big-iqs/devices".format( + client.provider['server'], + client.provider['server_port'], + ) + query = "?$select=version" + + resp = client.api.get(uri + query) + + 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) + + if 'items' in response: + version = response['items'][0]['version'] + return version + + raise F5ModuleError( + 'Failed to retrieve BIG-IQ version information.' + ) + + +def module_provisioned(client, module_name): + provisioned = modules_provisioned(client) + if module_name in provisioned: + return True + return False + + +def package_installed(client, package_name): + provisioned = packages_installed(client) + if package_name in provisioned: + return True + return False + + +def modules_provisioned(client): + """Returns a list of all provisioned modules + + Args: + client: Client connection to the BIG-IP + + Returns: + A list of provisioned modules in their short name for. + For example, ['afm', 'asm', 'ltm'] + """ + uri = "https://{0}:{1}/mgmt/tm/sys/provision".format( + client.provider['server'], + client.provider['server_port'] + ) + resp = client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + return [x['name'] for x in response['items'] if x['level'] != 'none'] + + +def packages_installed(client): + """Returns a list of installed ATC packages + + Args: + client: Client connection to the BIG-IP + + Returns: + A list of installed packages in their short name for. + For example, ['as3', 'do', 'ts'] + """ + packages = { + "f5-declarative-onboarding": "do", + "f5-appsvcs": "as3", + "f5-appsvcs-templates": "fast", + "f5-cloud-failover": "cfe", + "f5-telemetry": "ts", + "f5-service-discovery": "sd" + + } + + uri = "https://{0}:{1}/mgmt/shared/iapp/global-installed-packages".format( + client.provider['server'], + client.provider['server_port'] + ) + resp = client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 404: + return [] + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + if 'items' not in response: + return [] + + result = [packages[x['appName']] for x in response['items'] if x['appName'] in packages.keys()] + return result diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/ipaddress.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/ipaddress.py new file mode 100644 index 00000000..4664b794 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/ipaddress.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018 F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + validate_ip_address, validate_ip_v6_address +) + +from ipaddress import ip_interface, ip_network + + +def is_valid_ip(addr, type='all'): + if type in ['all', 'ipv4']: + if validate_ip_address(addr): + return True + if type in ['all', 'ipv6']: + if validate_ip_v6_address(addr): + return True + return False + + +def ipv6_netmask_to_cidr(mask): + """converts an IPv6 netmask to CIDR form + + According to the link below, CIDR is the only official way to specify + a subset of IPv6. With that said, the same link provides a way to + loosely convert an netmask to a CIDR. + + Arguments: + mask (string): The IPv6 netmask to convert to CIDR + + Returns: + int: The CIDR representation of the netmask + + References: + https://stackoverflow.com/a/33533007 + http://v6decode.com/ + """ + bit_masks = [ + 0, 0x8000, 0xc000, 0xe000, 0xf000, 0xf800, + 0xfc00, 0xfe00, 0xff00, 0xff80, 0xffc0, + 0xffe0, 0xfff0, 0xfff8, 0xfffc, 0xfffe, + 0xffff + ] + count = 0 + try: + for w in mask.split(':'): + if not w or int(w, 16) == 0: + break + count += bit_masks.index(int(w, 16)) + return count + except Exception: + return -1 + + +def is_valid_ip_network(address): + try: + ip_network(u'{0}'.format(address)) + return True + except ValueError: + return False + + +def is_valid_ip_interface(address): + try: + ip_interface(u'{0}'.format(address)) + return True + except ValueError: + return False + + +def get_netmask(address): + addr = ip_network(u'{0}'.format(address)) + netmask = addr.netmask.compressed + return netmask + + +def compress_address(address): + addr = ip_network(u'{0}'.format(address)) + result = addr.compressed.split('/', maxsplit=1)[0] + return result diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/teem.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/teem.py new file mode 100644 index 00000000..d226af00 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/teem.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2020 F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import json +import os +import sys +import uuid +import random +import re +import socket + + +from datetime import datetime +from ssl import SSLError +from time import time + +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import ( + HTTPError, URLError +) + +from .constants import ( + TEEM_ENDPOINT, TEEM_KEY, TEEM_TIMEOUT, TEEM_VERIFY, BASE_HEADERS, PLATFORM, CICD_ENV +) + +from .version import CURRENT_COLL_VERSION + + +class TeemClient(object): + def __init__(self, start_time, module, version): + self.module_name = module._name + self.ansible_version = module.ansible_version + self.version = version + self.start_time = start_time + self.docker = False + self.in_ci = False + self.coll_name = 'F5_MODULES' + + def prepare_request(self): + self.docker = in_docker() + user_agent = '{0}/{1}'.format(self.coll_name, CURRENT_COLL_VERSION) + dai = generate_asset_id(socket.gethostname()) + telemetry = self.build_telemetry() + url = 'https://%s/ee/v1/telemetry' % TEEM_ENDPOINT + headers = { + 'F5-ApiKey': TEEM_KEY, + 'F5-DigitalAssetId': str(dai), + 'F5-TraceId': str(uuid.uuid4()), + 'User-Agent': user_agent + } + headers.update(BASE_HEADERS) + data = { + 'digitalAssetName': self.coll_name, + 'digitalAssetVersion': CURRENT_COLL_VERSION, + 'digitalAssetId': str(dai), + 'documentType': '{0} Ansible Collection'.format(self.coll_name), + 'documentVersion': '1', + 'observationStartTime': self.start_time, + 'observationEndTime': datetime.now().isoformat(), + 'epochTime': time(), + 'telemetryId': str(uuid.uuid4()), + 'telemetryRecords': telemetry + } + + return url, headers, data + + def send(self): + url, headers, data = self.prepare_request() + payload = json.dumps(data) + try: + response = open_url( + url=url, + method='POST', + headers=headers, + timeout=TEEM_TIMEOUT, + validate_certs=TEEM_VERIFY, + data=payload + ) + # we need to ensure that any connection errors to TEEM do not cause failure of module to run. + except (HTTPError, URLError, SSLError): + return None + + ok = re.search(r'20[01-4]', str(response.code)) + if ok: + return True + return False + + def build_telemetry(self): + platform = self.get_platform() + self.in_ci, ci_name = in_cicd() + python_version = sys.version.split(' ', maxsplit=1)[0] + + return [{ + 'CollectionName': '{0}'.format(self.coll_name), + 'CollectionVersion': CURRENT_COLL_VERSION, + 'CollectionModuleName': self.module_name, + 'f5Platform': platform, + 'f5SoftwareVersion': self.version if self.version else 'none', + 'ControllerAnsibleVersion': self.ansible_version, + 'ControllerPythonVersion': python_version, + 'ControllerAsDocker': self.docker, + 'DockerHostname': socket.gethostname() if self.docker else 'none', + 'RunningInCiEnv': self.in_ci, + 'CiEnvName': ci_name if self.in_ci else 'none' + }] + + def get_platform(self): + if self.coll_name.lower() in self.module_name: + self.module_name = self.module_name.split('.')[2] + return PLATFORM.get(self.module_name.split('_')[0], 'unknown') + return PLATFORM.get(self.module_name.split('_')[0], 'unknown') + + +def in_docker(): + """Check to see if we are running in a container + + Returns: + bool: True if in a container. False otherwise. + """ + try: + with open('/proc/1/cgroup') as fh: + lines = fh.readlines() + except IOError: + return False + if any('/docker/' in x for x in lines): + return True + return False + + +def in_cicd(): + env = determine_environment() + if env: + return True, env + return False, None + + +def determine_environment(): + for key in CICD_ENV: + env = os.getenv(key) + if env: + if key == 'CI_NAME' and env == 'codeship': + return CICD_ENV[key] + if key == 'CI_NAME' and env != 'codeship': + return None + return CICD_ENV[key] + + +def generate_asset_id(seed): + rd = random.Random() + rd.seed(seed) + result = uuid.UUID(int=rd.getrandbits(128)) + return result + + +def send_teem(start_time, client, module, version=None): + """ Sends Teem Data if allowed.""" + if client.provider['no_f5_teem'] is True: + return False + teem = TeemClient(start_time, module, version) + teem.send() diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/urls.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/urls.py new file mode 100644 index 00000000..c27ba514 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/urls.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import re + +from .common import F5ModuleError + +_CLEAN_HEADER_REGEX_BYTE = re.compile(b'^\\S[^\\r\\n]*$|^$') +_CLEAN_HEADER_REGEX_STR = re.compile(r'^\S[^\r\n]*$|^$') + + +def check_header_validity(header): + """Verifies that header value is a string which doesn't contain + leading whitespace or return characters. + + NOTE: This is a slightly modified version of the original function + taken from the requests library: + http://docs.python-requests.org/en/master/_modules/requests/utils/ + + :param header: string containing ':'. + """ + try: + name, value = header.split(':') + except ValueError: + raise F5ModuleError('Invalid header format: {0}'.format(header)) + if name == '': + raise F5ModuleError('Invalid header format: {0}'.format(header)) + + if isinstance(value, bytes): + pat = _CLEAN_HEADER_REGEX_BYTE + else: + pat = _CLEAN_HEADER_REGEX_STR + try: + if not pat.match(value): + raise F5ModuleError("Invalid return character or leading space in header: %s" % name) + except TypeError: + raise F5ModuleError("Value for header {%s: %s} must be of type str or " + "bytes, not %s" % (name, value, type(value))) + + +def build_service_uri(base_uri, partition, name): + """Build the proper uri for a service resource. + This follows the scheme: + /~~<.app>~ + :param base_uri: str -- base uri of the REST endpoint + :param partition: str -- partition for the service + :param name: str -- name of the service + :returns: str -- uri to access the service + """ + name = name.replace('/', '~') + return '%s~%s~%s.app~%s' % (base_uri, partition, name, name) + + +def parseStats(entry): + if 'description' in entry: + return entry['description'] + elif 'value' in entry: + return entry['value'] + elif 'entries' in entry or 'nestedStats' in entry and 'entries' in entry['nestedStats']: + if 'entries' in entry: + entries = entry['entries'] + else: + entries = entry['nestedStats']['entries'] + result = None + + for name in entries: + entry = entries[name] + if 'https://localhost' in name: + name = name.split('/') + name = name[-1] + if result and isinstance(result, list): + result.append(parseStats(entry)) + elif result and isinstance(result, dict): + result[name] = parseStats(entry) + else: + try: + int(name) + result = list() + result.append(parseStats(entry)) + except ValueError: + result = dict() + result[name] = parseStats(entry) + else: + if '.' in name: + names = name.split('.') + key = names[0] + value = names[1] + if result is None: + # result can be None if this branch is reached first + # + # For example, the mgmt/tm/net/trunk/NAME/stats API + # returns counters.bitsIn before anything else. + result = dict() + result[key] = dict() + elif key not in result: + result[key] = dict() + elif result[key] is None: + result[key] = dict() + result[key][value] = parseStats(entry) + else: + if result and isinstance(result, list): + result.append(parseStats(entry)) + elif result and isinstance(result, dict): + result[name] = parseStats(entry) + else: + try: + int(name) + result = list() + result.append(parseStats(entry)) + except ValueError: + result = dict() + result[name] = parseStats(entry) + return result diff --git a/ansible_collections/f5networks/f5_modules/plugins/module_utils/version.py b/ansible_collections/f5networks/f5_modules/plugins/module_utils/version.py new file mode 100644 index 00000000..a24a3745 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/module_utils/version.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2021, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# This collection version needs to be updated at each release +CURRENT_COLL_VERSION = "1.22.1" diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/__init__.py b/ansible_collections/f5networks/f5_modules/plugins/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_acl.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_acl.py new file mode 100644 index 00000000..29ff0b0f --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_acl.py @@ -0,0 +1,998 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_apm_acl +short_description: Manage user-defined APM ACLs +description: + - Manage user-defined APM ACLs. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the ACL to manage. + type: str + required: True + description: + description: + - User created ACL description. + type: str + type: + description: + - Specifies the type of ACL to create. + - Once the type is set it cannot be changed. + type: str + choices: + - static + - dynamic + acl_order: + description: + - Specifies a number that indicates the order of this ACL relative to other ACLs. + - When not set, the device will always place the ACL after the last one created. + - The lower the number, the higher the ACL will be in the general order, with the lowest number C(0) being the topmost one. + - Valid range of values is between C(0) and C(65535) inclusive. + type: int + path_match_case: + description: + - Specifies whether alphabetic case is considered when matching paths in an access control entry. + type: bool + entries: + description: + - Access control entries that define the ACL matching and its respective behavior. + - The order in which the rules are placed as arguments to this parameter determines their order in the ACL, + in other words changing the order of the same elements will cause a change on the unit. + - Changes in the number of rules will always trigger device change. This means user input will take + precedence over what is on device. + type: list + elements: dict + suboptions: + action: + description: + - Specifies the action that the access control entry takes when a match for this access control entry + is encountered. + type: str + required: True + choices: + - allow + - reject + - discard + - continue + dst_port: + description: + - Specifies the destination port for the access control entry. + - Can be set to C(*) to indicate all ports. + - Parameter is mutually exclusive with C(dst_port_range). + type: str + dst_port_range: + description: + - Specifies the destination port range for the access control entry. + - Parameter is mutually exclusive with C(dst_port_range). + - To indicate all ports the C(dst_port) parameter must be used and set to C(*). + type: str + src_port: + description: + - Specifies the source port for the access control entry. + - Can be set to C(*) to indicate all ports. + - Parameter is mutually exclusive with C(src_port_range). + type: str + src_port_range: + description: + - Specifies the source port range for the access control entry. + - Parameter is mutually exclusive with C(src_port_range). + - To indicate all ports the C(src_port) parameter must be used and set to C(*). + type: str + dst_addr: + description: + - Specifies the destination IP address for the access control entry. + - When set to C(any) the ACL will match any destination address, C(dst_mask) is ignored in this case. + type: str + dst_mask: + description: + - Optional parameter that specifies the destination network mask for the access control entry. + - If not specified and C(dst_addr) is not C(any), the C(dst_addr) is deemed to be host address. + type: str + src_addr: + description: + - Specifies the source IP address for the access control entry. + - When set to C(any) the ACL will match any source address, C(src_mask) is ignored in this case. + type: str + src_mask: + description: + - Optional parameter that specifies the source network mask for the access control entry. + - If not specified and C(src_addr) is not C(any), the C(src_addr) is deemed to be host address. + type: str + scheme: + description: + - This parameter applies to Layer 7 access control entries only. + - "Specifies the URI scheme: C(http), C(https) or C(any) on which the access control entry operates." + type: str + choices: + - http + - https + - any + protocol: + description: + - This parameter applies to Layer 4 access control entries only. + - "Specifies the protocol: C(tcp), C(udp), C(icmp) or C(all) protocols, + to which the access control entry applies." + type: str + choices: + - tcp + - icmp + - udp + - all + host_name: + description: + - This parameter applies to Layer 7 access control entries only. + - Specifies a host to which the access control entry applies. + type: str + paths: + description: + - This parameter applies to Layer 7 access control entries only. + - Specifies the path or paths to which the access control entry applies. + type: str + log: + description: + - Specifies the log level that is logged when actions of this type occur. + - When C(none) it will log nothing, which is a default action. + - When C(packet) it will log the matched packet. + type: str + choices: + - none + - packet + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures that the ACL exists. + - When C(state) is C(absent), ensures that the ACL is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a static ACL with L4 entries + bigip_apm_acl: + name: L4foo + acl_order: 0 + type: static + entries: + - action: allow + dst_port: '80' + dst_addr: '192.168.1.1' + src_port: '443' + src_addr: '10.10.10.0' + src_mask: '255.255.255.128' + protocol: tcp + - action: reject + dst_port: '*' + dst_addr: '192.168.1.1' + src_port: '*' + src_addr: '10.10.10.0' + src_mask: '255.255.255.128' + protocol: tcp + log: packet + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a static ACL with L7 entries + bigip_apm_acl: + name: L7foo + acl_order: 1 + type: static + path_match_case: no + entries: + - action: allow + host_name: 'foobar.com' + paths: '/shopfront' + scheme: https + - action: reject + host_name: 'internal_foobar.com' + paths: '/admin' + scheme: any + log: packet + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a static ACL with L7/L4 entries + bigip_apm_acl: + name: L7L4foo + acl_order: 2 + type: static + path_match_case: no + entries: + - action: allow + host_name: 'foobar.com' + paths: '/shopfront' + scheme: https + dst_port: '8181' + dst_addr: '192.168.1.1' + protocol: tcp + - action: reject + dst_addr: '192.168.1.1' + host_name: 'internal_foobar.com' + paths: '/admin' + scheme: any + protocol: all + log: packet + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify a static ACL entries + bigip_apm_acl: + name: L4foo + entries: + - action: allow + dst_port: '80' + dst_addr: '192.168.1.1' + src_port: '443' + src_addr: '10.10.10.0' + src_mask: '255.255.255.128' + protocol: tcp + - action: discard + dst_port: '*' + dst_addr: 192.168.1.1 + src_port: '*' + src_addr: '10.10.10.0' + src_mask: '255.2155.255.128' + protocol: all + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove static ACL + bigip_apm_acl: + name: L4foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the ACL. + returned: changed + type: str + sample: My ACL +type: + description: The type of ACL to create. + returned: changed + type: str + sample: static +acl_order: + description: The order of this ACL relative to other ACLs. + returned: changed + type: int + sample: 10 +path_match_case: + description: Specifies whether alphabetic case is considered when matching paths in an access control entry. + returned: changed + type: bool + sample: yes +entries: + description: Access control entries that define the ACL matching and its respective behavior. + type: complex + returned: changed + contains: + action: + description: Action the access control entry takes when a match for this access control entry is encountered. + returned: changed + type: str + sample: allow + dst_port: + description: The destination port for the access control entry. + returned: changed + type: str + sample: '80' + dst_port_range: + description: The destination port range for the access control entry. + returned: changed + type: str + sample: '80-81' + src_port: + description: The source port for the access control entry. + returned: changed + type: str + sample: '80' + src_port_range: + description: The source port range for the access control entry. + returned: changed + type: str + sample: '80-81' + dst_addr: + description: The destination IP address for the access control entry. + returned: changed + type: str + sample: 192.168.0.1 + dst_mask: + description: The destination network mask for the access control entry. + returned: changed + type: str + sample: 255.255.255.128 + src_addr: + description: The source IP address for the access control entry. + returned: changed + type: str + sample: 192.168.0.1 + src_mask: + description: The source network mask for the access control entry. + returned: changed + type: str + sample: 255.255.255.128 + scheme: + description: The URI scheme on which the access control entry operates. + returned: changed + type: str + sample: https + protocol: + description: The protocol to which the access control entry applies. + returned: changed + type: str + sample: tcp + host_name: + description: The host to which the access control entry applies. + returned: changed + type: str + sample: foobar.com + paths: + description: The path or paths to which the access control entry applies. + returned: changed + type: str + sample: /fooshop + log: + description: The log level that is logged when actions of this type occur. + returned: changed + type: str + sample: packet + sample: hash/dictionary of values +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ipaddress import ( + ip_network, ip_interface +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import ( + is_valid_ip, is_valid_ip_network +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'aclOrder': 'acl_order', + 'pathMatchCase': 'path_match_case' + } + + api_attributes = [ + 'entries', + 'description', + 'aclOrder', + 'pathMatchCase', + 'type', + ] + + returnables = [ + 'entries', + 'acl_order', + 'path_match_case', + 'type', + 'description', + ] + + updatables = [ + 'entries', + 'acl_order', + 'path_match_case', + 'type', + 'description', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + protocol_map = { + 'icmp': 1, + 'tcp': 6, + 'udp': 17, + 'all': 0 + } + + @property + def path_match_case(self): + result = flatten_boolean(self._values['path_match_case']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + + @property + def acl_order(self): + if self._values['acl_order'] is None: + return None + if 0 < self._values['acl_order'] > 65535: + raise F5ModuleError( + "Specified number is out of valid range, correct range is between 0 and 65535." + ) + return self._values['acl_order'] + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def entries(self): + if self._values['entries'] is None: + return None + if self._values['entries'] == 'none': + return [] + result = [] + for x in self._values['entries']: + element = dict() + element['action'] = x['action'] + if 'dst_port' in x and x['dst_port'] is not None: + if x['dst_port'] == '*': + element['dstEndPort'] = 0 + element['dstStartPort'] = 0 + else: + self._validate_port(int(x['dst_port'])) + element['dstEndPort'] = int(x['dst_port']) + element['dstStartPort'] = int(x['dst_port']) + if 'dst_port_range' in x and x['dst_port_range'] is not None: + start, stop = self._validate_ports(x['dst_port_range']) + element['dstEndPort'] = stop + element['dstStartPort'] = start + if 'src_port' in x and x['src_port'] is not None: + if x['src_port'] == '*': + element['srcEndPort'] = 0 + element['srcStartPort'] = 0 + else: + self._validate_port(int(x['src_port'])) + element['srcEndPort'] = int(x['src_port']) + element['srcStartPort'] = int(x['src_port']) + if 'src_port_range' in x and x['src_port_range'] is not None: + start, stop = self._validate_ports(x['src_port_range']) + element['srcEndPort'] = stop + element['srcStartPort'] = start + if 'dst_addr' in x and x['dst_addr'] is not None: + if 'dst_mask' in x and x['dst_mask'] is not None: + element['dstSubnet'] = self._convert_address(x['dst_addr'], x['dst_mask']) + else: + element['dstSubnet'] = self._convert_address(x['dst_addr']) + if 'src_addr' in x and x['src_addr'] is not None: + if 'src_mask' in x and x['src_mask'] is not None: + element['srcSubnet'] = self._convert_address(x['src_addr'], x['src_mask']) + else: + element['srcSubnet'] = self._convert_address(x['src_addr']) + if 'scheme' in x and x['scheme'] is not None: + element['scheme'] = x['scheme'] + if 'protocol' in x and x['protocol'] is not None: + element['protocol'] = self.protocol_map[x['protocol']] + if 'host_name' in x and x['host_name'] is not None: + element['host'] = x['host_name'] + if 'paths' in x and x['paths'] is not None: + element['paths'] = x['paths'] + if 'log' in x and x['log'] is not None: + element['log'] = x['log'] + result.append(element) + return result + + def _validate_port(self, item): + if 0 < item > 65535: + raise F5ModuleError( + "Specified port number is out of valid range, correct range is between 0 and 65535." + ) + + def _validate_ports(self, item): + start, stop = item.split('-') + start = int(start.strip()) + stop = int(stop.strip()) + if 0 < start > 65535 or 0 < stop > 65535: + raise F5ModuleError( + "Specified port number is out of valid range, correct range is between 0 and 65535." + ) + return start, stop + + def _convert_address(self, item, mask=None): + if item == 'any': + return '0.0.0.0/0' + if not is_valid_ip(item): + raise F5ModuleError('The provided IP address is not a valid IP address.') + if mask: + msk = self._convert_netmask(mask) + network = '{0}/{1}'.format(item, msk) + if is_valid_ip_network(u'{0}'.format(network)): + return network + else: + raise F5ModuleError( + 'The provided IP and Mask are not a valid IP network.' + ) + host = ip_interface(u'{0}'.format(item)) + return host.with_prefixlen + + def _convert_netmask(self, item): + result = -1 + try: + result = int(item) + if 0 < result < 256: + pass + except ValueError: + if is_valid_ip(item): + ip = ip_network(u'0.0.0.0/%s' % str(item)) + result = ip.prefixlen + if result < 0: + raise F5ModuleError( + 'The provided netmask {0} is neither in IP or CIDR format'.format(result) + ) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + protocol_map = { + 1: 'icmp', + 6: 'tcp', + 17: 'udp', + 0: 'all' + } + + @property + def path_match_case(self): + result = flatten_boolean(self._values['path_match_case']) + return result + + @property + def entries(self): + if self._values['entries'] is None: + return None + if not self._values['entries']: + return 'none' + result = [] + for x in self._values['entries']: + to_filter = dict() + to_filter['action'] = x['action'] + if 'dstStartPort' in x and 'dstEndPort' in x: + if x['dstStartPort'] == x['dstEndPort']: + if x['dstStartPort'] == 0: + to_filter['dst_port'] = '*' + else: + to_filter['dst_port'] = str(x['dstStartPort']) + else: + to_filter['dst_port_range'] = '{0}-{1}'.format(x['dstStartPort'], x['dstEndPort']) + if 'srcStartPort' in x and 'srcEndPort' in x: + if x['srcStartPort'] == x['srcEndPort']: + if x['srcStartPort'] == 0: + to_filter['src_port'] = '*' + else: + to_filter['src_port'] = str(x['srcStartPort']) + else: + to_filter['src_port_range'] = '{0}-{1}'.format(x['srcStartPort'], x['srcEndPort']) + if 'dstSubnet' in x: + to_filter['dst_addr'], to_filter['dst_mask'] = self._convert_address(x['dstSubnet']) + if 'srcSubnet' in x: + to_filter['src_addr'], to_filter['src_mask'] = self._convert_address(x['srcSubnet']) + if 'scheme' in x: + to_filter['scheme'] = x['scheme'] + if 'protocol' in x: + to_filter['protocol'] = self.protocol_map[x['protocol']] + if 'host' in x: + to_filter['host_name'] = x['host'] + if 'paths' in x: + to_filter['paths'] = x['paths'] + if 'log' in x: + to_filter['log'] = x['log'] + element = self._filter_params(to_filter) + result.append(element) + return result + + def _convert_address(self, item): + if item == '0.0.0.0/0': + return 'any', None + result = ip_network(u'{0}'.format(item)) + if result.prefixlen == 32: + return str(result.network_address), None + else: + return str(result.network_address), str(result.netmask) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def entries(self): + if self.want.entries is None: + return None + if self.have.entries is None and self.want.entries == []: + return None + + want = self.want.entries + have = list() + # First we compare if both lists are equal, if want is bigger or smaller than have, we assume user change + if len(self.want.entries) > len(self.have.entries) or len(self.want.entries) < len(self.have.entries): + return self.want.entries + + # If lists are equal then we compare items to verify change was made + + # First we remove extra keys in have + for idx, item in enumerate(want): + entry = self._filter_have(item, self.have.entries[idx]) + have.append(entry) + # Compare each element in the list by position + for idx, item in enumerate(want): + if item != have[idx]: + return self.want.entries + + def _filter_have(self, want, have): + to_check = set(want.keys()).intersection(set(have.keys())) + result = dict() + for k in list(to_check): + result[k] = have[k] + return result + + @property + def type(self): + if self.want.type is None: + return None + if self.want.type == self.have.type: + return None + raise F5ModuleError( + "ACL type cannot be changed after ACL creation." + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'apm'): + raise F5ModuleError( + "APM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/apm/acl/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/apm/acl/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/apm/acl/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/apm/acl/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/apm/acl/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + acl_order=dict(type='int'), + description=dict(), + path_match_case=dict(type='bool'), + type=dict( + choices=['static', 'dynamic'], + ), + entries=dict( + type='list', + elements='dict', + options=dict( + action=dict( + choices=['allow', 'reject', 'discard', 'continue'], + required=True + ), + dst_port=dict(), + dst_port_range=dict(), + src_port=dict(), + src_port_range=dict(), + dst_addr=dict(), + dst_mask=dict(), + src_addr=dict(), + src_mask=dict(), + scheme=dict( + choices=['any', 'https', 'http'] + ), + protocol=dict( + choices=['tcp', 'icmp', 'udp', 'all'] + ), + host_name=dict(), + paths=dict(), + log=dict( + choices=['packet', 'none'] + ), + ), + mutually_exclusive=[ + ['dst_port', 'dst_port_range'], + ['src_port', 'src_port_range'], + ], + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_network_access.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_network_access.py new file mode 100644 index 00000000..fd2815ac --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_network_access.py @@ -0,0 +1,1031 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_apm_network_access +short_description: Manage APM Network Access resource +description: + - Manage APM Network Access resource. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the APM network access to manage/create. + type: str + required: True + description: + description: + - User created network access description. + type: str + ip_version: + description: + - Supported IP version on the network access resource. + type: str + choices: + - ipv4 + - ipv4-ipv6 + allow_local_subnet: + description: + - Enables local subnet access and local access to any host or subnet in routes specified in the client routing + table. + - When C(yes) the system does not support integrated IP filtering. + type: bool + allow_local_dns: + description: + - Enables local access to DNS servers configured on the client prior to establishing a network access connection. + type: bool + split_tunnel: + description: + - Specifies that only the traffic targeted to a specified address space is sent over the network access tunnel. + type: bool + snat_pool: + description: + - Specifies the name of a SNAT pool used for implementing selective and intelligent SNATs. + - When C(none) the system uses no SNAT pool for this network resource. + - When C(automap) the system uses all of the self IP addresses as the translation addresses for the pool. + type: str + dtls: + description: + - When C(yes) the network access connection uses Datagram Transport Level Security instead of TCP, + to provide better throughput for high demand applications like VoIP or streaming video. + type: bool + dtls_port: + description: + - Specifies the port number the network access resource uses for secure UDP traffic with DTLS. + type: int + ipv4_lease_pool: + description: + - Specifies the IPV4 lease pool resource to use with network access. + - Referencing a lease pool can be done in the full path format, for example C(/Common/pool_name). + - When a lease pool is referenced in full path format, the C(partition) parameter is ignored. + type: str + ipv6_lease_pool: + description: + - Specifies the IPV6 lease pool resource to use with network access. + - Referencing a lease pool can be done in the full path format, for example C(/Common/pool_name). + - When a lease pool is referenced in full path format, the C(partition) parameter is ignored. + type: str + excluded_ipv6_adresses: + description: + - Specifies IPV6 address spaces for which traffic is not forced through the tunnel. + type: list + elements: dict + suboptions: + subnet: + description: + - "The address of a subnet in CIDR format, e.g. C(2001:db8:abcd:8000::/52)" + - Host addresses can be specified without the CIDR mask notation. + type: str + excluded_ipv4_adresses: + description: + - Specifies IPV4 address spaces for which traffic is not forced through the tunnel. + type: list + elements: dict + suboptions: + subnet: + description: + - "The address of subnet in CIDR format, e.g. C(192.168.1.0/24)" + - Host addresses can be specified without the CIDR mask notation. + type: str + excluded_dns_addresses: + description: + - Specifies the DNS address spaces for which traffic is not forced through the tunnel. + type: list + elements: str + dns_address_space: + description: + - Specifies a list of domain names describing the target LAN DNS addresses. + type: list + elements: str + ipv4_address_space: + description: + - Specifies a list of IPv4 hosts or networks describing the target LAN. + - This option is mandatory when creating a new resource and C(split_tunnel) is set to C(yes). + type: list + elements: dict + suboptions: + subnet: + description: + - "The address of subnet in CIDR format, e.g. C(192.168.1.0/24)" + - Host addresses can be specified without the CIDR mask notation. + type: str + ipv6_address_space: + description: + - Specifies a list of IPv6 hosts or networks describing the target LAN. + - This option is mandatory when creating a new resource and C(split_tunnel) is set to C(yes). + type: list + elements: dict + suboptions: + subnet: + description: + - "The address of subnet in CIDR format, e.g. C(2001:db8:abcd:8000::/52)" + - Host addresses can be specified without the CIDR mask notation. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures the ACL exists. + - When C(state) is C(absent), ensures the ACL is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a split tunnel IPV4 Network Access + bigip_apm_network_access: + name: foobar + ip_version: ipv4 + split_tunnel: yes + snat_pool: "none" + ipv4_lease_pool: leasefoo + ipv4_address_space: + - subnet: 10.10.1.1 + - subnet: 10.10.2.0/24 + excluded_ipv4_adresses: + - subnet: 192.168.1.0/24 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify a split tunnel IPV4 Network Access + bigip_apm_network_access: + name: foobar + snat_pool: /Common/poolsnat + ipv4_address_space: + - subnet: 172.16.23.0/24 + excluded_ipv4_adresses: + - subnet: 10.10.2.0/24 + allow_local_subnet: yes + allow_local_dns: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove Network Access + bigip_apm_network_access: + name: foobar + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of Network Access. + returned: changed + type: str + sample: My Access +ip_version: + description: Supported IP version on the network access resource. + returned: changed + type: str + sample: ipv4-ipv6 +allow_local_subnet: + description: Enables local subnet access. + returned: changed + type: bool + sample: yes +allow_local_dns: + description: Enables local access to DNS servers configured on the client. + returned: changed + type: bool + sample: yes +split_tunnel: + description: Enables split tunnel on the network access resource. + returned: changed + type: bool + sample: yes +snat_pool: + description: The name of a SNAT pool used by the network access resource. + returned: changed + type: str + sample: /Common/my-pool +dtls: + description: Enables use of DTLS by network access. + returned: changed + type: bool + sample: no +dtls_port: + description: Specifies the port number the network access resource uses for DTLS. + returned: changed + type: int + sample: 4433 +ipv4_lease_pool: + description: Specifies a IPV4 lease pool resource to use with network access. + returned: changed + type: str + sample: /Common/leasepoolv4 +ipv6_lease_pool: + description: Specifies a IPV6 lease pool resource to use with network access. + returned: changed + type: str + sample: /Common/leasepoolv6 +excluded_ipv6_adresses: + description: Specifies IPV6 address spaces for which traffic is not forced through the tunnel. + type: complex + returned: changed + contains: + subnet: + description: The host or network address. + returned: changed + type: str + sample: "2001:DB8:ABCD:0012::0" + sample: hash/dictionary of values +excluded_ipv4_adresses: + description: Specifies IPV4 address spaces for which traffic is not forced through the tunnel. + type: complex + returned: changed + contains: + subnet: + description: The host or network address. + returned: changed + type: str + sample: 192.168.10.1 + sample: hash/dictionary of values +excluded_dns_addresses: + description: Specifies the DNS address spaces for which traffic is not forced through the tunnel. + returned: changed + type: list + sample: ['foobar.com', 'bazbar.org'] +dns_address_space: + description: Specifies a list of domain names describing the target LAN DNS addresses. + returned: changed + type: list + sample: ['internal.net', '*.engnet.org'] +ipv6_address_space: + description: Specifies a list of IPv6 hosts or networks describing the target LAN. + type: complex + returned: changed + contains: + subnet: + description: The host or network address. + returned: changed + type: str + sample: "2001:DB8:ABCD:0012::0" + sample: hash/dictionary of values +ipv4_address_space: + description: Specifies a list of IPv4 hosts or networks describing the target LAN. + type: complex + returned: changed + contains: + subnet: + description: The host or network address. + returned: changed + type: str + sample: 192.168.10.1 + sample: hash/dictionary of values +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, is_empty_list, fq_name +) +from ..module_utils.compare import ( + cmp_str_with_none, cmp_simple_list, compare_complex_list +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import ip_network +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'supportedIpVersion': 'ip_version', + 'splitTunneling': 'split_tunnel', + 'addressSpaceLocalSubnetsExcluded': 'allow_local_subnet', + 'addressSpaceLocDnsServersExcluded': 'allow_local_dns', + 'dtlsPort': 'dtls_port', + 'leasepoolName': 'ipv4_lease_pool', + 'ipv6LeasepoolName': 'ipv6_lease_pool', + 'ipv6AddressSpaceExcludeSubnet': 'excluded_ipv6_adresses', + 'addressSpaceExcludeSubnet': 'excluded_ipv4_adresses', + 'addressSpaceExcludeDnsName': 'excluded_dns_addresses', + 'addressSpaceIncludeDnsName': 'dns_address_space', + 'addressSpaceIncludeSubnet': 'ipv4_address_space', + 'ipv6AddressSpaceIncludeSubnet': 'ipv6_address_space', + 'snatpool': 'snat_pool', + } + + api_attributes = [ + 'supportedIpVersion', + 'splitTunneling', + 'addressSpaceLocalSubnetsExcluded', + 'addressSpaceLocDnsServersExcluded', + 'dtlsPort', + 'leasepoolName', + 'ipv6LeasepoolName', + 'ipv6AddressSpaceExcludeSubnet', + 'addressSpaceExcludeSubnet', + 'addressSpaceExcludeDnsName', + 'addressSpaceIncludeSubnet', + 'ipv6AddressSpaceIncludeSubnet', + 'addressSpaceIncludeDnsName', + 'snat', + 'snatpool', + 'dtls', + 'description', + ] + + returnables = [ + 'description', + 'ip_version', + 'split_tunnel', + 'allow_local_subnet', + 'allow_local_dns', + 'snat_pool', + 'dtls', + 'dtls_port', + 'ipv4_lease_pool', + 'ipv6_lease_pool', + 'excluded_ipv6_adresses', + 'excluded_ipv4_adresses', + 'excluded_dns_addresses', + 'dns_address_space', + 'ipv4_address_space', + 'ipv6_address_space', + ] + + updatables = [ + 'description', + 'ip_version', + 'split_tunnel', + 'allow_local_subnet', + 'allow_local_dns', + 'snat_pool', + 'dtls', + 'dtls_port', + 'ipv4_lease_pool', + 'ipv6_lease_pool', + 'excluded_ipv6_adresses', + 'excluded_ipv4_adresses', + 'excluded_dns_addresses', + 'dns_address_space', + 'ipv4_address_space', + 'ipv6_address_space', + ] + + +class ApiParameters(Parameters): + @property + def snat_pool(self): + if self._values['snat'] is None and self._values['snat_pool'] is None: + return None + if self._values['snat'] in ['automap', 'none']: + return self._values['snat'] + return self._values['snat_pool'] + + +class ModuleParameters(Parameters): + def _handle_booleans(self, item): + result = flatten_boolean(item) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + return None + + def _convert_address(self, item): + ip = ip_network(u'{0}'.format(item)) + return ip.with_prefixlen + + def _format_subnets(self, items): + result = [] + for x in items: + to_change = dict() + to_change['subnet'] = self._convert_address(x['subnet']) + result.append(to_change) + return result + + @property + def description(self): + if self._values['description'] is None: + return None + if self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def split_tunnel(self): + return self._handle_booleans(self._values['split_tunnel']) + + @property + def allow_local_subnet(self): + return self._handle_booleans(self._values['allow_local_subnet']) + + @property + def allow_local_dns(self): + return self._handle_booleans(self._values['allow_local_dns']) + + @property + def dtls(self): + return self._handle_booleans(self._values['dtls']) + + @property + def dtls_port(self): + if self._values['dtls_port'] is None: + return None + if 0 < self._values['dtls_port'] > 65535: + raise F5ModuleError( + "Specified port number is out of valid range, correct range is between 0 and 65535." + ) + return self._values['dtls_port'] + + @property + def ipv4_lease_pool(self): + if self._values['ipv4_lease_pool'] is None: + return None + if self._values['ipv4_lease_pool'] in ['none', '']: + return '' + return fq_name(self.partition, self._values['ipv4_lease_pool']) + + @property + def ipv6_lease_pool(self): + if self._values['ipv6_lease_pool'] is None: + return None + if self._values['ipv6_lease_pool'] in ['none', '']: + return '' + return fq_name(self.partition, self._values['ipv6_lease_pool']) + + @property + def excluded_ipv6_adresses(self): + if self._values['excluded_ipv6_adresses'] is None: + return None + if is_empty_list(self._values['excluded_ipv6_adresses']): + return [] + result = self._format_subnets(self._values['excluded_ipv6_adresses']) + return result + + @property + def excluded_ipv4_adresses(self): + if self._values['excluded_ipv4_adresses'] is None: + return None + if is_empty_list(self._values['excluded_ipv4_adresses']): + return [] + result = self._format_subnets(self._values['excluded_ipv4_adresses']) + return result + + @property + def ipv4_address_space(self): + if self._values['ipv4_address_space'] is None: + return None + if is_empty_list(self._values['ipv4_address_space']): + return [] + result = self._format_subnets(self._values['ipv4_address_space']) + return result + + @property + def ipv6_address_space(self): + if self._values['ipv6_address_space'] is None: + return None + if is_empty_list(self._values['ipv6_address_space']): + return [] + result = self._format_subnets(self._values['ipv6_address_space']) + return result + + @property + def dns_address_space(self): + if self._values['dns_address_space'] is None: + return None + if is_empty_list(self._values['dns_address_space']): + return [] + return self._values['dns_address_space'] + + @property + def excluded_dns_addresses(self): + if self._values['excluded_dns_addresses'] is None: + return None + if is_empty_list(self._values['excluded_dns_addresses']): + return [] + return self._values['excluded_dns_addresses'] + + @property + def snat_pool(self): + if self._values['snat_pool'] is None: + return None + if self._values['snat_pool'] in ['automap', 'none']: + return self._values['snat_pool'] + result = fq_name(self.partition, self._values['snat_pool']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def snat(self): + if self._values['snat_pool'] is None: + return None + if self._values['snat_pool'] in ['automap', 'none']: + return self._values['snat_pool'] + + @property + def snat_pool(self): + if self._values['snat_pool'] in [None, 'automap', 'none']: + return None + return self._values['snat_pool'] + + +class ReportableChanges(Changes): + def _parse_hosts(self, item): + ip = ip_network(u'{0}'.format(item)) + if ip.prefixlen in [32, 128]: + result = item.split('/')[0] + return result + return item + + def _format_subnets(self, items): + result = [] + for x in items: + to_change = dict() + to_change['subnet'] = self._parse_hosts(x['subnet']) + result.append(to_change) + return result + + @property + def split_tunnel(self): + return flatten_boolean(self._values['split_tunnel']) + + @property + def allow_local_subnet(self): + return flatten_boolean(self._values['allow_local_subnet']) + + @property + def allow_local_dns(self): + return flatten_boolean(self._values['allow_local_dns']) + + @property + def dtls(self): + return flatten_boolean(self._values['dtls']) + + @property + def excluded_ipv4_adresses(self): + if self._values['excluded_ipv4_adresses'] is None: + return None + if not self._values['excluded_ipv4_adresses']: + return [] + result = self._format_subnets(self._values['excluded_ipv4_adresses']) + return result + + @property + def excluded_ipv6_adresses(self): + if self._values['excluded_ipv6_adresses'] is None: + return None + if not self._values['excluded_ipv6_adresses']: + return [] + result = self._format_subnets(self._values['excluded_ipv6_adresses']) + return result + + @property + def ipv4_address_space(self): + if self._values['ipv4_address_space'] is None: + return None + if not self._values['ipv4_address_space']: + return [] + result = self._format_subnets(self._values['ipv4_address_space']) + return result + + @property + def ipv6_address_space(self): + if self._values['ipv6_address_space'] is None: + return None + if not self._values['ipv6_address_space']: + return [] + result = self._format_subnets(self._values['ipv6_address_space']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + result = cmp_str_with_none(self.want.description, self.have.description) + return result + + @property + def ipv4_lease_pool(self): + result = cmp_str_with_none(self.want.ipv4_lease_pool, self.have.ipv4_lease_pool) + return result + + @property + def ipv6_lease_pool(self): + result = cmp_str_with_none(self.want.ipv6_lease_pool, self.have.ipv6_lease_pool) + return result + + @property + def excluded_dns_addresses(self): + result = cmp_simple_list(self.want.excluded_dns_addresses, self.have.excluded_dns_addresses) + return result + + @property + def dns_address_space(self): + result = cmp_simple_list(self.want.dns_address_space, self.have.dns_address_space) + return result + + @property + def excluded_ipv4_adresses(self): + result = compare_complex_list(self.want.excluded_ipv4_adresses, self.have.excluded_ipv4_adresses) + return result + + @property + def excluded_ipv6_adresses(self): + result = compare_complex_list(self.want.excluded_ipv6_adresses, self.have.excluded_ipv6_adresses) + return result + + @property + def ipv4_address_space(self): + result = compare_complex_list(self.want.ipv4_address_space, self.have.ipv4_address_space) + return result + + @property + def ipv6_address_space(self): + result = compare_complex_list(self.want.ipv6_address_space, self.have.ipv6_address_space) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'apm'): + raise F5ModuleError( + "APM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + self.check_required_params() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self.check_required_params() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def check_required_params(self): + if self.want.split_tunnel == 'true': + if self.want.ip_version == 'ipv4': + if self.want.ipv4_address_space in [None, []]: + raise F5ModuleError( + 'The ipv4_address_space cannot be empty, when split_tunnel is set to {0}'.format( + self.want.split_tunnel + ) + ) + if self.want.ip_version == 'ipv4-ipv6': + if self.want.ipv4_address_space in [None, []]: + raise F5ModuleError( + 'The ipv4_address_space cannot be empty, when split_tunnel is set to {0}'.format( + self.want.split_tunnel + ) + ) + if self.want.ipv6_address_space in [None, []]: + raise F5ModuleError( + 'The ipv6_address_space cannot be empty, when split_tunnel is set to {0}'.format( + self.want.split_tunnel + ) + ) + if self.have.split_tunnel == 'true': + if self.have.ip_version == 'ipv4': + if self.want.ipv4_address_space is not None and not self.want.ipv4_address_space: + raise F5ModuleError( + 'Cannot remove ipv4_address_space when split_tunnel on device is: {0}'.format( + self.have.split_tunnel + ) + ) + if self.have.ip_version == 'ipv4-ipv6': + if self.want.ipv4_address_space is not None and not self.want.ipv4_address_space: + raise F5ModuleError( + 'Cannot remove ipv4_address_space when split_tunnel on device is: {0}'.format( + self.have.split_tunnel + ) + ) + if self.want.ipv6_address_space is not None and not self.want.ipv6_address_space: + raise F5ModuleError( + 'Cannot remove ipv6_address_space when split_tunnel on device is: {0}'.format( + self.have.split_tunnel + ) + ) + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/apm/resource/network-access/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/apm/resource/network-access/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/apm/resource/network-access/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/apm/resource/network-access/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/apm/resource/network-access/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + ip_version=dict( + choices=['ipv4', 'ipv4-ipv6'] + ), + split_tunnel=dict( + type='bool', + ), + allow_local_subnet=dict(type='bool'), + allow_local_dns=dict(type='bool'), + snat_pool=dict(), + description=dict(), + dtls=dict(type='bool'), + dtls_port=dict(type='int'), + ipv4_lease_pool=dict(), + ipv6_lease_pool=dict(), + excluded_ipv6_adresses=dict( + type='list', + elements='dict', + options=dict( + subnet=dict(), + ) + ), + excluded_ipv4_adresses=dict( + type='list', + elements='dict', + options=dict( + subnet=dict(), + ) + ), + excluded_dns_addresses=dict( + type='list', + elements='str', + ), + dns_address_space=dict( + type='list', + elements='str', + ), + ipv4_address_space=dict( + type='list', + elements='dict', + options=dict( + subnet=dict(), + ) + ), + ipv6_address_space=dict( + type='list', + elements='dict', + options=dict( + subnet=dict(), + ) + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_policy_fetch.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_policy_fetch.py new file mode 100644 index 00000000..8e2dd284 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_policy_fetch.py @@ -0,0 +1,507 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_apm_policy_fetch +short_description: Exports the APM policy or APM access profile from remote nodes. +description: + - Exports the APM policy or APM access profile from remote nodes. +version_added: "1.0.0" +options: + name: + description: + - The name of the APM policy or APM access profile exported to create a file on the remote device for downloading. + type: str + required: True + dest: + description: + - A directory to save the file into. + type: path + file: + description: + - The name of the file to be created on the remote device for downloading. + type: str + type: + description: + - Specifies the type of item to export from the device. + type: str + choices: + - profile_access + - access_policy + default: profile_access + force: + description: + - If C(no), the file will only be transferred if it does not exist in the the destination. + type: bool + default: yes + partition: + description: + - Device partition which contains the APM policy or APM access profile to export. + type: str + default: Common +notes: + - Due to ID685681 it is not possible to execute ng_* tools via REST API on v12.x and 13.x, once this is fixed + this restriction will be removed. + - Requires BIG-IP >= 14.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Export APM access profile + bigip_apm_policy_fetch: + name: foobar + file: export_foo + dest: /root/download + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Export APM access policy + bigip_apm_policy_fetch: + name: foobar + file: export_foo + dest: /root/download + type: access_policy + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Export APM access profile, autogenerate name + bigip_apm_policy_fetch: + name: foobar + dest: /root/download + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +name: + description: Name of the APM policy or APM access profile to be exported. + returned: changed + type: str + sample: APM_policy_global +file: + description: + - Name of the exported file on the remote BIG-IP to download. If not + specified, then this will be a randomly generated filename. + returned: changed + type: str + sample: foobar_file +dest: + description: Local path to download the exported APM policy. + returned: changed + type: str + sample: /root/downloads/profile-foobar_file.conf.tar.gz +type: + description: Set to specify the type of item to export. + returned: changed + type: str + sample: access_policy +''' + +import os +import tempfile +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version, download_file +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = {} + + api_attributes = [] + + returnables = [ + 'name', + 'file', + 'dest', + 'type', + 'force', + ] + + updatables = [] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def file(self): + if self._values['file'] is not None: + return self._values['file'] + result = next(tempfile._get_candidate_names()) + '.tar.gz' + self._values['file'] = result + return result + + @property + def fulldest(self): + if os.path.isdir(self.dest): + result = os.path.join(self.dest, self.file) + else: + if os.path.exists(os.path.dirname(self.dest)): + result = self.dest + else: + try: + # os.path.exists() can return false in some + # circumstances where the directory does not have + # the execute bit for the current user set, in + # which case the stat() call will raise an OSError + result = self.dest + os.stat(os.path.dirname(result)) + except OSError as e: + if "permission denied" in str(e).lower(): + raise F5ModuleError( + "Destination directory {0} is not accessible".format(os.path.dirname(self.dest)) + ) + raise F5ModuleError( + "Destination directory {0} does not exist".format(os.path.dirname(self.dest)) + ) + + if not os.access(os.path.dirname(result), os.W_OK): + raise F5ModuleError( + "Destination {0} not writable".format(os.path.dirname(result)) + ) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'apm'): + raise F5ModuleError( + "APM must be provisioned to use this module." + ) + + if self.version_less_than_14(): + raise F5ModuleError('Due to bug ID685681 it is not possible to use this module on TMOS version below 14.x') + + result = dict() + + self.export() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=True)) + send_teem(start, self.client, self.module, version) + return result + + def version_less_than_14(self): + version = tmos_version(self.client) + if Version(version) < Version('14.0.0'): + return True + return False + + def export(self): + if self.exists(): + return self.update() + else: + return self.create() + + def update(self): + if not self.want.force: + raise F5ModuleError( + "File '{0}' already exists.".format(self.want.fulldest) + ) + self.create_on_device() + self.execute() + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + self.execute() + return True + + def download(self): + self.download_from_device(self.want.fulldest) + if os.path.exists(self.want.fulldest): + return True + raise F5ModuleError( + "Failed to download the remote file." + ) + + def execute(self): + self.download() + self.remove_temp_file_from_device() + return True + + def exists(self): + self.policy_exists() + if os.path.exists(self.want.fulldest): + return True + return False + + def policy_exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + if self.want.type == 'access_policy': + uri = 'https://{0}:{1}/mgmt/tm/apm/policy/access-policy/{2}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + else: + uri = 'https://{0}:{1}/mgmt/tm/apm/profile/access/{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 resp.status == 404 or 'code' in response and response['code'] == 404: + raise F5ModuleError('The provided {0} with the name {1} does not exist on device.'.format( + self.want.type, self.want.name) + ) + + 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']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + cmd = 'ng_export -t {0} {1} {1} -p {2}'.format( + self.want.type, self.want.name, self.want.partition + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(cmd) + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'commandResult' in response: + raise F5ModuleError('Item export command failed.') + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + self._move_file_to_download() + + return True + + def _move_file_to_download(self): + if self.want.type == 'access_policy': + item = 'policy' + else: + item = 'profile' + + name = '{0}-{1}.conf.tar.gz'.format(item, self.want.name) + move_path = '/shared/tmp/{0} {1}/{2}'.format( + name, + '/shared/images', + self.want.file + ) + params = dict( + command='run', + utilCmdArgs=move_path + ) + + uri = "https://{0}:{1}/mgmt/tm/util/unix-mv/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + resp = self.client.api.post(uri, json=params) + + try: + response = resp.json() + if 'commandResult' in response: + if 'cannot stat' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def download_from_device(self, dest): + url = 'https://{0}:{1}/mgmt/cm/autodeploy/software-image-downloads/{2}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.file + ) + try: + download_file(self.client, url, dest) + except F5ModuleError: + raise F5ModuleError( + "Failed to download the file." + ) + if os.path.exists(self.want.dest): + return True + return False + + def remove_temp_file_from_device(self): + tpath_name = '/shared/images/{0}'.format(self.want.file) + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs=tpath_name + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True, + ), + dest=dict( + type='path' + ), + type=dict( + default='profile_access', + choices=['profile_access', 'access_policy'] + ), + file=dict(), + force=dict( + default='yes', + type='bool' + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_policy_import.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_policy_import.py new file mode 100644 index 00000000..7a1b3c21 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_apm_policy_import.py @@ -0,0 +1,453 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_apm_policy_import +short_description: Manage BIG-IP APM policy or APM access profile imports +description: + - Manage BIG-IP APM policy or APM access profile imports. +version_added: "1.0.0" +options: + name: + description: + - The name of the APM policy or APM access profile to create or override. + type: str + required: True + type: + description: + - Specifies the type of item to export from the device. + type: str + choices: + - profile_access + - access_policy + - profile_api_protection + default: profile_access + source: + description: + - Full path to a file to be imported into the BIG-IP APM. + type: path + force: + description: + - When set to C(yes) any existing policy with the same name will be overwritten by the new import. + - If a policy does not exist, this setting is ignored. + default: no + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + reuse_objects: + description: + - When set to C(yes) and objects referred within the policy exist on the BIG-IP, + those will be used instead of the objects defined in the policy. + - Reusing existing objects reduces configuration size. + - The configuration of existing objects might differ to the configuration of the objects defined in the policy! + default: yes + type: bool +notes: + - Due to ID685681 it is not possible to execute ng_* tools via REST API on v12.x and 13.x, once this is fixed + this restriction will be removed. + - Requires BIG-IP >= 14.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Import APM profile + bigip_apm_policy_import: + name: new_apm_profile + source: /root/apm_profile.tar.gz + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Import APM policy + bigip_apm_policy_import: + name: new_apm_policy + source: /root/apm_policy.tar.gz + type: access_policy + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Override existing APM policy + bigip_asm_policy: + name: new_apm_policy + source: /root/apm_policy.tar.gz + force: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Import APM profile without re-using existing configuration objects + bigip_apm_policy_import: + name: new_apm_profile + source: /root/apm_profile.tar.gz + reuse_objects: false + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +source: + description: Local path to APM policy file. + returned: changed + type: str + sample: /root/some_policy.tar.gz +name: + description: Name of the APM policy or APM access profile to be created/overwritten. + returned: changed + type: str + sample: APM_policy_global +type: + description: Set to specify type of item to export. + returned: changed + type: str + sample: access_policy +force: + description: Set when overwriting an existing policy or profile. + returned: changed + type: bool + sample: yes +reuse_objects: + description: Set when reusing existing objects on the BIG-IP. + returned: changed + type: bool + sample: yes +''' + +import os +import traceback +from datetime import datetime + + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version, upload_file +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + + ] + + returnables = [ + 'name', + 'source', + 'type', + + ] + + updatables = [ + + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'apm'): + raise F5ModuleError( + "APM must be provisioned to use this module." + ) + + if self.version_less_than_14(): + raise F5ModuleError('Due to bug ID685681 it is not possible to use this module on TMOS version below 14.x') + + result = dict() + + changed = self.policy_import() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def version_less_than_14(self): + version = tmos_version(self.client) + if Version(version) < Version('14.0.0'): + return True + return False + + def policy_import(self): + self._set_changed_options() + if self.module.check_mode: + return True + if self.exists(): + if self.want.force is False: + return False + + self.import_file_to_device() + self.remove_temp_file_from_device() + return True + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + if self.want.type == 'access_policy': + uri = "https://{0}:{1}/mgmt/tm/apm/policy/access-policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + else: + uri = "https://{0}:{1}/mgmt/tm/apm/profile/access/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def import_file_to_device(self): + name = os.path.split(self.want.source)[1] + self.upload_file_to_device(self.want.source, name) + + if self.want.reuse_objects is True: + reuse_objects = "-s" + else: + reuse_objects = "" + + cmd = 'ng_import {0} /var/config/rest/downloads/{1} {2} -p {3} -t {4}'.format( + reuse_objects, name, self.want.name, self.want.partition, self.want.type + ) + + uri = "https://{0}:{1}/mgmt/tm/util/bash/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(cmd) + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + if 'commandResult' in response: + raise F5ModuleError(response['commandResult']) + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_temp_file_from_device(self): + name = os.path.split(self.want.source)[1] + tpath_name = '/var/config/rest/downloads/{0}'.format(name) + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs=tpath_name + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True, + ), + source=dict(type='path'), + force=dict( + type='bool', + default='no' + ), + type=dict( + default='profile_access', + choices=['profile_access', 'access_policy', 'profile_api_protection'] + ), + reuse_objects=dict( + type='bool', + default='yes' + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_advanced_settings.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_advanced_settings.py new file mode 100644 index 00000000..1012a539 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_advanced_settings.py @@ -0,0 +1,432 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2020, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_asm_advanced_settings +short_description: Manage BIG-IP system ASM advanced settings +description: + - Manage BIG-IP system ASM advanced settings. +version_added: "1.4.0" +options: + name: + description: + - The ASM setting to manipulate. + type: str + required: True + state: + description: + - The state of the setting on the system. When C(present), guarantees + that an existing setting is set to C(value). When C(reset), sets the + setting back to the default value. At least one of value and state + C(reset) are required. + type: str + choices: + - present + - reset + default: present + value: + description: + - The value to set the key to. At least one of value and state C(reset) + are required. + type: str +notes: + - Requires BIG-IP version 12.0.0 or greater +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Set the long_request_buffer_size asm setting + bigip_asm_advanced_settings: + name: long_request_buffer_size + value: 20000000 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Reset the long_request_buffer_size to default value + bigip_asm_advanced_settings: + name: long_request_buffer_size + state: reset + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +name: + description: The name of the ASM setting that was specified + returned: changed and success + type: str + sample: long_request_buffer_size +default_value: + description: The default value of the specified ASM setting + returned: changed and success + type: str + sample: '10000000' +value: + description: The value you set the ASM setting to + returned: changed and success + type: str + sample: '20000000' +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultValue': 'default_value', + } + api_attributes = [ + 'value', + ] + updatables = [ + 'value', + ] + returnables = [ + 'name', + 'value', + 'default_value', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + + @property + def value(self): + if self._values['value'] is None: + return None + try: + return int(self._values['value']) + except ValueError: + return self._values['value'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def value(self): + if self._values['value'] is None: + return None + return str(self._values['value']) + + @property + def default_value(self): + if self._values['default_value'] is None: + return None + return str(self._values['default_value']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def value(self): + if self.want.state == 'reset': + if str(self.have.value) != str(self.have.default_value): + return self.have.default_value + if self.want.value != self.have.value: + return self.want.value + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.pop('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + self.setting_id = None + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + changed['name'] = self.want.name + changed['default_value'] = self.have.default_value + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'asm'): + raise F5ModuleError( + "ASM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "reset": + changed = self.reset() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return False + else: + return self.update() + + def reset(self): + self._get_setting_id() + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.reset_on_device() + self.want.update({'name': self.want.name}) + self.want.update({'value': self.have.default_value}) + if self.exists(): + return True + else: + raise F5ModuleError( + "Failed to reset the: {0} asm setting.".format(self.want.key) + ) + + def update(self): + if self.want.value is None: + raise F5ModuleError( + "When setting a key, a value must be supplied" + ) + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def _get_setting_id(self): + uri = "https://{0}:{1}/mgmt/tm/asm/advanced-settings/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$filter=name+eq+'{0}'&$select=id".format(self.want.name) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + if 'items' in response and response['items'] != []: + self.setting_id = response['items'][0]['id'] + + if not self.setting_id: + raise F5ModuleError("The setting: {0} was not found.".format(self.want.name)) + + def exists(self): + self._get_setting_id() + uri = "https://{0}:{1}/mgmt/tm/asm/advanced-settings/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.setting_id + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if str(response['value']) == str(self.want.value): + return True + return False + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/asm/advanced-settings/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.setting_id + ) + + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + return ApiParameters(params=response) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/asm/advanced-settings/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.setting_id + ) + + resp = self.client.api.patch(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def reset_on_device(self): + uri = "https://{0}:{1}/mgmt/tm/asm/advanced-settings/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.setting_id + ) + params = dict( + value=self.have.default_value + ) + + resp = self.client.api.patch(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + state=dict( + default='present', + choices=['present', 'reset'] + ), + value=dict() + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() 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 new file mode 100644 index 00000000..262e99b7 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_dos_application.py @@ -0,0 +1,1346 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_asm_dos_application +short_description: Manage application settings for a DOS profile +description: + - Manages Application settings for an ASM/AFM DOS profile. +version_added: "1.0.0" +options: + profile: + description: + - Specifies the name of the profile to manage application settings in. + type: str + required: True + rtbh_duration: + description: + - Specifies the duration of the RTBH BGP route advertisement, in seconds. + - The acceptable range is between 0 and 4294967295 inclusive. + type: int + rtbh_enable: + description: + - Specifies whether to enable Remote Triggered Black Hole C(RTBH) of attacking IPs by advertising BGP routes. + type: bool + scrubbing_duration: + description: + - Specifies the duration of the Traffic Scrubbing BGP route advertisement, in seconds. + - The acceptable range is between 0 and 4294967295 inclusive. + type: int + scrubbing_enable: + description: + - Specifies whether to enable Traffic Scrubbing during attacks by advertising BGP routes. + type: bool + single_page_application: + description: + - Specifies, when C(yes), that the system supports a Single Page Applications. + type: bool + trigger_irule: + description: + - Specifies, when C(yes), that the system activates an Application DoS iRule event. + type: bool + geolocations: + description: + - Manages the geolocations countries whitelist, blacklist. + type: dict + suboptions: + whitelist: + description: + - A list of countries to be put on the whitelist, must not have overlapping elements with C(blacklist). + type: list + elements: str + blacklist: + description: + - A list of countries to be put on the blacklist, must not have overlapping elements with C(whitelist). + type: list + elements: str + heavy_urls: + description: + - Manages Heavy URL protection. + - Heavy URLs are a small number of site URLs that might consume considerable server resources per request. + type: dict + suboptions: + auto_detect: + description: + - Enables or disables automatic heavy URL detection. + type: bool + latency_threshold: + description: + - Specifies the latency threshold for automatic heavy URL detection. + - The acceptable range is between 0 and 4294967295 miliseconds inclusive. + type: int + exclude: + description: + - Specifies a list of URLs or wildcards to exclude from the heavy URLs. + type: list + elements: str + include: + description: + - Configures additional URLs to include in the heavy URLs that were auto-detected. + type: list + elements: dict + suboptions: + url: + description: + - Specifies the URL to be added to the list of heavy URLs, in addition to those automatically detected. + type: str + required: True + threshold: + description: + - Specifies the threshold of requests per second, where the URL in question is considered under attack. + - The acceptable range is between 1 and 4294967295 inclusive, or C(auto). + type: str + mobile_detection: + description: + - Configures detection of mobile applications built with the Anti-Bot Mobile SDK and defines how requests + from these mobile application clients are handled. + type: dict + suboptions: + enabled: + description: + - When C(yes), requests from mobile applications built with Anti-Bot Mobile SDK will be detected and handled + according to the parameters set. + - When C(no), these requests will be handled like any other request which may let attacks in, or cause false + positives. + type: bool + allow_android_rooted_device: + description: + - When C(yes) the device will allow traffic from rooted Android devices. + type: bool + allow_any_android_package: + description: + - When C(yes) allows any application publisher. + - A publisher is identified by the certificate used to sign the application. + type: bool + allow_any_ios_package: + description: + - When C(yes) allows any iOS package. + - A package name is the unique identifier of the mobile application. + type: bool + allow_jailbroken_devices: + description: + - When C(yes) allows traffic from jailbroken iOS devices. + type: bool + allow_emulators: + description: + - When C(yes) allows traffic from applications run on emulators. + type: bool + client_side_challenge_mode: + description: + - Action to take when a CAPTCHA or Client Side Integrity challenge needs to be presented. + - The mobile application user will not see a CAPTCHA challenge and the mobile application will not be + presented with the Client Side Integrity challenge. The such options for mobile applications are C(pass) + or C(cshui). + - When C(pass) the traffic is passed without incident. + - When C(cshui) the SDK checks for human interactions with the screen in the last few seconds. + If none are detected, the traffic is blocked. + type: str + choices: + - pass + - cshui + ios_allowed_package_names: + description: + - Specifies the names of iOS packages to allow traffic on. + - This option has no effect when C(allow_any_ios_package) is set to C(yes). + type: list + elements: str + android_publishers: + description: + - This option has no effect when C(allow_any_android_package) is set to C(yes). + - Specifies the allowed publisher certificates for android applications. + - The publisher certificate needs to be installed on the BIG-IP beforehand. + - "The certificate name located on a different partition than the one specified + in the C(partition) parameter needs to be provided in C(full_path) format, e.g. C(/Foo/cert.crt)." + type: list + elements: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures that the Application object exists. + - When C(state) is C(absent), ensures that the Application object is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP >= 13.1.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an ASM dos application profile + bigip_asm_dos_application: + profile: dos_foo + geolocations: + blacklist: + - Afghanistan + - Andora + whitelist: + - Cuba + heavy_urls: + auto_detect: yes + latency_threshold: 1000 + rtbh_duration: 3600 + rtbh_enable: yes + single_page_application: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Update an ASM dos application profile + bigip_asm_dos_application: + profile: dos_foo + mobile_detection: + enabled: yes + allow_any_ios_package: yes + allow_emulators: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove an ASM dos application profile + bigip_asm_dos_application: + profile: dos_foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +rtbh_enable: + description: Enables Remote Triggered Black Hole of attacking IPs. + returned: changed + type: bool + sample: no +rtbh_duration: + description: The duration of the RTBH BGP route advertisement. + returned: changed + type: int + sample: 3600 +scrubbing_enable: + description: Enables Traffic Scrubbing during attacks. + returned: changed + type: bool + sample: yes +scrubbing_duration: + description: The duration of the Traffic Scrubbing BGP route advertisement. + returned: changed + type: int + sample: 3600 +single_page_application: + description: Enables support of Single Page Applications. + returned: changed + type: bool + sample: no +trigger_irule: + description: Activates an Application DoS iRule event. + returned: changed + type: bool + sample: yes +geolocations: + description: Specifies geolocations countries whitelist, blacklist. + type: complex + returned: changed + contains: + whitelist: + description: A list of countries to be put on the whitelist. + returned: changed + type: list + sample: ['United States, United Kingdom'] + blacklist: + description: A list of countries to be put on the blacklist. + returned: changed + type: list + sample: ['Russia', 'Germany'] + sample: hash/dictionary of values +heavy_urls: + description: Manages Heavy URL protection. + type: complex + returned: changed + contains: + auto_detect: + description: Enables or disables automatic heavy URL detection. + returned: changed + type: bool + sample: yes + latency_threshold: + description: Specifies the latency threshold for automatic heavy URL detection. + returned: changed + type: int + sample: 2000 + exclude: + description: Specifies a list of URLs or wildcards to exclude from the heavy URLs. + returned: changed + type: list + sample: ['/exclude.html', '/exclude2.html'] + include: + description: Configures additional URLs to include in the heavy URLs. + type: complex + returned: changed + contains: + url: + description: The URL to be added to the list of heavy URLs. + returned: changed + type: str + sample: /include.html + threshold: + description: The threshold of requests per second. + returned: changed + type: str + sample: auto + sample: hash/dictionary of values + sample: hash/dictionary of values +mobile_detection: + description: Configures detection of mobile applications built with the Anti-Bot Mobile SDK. + type: complex + returned: changed + contains: + enable: + description: Enables or disables automatic mobile detection. + returned: changed + type: bool + sample: yes + allow_android_rooted_device: + description: Allows traffic from rooted Android devices. + returned: changed + type: bool + sample: no + allow_any_android_package: + description: Allows any application publisher. + returned: changed + type: bool + sample: no + allow_any_ios_package: + description: Allows any iOS package. + returned: changed + type: bool + sample: yes + allow_jailbroken_devices: + description: Allows traffic from jailbroken iOS devices. + returned: changed + type: bool + sample: no + allow_emulators: + description: Allows traffic from applications run on emulators. + returned: changed + type: bool + sample: yes + client_side_challenge_mode: + description: Action to take when a CAPTCHA or Client Side Integrity challenge needs to be presented. + returned: changed + type: str + sample: pass + ios_allowed_package_names: + description: The names of iOS packages to allow traffic on. + returned: changed + type: list + sample: ['package1','package2'] + android_publishers: + description: The allowed publisher certificates for android applications. + returned: changed + type: list + sample: ['/Common/cert1.crt', '/Common/cert2.crt'] + sample: hash/dictionary of values +''' + +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import ( + cmp_simple_list, compare_complex_list +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'rtbhDurationSec': 'rtbh_duration', + 'rtbhEnable': 'rtbh_enable', + 'scrubbingDurationSec': 'scrubbing_duration', + 'scrubbingEnable': 'scrubbing_enable', + 'singlePageApplication': 'single_page_application', + 'triggerIrule': 'trigger_irule', + 'heavyUrls': 'heavy_urls', + 'mobileDetection': 'mobile_detection', + } + + api_attributes = [ + 'geolocations', + 'rtbhDurationSec', + 'rtbhEnable', + 'scrubbingDurationSec', + 'scrubbingEnable', + 'singlePageApplication', + 'triggerIrule', + 'heavyUrls', + 'mobileDetection', + ] + + returnables = [ + 'rtbh_duration', + 'rtbh_enable', + 'scrubbing_duration', + 'scrubbing_enable', + 'single_page_application', + 'trigger_irule', + 'enable_mobile_detection', + 'allow_android_rooted_device', + 'allow_any_android_package', + 'allow_any_ios_package', + 'allow_jailbroken_devices', + 'allow_emulators', + 'client_side_challenge_mode', + 'ios_allowed_package_names', + 'android_publishers', + 'auto_detect', + 'latency_threshold', + 'hw_url_exclude', + 'hw_url_include', + 'geo_blacklist', + 'geo_whitelist', + ] + + updatables = [ + 'rtbh_duration', + 'rtbh_enable', + 'scrubbing_duration', + 'scrubbing_enable', + 'single_page_application', + 'trigger_irule', + 'enable_mobile_detection', + 'allow_android_rooted_device', + 'allow_any_android_package', + 'allow_any_ios_package', + 'allow_jailbroken_devices', + 'allow_emulators', + 'client_side_challenge_mode', + 'ios_allowed_package_names', + 'android_publishers', + 'auto_detect', + 'latency_threshold', + 'hw_url_exclude', + 'hw_url_include', + 'geo_blacklist', + 'geo_whitelist', + ] + + +class ApiParameters(Parameters): + @property + def enable_mobile_detection(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection']['enabled'] + + @property + def allow_android_rooted_device(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection']['allowAndroidRootedDevice'] + + @property + def allow_any_android_package(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection']['allowAnyAndroidPackage'] + + @property + def allow_any_ios_package(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection']['allowAnyIosPackage'] + + @property + def allow_jailbroken_devices(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection']['allowJailbrokenDevices'] + + @property + def allow_emulators(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection']['allowEmulators'] + + @property + def client_side_challenge_mode(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection']['clientSideChallengeMode'] + + @property + def ios_allowed_package_names(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection'].get('iosAllowedPackageNames', None) + + @property + def android_publishers(self): + if self._values['mobile_detection'] is None or 'androidPublishers' not in self._values['mobile_detection']: + return None + result = [fq_name(publisher['partition'], publisher['name']) + for publisher in self._values['mobile_detection']['androidPublishers']] + return result + + @property + def auto_detect(self): + if self._values['heavy_urls'] is None: + return None + return self._values['heavy_urls']['automaticDetection'] + + @property + def latency_threshold(self): + if self._values['heavy_urls'] is None: + return None + return self._values['heavy_urls']['latencyThreshold'] + + @property + def hw_url_exclude(self): + if self._values['heavy_urls'] is None: + return None + return self._values['heavy_urls'].get('exclude', None) + + @property + def hw_url_include(self): + if self._values['heavy_urls'] is None: + return None + return self._values['heavy_urls'].get('includeList', None) + + @property + def geo_blacklist(self): + if self._values['geolocations'] is None: + return None + result = list() + for item in self._values['geolocations']: + if 'blackListed' in item and item['blackListed'] is True: + result.append(item['name']) + if result: + return result + + @property + def geo_whitelist(self): + if self._values['geolocations'] is None: + return None + result = list() + for item in self._values['geolocations']: + if 'whiteListed' in item and item['whiteListed'] is True: + result.append(item['name']) + if result: + return result + + +class ModuleParameters(Parameters): + @property + def rtbh_duration(self): + if self._values['rtbh_duration'] is None: + return None + if 0 <= self._values['rtbh_duration'] <= 4294967295: + return self._values['rtbh_duration'] + raise F5ModuleError( + "Valid 'rtbh_duration' must be in range 0 - 4294967295 seconds." + ) + + @property + def rtbh_enable(self): + result = flatten_boolean(self._values['rtbh_enable']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def scrubbing_duration(self): + if self._values['scrubbing_duration'] is None: + return None + if 0 <= self._values['scrubbing_duration'] <= 4294967295: + return self._values['scrubbing_duration'] + raise F5ModuleError( + "Valid 'scrubbing_duration' must be in range 0 - 4294967295 seconds." + ) + + @property + def scrubbing_enable(self): + result = flatten_boolean(self._values['scrubbing_enable']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def single_page_application(self): + result = flatten_boolean(self._values['single_page_application']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def trigger_irule(self): + result = flatten_boolean(self._values['trigger_irule']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def enable_mobile_detection(self): + if self._values['mobile_detection'] is None: + return None + result = flatten_boolean(self._values['mobile_detection']['enabled']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def allow_android_rooted_device(self): + if self._values['mobile_detection'] is None: + return None + result = flatten_boolean(self._values['mobile_detection']['allow_android_rooted_device']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + return result + + @property + def allow_any_android_package(self): + if self._values['mobile_detection'] is None: + return None + result = flatten_boolean(self._values['mobile_detection']['allow_any_android_package']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + return result + + @property + def allow_any_ios_package(self): + if self._values['mobile_detection'] is None: + return None + result = flatten_boolean(self._values['mobile_detection']['allow_any_ios_package']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + return result + + @property + def allow_jailbroken_devices(self): + if self._values['mobile_detection'] is None: + return None + result = flatten_boolean(self._values['mobile_detection']['allow_jailbroken_devices']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + return result + + @property + def allow_emulators(self): + if self._values['mobile_detection'] is None: + return None + result = flatten_boolean(self._values['mobile_detection']['allow_emulators']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + return result + + @property + def client_side_challenge_mode(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection']['client_side_challenge_mode'] + + @property + def ios_allowed_package_names(self): + if self._values['mobile_detection'] is None: + return None + return self._values['mobile_detection']['ios_allowed_package_names'] + + @property + def android_publishers(self): + if self._values['mobile_detection'] is None or self._values['mobile_detection']['android_publishers'] is None: + return None + result = [fq_name(self.partition, item) for item in self._values['mobile_detection']['android_publishers']] + return result + + @property + def auto_detect(self): + if self._values['heavy_urls'] is None: + return None + result = flatten_boolean(self._values['heavy_urls']['auto_detect']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def latency_threshold(self): + if self._values['heavy_urls'] is None or self._values['heavy_urls']['latency_threshold'] is None: + return None + if 0 <= self._values['heavy_urls']['latency_threshold'] <= 4294967295: + return self._values['heavy_urls']['latency_threshold'] + raise F5ModuleError( + "Valid 'latency_threshold' must be in range 0 - 4294967295 milliseconds." + ) + + @property + def hw_url_exclude(self): + if self._values['heavy_urls'] is None: + return None + return self._values['heavy_urls']['exclude'] + + @property + def hw_url_include(self): + if self._values['heavy_urls'] is None or self._values['heavy_urls']['include'] is None: + return None + result = list() + for item in self._values['heavy_urls']['include']: + element = dict() + element['url'] = self._correct_url(item['url']) + element['name'] = 'URL{0}'.format(self._correct_url(item['url'])) + if 'threshold' in item: + element['threshold'] = self._validate_threshold(item['threshold']) + result.append(element) + return result + + def _validate_threshold(self, item): + if item == 'auto': + return item + if 1 <= int(item) <= 4294967295: + return item + raise F5ModuleError( + "Valid 'url threshold' must be in range 1 - 4294967295 requests per second or 'auto'." + ) + + def _correct_url(self, item): + if item.startswith('/'): + return item + return "/{0}".format(item) + + @property + def geo_blacklist(self): + if self._values['geolocations'] is None: + return None + whitelist = self.geo_whitelist + blacklist = self._values['geolocations']['blacklist'] + if whitelist and blacklist: + if not set(whitelist).isdisjoint(set(blacklist)): + raise F5ModuleError('Cannot specify the same element in blacklist and whitelist.') + return blacklist + + @property + def geo_whitelist(self): + if self._values['geolocations'] is None: + return None + return self._values['geolocations']['whitelist'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def geolocations(self): + if self._values['geo_blacklist'] is None and self._values['geo_whitelist'] is None: + return None + result = list() + if self._values['geo_blacklist']: + for item in self._values['geo_blacklist']: + element = dict() + element['name'] = item + element['blackListed'] = True + result.append(element) + if self._values['geo_whitelist']: + for item in self._values['geo_whitelist']: + element = dict() + element['name'] = item + element['whiteListed'] = True + result.append(element) + if result: + return result + + @property + def heavy_urls(self): + tmp = dict() + tmp['automaticDetection'] = self._values['auto_detect'] + tmp['latencyThreshold'] = self._values['latency_threshold'] + tmp['exclude'] = self._values['hw_url_exclude'] + tmp['includeList'] = self._values['hw_url_include'] + result = self._filter_params(tmp) + if result: + return result + + @property + def mobile_detection(self): + tmp = dict() + tmp['enabled'] = self._values['enable_mobile_detection'] + tmp['allowAndroidRootedDevice'] = self._values['allow_android_rooted_device'] + tmp['allowAnyAndroidPackage'] = self._values['allow_any_android_package'] + tmp['allowAnyIosPackage'] = self._values['allow_any_ios_package'] + tmp['allowJailbrokenDevices'] = self._values['allow_jailbroken_devices'] + tmp['allowEmulators'] = self._values['allow_emulators'] + tmp['clientSideChallengeMode'] = self._values['client_side_challenge_mode'] + tmp['iosAllowedPackageNames'] = self._values['ios_allowed_package_names'] + tmp['androidPublishers'] = self._values['android_publishers'] + result = self._filter_params(tmp) + if result: + return result + + +class ReportableChanges(Changes): + returnables = [ + 'rtbh_duration', + 'rtbh_enable', + 'scrubbing_duration', + 'scrubbing_enable', + 'single_page_application', + 'trigger_irule', + 'heavy_urls', + 'mobile_detection', + 'geolocations', + ] + + def _convert_include_list(self, items): + if items is None: + return None + result = list() + for item in items: + element = dict() + element['url'] = item['url'] + if 'threshold' in item: + element['threshold'] = item['threshold'] + result.append(element) + if result: + return result + + @property + def geolocations(self): + tmp = dict() + tmp['blacklist'] = self._values['geo_blacklist'] + tmp['whitelist'] = self._values['geo_whitelist'] + result = self._filter_params(tmp) + if result: + return result + + @property + def heavy_urls(self): + tmp = dict() + tmp['auto_detect'] = flatten_boolean(self._values['auto_detect']) + tmp['latency_threshold'] = self._values['latency_threshold'] + tmp['exclude'] = self._values['hw_url_exclude'] + tmp['include'] = self._convert_include_list(self._values['hw_url_include']) + result = self._filter_params(tmp) + if result: + return result + + @property + def mobile_detection(self): + tmp = dict() + tmp['enabled'] = flatten_boolean(self._values['enable_mobile_detection']) + tmp['allow_android_rooted_device'] = flatten_boolean(self._values['allow_android_rooted_device']) + tmp['allow_any_android_package'] = flatten_boolean(self._values['allow_any_android_package']) + tmp['allow_any_ios_package'] = flatten_boolean(self._values['allow_any_ios_package']) + tmp['allow_jailbroken_devices'] = flatten_boolean(self._values['allow_jailbroken_devices']) + tmp['allow_emulators'] = flatten_boolean(self._values['allow_emulators']) + tmp['client_side_challenge_mode'] = self._values['client_side_challenge_mode'] + tmp['ios_allowed_package_names'] = self._values['ios_allowed_package_names'] + tmp['android_publishers'] = self._values['android_publishers'] + result = self._filter_params(tmp) + if result: + return result + + @property + def rtbh_enable(self): + result = flatten_boolean(self._values['rtbh_enable']) + return result + + @property + def scrubbing_enable(self): + result = flatten_boolean(self._values['scrubbing_enable']) + return result + + @property + def single_page_application(self): + result = flatten_boolean(self._values['single_page_application']) + return result + + @property + def trigger_irule(self): + result = flatten_boolean(self._values['trigger_irule']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def hw_url_include(self): + if self.want.hw_url_include is None: + return None + if self.have.hw_url_include is None and self.want.hw_url_include == []: + return None + if self.have.hw_url_include is None: + return self.want.hw_url_include + + wants = self.want.hw_url_include + haves = list() + # First we remove extra keys in have for the same elements + for want in wants: + for have in self.have.hw_url_include: + if want['url'] == have['url']: + entry = self._filter_have(want, have) + haves.append(entry) + # Next we do compare the lists as normal + result = compare_complex_list(wants, haves) + return result + + def _filter_have(self, want, have): + to_check = set(want.keys()).intersection(set(have.keys())) + result = dict() + for k in list(to_check): + result[k] = have[k] + return result + + @property + def hw_url_exclude(self): + result = cmp_simple_list(self.want.hw_url_exclude, self.have.hw_url_exclude) + return result + + @property + def geo_blacklist(self): + result = cmp_simple_list(self.want.geo_blacklist, self.have.geo_blacklist) + return result + + @property + def geo_whitelist(self): + result = cmp_simple_list(self.want.geo_whitelist, self.have.geo_whitelist) + return result + + @property + def android_publishers(self): + result = cmp_simple_list(self.want.android_publishers, self.have.android_publishers) + return result + + @property + def ios_allowed_package_names(self): + result = cmp_simple_list(self.want.ios_allowed_package_names, self.have.ios_allowed_package_names) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'asm'): + raise F5ModuleError( + "ASM must be provisioned to use this module." + ) + + if self.version_less_than_13_1(): + raise F5ModuleError('Module supported on TMOS versions 13.1.x and above') + + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def version_less_than_13_1(self): + version = tmos_version(self.client) + if Version(version) < Version('13.1.0'): + return True + return False + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def profile_exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + 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), + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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( + 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: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.profile + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile), + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".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.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".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: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".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: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + profile=dict( + required=True, + ), + geolocations=dict( + type='dict', + options=dict( + blacklist=dict( + type='list', + elements='str', + ), + whitelist=dict( + type='list', + elements='str', + ), + ), + ), + heavy_urls=dict( + type='dict', + options=dict( + auto_detect=dict(type='bool'), + latency_threshold=dict(type='int'), + exclude=dict( + type='list', + elements='str', + ), + include=dict( + type='list', + elements='dict', + options=dict( + url=dict(required=True), + threshold=dict(), + ), + ) + ), + ), + mobile_detection=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + allow_android_rooted_device=dict(type='bool'), + allow_any_android_package=dict(type='bool'), + allow_any_ios_package=dict(type='bool'), + allow_jailbroken_devices=dict(type='bool'), + allow_emulators=dict(type='bool'), + client_side_challenge_mode=dict(choices=['cshui', 'pass']), + ios_allowed_package_names=dict( + type='list', + elements='str', + ), + android_publishers=dict( + type='list', + elements='str', + ) + ) + ), + rtbh_duration=dict(type='int'), + rtbh_enable=dict(type='bool'), + scrubbing_duration=dict(type='int'), + scrubbing_enable=dict(type='bool'), + single_page_application=dict(type='bool'), + trigger_irule=dict(type='bool'), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_fetch.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_fetch.py new file mode 100644 index 00000000..f6e59bd6 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_fetch.py @@ -0,0 +1,703 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_asm_policy_fetch +short_description: Exports the ASM policy from remote nodes. +description: + - Exports the ASM policy from remote nodes. +version_added: "1.0.0" +options: + name: + description: + - The name of the policy exported to create a file on the remote device for downloading. + type: str + required: True + dest: + description: + - A directory to save the policy file into. + - This option is ignored when C(inline) is set to c(yes). + type: path + file: + description: + - The name of the file to be created on the remote device for downloading. + - When C(binary) is set to C(no) the ASM policy is in XML format. + type: str + inline: + description: + - If C(yes), the ASM policy is exported C(inline) as a string instead of a file. + - The policy can be be retrieved in the playbook C(result) dictionary under the C(inline_policy) key. + type: bool + compact: + description: + - If C(yes), only the ASM policy custom settings is exported. + - Only applies to XML type ASM policy exports. + type: bool + base64: + description: + - If C(yes), the returned C(inline) ASM policy content is Base64 encoded. + - Only applies to C(inline) ASM policy exports. + type: bool + binary: + description: + - If C(yes), the exported ASM policy is in binary format. + - Only applies to C(file) ASM policy exports. + type: bool + force: + description: + - If C(no), the file will only be transferred if it does not exist in the the destination. + default: yes + type: bool + partition: + description: + - Device partition which contains the ASM policy to export. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) + - Nitin Khanna (@nitinthewiz) +''' + +EXAMPLES = r''' +- name: Export policy in binary format + bigip_asm_policy_fetch: + name: foobar + file: export_foo + dest: /root/download + binary: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Export policy inline base64 encoded format + bigip_asm_policy_fetch: + name: foobar + inline: yes + base64: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Export policy in XML format + bigip_asm_policy_fetch: + name: foobar + file: export_foo + dest: /root/download + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Export compact policy in XML format + bigip_asm_policy_fetch: + name: foobar + file: export_foo.xml + dest: /root/download/ + compact: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Export policy in binary format, autogenerate name + bigip_asm_policy_fetch: + name: foobar + dest: /root/download/ + binary: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +name: + description: Name of the ASM policy to be exported. + returned: changed + type: str + sample: Asm_APP1_Transparent +dest: + description: Local path to download the exported ASM policy. + returned: changed + type: str + sample: /root/downloads/foobar.xml +file: + description: + - Name of the policy file on the remote BIG-IP to download. If not + specified, then this is a randomly generated filename. + returned: changed + type: str + sample: foobar.xml +inline: + description: Set when the ASM policy to be exported is inline + returned: changed + type: bool + sample: yes +compact: + description: Set only to export custom ASM policy settings. + returned: changed + type: bool + sample: no +base64: + description: Set to encode inline export in Base64 format. + returned: changed + type: bool + sample: no +binary: + description: Set to export the ASM policy in binary format. + returned: changed + type: bool + sample: yes +''' + +import os +import time +import tempfile +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, download_asm_file, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'filename': 'file', + 'minimal': 'compact', + 'isBase64': 'base64', + } + + api_attributes = [ + 'inline', + 'minimal', + 'isBase64', + 'policyReference', + 'filename', + ] + + returnables = [ + 'file', + 'compact', + 'base64', + 'inline', + 'force', + 'binary', + 'dest', + 'name', + 'inline_policy', + ] + + updatables = [ + + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def file(self): + if self._values['file'] is not None: + return self._values['file'] + if self.binary: + result = next(tempfile._get_candidate_names()) + '.plc' + else: + result = next(tempfile._get_candidate_names()) + '.xml' + self._values['file'] = result + return result + + @property + def fulldest(self): + if os.path.isdir(self.dest): + result = os.path.join(self.dest, self.file) + else: + if os.path.exists(os.path.dirname(self.dest)): + result = self.dest + else: + try: + # os.path.exists() can return false in some + # circumstances where the directory does not have + # the execute bit for the current user set, in + # which case the stat() call will raise an OSError + result = self.dest + os.stat(os.path.dirname(result)) + except OSError as e: + if "permission denied" in str(e).lower(): + raise F5ModuleError( + "Destination directory {0} is not accessible".format(os.path.dirname(self.dest)) + ) + raise F5ModuleError( + "Destination directory {0} does not exist".format(os.path.dirname(self.dest)) + ) + + if not os.access(os.path.dirname(result), os.W_OK): + raise F5ModuleError( + "Destination {0} not writable".format(os.path.dirname(result)) + ) + return result + + @property + def inline(self): + result = flatten_boolean(self._values['inline']) + if result == 'yes': + return True + elif result == 'no': + return False + + @property + def compact(self): + result = flatten_boolean(self._values['compact']) + if result == 'yes': + return True + elif result == 'no': + return False + + @property + def base64(self): + result = flatten_boolean(self._values['base64']) + if result == 'yes': + return True + elif result == 'no': + return False + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'asm'): + raise F5ModuleError( + "ASM must be provisioned to use this module." + ) + result = dict() + + self.export() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=True)) + send_teem(start, self.client, self.module, version) + return result + + def export(self): + if self.exists(): + return self.update() + else: + return self.create() + + def update(self): + if not self.want.force: + raise F5ModuleError( + "File '{0}' already exists.".format(self.want.fulldest) + ) + self.create() + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + if self.want.binary: + self.export_binary() + return True + self.create_on_device() + if not self.want.inline: + self.execute() + return True + + def export_binary(self): + self.export_binary_on_device() + self.execute() + return True + + def download(self): + self.download_from_device(self.want.fulldest) + if os.path.exists(self.want.fulldest): + return True + raise F5ModuleError( + "Failed to download the remote file." + ) + + def execute(self): + self.download() + self.remove_temp_policy_from_device() + return True + + def exists(self): + self.policy_exists() + if not self.want.inline: + if os.path.exists(self.want.fulldest): + return True + return False + + def policy_exists(self): + uri = 'https://{0}:{1}/mgmt/tm/asm/policies/'.format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$filter=contains(name,'{0}')+and+contains(partition,'{1}')&$select=name,partition".format( + self.want.name, self.want.partition + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' in response and response['items'] != []: + # because api filter on ASM is broken when names that contain numbers at the end we need to work around it + for policy in response['items']: + if policy['name'] == self.want.name and policy['partition'] == self.want.partition: + return True + raise F5ModuleError( + "The specified ASM policy {0} on partition {1} does not exist on device.".format( + self.want.name, self.want.partition + ) + ) + + def create_on_device(self): + self._set_policy_link() + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/asm/tasks/export-policy/".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result, output, file_size = self.wait_for_task(response['id']) + if result and output: + if 'file' in output: + self.changes.update(dict(inline_policy=output['file'])) + if result: + self.want.file_size = file_size + return True + + def wait_for_task(self, task_id): + uri = "https://{0}:{1}/mgmt/tm/asm/tasks/export-policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + task_id + ) + while True: + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if response['status'] in ['COMPLETED', 'FAILURE']: + break + time.sleep(1) + + if response['status'] == 'FAILURE': + raise F5ModuleError( + 'Failed to export ASM policy.' + ) + if response['status'] == 'COMPLETED': + if not self.want.inline: + return True, None, response['result']['fileSize'] + else: + return True, response['result'], response['result']['fileSize'] + + def _set_policy_link(self): + policy_link = None + uri = 'https://{0}:{1}/mgmt/tm/asm/policies/'.format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$filter=contains(name,'{0}')+and+contains(partition,'{1}')&$select=name,partition".format( + self.want.name, self.want.partition + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'items' in response and response['items'] != []: + # because api filter on ASM is broken when names that contain numbers at the end we need to work around it + for policy in response['items']: + if policy['name'] == self.want.name and policy['partition'] == self.want.partition: + policy_link = policy['selfLink'] + + if not policy_link: + raise F5ModuleError("The policy was not found") + + self.changes.update(dict(policyReference={'link': policy_link})) + return True + + def export_binary_on_device(self): + full_name = fq_name(self.want.partition, self.want.name) + cmd = 'tmsh save asm policy {0} bin-file {1}'.format(full_name, self.want.file) + uri = "https://{0}:{1}/mgmt/tm/util/bash/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(cmd) + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + if 'commandResult' in response: + if 'Error' in response['commandResult'] or 'error' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + self._stat_binary_on_device() + self._move_binary_to_download() + + return True + + def _stat_binary_on_device(self): + params = dict( + command='run', + utilCmdArgs='/var/tmp/{0} -l'.format(self.want.file) + ) + + uri = "https://{0}:{1}/mgmt/tm/util/unix-ls/".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'commandResult' not in response: + raise F5ModuleError("Failed to obtain file information, aborting.") + + if 'Error' in response['commandResult'] or 'error' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + + if '/var/tmp/{0}'.format(self.want.file) not in response['commandResult']: + raise F5ModuleError("Cannot get size of exported binary file, aborting") + + size = response['commandResult'] + + self.want.file_size = int(size.split()[4]) + return True + + def _move_binary_to_download(self): + name = '{0}~{1}'.format(self.client.provider['user'], self.want.file) + move_path = '/var/tmp/{0} {1}/{2}'.format( + self.want.file, + '/ts/var/rest', + name + ) + params = dict( + command='run', + utilCmdArgs=move_path + ) + + uri = "https://{0}:{1}/mgmt/tm/util/unix-mv/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + resp = self.client.api.post(uri, json=params) + + try: + response = resp.json() + if 'commandResult' in response: + if 'cannot stat' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def download_from_device(self, dest): + url = 'https://{0}:{1}/mgmt/tm/asm/file-transfer/downloads/{2}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.file + ) + try: + download_asm_file(self.client, url, dest, self.want.file_size) + except F5ModuleError: + raise F5ModuleError( + "Failed to download the file." + ) + if os.path.exists(self.want.dest): + return True + return False + + def remove_temp_policy_from_device(self): + name = '{0}~{1}'.format(self.client.provider['user'], self.want.file) + tpath_name = '/ts/var/rest/{0}'.format(name) + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs=tpath_name + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True, + ), + dest=dict( + type='path' + ), + file=dict(), + inline=dict( + type='bool' + ), + compact=dict( + type='bool' + ), + base64=dict( + type='bool' + ), + binary=dict( + type='bool' + ), + force=dict( + default='yes', + type='bool' + ), + 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 = [ + ['binary', 'inline'], + ['binary', 'compact'], + ['dest', 'inline'], + ['file', 'inline'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_import.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_import.py new file mode 100644 index 00000000..e7ae63f0 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_import.py @@ -0,0 +1,640 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_asm_policy_import +short_description: Manage BIG-IP ASM policy imports +description: + - Manage the policy imports for BIG-IP ASM policies. +version_added: "1.0.0" +options: + name: + description: + - The ASM policy to create or override. + type: str + required: True + policy_type: + description: + - The type of the policy to import. + - When C(policy_type) is C(security), the policy is imported as an application security policy that you can apply + to a virtual server. + - When C(policy_type) is C(parent), the policy becomes a parent to which other Security policies attach, + inheriting its attributes. This policy type cannot be applied to Virtual Servers. + - This parameter is available on TMOS version 13.x and later and only takes effect when the C(inline) import method + is used. + type: str + default: security + choices: + - security + - parent + retain_inheritance_settings: + description: + - Indicates if an imported security type policy should retain settings when attached to parent policy. + - This parameter is available on TMOS version 13.x and later and only takes effect when the C(inline) import method + is used. + type: bool + parent_policy: + description: + - The parent policy to which the newly imported policy should be attached as child. + - When C(parent_policy) is specified, the imported C(policy_type) must not be C(parent). + - This parameter is available on TMOS version 13.x and later and only takes effect when C(inline) import method + is used. + type: str + base64: + description: + - Indicates if the imported policy string is encoded in Base64. + - This parameter only takes effect when using the C(inline) method of import. + type: bool + inline: + description: + - When specified, the ASM policy is created from a provided string. + - Content needs to be provided in a valid XML format, otherwise the operation will fail. + type: str + encoding: + description: + - Specifies the desired application language of the imported policy. + - The imported policy cannot be a C(parent) type or attached to a C(parent) policy when C(auto-detect) + encoding is set. + - When importing a policy to attach to a C(parent) policy, the C(encoding) of the imported policy, if different, + must be set to be the same value as C(parent_policy), otherwise import will fail. + - This parameter is available on TMOS version 13.x and later and only takes effect when the C(inline) import method + is used. + type: str + choices: + - windows-874 + - utf-8 + - koi8-r + - windows-1253 + - iso-8859-10 + - gbk + - windows-1256 + - windows-1250 + - iso-8859-13 + - iso-8859-9 + - windows-1251 + - iso-8859-6 + - big5 + - gb2312 + - iso-8859-1 + - windows-1252 + - iso-8859-4 + - iso-8859-2 + - iso-8859-3 + - gb18030 + - shift_jis + - iso-8859-8 + - euc-kr + - iso-8859-5 + - iso-8859-7 + - windows-1255 + - euc-jp + - iso-8859-15 + - windows-1257 + - iso-8859-16 + - auto-detect + source: + description: + - Full path to a policy file to be imported into the BIG-IP ASM. + - Policy files exported from newer versions of BIG-IP cannot be imported into older + versions of BIG-IP. However, policy files from older versions of BIG-IP can be + imported into newer versions of BIG-IP. + - The file format can be binary or XML. + type: path + force: + description: + - When set to C(yes), any existing policy with the same name will be overwritten by the new import. + - This works for both inline and file imports, if the policy does not exist this setting is ignored. + default: no + type: bool + partition: + description: + - Device partition on which to create the policy. + - This parameter is also applied to indicate the partition of the C(parent) policy. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) + - Nitin Khanna (@nitinthewiz) +''' + +EXAMPLES = r''' +- name: Import ASM policy + bigip_asm_policy_import: + name: new_asm_policy + file: /root/asm_policy.xml + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Import ASM policy inline + bigip_asm_policy_import: + name: foo-policy4 + inline: content + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Override existing ASM policy + bigip_asm_policy: + name: new_asm_policy + source: /root/asm_policy_new.xml + force: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +policy_type: + description: The type of the policy to import. + returned: changed + type: str + sample: security +retain_inheritance_settings: + description: Indicate if an imported security type policy should retain settings when attached to the parent policy. + returned: changed + type: bool + sample: yes +parent_policy: + description: The parent policy to which the newly imported policy should be attached as child. + returned: changed + type: str + sample: /Common/parent +base64: + description: Indicates if the imported policy string is encoded in Base64. + returned: changed + type: bool + sample: yes +encoding: + description: The desired application language of the imported policy. + returned: changed + type: str + sample: utf-8 +source: + description: Local path to an ASM policy file. + returned: changed + type: str + sample: /root/some_policy.xml +inline: + description: Contents of a policy as an inline string. + returned: changed + type: str + sample: foobar contents +name: + description: Name of the ASM policy to be created/overwritten. + returned: changed + type: str + sample: Asm_APP1_Transparent +force: + description: Set when overwriting an existing policy. + returned: changed + type: bool + sample: yes +''' + +import os +import time +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, upload_file, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + updatables = [] + + returnables = [ + 'name', + 'inline', + 'source', + 'force', + 'policy_type', + 'retain_inheritance_settings', + 'parent_policy', + 'base64', + 'encoding', + ] + + api_attributes = [ + 'file', + 'name', + 'policyType', + 'retainInheritanceSettings', + 'parentPolicy', + 'isBase64', + 'applicationLanguage', + ] + + api_map = { + 'file': 'inline', + 'filename': 'source', + 'policyType': 'policy_type', + 'retainInheritanceSettings': 'retain_inheritance_settings', + 'parentPolicy': 'parent_policy', + 'isBase64': 'base64', + 'applicationLanguage': 'encoding', + } + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def parent_policy(self): + if self._values['parent_policy'] is None: + return None + if self._values['policy_type'] == 'parent': + raise F5ModuleError( + "The 'policy_type' cannot be 'parent' if 'parent_policy' is defined." + ) + result = dict(fullPath=fq_name(self.partition, self._values['parent_policy'])) + return result + + @property + def base64(self): + result = flatten_boolean(self._values['base64']) + if result == 'yes': + return True + if result == 'no': + return False + + @property + def retain_inheritance_settings(self): + result = flatten_boolean(self._values['retain_inheritance_settings']) + if result == 'yes': + return True + if result == 'no': + return False + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def parent_policy(self): + if self._values['parent_policy'] is None: + return None + result = self._values['parent_policy']['fullPath'] + return result + + @property + def retain_inheritance_settings(self): + result = flatten_boolean(self._values['retain_inheritance_settings']) + return result + + @property + def base64(self): + result = flatten_boolean(self._values['base64']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'asm'): + raise F5ModuleError( + "ASM must be provisioned to use this module." + ) + + result = dict() + + changed = self.policy_import() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _clear_changes(self): + redundant = [ + 'policy_type', + 'retain_inheritance_settings', + 'parent_policy', + 'base64', + 'encoding', + ] + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None and key not in redundant: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def policy_import(self): + self._set_changed_options() + if self.module.check_mode: + return True + if self.exists(): + if self.want.force is False: + return False + if not self.exists() and self.want.force is True: + self.want.update({'force': None}) + if self.want.inline: + task = self.inline_import() + self.wait_for_task(task) + return True + self._clear_changes() + self.import_file_to_device() + return True + + def exists(self): + uri = 'https://{0}:{1}/mgmt/tm/asm/policies/'.format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + query = "?$filter=contains(name,'{0}')+and+contains(partition,'{1}')&$select=name,partition".format( + self.want.name, self.want.partition + ) + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + if 'items' in response and response['items'] != []: + # because api filter on ASM is broken when names that contain numbers at the end we need to work around it + for policy in response['items']: + if policy['name'] == self.want.name and policy['partition'] == self.want.partition: + return True + return False + + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/tm/asm/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def _get_policy_link(self): + uri = 'https://{0}:{1}/mgmt/tm/asm/policies/'.format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + query = "?$filter=contains(name,'{0}')+and+contains(partition,'{1}')&$select=name,partition".format( + self.want.name, self.want.partition + ) + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' in response and response['items'] != []: + # because api filter on ASM is broken when names that contain numbers at the end we need to work around it + for policy in response['items']: + if policy['name'] == self.want.name and policy['partition'] == self.want.partition: + policy_link = policy['selfLink'] + return policy_link + raise F5ModuleError( + 'Unable to retrieve policy link for policy {0}.'.format(self.want.name) + ) + + def inline_import(self): + params = self.changes.api_params() + params['name'] = fq_name(self.want.partition, self.want.name) + if self.want.source: + params['filename'] = os.path.split(self.want.source)[1] + uri = "https://{0}:{1}/mgmt/tm/asm/tasks/import-policy/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + if self.want.force: + params.update(dict(policyReference={'link': self._get_policy_link()})) + params.pop('name') + + resp = self.client.api.post(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + return response['id'] + + def wait_for_task(self, task_id): + uri = "https://{0}:{1}/mgmt/tm/asm/tasks/import-policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + task_id + ) + while True: + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if response['status'] in ['COMPLETED', 'FAILURE']: + break + time.sleep(1) + + if response['status'] == 'FAILURE': + raise F5ModuleError( + 'Failed to import ASM policy.' + ) + if response['status'] == 'COMPLETED': + return True + + def import_file_to_device(self): + name = os.path.split(self.want.source)[1] + self.upload_file_to_device(self.want.source, name) + time.sleep(2) + + task = self.inline_import() + self.wait_for_task(task) + return True + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.choices = [ + 'windows-874', + 'utf-8', + 'koi8-r', + 'windows-1253', + 'iso-8859-10', + 'gbk', + 'windows-1256', + 'windows-1250', + 'iso-8859-13', + 'iso-8859-9', + 'windows-1251', + 'iso-8859-6', + 'big5', + 'gb2312', + 'iso-8859-1', + 'windows-1252', + 'iso-8859-4', + 'iso-8859-2', + 'iso-8859-3', + 'gb18030', + 'shift_jis', + 'iso-8859-8', + 'euc-kr', + 'iso-8859-5', + 'iso-8859-7', + 'windows-1255', + 'euc-jp', + 'iso-8859-15', + 'windows-1257', + 'iso-8859-16', + 'auto-detect' + ] + argument_spec = dict( + name=dict( + required=True, + ), + source=dict(type='path'), + inline=dict(), + policy_type=dict( + default='security', + choices=['security', 'parent'] + ), + retain_inheritance_settings=dict(type='bool'), + base64=dict(type='bool'), + parent_policy=dict(), + encoding=dict(choices=self.choices), + force=dict( + type='bool', + default='no' + ), + 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 = [ + ['source', 'inline'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_manage.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_manage.py new file mode 100644 index 00000000..3c741ffc --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_manage.py @@ -0,0 +1,989 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_asm_policy_manage +short_description: Manage BIG-IP ASM policies +description: + - Manage BIG-IP ASM policies, create policies from templates, and manage global policy settings. +version_added: "1.0.0" +options: + active: + description: + - If C(yes), applies and activates the existing inactive policy. If C(no), it + deactivates the existing active policy. Generally should be C(yes) only in cases where + you want to activate new or existing policy. + - In TMOS v14 and later, deactivating the policy causes it to be detached from any other associated objects, + hence the default option of C(no) has been removed in order to prevent accidental disassociation. + type: bool + apply: + description: + - If C(yes) applies the policy if the policy has pending changes. + - This parameter supported on TMOS C(v14.x) and above. + type: bool + version_added: "1.4.0" + name: + description: + - The ASM policy to manage or create. + type: str + required: True + state: + description: + - When C(state) is C(present), and the C(template) parameter is provided, + a new ASM policy is created from the template with the given policy C(name). + - When C(state) is C(present) and no C(template) parameter is provided, a + new blank ASM policy is created with the given policy C(name). + - When C(state) is C(absent), ensures the policy is removed, even if it is + currently active. + type: str + choices: + - present + - absent + default: present + template: + description: + - An ASM policy built-in template. If the template does not exist, an error is raised. + - Once the policy has been created, this value cannot change. + - The C(Comprehensive), C(Drupal), C(Fundamental), C(Joomla), + C(Vulnerability Assessment Baseline), and C(Wordpress) templates are only available + on BIG-IP versions >= 13. + type: str + choices: + - ActiveSync v1.0 v2.0 (http) + - ActiveSync v1.0 v2.0 (https) + - Comprehensive + - Drupal + - Fundamental + - Joomla + - LotusDomino 6.5 (http) + - LotusDomino 6.5 (https) + - OWA Exchange 2003 (http) + - OWA Exchange 2003 (https) + - OWA Exchange 2003 with ActiveSync (http) + - OWA Exchange 2003 with ActiveSync (https) + - OWA Exchange 2007 (http) + - OWA Exchange 2007 (https) + - OWA Exchange 2007 with ActiveSync (http) + - OWA Exchange 2007 with ActiveSync (https) + - OWA Exchange 2010 (http) + - OWA Exchange 2010 (https) + - Oracle 10g Portal (http) + - Oracle 10g Portal (https) + - Oracle Applications 11i (http) + - Oracle Applications 11i (https) + - PeopleSoft Portal 9 (http) + - PeopleSoft Portal 9 (https) + - Rapid Deployment Policy + - SAP NetWeaver 7 (http) + - SAP NetWeaver 7 (https) + - SharePoint 2003 (http) + - SharePoint 2003 (https) + - SharePoint 2007 (http) + - SharePoint 2007 (https) + - SharePoint 2010 (http) + - SharePoint 2010 (https) + - Vulnerability Assessment Baseline + - Wordpress + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create ASM policy from template + bigip_asm_policy: + name: new_sharepoint_policy + template: SharePoint 2007 (http) + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create blank ASM policy + bigip_asm_policy: + name: new_blank_policy + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create blank ASM policy and activate + bigip_asm_policy_manage: + name: new_blank_policy + active: yes + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Activate ASM policy + bigip_asm_policy_manage: + name: inactive_policy + active: yes + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Deactivate ASM policy + bigip_asm_policy_manage: + name: active_policy + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +active: + description: Set when activating/deactivating an ASM policy. + returned: changed + type: bool + sample: yes +apply: + description: Set when applying pending changes to an ASM policy. + returned: changed when target policy has changes pending + type: bool + sample: yes +state: + description: Action performed on the target device. + returned: changed + type: str + sample: absent +template: + description: Name of the built-in ASM policy template. + returned: changed + type: str + sample: OWA Exchange 2007 (https) +name: + description: Name of the ASM policy to be managed/created. + returned: changed + type: str + sample: Asm_APP1_Transparent +''' + +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + updatables = [ + 'active', + 'apply', + ] + + returnables = [ + 'name', + 'template', + 'active', + 'apply', + ] + + api_attributes = [] + api_map = { + 'isModified': 'apply' + } + + @property + def template_link(self): + if self._values['template_link'] is not None: + return self._values['template_link'] + + result = None + + uri = "https://{0}:{1}/mgmt/tm/asm/policy-templates/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + query = "?$filter=name+eq+{0}".format(self.template.upper()) + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' in response and response['items'] != []: + result = dict(link=response['items'][0]['selfLink']) + + return result + + @property + def active(self): + result = flatten_boolean(self._values['active']) + if result is None: + return None + if result == 'yes': + return True + return False + + +class V1ModuleParameters(Parameters): + @property + def template(self): + if self._values['template'] is None: + return None + template_map = { + 'ActiveSync v1.0 v2.0 (http)': 'POLICY_TEMPLATE_ACTIVESYNC_V1_0_V2_0_HTTP', + 'ActiveSync v1.0 v2.0 (https)': 'POLICY_TEMPLATE_ACTIVESYNC_V1_0_V2_0_HTTPS', + 'LotusDomino 6.5 (http)': 'POLICY_TEMPLATE_LOTUSDOMINO_6_5_HTTP', + 'LotusDomino 6.5 (https)': 'POLICY_TEMPLATE_LOTUSDOMINO_6_5_HTTPS', + 'OWA Exchange 2003 (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_HTTP', + 'OWA Exchange 2003 (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_HTTPS', + 'OWA Exchange 2003 with ActiveSync (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_WITH_ACTIVESYNC_HTTP', + 'OWA Exchange 2003 with ActiveSync (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_WITH_ACTIVESYNC_HTTPS', + 'OWA Exchange 2007 (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_HTTP', + 'OWA Exchange 2007 (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_HTTPS', + 'OWA Exchange 2007 with ActiveSync (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_WITH_ACTIVESYNC_HTTP', + 'OWA Exchange 2007 with ActiveSync (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_WITH_ACTIVESYNC_HTTPS', + 'OWA Exchange 2010 (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2010_HTTP', + 'OWA Exchange 2010 (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2010_HTTPS', + 'Oracle 10g Portal (http)': 'POLICY_TEMPLATE_ORACLE_10G_PORTAL_HTTP', + 'Oracle 10g Portal (https)': 'POLICY_TEMPLATE_ORACLE_10G_PORTAL_HTTPS', + 'Oracle Applications 11i (http)': 'POLICY_TEMPLATE_ORACLE_APPLICATIONS_11I_HTTP', + 'Oracle Applications 11i (https)': 'POLICY_TEMPLATE_ORACLE_APPLICATIONS_11I_HTTPS', + 'PeopleSoft Portal 9 (http)': 'POLICY_TEMPLATE_PEOPLESOFT_PORTAL_9_HTTP', + 'PeopleSoft Portal 9 (https)': 'POLICY_TEMPLATE_PEOPLESOFT_PORTAL_9_HTTPS', + 'Rapid Deployment Policy': 'POLICY_TEMPLATE_RAPID_DEPLOYMENT', + 'SAP NetWeaver 7 (http)': 'POLICY_TEMPLATE_SAP_NETWEAVER_7_HTTP', + 'SAP NetWeaver 7 (https)': 'POLICY_TEMPLATE_SAP_NETWEAVER_7_HTTPS', + 'SharePoint 2003 (http)': 'POLICY_TEMPLATE_SHAREPOINT_2003_HTTP', + 'SharePoint 2003 (https)': 'POLICY_TEMPLATE_SHAREPOINT_2003_HTTPS', + 'SharePoint 2007 (http)': 'POLICY_TEMPLATE_SHAREPOINT_2007_HTTP', + 'SharePoint 2007 (https)': 'POLICY_TEMPLATE_SHAREPOINT_2007_HTTPS', + 'SharePoint 2010 (http)': 'POLICY_TEMPLATE_SHAREPOINT_2010_HTTP', + 'SharePoint 2010 (https)': 'POLICY_TEMPLATE_SHAREPOINT_2010_HTTPS' + } + if self._values['template'] in template_map: + return template_map[self._values['template']] + else: + raise F5ModuleError( + "The specified template is not valid for this version of BIG-IP." + ) + + @property + def apply(self): + result = flatten_boolean(self._values['apply']) + if result is None: + return None + if result == 'yes': + return True + return False + + +class V2ModuleParameters(Parameters): + @property + def template(self): + if self._values['template'] is None: + return None + template_map = { + 'ActiveSync v1.0 v2.0 (http)': 'POLICY_TEMPLATE_ACTIVESYNC_V1_0_V2_0_HTTP', + 'ActiveSync v1.0 v2.0 (https)': 'POLICY_TEMPLATE_ACTIVESYNC_V1_0_V2_0_HTTPS', + 'Comprehensive': 'POLICY_TEMPLATE_COMPREHENSIVE', # v13 + 'Drupal': 'POLICY_TEMPLATE_DRUPAL', # v13 + 'Fundamental': 'POLICY_TEMPLATE_FUNDAMENTAL', # v13 + 'Joomla': 'POLICY_TEMPLATE_JOOMLA', # v13 + 'LotusDomino 6.5 (http)': 'POLICY_TEMPLATE_LOTUSDOMINO_6_5_HTTP', + 'LotusDomino 6.5 (https)': 'POLICY_TEMPLATE_LOTUSDOMINO_6_5_HTTPS', + 'OWA Exchange 2003 (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_HTTP', + 'OWA Exchange 2003 (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_HTTPS', + 'OWA Exchange 2003 with ActiveSync (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_WITH_ACTIVESYNC_HTTP', + 'OWA Exchange 2003 with ActiveSync (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_WITH_ACTIVESYNC_HTTPS', + 'OWA Exchange 2007 (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_HTTP', + 'OWA Exchange 2007 (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_HTTPS', + 'OWA Exchange 2007 with ActiveSync (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_WITH_ACTIVESYNC_HTTP', + 'OWA Exchange 2007 with ActiveSync (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_WITH_ACTIVESYNC_HTTPS', + 'OWA Exchange 2010 (http)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2010_HTTP', + 'OWA Exchange 2010 (https)': 'POLICY_TEMPLATE_OWA_EXCHANGE_2010_HTTPS', + 'Oracle 10g Portal (http)': 'POLICY_TEMPLATE_ORACLE_10G_PORTAL_HTTP', + 'Oracle 10g Portal (https)': 'POLICY_TEMPLATE_ORACLE_10G_PORTAL_HTTPS', + 'Oracle Applications 11i (http)': 'POLICY_TEMPLATE_ORACLE_APPLICATIONS_11I_HTTP', + 'Oracle Applications 11i (https)': 'POLICY_TEMPLATE_ORACLE_APPLICATIONS_11I_HTTPS', + 'PeopleSoft Portal 9 (http)': 'POLICY_TEMPLATE_PEOPLESOFT_PORTAL_9_HTTP', + 'PeopleSoft Portal 9 (https)': 'POLICY_TEMPLATE_PEOPLESOFT_PORTAL_9_HTTPS', + 'Rapid Deployment Policy': 'POLICY_TEMPLATE_RAPID_DEPLOYMENT', + 'SAP NetWeaver 7 (http)': 'POLICY_TEMPLATE_SAP_NETWEAVER_7_HTTP', + 'SAP NetWeaver 7 (https)': 'POLICY_TEMPLATE_SAP_NETWEAVER_7_HTTPS', + 'SharePoint 2003 (http)': 'POLICY_TEMPLATE_SHAREPOINT_2003_HTTP', + 'SharePoint 2003 (https)': 'POLICY_TEMPLATE_SHAREPOINT_2003_HTTPS', + 'SharePoint 2007 (http)': 'POLICY_TEMPLATE_SHAREPOINT_2007_HTTP', + 'SharePoint 2007 (https)': 'POLICY_TEMPLATE_SHAREPOINT_2007_HTTPS', + 'SharePoint 2010 (http)': 'POLICY_TEMPLATE_SHAREPOINT_2010_HTTP', + 'SharePoint 2010 (https)': 'POLICY_TEMPLATE_SHAREPOINT_2010_HTTPS', + 'Vulnerability Assessment Baseline': 'POLICY_TEMPLATE_VULNERABILITY_ASSESSMENT', # v13 + 'Wordpress': 'POLICY_TEMPLATE_WORDPRESS' # v13 + } + return template_map[self._values['template']] + + @property + def apply(self): + result = flatten_boolean(self._values['apply']) + if result is None: + return None + if result == 'yes': + return True + return False + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def template(self): + if self._values['template'] is None: + return None + template_map = { + 'POLICY_TEMPLATE_ACTIVESYNC_V1_0_V2_0_HTTP': 'ActiveSync v1.0 v2.0 (http)', + 'POLICY_TEMPLATE_ACTIVESYNC_V1_0_V2_0_HTTPS': 'ActiveSync v1.0 v2.0 (https)', + 'POLICY_TEMPLATE_COMPREHENSIVE': 'Comprehensive', + 'POLICY_TEMPLATE_DRUPAL': 'Drupal', + 'POLICY_TEMPLATE_FUNDAMENTAL': 'Fundamental', + 'POLICY_TEMPLATE_JOOMLA': 'Joomla', + 'POLICY_TEMPLATE_LOTUSDOMINO_6_5_HTTP': 'LotusDomino 6.5 (http)', + 'POLICY_TEMPLATE_LOTUSDOMINO_6_5_HTTPS': 'LotusDomino 6.5 (https)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_HTTP': 'OWA Exchange 2003 (http)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_HTTPS': 'OWA Exchange 2003 (https)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_WITH_ACTIVESYNC_HTTP': 'OWA Exchange 2003 with ActiveSync (http)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2003_WITH_ACTIVESYNC_HTTPS': 'OWA Exchange 2003 with ActiveSync (https)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_HTTP': 'OWA Exchange 2007 (http)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_HTTPS': 'OWA Exchange 2007 (https)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_WITH_ACTIVESYNC_HTTP': 'OWA Exchange 2007 with ActiveSync (http)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2007_WITH_ACTIVESYNC_HTTPS': 'OWA Exchange 2007 with ActiveSync (https)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2010_HTTP': 'OWA Exchange 2010 (http)', + 'POLICY_TEMPLATE_OWA_EXCHANGE_2010_HTTPS': 'OWA Exchange 2010 (https)', + 'POLICY_TEMPLATE_ORACLE_10G_PORTAL_HTTP': 'Oracle 10g Portal (http)', + 'POLICY_TEMPLATE_ORACLE_10G_PORTAL_HTTPS': 'Oracle 10g Portal (https)', + 'POLICY_TEMPLATE_ORACLE_APPLICATIONS_11I_HTTP': 'Oracle Applications 11i (http)', + 'POLICY_TEMPLATE_ORACLE_APPLICATIONS_11I_HTTPS': 'Oracle Applications 11i (https)', + 'POLICY_TEMPLATE_PEOPLESOFT_PORTAL_9_HTTP': 'PeopleSoft Portal 9 (http)', + 'POLICY_TEMPLATE_PEOPLESOFT_PORTAL_9_HTTPS': 'PeopleSoft Portal 9 (https)', + 'POLICY_TEMPLATE_RAPID_DEPLOYMENT': 'Rapid Deployment Policy', + 'POLICY_TEMPLATE_SAP_NETWEAVER_7_HTTP': 'SAP NetWeaver 7 (http)', + 'POLICY_TEMPLATE_SAP_NETWEAVER_7_HTTPS': 'SAP NetWeaver 7 (https)', + 'POLICY_TEMPLATE_SHAREPOINT_2003_HTTP': 'SharePoint 2003 (http)', + 'POLICY_TEMPLATE_SHAREPOINT_2003_HTTPS': 'SharePoint 2003 (https)', + 'POLICY_TEMPLATE_SHAREPOINT_2007_HTTP': 'SharePoint 2007 (http)', + 'POLICY_TEMPLATE_SHAREPOINT_2007_HTTPS': 'SharePoint 2007 (https)', + 'POLICY_TEMPLATE_SHAREPOINT_2010_HTTP': 'SharePoint 2010 (http)', + 'POLICY_TEMPLATE_SHAREPOINT_2010_HTTPS': 'SharePoint 2010 (https)', + 'POLICY_TEMPLATE_VULNERABILITY_ASSESSMENT': 'Vulnerability Assessment Baseline', + 'POLICY_TEMPLATE_WORDPRESS': 'Wordpress', + } + return template_map[self._values['template']] + + @property + def apply(self): + result = flatten_boolean(self._values['apply']) + return result + + @property + def active(self): + result = flatten_boolean(self._values['active']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def active(self): + if self.want.active is None: + return None + if self.want.active is True and self.have.active is False: + return True + if self.want.active is False and self.have.active is True: + return False + + @property + def apply(self): + if self.want.apply is True and self.have.apply is True: + return True + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + self.have = None + self.changes = UsableChanges() + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if not self.exists(): + return False + else: + return self.remove() + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + if self.want.template is not None: + self.create_from_template() + if self.want.template is None: + self.create_blank() + if self.want.active: + self.activate() + return True + else: + return True + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + if self.changes.active or self.changes.apply: + self.activate() + return True + + def activate(self): + if not self.have: + self.have = self.read_current_from_device() + task_id = self.apply_on_device() + if self.wait_for_task(task_id): + self.wait_for_policy_apply() + return True + else: + raise F5ModuleError('Apply policy task failed.') + + def create_blank(self): + self.create_on_device() + if self.exists(): + return True + else: + raise F5ModuleError( + 'Failed to create ASM policy: {0}'.format(self.want.name) + ) + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError( + 'Failed to delete ASM policy: {0}'.format(self.want.name) + ) + return True + + def exists(self): + uri = 'https://{0}:{1}/mgmt/tm/asm/policies/'.format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + query = "?$filter=contains(name,'{0}')+and+contains(partition,'{1}')&$select=name,partition".format( + self.want.name, self.want.partition + ) + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + if 'items' in response and response['items'] != []: + # because api filter on ASM is broken when names that contain numbers at the end we need to work around it + for policy in response['items']: + if policy['name'] == self.want.name and policy['partition'] == self.want.partition: + return True + return False + + def wait_for_policy_apply(self): + """ + As the API is quite buggy and unstable there are cases where the policy still indicates pending changes + even after the apply-policy task has finished. Such state usually goes away after few seconds, + this function waits for the policy to achieve such a state for + maximum of 60 seconds. + + """ + # timer is required so that the api updates isModified state to a correct value. + time.sleep(3) + for x in range(0, 30): + self.have = self.read_current_from_device() + if self.have.apply is False: + break + time.sleep(2) + return True + + def wait_for_task(self, task_id): + uri = "https://{0}:{1}/mgmt/tm/asm/tasks/apply-policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + task_id + ) + while True: + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if response['status'] in ['COMPLETED', 'FAILURE']: + break + time.sleep(1) + + if response['status'] == 'FAILURE': + return False + if response['status'] == 'COMPLETED': + return True + + def _get_policy_id(self): + policy_id = None + uri = "https://{0}:{1}/mgmt/tm/asm/policies/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$filter=contains(name,'{0}')+and+contains(partition,'{1}')&$select=name,id,partition".format( + self.want.name, self.want.partition + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' in response and response['items'] != []: + # because api filter on ASM is broken when names that contain numbers at the end we need to work around it + for policy in response['items']: + if policy['name'] == self.want.name and policy['partition'] == self.want.partition: + policy_id = policy['id'] + + if not policy_id: + raise F5ModuleError( + "The policy with the name {0} was not found.".format(self.want.name) + ) + return policy_id + + def update_on_device(self): + params = self.changes.api_params() + # we need to remove active or apply from params as API will raise an error if the active or apply is set to yes, + # policies can only be activated or applied via apply-policy task endpoint. + + params.pop('active', None) + params.pop('isModified', None) + + if params: + policy_id = self._get_policy_id() + uri = "https://{0}:{1}/mgmt/tm/asm/policies/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id + ) + resp = self.client.api.patch(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + policy_id = self._get_policy_id() + uri = "https://{0}:{1}/mgmt/tm/asm/policies/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response.update((dict(self_link=response['selfLink']))) + return Parameters(params=response) + raise F5ModuleError(resp.content) + + def apply_on_device(self): + uri = "https://{0}:{1}/mgmt/tm/asm/tasks/apply-policy/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + params = dict(policyReference={'link': self.have.self_link}) + resp = self.client.api.post(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response['id'] + raise F5ModuleError(resp.content) + + def create_from_template_on_device(self): + full_name = fq_name(self.want.partition, self.want.name) + cmd = 'tmsh create asm policy {0} policy-template {1} encoding utf-8'.format(full_name, self.want.template) + uri = "https://{0}:{1}/mgmt/tm/util/bash/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(cmd) + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + if 'commandResult' in response: + if 'Error' in response['commandResult'] or 'error' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + # we need to remove active or apply from params as API will raise an error if the active or apply is set to yes, + # policies can only be activated or applied via apply-policy task endpoint. + params.pop('active', None) + params.pop('isModified', None) + uri = "https://{0}:{1}/mgmt/tm/asm/policies/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + time.sleep(2) + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + policy_id = self._get_policy_id() + uri = "https://{0}:{1}/mgmt/tm/asm/policies/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id + ) + response = self.client.api.delete(uri) + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def exec_module(self): + if not module_provisioned(self.client, 'asm'): + raise F5ModuleError( + "ASM must be provisioned to use this module." + ) + if self.version_is_less_than_13(): + manager = self.get_manager('v1') + else: + manager = self.get_manager('v2') + return manager.exec_module() + + def get_manager(self, type): + if type == 'v1': + return V1Manager(**self.kwargs) + elif type == 'v2': + return V2Manager(**self.kwargs) + + def version_is_less_than_13(self): + version = tmos_version(self.client) + if Version(version) < Version('13.0.0'): + return True + else: + return False + + +class V1Manager(BaseManager): + def __init__(self, *args, **kwargs): + module = kwargs.get('module', None) + client = F5RestClient(**module.params) + super(V1Manager, self).__init__(client=client, module=module) + self.want = V1ModuleParameters(params=module.params, client=client) + + def create_from_template(self): + self.create_from_template_on_device() + + +class V2Manager(BaseManager): + def __init__(self, *args, **kwargs): + module = kwargs.get('module', None) + client = F5RestClient(**module.params) + super(V2Manager, self).__init__(client=client, module=module) + self.want = V2ModuleParameters(params=module.params, client=client) + + # TODO Include creating ASM policies from custom templates in v13 + + def create_from_template(self): + if not self.create_from_template_on_device(): + return False + + +class ArgumentSpec(object): + def __init__(self): + self.template_map = [ + 'ActiveSync v1.0 v2.0 (http)', + 'ActiveSync v1.0 v2.0 (https)', + 'Comprehensive', + 'Drupal', + 'Fundamental', + 'Joomla', + 'LotusDomino 6.5 (http)', + 'LotusDomino 6.5 (https)', + 'OWA Exchange 2003 (http)', + 'OWA Exchange 2003 (https)', + 'OWA Exchange 2003 with ActiveSync (http)', + 'OWA Exchange 2003 with ActiveSync (https)', + 'OWA Exchange 2007 (http)', + 'OWA Exchange 2007 (https)', + 'OWA Exchange 2007 with ActiveSync (http)', + 'OWA Exchange 2007 with ActiveSync (https)', + 'OWA Exchange 2010 (http)', + 'OWA Exchange 2010 (https)', + 'Oracle 10g Portal (http)', + 'Oracle 10g Portal (https)', + 'Oracle Applications 11i (http)', + 'Oracle Applications 11i (https)', + 'PeopleSoft Portal 9 (http)', + 'PeopleSoft Portal 9 (https)', + 'Rapid Deployment Policy', + 'SAP NetWeaver 7 (http)', + 'SAP NetWeaver 7 (https)', + 'SharePoint 2003 (http)', + 'SharePoint 2003 (https)', + 'SharePoint 2007 (http)', + 'SharePoint 2007 (https)', + 'SharePoint 2010 (http)', + 'SharePoint 2010 (https)', + 'Vulnerability Assessment Baseline', + 'Wordpress', + ] + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True, + ), + template=dict( + choices=self.template_map + ), + active=dict( + type='bool' + ), + apply=dict( + type='bool', + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_server_technology.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_server_technology.py new file mode 100644 index 00000000..8e55b363 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_server_technology.py @@ -0,0 +1,496 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_asm_policy_server_technology +short_description: Manages Server Technology on an ASM policy +description: + - Manages Server Technology on ASM policies. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the server technology to apply on, or remove from, the ASM policy. + type: str + required: True + choices: + - jQuery + - Java Servlets/JSP + - ASP + - WebDAV + - IIS + - Front Page Server Extensions (FPSE) + - ASP.NET + - Microsoft Windows + - Unix/Linux + - Macromedia ColdFusion + - WordPress + - Apache Tomcat + - Apache/NCSA HTTP Server + - Outlook Web Access + - PHP + - Microsoft SQL Server + - Oracle + - MySQL + - Lotus Domino + - BEA Systems WebLogic Server + - Macromedia JRun + - Novell + - Cisco + - SSI (Server Side Includes) + - Proxy Servers + - CGI + - Sybase/ASE + - IBM DB2 + - PostgreSQL + - XML + - Apache Struts + - Elasticsearch + - JBoss + - Citrix + - Node.js + - Django + - MongoDB + - Ruby + - JavaServer Faces (JSF) + - Joomla + - Jetty + policy_name: + description: + - Specifies the name of an existing ASM policy to add or remove a server technology to. + type: str + required: True + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + default: present + choices: + - present + - absent + partition: + description: + - This parameter is only used when identifying an ASM policy. + type: str + default: Common +notes: + - This module is primarily used as a component of configuring an ASM policy in the Ansible Galaxy ASM Policy Role. + - Requires BIG-IP >= 13.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add Server Technology to ASM Policy + bigip_asm_policy_server_technology: + name: Joomla + policy_name: FooPolicy + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +- name: Remove Server Technology from ASM Policy + bigip_asm_policy_server_technology: + name: Joomla + policy_name: FooPolicy + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +policy_name: + description: The name of the ASM policy + returned: changed + type: str + sample: FooPolicy +name: + description: The name of Server Technology added/removed on the ASM policy. + returned: changed + type: str + sample: Joomla +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + + ] + + returnables = [ + 'policy_name', + 'name' + + ] + + updatables = [ + + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Changes(params=changed) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'asm'): + raise F5ModuleError( + "ASM must be provisioned to use this module." + ) + if self.version_is_less_than_13(): + raise F5ModuleError( + "This module requires TMOS version 13.x and above." + ) + + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def version_is_less_than_13(self): + version = tmos_version(self.client) + if Version(version) < Version('13.0.0'): + return True + else: + return False + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + policy_id = self._get_policy_id() + server_link = self._get_server_tech_link() + uri = 'https://{0}:{1}/mgmt/tm/asm/policies/{2}/server-technologies/'.format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id, + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if 'items' in response and response['items'] != []: + for st in response['items']: + if st['serverTechnologyReference']['link'] == server_link: + self.want.tech_id = st['id'] + return True + return False + + def _get_policy_id(self): + policy_id = None + uri = "https://{0}:{1}/mgmt/tm/asm/policies/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$filter=contains(name,'{0}')+and+contains(partition,'{1}')&$select=name,id,partition".format( + self.want.policy_name, self.want.partition + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' in response and response['items'] != []: + # because api filter on ASM is broken when names that contain numbers at the end we need to work around it + for policy in response['items']: + if policy['name'] == self.want.policy_name and policy['partition'] == self.want.partition: + policy_id = policy['id'] + + if not policy_id: + raise F5ModuleError( + "The policy with the name {0} was not found.".format(self.want.policy_name) + ) + return policy_id + + def _get_server_tech_link(self): + uri = "https://{0}:{1}/mgmt/tm/asm/server-technologies/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + name = self.want.name.replace(' ', '%20') + query = "?$filter=contains(serverTechnologyName,'{0}')".format(name) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' in response and response['items'] != []: + for item in response['items']: + if item['serverTechnologyName'] == self.want.name: + return item['selfLink'] + raise F5ModuleError("The following server technology: {0} was not found on the device.".format(self.want.name)) + + def create_on_device(self): + policy_id = self._get_policy_id() + + uri = "https://{0}:{1}/mgmt/tm/asm/policies/{2}/server-technologies/".format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id + ) + + params = dict(serverTechnologyReference={'link': self._get_server_tech_link()}) + resp = self.client.api.post(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + policy_id = self._get_policy_id() + tech_id = self.want.tech_id + uri = 'https://{0}:{1}/mgmt/tm/asm/policies/{2}/server-technologies/{3}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id, + tech_id, + ) + response = self.client.api.delete(uri) + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.tech = [ + 'jQuery', + 'Java Servlets/JSP', + 'ASP', + 'WebDAV', + 'IIS', + 'Front Page Server Extensions (FPSE)', + 'ASP.NET', + 'Microsoft Windows', + 'Unix/Linux', + 'Macromedia ColdFusion', + 'WordPress', + 'Apache Tomcat', + 'Apache/NCSA HTTP Server', + 'Outlook Web Access', + 'PHP', + 'Microsoft SQL Server', + 'Oracle', + 'MySQL', + 'Lotus Domino', + 'BEA Systems WebLogic Server', + 'Macromedia JRun', + 'Novell', + 'Cisco', + 'SSI (Server Side Includes)', + 'Proxy Servers', + 'CGI', + 'Sybase/ASE', + 'IBM DB2', + 'PostgreSQL', + 'XML', + 'Apache Struts', + 'Elasticsearch', + 'JBoss', + 'Citrix', + 'Node.js', + 'Django', + 'MongoDB', + 'Ruby', + 'JavaServer Faces (JSF)', + 'Joomla', + 'Jetty' + ] + argument_spec = dict( + policy_name=dict( + required=True + ), + name=dict( + choices=self.tech, + required=True + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_signature_set.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_signature_set.py new file mode 100644 index 00000000..eb8b5c9f --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_asm_policy_signature_set.py @@ -0,0 +1,722 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_asm_policy_signature_set +short_description: Manages Signature Sets on an ASM policy +description: + - Manages Signature Sets on an ASM policy. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the signature sets to apply on, or remove from, the ASM policy. + - Apart from built-in signature sets that ship with the device, you can create and use + custom signature sets. + - When C(All Response Signatures), configures all signatures in the attack signature + pool that can review responses. + - When C(All Signatures), configures all attack signatures in the attack signature pool. + - When C(Apache Struts Signatures), configures signatures that target attacks against + the Apache Struts web servers. Only available in version 13.x and later. + - When C(Apache Tomcat Signatures), configures signatures that target attacks against + the Apache Tomcat web servers. Only available in version 13.x and later. + - When C(Cisco Signatures), configures signatures that target attacks against Cisco systems. + Only available in version 13.x and later. + - When C(Command Execution Signatures), configures signatures involving attacks perpetrated by executing commands. + - When C(Cross Site Scripting Signatures), configures signatures that target attacks caused + by cross-site scripting techniques. + - When C(Directory Indexing Signatures), configures signatures targeting attacks that browse directory listings. + - When C(Generic Detection Signatures), configures signatures targeting well-known + or common web and application attacks. + - When C(HTTP Response Splitting Signatures), configures signatures targeting attacks that + take advantage of responses for which input values have not been sanitized. + - When C(High Accuracy Detection Evasion Signatures), configures signatures with a high level of accuracy + that produce few false positives when identifying evasion attacks. Only available in version 13.x and later. + - When C(High Accuracy Signatures), configures signatures with a high level of accuracy + that produce few false positives when identifying evasion attacks. + - When C(IIS and Windows Signatures), configures signatures that target attacks against Microsoft IIS + and Windows-based systems. Only available in version 13.x and later. + - When C(Information Leakage Signatures), configures signatures targeting attacks that are looking for system data + or debugging information that shows where the system is vulnerable to attack. + - When C(Java Servlets/JSP Signatures), configures signatures that target attacks against Java Servlets + and Java Server Pages (JSP) based applications. Only available in version 13.x and later. + - When C(Low Accuracy Signatures), configures signatures that may result in more false positives + when identifying attacks. + - When C(Medium Accuracy Signatures), configures signatures with a medium level of accuracy + when identifying attacks. + - When C(OS Command Injection Signatures), configures signatures targeting attacks + that attempt to run system level commands through a vulnerable application. + - When C(OWA Signatures), configures signatures that target attacks against + the Microsoft Outlook Web Access (OWA) application. + - When C(Other Application Attacks Signatures), configures signatures targeting miscellaneous attacks, + including session fixation, local file access, injection attempts, header tampering + and so on, affecting many applications. + - When C(Path Traversal Signatures), configures signatures targeting attacks that attempt to access files + and directories that are stored outside the web root folder. + - When C(Predictable Resource Location Signatures), configures signatures targeting attacks that attempt + to uncover hidden website content and functionality by forceful browsing, or by directory and file enumeration. + - When C(Remote File Include Signatures), configures signatures targeting attacks that attempt to exploit + a remote file include vulnerability that could enable a remote attacker to execute arbitrary commands + on the server hosting the application. + - When C(SQL Injection Signatures), configures signatures targeting attacks that attempt to insert (inject) + a SQL query using the input data from a client to an application. + - When C(Server Side Code Injection Signatures), configures signatures targeting code injection attacks + on the server side. + - When C(WebSphere signatures), configures signatures targeting attacks on many computing platforms + that are integrated using WebSphere, including general database, Microsoft Windows, IIS, + Microsoft SQL Server, Apache, Oracle, Unix/Linux, IBM DB2, PostgreSQL, and XML. + - When C(XPath Injection Signatures), configures signatures targeting attacks that attempt to gain access + to data structures or bypass permissions when a web site uses user-supplied information + to construct XPath queries for XML data. + type: str + required: True + policy_name: + description: + - Specifies the name of an existing ASM policy to add or remove signature sets to. + type: str + required: True + alarm: + description: + - Specifies if the security policy logs the request data in the Statistics screen + when a request matches a signature that is included in the signature set. + type: bool + block: + description: + - Effective when the security policy enforcement mode is Blocking. + - Determines how the system treats requests that match a signature included in the signature set. + - When C(yes), the system blocks all requests that match a signature, + and provides the client with a support ID number. + - When C(no), the system accepts those requests. + type: bool + learn: + description: + - Specifies if the security policy learns all requests that match a signature + that is included in the signature set. + type: bool + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + default: present + choices: + - present + - absent + partition: + description: + - This parameter is only used when identifying an ASM policy. + type: str + default: Common +notes: + - This module is primarily used as a component of configuring an ASM policy in the Ansible Galaxy ASM Policy Role. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add Signature Set to ASM Policy + bigip_asm_policy_signature_set: + name: IIS and Windows Signatures + policy_name: FooPolicy + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove Signature Set to ASM Policy + bigip_asm_policy_signature_set: + name: IIS and Windows Signatures + policy_name: FooPolicy + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +policy_name: + description: The name of the ASM policy. + returned: changed + type: str + sample: FooPolicy +name: + description: The name of the Signature Set added/removed on an ASM policy. + returned: changed + type: str + sample: Cisco Signatures +alarm: + description: Specifies whether the security policy logs the request data in the Statistics screen. + returned: changed + type: bool + sample: yes +block: + description: Determines how the system treats requests that match a signature included in the signature set. + returned: changed + type: bool + sample: no +learn: + description: Specifies if the policy learns all requests that match a signature that is included in the signature set. + returned: changed + type: bool + sample: yes +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + 'alarm', + 'block', + 'learn', + + ] + + returnables = [ + 'policy_name', + 'name', + 'alarm', + 'block', + 'learn', + + ] + + updatables = [ + 'alarm', + 'block', + 'learn', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def alarm(self): + result = flatten_boolean(self._values['alarm']) + if result: + if result == 'yes': + return True + return False + + @property + def block(self): + result = flatten_boolean(self._values['block']) + if result: + if result == 'yes': + return True + return False + + @property + def learn(self): + result = flatten_boolean(self._values['learn']) + if result: + if result == 'yes': + return True + return False + + def _signature_set_exists_on_device(self, name): + uri = "https://{0}:{1}/mgmt/tm/asm/signature-sets".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + query = "?$select=name" + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + if any(p['name'] == name for p in response['items']): + return True + return False + + @property + def name(self): + if self._values['name'] is None: + return None + + version = tmos_version(self.client) + + if Version(version) < Version('13.0.0'): + name_list = [ + 'All Response Signatures', + 'All Signatures', + 'Command Execution Signatures', + 'Cross Site Scripting Signatures', + 'Directory Indexing Signatures', + 'Generic Detection Signatures', + 'HTTP Response Splitting Signatures', + 'High Accuracy Signatures', + 'Information Leakage Signatures', + 'Low Accuracy Signatures', + 'Medium Accuracy Signatures', + 'OS Command Injection Signatures', + 'OWA Signatures', + 'Other Application Attacks Signatures', + 'Path Traversal Signatures', + 'Predictable Resource Location Signatures', + 'Remote File Include Signatures', + 'SQL Injection Signatures', + 'Server Side Code Injection Signatures', + 'WebSphere signatures', + 'XPath Injection Signatures' + ] + else: + name_list = [ + 'All Response Signatures', + 'All Signatures', + 'Apache Struts Signatures', + 'Apache Tomcat Signatures', + 'Cisco Signatures', + 'Command Execution Signatures', + 'Cross Site Scripting Signatures', + 'Directory Indexing Signatures', + 'Generic Detection Signatures', + 'HTTP Response Splitting Signatures', + 'High Accuracy Detection Evasion Signatures', + 'High Accuracy Signatures', + 'IIS and Windows Signatures', + 'Information Leakage Signatures', + 'Java Servlets/JSP Signatures', + 'Low Accuracy Signatures', + 'Medium Accuracy Signatures', + 'OS Command Injection Signatures', + 'OWA Signatures', + 'Other Application Attacks Signatures', + 'Path Traversal Signatures', + 'Predictable Resource Location Signatures', + 'Remote File Include Signatures', + 'SQL Injection Signatures', + 'Server Side Code Injection Signatures', + 'WebSphere signatures', + 'XPath Injection Signatures' + ] + + if self._values['name'] in name_list: + return self._values['name'] + + if self._signature_set_exists_on_device(self._values['name']): + return self._values['name'] + + raise F5ModuleError( + "The specified signature {0} set does not exist.".format( + self._values['name'] + ) + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def alarm(self): + return flatten_boolean(self._values['alarm']) + + @property + def learn(self): + return flatten_boolean(self._values['learn']) + + @property + def block(self): + return flatten_boolean(self._values['block']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params, client=self.client) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'asm'): + raise F5ModuleError( + "ASM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + policy_id = self._get_policy_id() + set_link = self._get_signature_set_link() + uri = 'https://{0}:{1}/mgmt/tm/asm/policies/{2}/signature-sets/'.format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id, + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + if 'items' in response and response['items'] != []: + for st in response['items']: + if st['signatureSetReference']['link'] == set_link['link']: + self.want.ss_id = st['id'] + return True + return False + + def _get_signature_set_link(self): + result = None + signature_set = self.want.name + uri = "https://{0}:{1}/mgmt/tm/asm/signature-sets".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + query = "?$select=name" + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + if 'items' in response and response['items'] != []: + for item in response['items']: + if item['name'] == signature_set: + result = dict(link=item['selfLink']) + if result: + return result + raise F5ModuleError("The following signature set: {0} was not found on the device. " + "Possibly name has changed in your TMOS version.".format(self.want.name)) + + def _get_policy_id(self): + policy_id = None + uri = "https://{0}:{1}/mgmt/tm/asm/policies/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$filter=contains(name,'{0}')+and+contains(partition,'{1}')&$select=name,id,partition".format( + self.want.policy_name, self.want.partition + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' in response and response['items'] != []: + # because api filter on ASM is broken when names that contain numbers at the end we need to work around it + for policy in response['items']: + if policy['name'] == self.want.policy_name and policy['partition'] == self.want.partition: + policy_id = policy['id'] + + if not policy_id: + raise F5ModuleError( + "The policy with the name {0} was not found.".format(self.want.policy_name) + ) + return policy_id + + def create_on_device(self): + policy_id = self._get_policy_id() + params = self.changes.api_params() + params['signatureSetReference'] = self._get_signature_set_link() + uri = "https://{0}:{1}/mgmt/tm/asm/policies/{2}/signature-sets/".format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + policy_id = self._get_policy_id() + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/asm/policies/{2}/signature-sets/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id, + self.want.ss_id + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + policy_id = self._get_policy_id() + uri = 'https://{0}:{1}/mgmt/tm/asm/policies/{2}/signature-sets/{3}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id, + self.want.ss_id + ) + response = self.client.api.delete(uri) + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + policy_id = self._get_policy_id() + uri = "https://{0}:{1}/mgmt/tm/asm/policies/{2}/signature-sets/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + policy_id, + self.want.ss_id + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + policy_name=dict( + required=True + ), + name=dict( + required=True + ), + alarm=dict( + type='bool' + ), + block=dict( + type='bool' + ), + learn=dict( + type='bool' + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cgnat_lsn_pool.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cgnat_lsn_pool.py new file mode 100644 index 00000000..64ee20bd --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cgnat_lsn_pool.py @@ -0,0 +1,1147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_cgnat_lsn_pool +short_description: Manage CGNAT LSN Pools +description: + - Manage CGNAT LSN (Large Scale NAT) Pools. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the LSN pool to manage. + type: str + required: True + description: + description: + - User created LSN pool description. + type: str + client_conn_limit: + description: + - Specifies the maximum number of simultaneous translated connections a client or subscriber is allowed to have. + - Valid range of values is between C(0) and C(4294967295) inclusive. + type: int + harpin_mode: + description: + - Enables or disables hairpinning for incoming connections to active translation end-points. + type: bool + icmp_echo: + description: + - Enables or disables ICMP echo on translated addresses. + type: bool + inbound_connections: + description: + - Controls whether or not the BIG-IP system supports inbound connections for each outbound mapping. + - When C(disabled), system does not support inbound connections for outbound mappings, which prevents + Port Control Protocol C(pcp) from functioning. + - When C(explicit), the system supports inbound connections for explicit outbound mappings. + - When C(automatic) the system supports inbound connections for every outbound mapping as it gets used. + type: str + choices: + - disabled + - explicit + - automatic + mode: + description: + - Specifies the translation address mapping mode. + - The C(napt) mode provides standard address and port translation allowing multiple clients in a private network + to access remote networks using the single IP address assigned to their router. + - The C(deterministic) address translation mode provides address translation that eliminates the logging of every + address mapping, while still allowing internal client address tracking using only an external address and port, + and a destination address and port. + - The C(pba) mode logs the allocation and release of port blocks for subscriber translation requests, + instead of separately logging each translation request. + type: str + choices: + - napt + - deterministic + - pba + persistence_mode: + description: + - Specifies the persistence settings for LSN translation entries. + - When C(address), the translation attempts to reuse the address mapping, but not the port mapping. + - When C(address-port), the translation attempts to reuse both the address and port mapping for subsequent + packets sent from the same internal IP address and port. + - When C(none), peristence is disabled. + type: str + choices: + - address + - address-port + - none + persistence_timeout: + description: + - Specifies the persistence timeout value for LSN translation entries. + - "If a particular mapping is unused for this length of time, the mapping expires and the public-side address/port + pair is free for use in other mappings." + - Valid range of values is between C(0) and C(31536000) inclusive. + type: int + route_advertisement: + description: + - Specifies whether the translation addresses are passed to the Advanced Routing Module + for advertisement through dynamic routing protocols. + type: bool + pba_block_idle_timeout: + description: + - Specifies the timeout duration subsequent to the point when the port block becomes idle. + - Valid range of values is between C(0) and C(4294967295) inclusive." + type: int + pba_block_lifetime: + description: + - Specifies the timeout for the port block, after which the block is not used for new port allocations. + - Valid range of values is between C(0) and C(4294967295) inclusive. + - The value of C(0) corresponds to an infinite timeout. + type: int + pba_block_size: + description: + - Specifies the number of ports in a block. + - Valid range of values is between C(0) and C(65535) inclusive. + - The C(pba_block_size) value should be less than or equal to the LSN pool range, i.e the range of ports defined by + C(port_range_low) and C(port_range_high) values. + type: int + pba_client_block_limit: + description: + - Specifies the number of blocks that can be assigned to a single subscriber IP address. + type: int + pba_zombie_timeout: + description: + - Specifies the timeout duration for a zombie port block, which is a timed out port block with one or more active + connections. When the timeout duration expires, connections using the zombie block are killed and the zombie + port block becomes an available port block. + - The value of C(0) corresponds to an infinite timeout. + - System ignores this parameter value if C(pba_block_lifetime) is C(0). + type: int + port_range_low: + description: + - Specifies the low end of the range of port numbers available for use with translation IP addresses. + - The C(port_range_low) must always be lower or equal to C(port_range_high) value. + - Valid range of values is between C(0) and C(65535) inclusive. + type: int + port_range_high: + description: + - Specifies the high end of the range of port numbers available for use with translation IP addresses. + - The C(port_range_high) must always be higher or equal to C(port_range_high) value. + - Valid range of values is between C(0) and C(65535) inclusive. + type: int + egress_intf_enabled: + description: + - Specifies how the system handles address translation on the interfaces specified in C(egress_interfaces). + - When set to C(yes), source address translation is allowed only on the specified C(egress_interfaces). + - When set to C(no), source address translation is disabled on the specified C(egress_interfaces). + type: bool + egress_interfaces: + description: + - Specifies the set of interfaces on which the source address translation is allowed or disallowed, + as determined by the C(egress_intf_enabled) setting. + type: list + elements: str + members: + description: + - Specifies the set of translation IP addresses available in the pool. This is a collection of IP prefixes with + their prefix lengths. + - All public-side addresses come from the addresses in this group of subnets. Members of two or more deterministic + LSN pools must not overlap. Every external address used for deterministic mapping must occur only in one LSN + pool. + type: list + elements: str + backup_members: + description: + - Specifies translation IP addresses available for backup members, which are used by Deterministic translation + mode if C(deterministic) mode translation fails and falls back to C(napt) mode. + - This is a collection of IP prefixes with their prefix lengths. + type: list + elements: str + log_profile: + description: + - Specifies the name of the logging profile the pool uses. + type: str + log_publisher: + description: + - Specifies the name of the log publisher that logs translation events. + type: str + partition: + description: + - Device partition on which to manage resources. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures the LSN pool exists. + - When C(state) is C(absent), ensures the LSN pool is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires CGNAT is licensed and enabled on BIG-IP. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an lsn pool + bigip_cgnat_lsn_pool: + name: foo + mode: napt + client_conn_limit: 100 + log_profile: foo_profile + log_publisher: foo_publisher + members: + - 10.1.1.0/24 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Update lsn pool + bigip_cgnat_lsn_pool: + name: foo + mode: pba + pba_block_size: 128 + pba_block_lifetime: 7200 + pba_block_idle_timeout: 1800 + pba_zombie_timeout: 900 + log_profile: foo_profile + log_publisher: foo_publisher + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove lsn pool + bigip_cgnat_lsn_pool: + name: foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: User created LSN pool description. + returned: changed + type: str + sample: some description +client_conn_limit: + description: The maximum number of simultaneous translated connections a client or subscriber is allowed to have. + returned: changed + type: int + sample: 50 +harpin_mode: + description: Enables or disables hairpinning for incoming connections to active translation end-points. + returned: changed + type: bool + sample: yes +icmp_echo: + description: Enables or disables ICMP echo on translated addresses. + returned: changed + type: bool + sample: no +inbound_connections: + description: Controls BIG-IP system support of inbound connections for each outbound mapping. + returned: changed + type: str + sample: explicit +mode: + description: Specifies the translation address mapping mode. + returned: changed + type: str + sample: napt +persistence_mode: + description: Specifies the persistence settings for LSN translation entries. + returned: changed + type: str + sample: address +persistence_timeout: + description: Specifies the persistence timeout value for LSN translation entries. + returned: changed + type: int + sample: 500 +route_advertisement: + description: Specifies whether the translation addresses are advertised through dynamic routing protocols. + returned: changed + type: bool + sample: yes +pba_block_idle_timeout: + description: The timeout duration subsequent to the point when the port block becomes idle. + returned: changed + type: int + sample: 3600 +pba_block_lifetime: + description: The timeout for the port block. + returned: changed + type: int + sample: 7200 +pba_block_size: + description: The number of ports in a block. + returned: changed + type: int + sample: 128 +pba_client_block_limit: + description: The number of blocks that can be assigned to a single subscriber IP address. + returned: changed + type: int + sample: 3 +pba_zombie_timeout: + description: The timeout duration for a zombie port block. + returned: changed + type: int + sample: 180 +port_range_low: + description: The low end of the range of port numbers available for use with translation IP addresses. + returned: changed + type: int + sample: 1025 +port_range_high: + description: The high end of the range of port numbers available for use with translation IP addresses. + returned: changed + type: int + sample: 65535 +egress_intf_enabled: + description: Specifies how the system handles address translation on the egress interfaces. + returned: changed + type: bool + sample: no +egress_interfaces: + description: The set of interfaces on which source address translation is allowed or disallowed. + returned: changed + type: list + sample: ['/Common/tunnel1', '/Common/tunnel2'] +members: + description: The set of translation IP addresses available in the pool. + returned: changed + type: list + sample: ['/Common/10.10.10.0/24', '/Common/11.11.11.0/25'] +backup_members: + description: The translation IP addresses available for backup members. + returned: changed + type: list + sample: ['/Common/10.10.10.0/24', '/Common/11.11.11.0/25'] +log_profile: + description: The name of the logging profile the pool uses. + returned: changed + type: str + sample: /Common/foo_log_profile +log_publisher: + description: The name of the log publisher that logs translation events. + returned: changed + type: list + sample: /Common/publisher_1 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean, is_empty_list, fq_name, transform_name +) +from ..module_utils.compare import cmp_simple_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'clientConnectionLimit': 'client_conn_limit', + 'hairpinMode': 'harpin_mode', + 'routeAdvertisement': 'route_advertisement', + 'egressInterfaces': 'egress_interfaces', + 'icmpEcho': 'icmp_echo', + 'inboundConnections': 'inbound_connections', + 'backupMembers': 'backup_members', + 'portBlockAllocation': 'port_block_allocation', + 'translationPortRange': 'translation_port_range', + 'egressInterfacesEnabled': 'egress_interfaces_enabled', + 'egressInterfacesDisabled': 'egress_interfaces_disabled', + 'logProfile': 'log_profile', + 'logPublisher': 'log_publisher', + } + + api_attributes = [ + 'clientConnectionLimit', + 'egressInterfacesEnabled', + 'egressInterfacesDisabled', + 'hairpinMode', + 'icmpEcho', + 'inboundConnections', + 'mode', + 'persistence', + 'portBlockAllocation', + 'routeAdvertisement', + 'translationPortRange', + 'egressInterfaces', + 'members', + 'backupMembers', + 'logProfile', + 'logPublisher', + 'description', + ] + + returnables = [ + 'client_conn_limit', + 'harpin_mode', + 'icmp_echo', + 'inbound_connections', + 'mode', + 'persistence_mode', + 'persistence_timeout', + 'route_advertisement', + 'pba_block_idle_timeout', + 'pba_block_lifetime', + 'pba_block_size', + 'pba_client_block_limit', + 'pba_zombie_timeout', + 'port_range_low', + 'port_range_high', + 'egress_intf_enabled', + 'egress_interfaces', + 'translation_port_range', + 'members', + 'backup_members', + 'log_profile', + 'log_publisher', + 'description', + ] + + updatables = [ + 'client_conn_limit', + 'harpin_mode', + 'icmp_echo', + 'inbound_connections', + 'mode', + 'persistence_mode', + 'persistence_timeout', + 'route_advertisement', + 'translation_port_range', + 'port_range_low', + 'port_range_high', + 'pba_block_idle_timeout', + 'pba_block_lifetime', + 'pba_block_size', + 'pba_client_block_limit', + 'pba_zombie_timeout', + 'egress_intf_enabled', + 'egress_interfaces', + 'members', + 'backup_members', + 'log_profile', + 'log_publisher', + 'description', + ] + + +class ApiParameters(Parameters): + @property + def pba_block_idle_timeout(self): + if self._values['port_block_allocation'] is None: + return None + return self._values['port_block_allocation']['blockIdleTimeout'] + + @property + def pba_block_lifetime(self): + if self._values['port_block_allocation'] is None: + return None + return self._values['port_block_allocation']['blockLifetime'] + + @property + def pba_block_size(self): + if self._values['port_block_allocation'] is None: + return None + return self._values['port_block_allocation']['blockSize'] + + @property + def pba_client_block_limit(self): + if self._values['port_block_allocation'] is None: + return None + return self._values['port_block_allocation']['clientBlockLimit'] + + @property + def pba_zombie_timeout(self): + if self._values['port_block_allocation'] is None: + return None + return self._values['port_block_allocation']['zombieTimeout'] + + @property + def port_range_low(self): + if self._values['translation_port_range'] is None: + return None + return int(self._values['translation_port_range'].split('-')[0]) + + @property + def port_range_high(self): + if self._values['translation_port_range'] is None: + return None + return int(self._values['translation_port_range'].split('-')[1]) + + @property + def egress_intf_enabled(self): + if self._values['egress_interfaces_enabled'] is None and self._values['egress_interfaces_disabled'] is True: + return 'no' + if self._values['egress_interfaces_disabled'] is None and self._values['egress_interfaces_enabled'] is True: + return 'yes' + + @property + def persistence_mode(self): + if self._values['persistence'] is None: + return None + return self._values['persistence']['mode'] + + @property + def persistence_timeout(self): + if self._values['persistence'] is None: + return None + return self._values['persistence']['timeout'] + + +class ModuleParameters(Parameters): + @property + def client_conn_limit(self): + if self._values['client_conn_limit'] is None: + return None + if 0 <= self._values['client_conn_limit'] <= 4294967295: + return self._values['client_conn_limit'] + raise F5ModuleError( + "Valid 'client_conn_limit' must be in range 0 - 4294967295." + ) + + @property + def harpin_mode(self): + result = flatten_boolean(self._values['harpin_mode']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def icmp_echo(self): + result = flatten_boolean(self._values['icmp_echo']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def persistence_timeout(self): + if self._values['persistence_timeout'] is None: + return None + if 0 <= self._values['persistence_timeout'] <= 31536000: + return self._values['persistence_timeout'] + raise F5ModuleError( + "Valid 'persistence_timeout' must be in range 0 - 31536000." + ) + + @property + def route_advertisement(self): + result = flatten_boolean(self._values['route_advertisement']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def pba_block_idle_timeout(self): + if self._values['pba_block_idle_timeout'] is None: + return None + if 0 <= self._values['pba_block_idle_timeout'] <= 4294967295: + return self._values['pba_block_idle_timeout'] + raise F5ModuleError( + "Valid 'pba_block_idle_timeout' must be in range 0 - 4294967295." + ) + + @property + def pba_block_lifetime(self): + if self._values['pba_block_lifetime'] is None: + return None + if 0 <= self._values['pba_block_lifetime'] <= 4294967295: + return self._values['pba_block_lifetime'] + raise F5ModuleError( + "Valid 'pba_block_lifetime' must be in range 0 - 4294967295." + ) + + @property + def pba_block_size(self): + if self._values['pba_block_size'] is None: + return None + if 0 <= self._values['pba_block_size'] <= 65535: + return self._values['pba_block_size'] + raise F5ModuleError( + "Valid 'pba_block_size' must be in range 0 - 65535." + ) + + @property + def pba_client_block_limit(self): + if self._values['pba_client_block_limit'] is None: + return None + if 0 <= self._values['pba_client_block_limit'] <= 65535: + return self._values['pba_client_block_limit'] + raise F5ModuleError( + "Valid 'pba_client_block_limit' must be in range 0 - 65535." + ) + + @property + def pba_zombie_timeout(self): + if self._values['pba_zombie_timeout'] is None: + return None + if 0 <= self._values['pba_zombie_timeout'] <= 4294967295: + return self._values['pba_zombie_timeout'] + raise F5ModuleError( + "Valid 'pba_zombie_timeout' must be in range 0 - 4294967295." + ) + + @property + def port_range_low(self): + if self._values['port_range_low'] is None: + return None + high = self.port_range_high + if 0 <= self._values['port_range_low'] <= 65535: + if high: + if high < self._values['port_range_low']: + raise F5ModuleError( + "The 'port_range_low' value: {0} is lower than 'port_range_high' value: {1}".format( + self._values['port_range_low'], high) + ) + return self._values['port_range_low'] + raise F5ModuleError( + "Valid 'port_range_low' must be in range 0 - 65535." + ) + + @property + def translation_port_range(self): + if self._values['port_range_low'] is None: + return None + result = '{0}-{1}'.format(self.port_range_low, self.port_range_high) + return result + + @property + def port_range_high(self): + if self._values['port_range_high'] is None: + return None + if 0 <= self._values['port_range_high'] <= 65535: + return self._values['port_range_high'] + raise F5ModuleError( + "Valid 'port_range_high' must be in range 0 - 65535." + ) + + @property + def egress_intf_enabled(self): + result = flatten_boolean(self._values['egress_intf_enabled']) + return result + + @property + def egress_interfaces(self): + if self._values['egress_interfaces'] is None: + return None + if is_empty_list(self._values['egress_interfaces']): + return '' + result = [] + for interface in self._values['egress_interfaces']: + result.append(fq_name(self.partition, interface)) + return result + + @property + def log_profile(self): + if self._values['log_profile'] is None: + return None + if self._values['log_profile'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['log_profile']) + return result + + @property + def log_publisher(self): + if self._values['log_publisher'] is None: + return None + if self._values['log_publisher'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['log_publisher']) + return result + + @property + def members(self): + if self._values['members'] is None: + return None + if is_empty_list(self._values['members']): + return '' + return self._values['members'] + + @property + def backup_members(self): + if self._values['backup_members'] is None: + return None + if is_empty_list(self._values['backup_members']): + return '' + return self._values['backup_members'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def persistence(self): + to_filter = dict( + mode=self._values['persistence_mode'], + timeout=self._values['persistence_timeout'], + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def port_block_allocation(self): + to_filter = dict( + blockIdleTimeout=self._values['pba_block_idle_timeout'], + blockLifetime=self._values['pba_block_lifetime'], + blockSize=self._values['pba_block_size'], + clientBlockLimit=self._values['pba_client_block_limit'], + zombieTimeout=self._values['pba_zombie_timeout'], + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def egress_interfaces_enabled(self): + if self._values['egress_intf_enabled'] is None: + return None + if self._values['egress_intf_enabled'] == 'yes': + return True + + @property + def egress_interfaces_disabled(self): + if self._values['egress_intf_enabled'] is None: + return None + if self._values['egress_intf_enabled'] == 'no': + return True + + +class ReportableChanges(Changes): + returnables = [ + 'client_conn_limit', + 'harpin_mode', + 'icmp_echo', + 'inbound_connections', + 'mode', + 'persistence_mode', + 'persistence_timeout', + 'route_advertisement', + 'pba_block_idle_timeout', + 'pba_block_lifetime', + 'pba_block_size', + 'pba_client_block_limit', + 'pba_zombie_timeout', + 'port_range_low', + 'port_range_high', + 'egress_intf_enabled', + 'egress_interfaces', + 'members', + 'backup_members', + 'log_profile', + 'log_publisher', + 'description', + ] + + @property + def route_advertisement(self): + result = flatten_boolean(self._values['route_advertisement']) + return result + + @property + def icmp_echo(self): + result = flatten_boolean(self._values['icmp_echo']) + return result + + @property + def harpin_mode(self): + result = flatten_boolean(self._values['harpin_mode']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def translation_port_range(self): + if self.want.port_range_low is None: + return None + if self.want.port_range_low != self.have.port_range_low: + result = "{0}-{1}".format(self.want.port_range_low, self.want.port_range_high) + return result + if self.want.port_range_high != self.have.port_range_high: + result = "{0}-{1}".format(self.want.port_range_low, self.want.port_range_high) + return result + return None + + @property + def members(self): + return cmp_simple_list(self.want.members, self.have.members) + + @property + def backup_members(self): + return cmp_simple_list(self.want.backup_members, self.have.backup_members) + + @property + def egress_interfaces(self): + return cmp_simple_list(self.want.egress_interfaces, self.have.egress_interfaces) + + @property + def log_profile(self): + if self.want.log_profile is None: + return None + if self.want.log_profile == '' and self.have.log_profile in [None, 'none']: + return None + if self.want.log_profile == '': + if self.have.log_publisher not in [None, 'none'] and self.want.log_publisher is None: + raise F5ModuleError( + "The log_profile cannot be removed if log_publisher is defined on device." + ) + if self.want.log_profile != '': + if self.want.log_publisher is None and self.have.log_publisher in [None, 'none']: + raise F5ModuleError( + "The log_profile cannot be specified without an existing valid log_publisher." + ) + if self.want.log_profile != self.have.log_profile: + return self.want.log_profile + + @property + def log_publisher(self): + if self.want.log_publisher is None: + return None + if self.want.log_publisher == '' and self.have.log_publisher in [None, 'none']: + return None + if self.want.log_publisher == '': + if self.want.log_profile is None and self.have.log_profile not in [None, 'none']: + raise F5ModuleError( + "The log_publisher cannot be removed if log_profile is defined on device." + ) + if self.want.log_publisher != self.have.log_publisher: + return self.want.log_publisher + + @property + def description(self): + if self.want.description is None: + return None + if self.have.description in [None, 'none'] and self.want.description == '': + return None + if self.want.description != self.have.description: + return self.want.description + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self.check_create_dependencies() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def check_create_dependencies(self): + if self.want.log_publisher is None: + if self.want.log_profile is not None: + raise F5ModuleError( + "The 'log_profile' cannot be used without a defined 'log_publisher'." + ) + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/ltm/lsn-pool/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/lsn-pool/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/lsn-pool/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/lsn-pool/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/lsn-pool/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + client_conn_limit=dict(type='int'), + harpin_mode=dict(type='bool'), + icmp_echo=dict(type='bool'), + inbound_connections=dict(choices=['disabled', 'explicit', 'automatic']), + mode=dict(choices=['deterministic', 'pba', 'napt']), + persistence_mode=dict(choices=['address', 'address-port', 'none']), + persistence_timeout=dict(type='int'), + route_advertisement=dict(type='bool'), + pba_block_idle_timeout=dict(type='int'), + pba_block_lifetime=dict(type='int'), + pba_block_size=dict(type='int'), + pba_client_block_limit=dict(type='int'), + pba_zombie_timeout=dict(type='int'), + port_range_low=dict(type='int'), + port_range_high=dict(type='int'), + egress_intf_enabled=dict(type='bool'), + egress_interfaces=dict( + type='list', + elements='str', + ), + members=dict( + type='list', + elements='str', + ), + backup_members=dict( + type='list', + elements='str', + ), + log_profile=dict(), + log_publisher=dict(), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.required_together = [ + ['port_range_low', 'port_range_high'] + ] + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_together=spec.required_together, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cli_alias.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cli_alias.py new file mode 100644 index 00000000..201ac7b5 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cli_alias.py @@ -0,0 +1,416 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_cli_alias +short_description: Manage CLI aliases on a BIG-IP +description: + - Allows for managing both private and shared aliases on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the alias. + type: str + required: True + scope: + description: + - The scope of the alias; whether it is shared on the system, or usable only + for the user who created it. + type: str + default: shared + choices: + - private + - shared + command: + description: + - The command to alias. + type: str + description: + description: + - Description of the alias. + type: str + partition: + description: + - Device partition on which to manage resources. + - This parameter is disregarded when the C(scope) is C(private). + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + default: present + choices: + - present + - absent +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a new alias + bigip_cli_alias: + name: sync_device_to_bside + scope: shared + command: save /sys config partitions all; run /cm config-sync to-group device-group-1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +command: + description: The new command that is aliased. + returned: changed + type: str + sample: run /util bash +description: + description: The new description of the alias. + returned: changed + type: str + sample: Run the bash shell +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, transform_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'tmCommand': 'command' + } + + api_attributes = [ + 'tmCommand', + 'description', + ] + + returnables = [ + 'command', + 'description', + ] + + updatables = [ + 'command', + 'description', + ] + + @property + def full_name(self): + if self.scope == 'shared': + return transform_name(self.partition, self.name) + else: + return self.name + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/cli/alias/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.scope, + self.want.full_name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/cli/alias/{2}/".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.scope + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/cli/alias/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.scope, + self.want.full_name + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cli/alias/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.scope, + self.want.full_name + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cli/alias/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.scope, + self.want.full_name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + scope=dict( + choices=['private', 'shared'], + default='shared' + ), + description=dict(), + command=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cli_script.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cli_script.py new file mode 100644 index 00000000..d6049cbf --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_cli_script.py @@ -0,0 +1,457 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_cli_script +short_description: Manage CLI scripts on a BIG-IP +description: + - Manages CLI scripts on a BIG-IP. CLI scripts, otherwise known as tmshell scripts + or TMSH scripts, allow you to create custom scripts that can run to manage objects + within a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the script. + type: str + required: True + content: + description: + - The content of the script. + - This parameter is typically used in conjunction with Ansible's C(file) or + template lookup plugins. See the examples in this documentation. + type: str + description: + description: + - Description of the cli script. + type: str + partition: + description: + - Device partition on which to manage resources. + type: str + default: Common + state: + description: + - When C(present), ensures the script exists. + - When C(absent), ensures the script is removed. + type: str + default: present + choices: + - present + - absent +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a cli script from an existing file + bigip_cli_script: + name: foo + content: "{{ lookup('file', '/absolute/path/to/cli/script.tcl') }}" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a cli script from a jinja template representing a cli script + bigip_cli_script: + name: foo + content: "{{ lookup('template', '/absolute/path/to/cli/script.tcl') }}" + 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. + returned: changed + type: str + sample: Foo is bar +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, transform_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'apiAnonymous': 'content', + 'scriptChecksum': 'checksum', + } + + api_attributes = [ + 'apiAnonymous', + 'description', + ] + + returnables = [ + 'description', + 'content', + ] + + updatables = [ + 'description', + 'content', + ] + + +class ApiParameters(Parameters): + @property + def ignore_verification(self): + return "true" + + @property + def content(self): + return self._values['content'].strip() + + +class ModuleParameters(Parameters): + @property + def ignore_verification(self): + return "true" + + @property + def content(self): + if self._values['content'] is None: + return None + return self._values['content'].strip() + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def content(self): + if self.want.content is None: + return None + if self.have.content is None: + return self.want.content + if self.want.content != self.have.content: + return self.want.content + + @property + def description(self): + if self.want.description is None: + return None + if self.have.description is None and self.want.description == '': + return None + if self.want.description != self.have.description: + return self.want.description + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/cli/script/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + + # Update any missing params + # + # The cli/script API is kinda weird in that it wont let us individually + # PATCH the description. We appear to need to include the content otherwise + # we get errors about us trying to replace procs that are needed by other + # scripts, ie, the script we're trying to update. + params = self.changes.api_params() + if 'description' in params and 'content' not in params: + self.changes.update({'content': self.have.content}) + if 'content' in params and 'description' not in params: + self.changes.update({'description': self.have.description}) + + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/cli/script/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/cli/script/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cli/script/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cli/script/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + content=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_command.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_command.py new file mode 100644 index 00000000..3a1b2e75 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_command.py @@ -0,0 +1,745 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_command +short_description: Run TMSH and BASH commands on F5 devices +description: + - Sends a TMSH or BASH command to a BIG-IP node and returns the results + read from the device. This module includes an argument that will cause + the module to wait for a specific condition before returning or timing + out if the condition is not met. + - This module is B(not) idempotent, nor will it ever be. It is intended as + a stop-gap measure to satisfy automation requirements until such a time as + a real module has been developed to configure in the way you need. + - If you are using this module, we recommend also filing an issue + to have a B(real) module created for your needs. +version_added: "1.0.0" +options: + commands: + description: + - The commands to send to the remote BIG-IP device over the + configured provider. The resulting output from the command + is returned. If the I(wait_for) argument is provided, the + module is not returned until the condition is satisfied or + the number of retries has expired. + - Only C(tmsh) commands are supported. If you are piping or adding additional + logic that is outside of C(tmsh) (such as grep'ing, awk'ing or other shell + related logic that are not C(tmsh)), this behavior is not supported. + required: True + type: raw + wait_for: + description: + - Specifies what to evaluate from the output of the command + and what conditionals to apply. This argument will cause + the task to wait for a particular conditional to be true + before moving forward. If the conditional is not true + by the configured retries, the task fails. See the examples. + type: list + elements: str + aliases: ['waitfor'] + match: + description: + - The I(match) argument is used in conjunction with the + I(wait_for) argument to specify the match policy. Valid + values are C(all) or C(any). If the value is set to C(all), + then all conditionals in the I(wait_for) must be satisfied. If + the value is set to C(any) then only one of the values must be + satisfied. + type: str + choices: + - any + - all + default: all + retries: + description: + - Specifies the number of retries a command should be tried + before it is considered failed. The command is run on the + target device every retry and evaluated against the I(wait_for) + conditionals. + type: int + default: 10 + interval: + description: + - Configures the interval in seconds to wait between retries + of the command. If the command does not pass the specified + conditional, the interval indicates how to long to wait before + trying the command again. + type: int + default: 1 + warn: + description: + - Whether the module should raise warnings related to command idempotency + or not. + - Note that the F5 Ansible developers specifically leave this on to make you + aware that your usage of this module may be better served by official F5 + Ansible modules. This module should always be used as a last resort. + default: True + type: bool + chdir: + description: + - Change into this directory before running the command. + type: str +notes: + - When running this module in an HA environment via SSH connection and using a role other than C(admin) + or C(root), you may see a C(Change Pending) status, even if you did not make any changes. + This is being tracked with ID429869. + - When using the bigip_command module with the REST API, there are a number of places regex is used + internally to escape characters such as quotation marks. If your TMSH command contains regex characters itself, + such as datagroup wildcards C(*), then a large amount of escape characters may be needed. +extends_documentation_fragment: f5networks.f5_modules.f5_rest_cli +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: run show version on remote devices + bigip_command: + commands: show sys version + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost + +- name: run show version and check to see if output contains BIG-IP + bigip_command: + commands: show sys version + wait_for: result[0] contains BIG-IP + provider: + server: lb.mydomain.com + password: secret + user: admin + register: result + delegate_to: localhost + +- name: run multiple commands on remote nodes + bigip_command: + commands: + - show sys version + - list ltm virtual + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost + +- name: run multiple commands and evaluate the output + bigip_command: + commands: + - show sys version + - list ltm virtual + wait_for: + - result[0] contains BIG-IP + - result[1] contains my-vs + provider: + server: lb.mydomain.com + password: secret + user: admin + register: result + delegate_to: localhost + +- name: tmsh prefixes will automatically be handled + bigip_command: + commands: + - show sys version + - tmsh list ltm virtual + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost + +- name: Delete all LTM nodes in Partition1, assuming no dependencies exist + bigip_command: + commands: + - delete ltm node all + chdir: Partition1 + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost + +- name: Command that contains wildcard character to be passed to tmsh + bigip_command: + commands: + - modify ltm data-group internal dg_string records add { "my test\\\\\\\*string" { data "value" }} + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost +''' + +RETURN = r''' +stdout: + description: The set of responses from the commands. + returned: always + type: list + sample: ['...', '...'] +stdout_lines: + description: The value of stdout split into a list. + returned: always + type: list + sample: [['...', '...'], ['...'], ['...']] +failed_conditions: + description: The list of conditionals that have failed. + returned: failed + type: list + sample: ['...', '...'] +warn: + description: Whether or not to raise warnings about modification commands. + returned: changed + type: bool + sample: True +''' + +import copy +import re +import shlex +import time +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import Conditional +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + ComplexList, to_list +) + +from collections import deque + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, is_cli +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + +try: + from ..module_utils.common import run_commands + HAS_CLI_TRANSPORT = True +except ImportError: + HAS_CLI_TRANSPORT = False + + +class NoChangeReporter(object): + stdout_re = [ + # A general error when a resource already exists + re.compile(r"The requested.*already exists"), + + # Returned when creating a duplicate cli alias + re.compile(r"Data Input Error: shared.*already exists"), + ] + + def find_no_change(self, responses): + """Searches the response for something that looks like a change + + This method borrows heavily from Ansible's ``_find_prompt`` method + defined in the ``lib/ansible/plugins/connection/network_cli.py::Connection`` + class. + + Arguments: + response (string): The output from the command. + + Returns: + bool: True when change is detected. False otherwise. + """ + for response in responses: + for regex in self.stdout_re: + if regex.search(response): + return True + return False + + +class Parameters(AnsibleF5Parameters): + returnables = ['stdout', 'stdout_lines', 'warnings', 'executed_commands'] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + except Exception: + return result + + @property + def raw_commands(self): + if self._values['commands'] is None: + return [] + if isinstance(self._values['commands'], string_types): + result = [self._values['commands']] + else: + result = self._values['commands'] + return result + + def cmd_has_pipe(self, cmd): + lex = shlex.shlex(cmd, posix=True) + lex.whitespace = '|' + lex.whitespace_split = True + return len(list(lex)) > 1 + + def convert_commands(self, commands): + result = [] + for command in commands: + tmp = dict( + command='', + pipeline='' + ) + + command = command.replace("'", "\\'") + pipeline = command.split('|', 1) if self.cmd_has_pipe(command) else [command] + tmp['command'] = pipeline[0] + try: + tmp['pipeline'] = pipeline[1] + except IndexError: + pass + result.append(tmp) + return result + + def convert_commands_cli(self, commands): + result = [] + for command in commands: + tmp = dict( + command='', + pipeline='' + ) + + pipeline = command.split('|', 1) if self.cmd_has_pipe(command) else [command] + tmp['command'] = pipeline[0] + try: + tmp['pipeline'] = pipeline[1] + except IndexError: + pass + result.append(tmp) + return result + + def merge_command_dict(self, command): + if command['pipeline'] != '': + escape_patterns = r'([$"])' + command['pipeline'] = re.sub(escape_patterns, r'\\\1', command['pipeline']) + command['command'] = '{0} | {1}'.format(command['command'], command['pipeline']).strip() + + def merge_command_dict_cli(self, command): + if command['pipeline'] != '': + command['command'] = '{0} | {1}'.format(command['command'], command['pipeline']).strip() + + @property + def rest_commands(self): + # ['list ltm virtual'] + commands = self.normalized_commands + commands = self.convert_commands(commands) + if self.chdir: + # ['cd /Common; list ltm virtual'] + for command in commands: + self.addon_chdir(command) + # ['tmsh -c "cd /Common; list ltm virtual"'] + for command in commands: + self.addon_tmsh(command) + for command in commands: + self.merge_command_dict(command) + result = [x['command'] for x in commands] + return result + + @property + def cli_commands(self): + # ['list ltm virtual'] + commands = self.normalized_commands + commands = self.convert_commands_cli(commands) + if self.chdir: + # ['cd /Common; list ltm virtual'] + for command in commands: + self.addon_chdir(command) + if not self.is_tmsh: + # ['tmsh -c "cd /Common; list ltm virtual"'] + for command in commands: + self.addon_tmsh_cli(command) + for command in commands: + self.merge_command_dict_cli(command) + result = [x['command'] for x in commands] + return result + + @property + def normalized_commands(self): + if self._values['normalized_commands'] is None: + return None + return deque(self._values['normalized_commands']) + + @property + def chdir(self): + if self._values['chdir'] is None: + return None + if self._values['chdir'].startswith('/'): + return self._values['chdir'] + return '/{0}'.format(self._values['chdir']) + + @property + def user_commands(self): + commands = self.raw_commands + return map(self._ensure_tmsh_prefix, commands) + + @property + def wait_for(self): + return self._values['wait_for'] or list() + + def addon_tmsh(self, command): + escape_patterns = r'([$"])' + if command['command'].count('"') % 2 != 0: + raise Exception('Double quotes are unbalanced') + command['command'] = re.sub(escape_patterns, r'\\\\\\\1', command['command']) + command['command'] = 'tmsh -c \\\"{0}\\\"'.format(command['command']) + + def addon_tmsh_cli(self, command): + if command['command'].count('"') % 2 != 0: + raise Exception('Double quotes are unbalanced') + command['command'] = 'tmsh -c "{0}"'.format(command['command']) + + def addon_chdir(self, command): + command['command'] = "cd {0}; {1}".format(self.chdir, command['command']) + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = Parameters(params=self.module.params) + self.want.update({'module': self.module}) + self.changes = Parameters(module=self.module) + self.valid_configs = [ + 'list', 'show', 'modify cli preference pager disabled' + ] + self.changed_command_prefixes = ('modify', 'create', 'delete') + self.warnings = list() + + def _to_lines(self, stdout): + lines = list() + for item in stdout: + if isinstance(item, string_types): + item = item.split('\n') + lines.append(item) + return lines + + def _announce_warnings(self, result): + warnings = result.pop('warnings', []) + for warning in warnings: + self.module.warn(warning) + + def notify_non_idempotent_commands(self, commands): + for index, item in enumerate(commands): + if any(item.startswith(x) for x in self.valid_configs): + return + else: + self.warnings.append( + 'Using "write" commands is not idempotent. You should use ' + 'a module that is specifically made for that. If such a ' + 'module does not exist, then please file a bug. The command ' + 'in question is "{0}..."'.format(item[0:40]) + ) + + @staticmethod + def normalize_commands(raw_commands): + if not raw_commands: + return None + result = [] + for command in raw_commands: + command = command.strip() + if command[0:5] == 'tmsh ': + command = command[4:].strip() + result.append(command) + return result + + def parse_commands(self): + results = [] + commands = self._transform_to_complex_commands(self.commands) + + for index, item in enumerate(commands): + # This needs to be removed so that the ComplexList used in to_commands + # will work correctly. + output = item.pop('output', None) + + if output == 'one-line' and 'one-line' not in item['command']: + item['command'] += ' one-line' + elif output == 'text' and 'one-line' in item['command']: + item['command'] = item['command'].replace('one-line', '') + + results.append(item) + return results + + def execute(self): + if self.want.normalized_commands: + result = self.want.normalized_commands + else: + result = self.normalize_commands(self.want.raw_commands) + self.want.update({'normalized_commands': result}) + if not result: + return False + self.notify_non_idempotent_commands(self.want.normalized_commands) + + commands = self.parse_commands() + retries = self.want.retries + conditionals = [Conditional(c) for c in self.want.wait_for] + + if self.module.check_mode: + return + + while retries > 0: + responses = self._execute(commands) + self._check_known_errors(responses) + for item in list(conditionals): + if item(responses): + if self.want.match == 'any': + conditionals = list() + break + conditionals.remove(item) + if not conditionals: + break + + time.sleep(self.want.interval) + retries -= 1 + else: + failed_conditions = [item.raw for item in conditionals] + errmsg = 'The following wait_for conditional statements have not been satisfied.' + raise F5ModuleError(errmsg, failed_conditions) + stdout_lines = self._to_lines(responses) + changes = { + 'stdout': responses, + 'stdout_lines': stdout_lines, + 'executed_commands': self.commands + } + if self.want.warn: + changes['warnings'] = self.warnings + self.changes = Parameters(params=changes, module=self.module) + return self.determine_change(responses) + + def determine_change(self, responses): + changer = NoChangeReporter() + if changer.find_no_change(responses): + return False + if any(x for x in self.want.normalized_commands if x.startswith(self.changed_command_prefixes)): + return True + return False + + def _check_known_errors(self, responses): + # A regex to match the error IDs used in the F5 v2 logging framework. + # pattern = r'^[0-9A-Fa-f]+:?\d+?:' + + for resp in responses: + if 'usage: tmsh' in resp: + raise F5ModuleError( + "tmsh command printed its 'help' message instead of running your command. " + "This usually indicates unbalanced quotes." + ) + + def _transform_to_complex_commands(self, commands): + spec = dict( + command=dict(key=True), + output=dict( + default='text', + choices=['text', 'one-line'] + ), + ) + transform = ComplexList(spec, self.module) + result = transform(commands) + return result + + +class V1Manager(BaseManager): + """Supports CLI (SSH) communication with the remote device + + """ + def _execute(self, commands): + if self.want.is_tmsh: + command = dict( + command="modify cli preference pager disabled" + ) + else: + command = dict( + command="tmsh modify cli preference pager disabled" + ) + self.execute_on_device(command) + return self.execute_on_device(commands) + + @property + def commands(self): + return self.want.cli_commands + + def is_tmsh(self): + try: + self.execute_on_device('tmsh -v') + except Exception as ex: + if 'Syntax Error:' in str(ex): + return True + raise + return False + + def exec_module(self): + result = dict() + + changed = self.execute() + + result.update(**self.changes.to_return()) + result.update(dict(changed=changed)) + self._announce_warnings(result) + return result + + def execute(self): + self.want.update({'is_tmsh': self.is_tmsh()}) + return super(V1Manager, self).execute() + + def execute_on_device(self, commands): + result = run_commands(self.module, commands) + return result + + +class V2Manager(BaseManager): + """Supports REST communication with the remote device + + """ + def _execute(self, commands): + return self.execute_on_device(commands) + + @property + def commands(self): + return self.want.rest_commands + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.execute() + + result.update(**self.changes.to_return()) + result.update(dict(changed=changed)) + self._announce_warnings(result) + send_teem(start, self.client, self.module, version) + return result + + def execute_on_device(self, commands): + responses = [] + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + for item in to_list(commands): + try: + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(item['command']) + ) + resp = self.client.api.post(uri, json=args) + response = resp.json() + if 'commandResult' in response: + output = u'{0}'.format(response['commandResult']) + responses.append(output.strip()) + 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) + return responses + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + self.module = kwargs.get('module', None) + + def exec_module(self): + if is_cli(self.module) and HAS_CLI_TRANSPORT: + manager = self.get_manager('v1') + else: + manager = self.get_manager('v2') + result = manager.exec_module() + return result + + def get_manager(self, type): + if type == 'v1': + return V1Manager(**self.kwargs) + elif type == 'v2': + return V2Manager(**self.kwargs) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + commands=dict( + type='raw', + required=True + ), + wait_for=dict( + type='list', + elements='str', + aliases=['waitfor'] + ), + match=dict( + default='all', + choices=['any', 'all'] + ), + retries=dict( + default=10, + type='int' + ), + interval=dict( + default=1, + type='int' + ), + warn=dict( + type='bool', + default='yes' + ), + chdir=dict() + ) + # required to add CLI to choices and ssh_keyfile as per documentation + provider_update = dict( + transport=dict( + type='str', + default='rest', + choices=['cli', 'rest'] + ), + ssh_keyfile=dict( + type='path' + ), + + ) + new_spec = copy.deepcopy(f5_argument_spec) + self.argument_spec = {} + self.argument_spec.update(new_spec) + self.argument_spec['provider']['options'].update(provider_update) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_config.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_config.py new file mode 100644 index 00000000..adefe688 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_config.py @@ -0,0 +1,412 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_config +short_description: Manage BIG-IP configuration sections +description: + - Manages a BIG-IP configuration by allowing TMSH commands that + modify the running configuration, or merge SCF formatted files into + the running configuration. Additionally, this module is of + significant importance because it allows you to save your running + configuration to disk. Since all F5 modules manipulate the running + configuration, it is important you use this module to save + that running config. +version_added: "1.0.0" +options: + save: + description: + - The C(save) argument instructs the module to save the + running-config to startup-config. + - This operation is performed after any changes are made to the + current running config. If no changes are made, the configuration + is still saved to the startup config. + - This option will always cause the module to return B(changed). + type: bool + default: yes + reset: + description: + - Loads the default configuration on the device. + - If this option is specified, the default configuration will be + loaded before any commands or other provided configuration is run. + type: bool + default: no + merge_content: + description: + - Loads the specified configuration that you want to merge into + the running configuration. This is equivalent to using the + C(tmsh) command C(load sys config from-terminal merge). + - If you need to read the configuration from a file or template, use + Ansible's C(file) or C(template) lookup plugins respectively. + type: str + verify: + description: + - Validates the specified configuration to see whether it is + valid to replace the running configuration. + - The running configuration will not be changed. + - When this parameter is set to C(yes), no change will be reported + by the module. + type: bool + default: no +notes: + - This module requires that sys db variable on device C(systemauth.disablebash) is set to C(false). +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Save the running configuration of the BIG-IP + bigip_config: + save: yes + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost + +- name: Reset the BIG-IP configuration, for example, to RMA the device + bigip_config: + reset: yes + save: yes + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost + +- name: Load an SCF configuration + bigip_config: + merge_content: "{{ lookup('file', '/path/to/config.scf') }}" + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost +''' + +RETURN = r''' +stdout: + description: The set of responses from the options. + returned: always + type: list + sample: ['...', '...'] +stdout_lines: + description: The value of stdout split into a list. + returned: always + type: list + sample: [['...', '...'], ['...'], ['...']] +''' +from datetime import datetime + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +import os +import tempfile + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import ( + upload_file, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + returnables = ['stdout', 'stdout_lines'] + + def to_return(self): + result = {} + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = Parameters(params=self.module.params) + self.changes = Parameters() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Parameters(params=changed) + + def _to_lines(self, stdout): + lines = list() + for item in stdout: + if isinstance(item, str): + item = str(item).split('\n') + lines.append(item) + return lines + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = {} + + changed = self.execute() + + result.update(**self.changes.to_return()) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def execute(self): + responses = [] + if self.want.reset: + response = self.reset() + responses.append(response) + + if self.want.merge_content: + if self.want.verify: + response = self.merge(verify=True) + responses.append(response) + else: + response = self.merge(verify=False) + responses.append(response) + + if self.want.save: + response = self.save() + responses.append(response) + if not self.module.check_mode: + self._detect_errors(responses) + changes = { + 'stdout': responses, + 'stdout_lines': self._to_lines(responses) + } + self.changes = Parameters(params=changes) + if self.want.verify: + return False + return True + + def _detect_errors(self, stdout): + errors = [ + 'Unexpected Error:' + ] + + msg = [x for x in stdout for y in errors if y in x] + if msg: + # Error only contains the lines that include the error + raise F5ModuleError(' '.join(msg)) + + def reset(self): + if self.module.check_mode: + return True + return self.reset_device() + + def reset_device(self): + command = 'tmsh load sys config default' + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(command) + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'commandResult' in response: + return str(response['commandResult']) + else: + return + raise F5ModuleError(resp.content) + + def merge(self, verify=True): + temp_name = next(tempfile._get_candidate_names()) + remote_path = "/var/config/rest/downloads/{0}".format(temp_name) + temp_path = '/tmp/' + temp_name + + if self.module.check_mode: + return True + + self.upload_to_device(temp_name) + self.move_on_device(remote_path) + response = self.merge_on_device( + remote_path=temp_path, verify=verify + ) + self.remove_temporary_file(remote_path=temp_path) + return response + + def merge_on_device(self, remote_path, verify=True): + command = 'tmsh load sys config file {0} merge'.format( + remote_path + ) + if verify: + command += ' verify' + + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(command) + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'commandResult' in response: + return str(response['commandResult']) + else: + return + raise F5ModuleError(resp.content) + + def remove_temporary_file(self, remote_path): + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs=remote_path + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def move_on_device(self, remote_path): + uri = "https://{0}:{1}/mgmt/tm/util/unix-mv".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='{0} /tmp/{1}'.format( + remote_path, os.path.basename(remote_path) + ) + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def upload_to_device(self, temp_name): + template = StringIO(self.want.merge_content) + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, template, temp_name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def save(self): + if self.module.check_mode: + return True + return self.save_on_device() + + def save_on_device(self): + command = 'tmsh save sys config' + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(command) + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'commandResult' in response: + return str(response['commandResult']) + else: + return + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + reset=dict( + type='bool', + default=False + ), + merge_content=dict(), + verify=dict( + type='bool', + default=False + ), + save=dict( + type='bool', + default='yes' + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_configsync_action.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_configsync_action.py new file mode 100644 index 00000000..3f5c97bb --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_configsync_action.py @@ -0,0 +1,432 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_configsync_action +short_description: Perform different actions related to config-sync +description: + - Allows running different config-sync actions. These actions allow + you to manually sync your configuration across multiple BIG-IPs when + those devices are in an HA pair. +version_added: "1.0.0" +options: + device_group: + description: + - The device group on which you want to perform config-sync actions. + type: str + required: True + sync_device_to_group: + description: + - Specifies the system synchronizes configuration data from this + device to other members of the device group. In this case, the device + will do a "push" to all the other devices in the group. This option + is mutually exclusive with the C(sync_group_to_device) option. + type: bool + sync_group_to_device: + description: + - Specifies the system synchronizes configuration data from the + device with the most recent configuration. In this case, the device + will do a "pull" from the most recently updated device. This option + is mutually exclusive with the C(sync_device_to_group) options. + type: bool + overwrite_config: + description: + - Indicates the sync operation overwrites the configuration on + the target. + type: bool + default: no +notes: + - Requires the objectpath Python package on the host. This is as easy as + running C(pip install objectpath). +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Sync configuration from device to group + bigip_configsync_action: + device_group: foo-group + sync_device_to_group: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Sync configuration from most recent device to the current host + bigip_configsync_action: + device_group: foo-group + sync_group_to_device: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Perform an initial sync of a device to a new device group + bigip_configsync_action: + device_group: new-device-group + sync_device_to_group: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import re +import time +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + +try: + from objectpath import Tree + HAS_OBJPATH = True +except ImportError: + HAS_OBJPATH = False + + +class Parameters(AnsibleF5Parameters): + api_attributes = [] + returnables = [] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def direction(self): + if self.sync_device_to_group: + return 'to-group' + else: + return 'from-group' + + @property + def sync_device_to_group(self): + result = flatten_boolean(self._values['sync_device_to_group']) + if result == 'yes': + return True + if result == 'no': + return False + + @property + def sync_group_to_device(self): + result = flatten_boolean(self._values['sync_group_to_device']) + if result == 'yes': + return True + if result == 'no': + return False + + @property + def force_full_push(self): + if self.overwrite_config: + return 'force-full-load-push' + else: + return '' + + @property + def overwrite_config(self): + result = flatten_boolean(self._values['overwrite_config']) + if result == 'yes': + return True + if result == 'no': + return False + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + pass + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if not self._device_group_exists(): + raise F5ModuleError( + "The specified 'device_group' not not exist." + ) + if self._sync_to_group_required(): + raise F5ModuleError( + "This device group needs an initial sync. Please use " + "'sync_device_to_group'" + ) + if self.exists(): + return False + else: + return self.execute() + + def _sync_to_group_required(self): + status = self._get_status_from_resource() + if status == 'Awaiting Initial Sync' and self.want.sync_group_to_device: + return True + return False + + def _device_group_exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.device_group + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def execute(self): + self.execute_on_device() + self._wait_for_sync() + return True + + def exists(self): + status = self._get_status_from_resource() + if status == 'In Sync': + return True + else: + return False + + def execute_on_device(self): + sync_cmd = 'config-sync {0} {1} {2}'.format( + self.want.direction, + self.want.device_group, + self.want.force_full_push + ) + uri = "https://{0}:{1}/mgmt/tm/cm".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs=sync_cmd + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def _wait_for_sync(self): + # Wait no more than half an hour + for x in range(1, 180): + time.sleep(3) + status = self._get_status_from_resource() + + # Changes Pending: + # The existing device has changes made to it that + # need to be sync'd to the group. + # + # Awaiting Initial Sync: + # This is a new device group and has not had any sync + # done yet. You _must_ `sync_device_to_group` in this + # case. + # + # Not All Devices Synced: + # A device group will go into this state immediately + # after starting the sync and stay until all devices finish. + # + if status in ['Changes Pending']: + details = self._get_details_from_resource() + self._validate_pending_status(details) + elif status in ['Awaiting Initial Sync', 'Not All Devices Synced']: + pass + elif status == 'In Sync': + return + elif status == 'Disconnected': + raise F5ModuleError( + "One or more devices are unreachable (disconnected). " + "Resolve any communication problems before attempting to sync." + ) + else: + raise F5ModuleError(status) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cm/sync-status/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response + raise F5ModuleError(resp.content) + + def _get_status_from_resource(self): + resource = self.read_current_from_device() + entries = resource['entries'].copy() + k, v = entries.popitem() + status = v['nestedStats']['entries']['status']['description'] + return status + + def _get_details_from_resource(self): + resource = self.read_current_from_device() + stats = resource['entries'].copy() + if HAS_OBJPATH: + tree = Tree(stats) + else: + raise F5ModuleError( + "objectpath module required, install objectpath module to continue. " + ) + details = list(tree.execute('$..*["details"]["description"]')) + result = details[::-1] + return result + + def _validate_pending_status(self, details): + """Validate the content of a pending sync operation + + This is a hack. The REST API is not consistent with its 'status' values + so this method is here to check the returned strings from the operation + and see if it reported any of these inconsistencies. + + :param details: + :raises F5ModuleError: + """ + pattern1 = r'.*(?PRecommended\s+action.*)' + for detail in details: + matches = re.search(pattern1, detail) + if matches: + raise F5ModuleError(matches.group('msg')) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = False + + argument_spec = dict( + sync_device_to_group=dict( + type='bool' + ), + sync_group_to_device=dict( + type='bool' + ), + overwrite_config=dict( + type='bool', + default='no' + ), + device_group=dict( + required=True + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + self.required_one_of = [ + ['sync_device_to_group', 'sync_group_to_device'] + ] + self.mutually_exclusive = [ + ['sync_device_to_group', 'sync_group_to_device'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + required_one_of=spec.required_one_of + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_data_group.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_data_group.py new file mode 100644 index 00000000..bcc1895b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_data_group.py @@ -0,0 +1,1502 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_data_group +short_description: Manage data groups on a BIG-IP +description: + - Allows for managing data groups on a BIG-IP. Data groups provide a way to store collections + of values on a BIG-IP for later use in things such as LTM rules, iRules, and ASM policies. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the data group. + type: str + required: True + description: + description: + - The description of the data group. + type: str + type: + description: + - The type of records in this data group. + - This parameter is important because it causes the BIG-IP to store your data + in different ways to optimize access to it. For example, it would be wrong + to specify a list of records containing IP addresses, but label them as a C(string) + type. + - This value cannot be changed once the data group is created. + type: str + choices: + - address + - addr + - ip + - string + - integer + - int + default: string + internal: + description: + - The type of this data group. + - You should only consider setting this value in cases where you know exactly what + you are doing, B(or), you are working with a pre-existing internal data group. + - Be aware that if you deliberately force this parameter to C(yes), and you have a + either a large number of records or a large total records size, this large amount + of data will be reflected in your BIG-IP configuration. This can lead to B(long) + system configuration load times due to parsing and verifying the large + configuration. + - There is a limit of either 4 megabytes or 65,000 records (whichever is more restrictive) + for uploads when this parameter is C(yes). + - This value cannot be changed once the data group is created. + type: bool + default: no + external_file_name: + description: + - When creating a new data group, this specifies the file name you want to give an + external data group file on the BIG-IP. + - This parameter is ignored when C(internal) is C(yes). + - This parameter can be used to select an existing data group file to use with an + existing external data group. + - If this value is not provided, it will be given the value specified in C(name) and, + therefore, match the name of the data group. + - This value may only contain letters, numbers, underscores, dashes, or a period. + type: str + records: + description: + - Specifies the records you want to add to a data group. + - If you have a large number of records, we recommend you use C(records_src) + instead of typing all those records here. + - The technical limit of either the number of records, or the total size of all + records. Varies with the size of the total resources on your system; in particular, + RAM. + - When C(internal) is C(no), at least one record must be specified in either C(records) + or C(records_src). + - "When C(type) is: C(ip), C(address), C(addr) if the addresses use a non-default route domain, + they must be explicit about it, meaning they must contain a route domain notation C(%) e.g. 10.10.1.1%11. + This is true regardless if the data group resides in a partition or not." + type: list + elements: raw + suboptions: + key: + description: + - The key describing the record in the data group. + - The key will be used for validation of the C(type) parameter to this module. + type: str + required: True + value: + description: + - The value of the key describing the record in the data group. + type: raw + records_src: + description: + - Path to a file with records in it. + - The file should be well-formed. This means it includes records, one per line, + that resemble the following format "key separator value". For example, C(foo := bar). + - BIG-IP is strict about this format, but this module is a bit more lax. It will allow + you to include arbitrary amounts (including none) of empty space on either side of + the separator. For an illustration of this, see the Examples section. + - Record keys are limited in length to no more than 65520 characters. + - Values of record keys are limited in length to no more than 65520 characters. + - The total number of records you can have in your BIG-IP is limited by the memory + of the BIG-IP itself. + - The format of this content is slightly different depending on whether you specify + a C(type) of C(address), C(integer), or C(string). See the examples section for + examples of the different types of payload formats that are expected in your data + group file. + - When C(internal) is C(no), at least one record must be specified in either C(records) + or C(records_src). + type: path + separator: + description: + - When specifying C(records_src), this is the string of characters that will + be used to break apart entries in the C(records_src) into key/value pairs. + - By default, the value of this parameter is C(:=). + - This value cannot be changed once it is set. + - This parameter is only relevant when C(internal) is C(no). It will be ignored + otherwise. + type: str + default: ":=" + delete_data_group_file: + description: + - When C(yes), ensures the remote data group file is deleted. + - This parameter is only relevant when C(state) is C(absent) and C(internal) is C(no). + type: bool + default: no + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures the data group exists. + - When C(state) is C(absent), ensures the data group is removed. + - The use of state in this module refers to the entire data group, not its members. + type: str + choices: + - present + - absent + default: present +notes: + - This module does NOT support atomic updates of data group members in a type C(internal) data group. + - Addition/Deletion of data group members in a type C(external) data group should be done through Ansible modules only, + if changes are made manually, the Ansible module will not detect those changes. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) + - Greg Crosby (@crosbygw) +''' + +EXAMPLES = r''' +- name: Create a data group of addresses + bigip_data_group: + name: foo + internal: yes + records: + - key: 0.0.0.0/32 + value: External_NAT + - key: 10.10.10.10 + value: No_NAT + type: address + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a data group of strings + bigip_data_group: + name: foo + internal: yes + records: + - key: caddy + value: "" + - key: cafeteria + value: "" + - key: cactus + value: "" + type: string + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a data group of IP addresses from a file + bigip_data_group: + name: foo + records_src: /path/to/dg-file + type: address + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Update an existing internal data group of strings + bigip_data_group: + name: foo + internal: yes + records: + - key: caddy + value: "" + - key: cafeteria + value: "" + - key: cactus + value: "" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Show the data format expected for records_content - address 1 + copy: + dest: /path/to/addresses.txt + content: | + network 10.0.0.0 prefixlen 8 := "Network1", + network 172.16.0.0 prefixlen 12 := "Network2", + network 192.168.0.0 prefixlen 16 := "Network3", + network 2402:9400:1000:0:: prefixlen 64 := "Network4", + host 192.168.20.1 := "Host1", + host 172.16.1.1 := "Host2", + host 172.16.1.1 := "Host3", + host 2001:0db8:85a3:0000:0000:8a2e:0370:7334 := "Host4", + host 2001:0db8:85a3:0000:0000:8a2e:0370:7334 := "Host5" + +- name: Show the data format expected for records_content - address 2 + copy: + dest: /path/to/addresses.txt + content: | + 10.0.0.0/8 := "Network1", + 172.16.0.0/12 := "Network2", + 192.168.0.0/16 := "Network3", + 2402:9400:1000:0::/64 := "Network4", + 192.168.20.1 := "Host1", + 172.16.1.1 := "Host2", + 172.16.1.1/32 := "Host3", + 2001:0db8:85a3:0000:0000:8a2e:0370:7334 := "Host4", + 2001:0db8:85a3:0000:0000:8a2e:0370:7334/128 := "Host5" + +- name: Show the data format expected for records_content - string + copy: + dest: /path/to/strings.txt + content: | + a := alpha, + b := bravo, + c := charlie, + x := x-ray, + y := yankee, + z := zulu, + +- name: Show the data format expected for records_content - integer + copy: + dest: /path/to/integers.txt + content: | + 1 := bar, + 2 := baz, + 3, + 4, +''' + +RETURN = r''' +# only common fields returned +''' + +import hashlib +import os +import re +from datetime import datetime + +from ansible.module_utils.six import iteritems +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ipaddress import ( + ip_network, ip_interface +) + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, transform_name +) +from ..module_utils.compare import ( + cmp_str_with_none +) +from ..module_utils.icontrol import ( + upload_file, tmos_version +) +from ..module_utils.ipaddress import is_valid_ip_interface +from ..module_utils.teem import send_teem + +LINE_LIMIT = 65000 +SIZE_LIMIT_BYTES = 4000000 + + +def zero_length(content): + content.seek(0, os.SEEK_END) + length = content.tell() + content.seek(0) + if length == 0: + return True + return False + + +def size_exceeded(content): + records = content + records.seek(0, os.SEEK_END) + size = records.tell() + records.seek(0) + if size > SIZE_LIMIT_BYTES: + return True + return False + + +def lines_exceeded(content): + result = False + for i, line in enumerate(content): + if i > LINE_LIMIT: + result = True + content.seek(0) + return result + + +class RecordsEncoder(object): + def __init__(self, record_type=None, separator=None): + self._record_type = record_type + self._separator = separator + self._network_pattern = re.compile(r'^network\s+(?P[^ ]+)\s+prefixlen\s+(?P\d+)\s+.*') + self._rd_net_prefix_ptrn = re.compile( + r'^network\s+(?P[^%]+)%(?P[0-9]+)\s+prefixlen\s+(?P\d+)\s+.*' + ) + self._host_pattern = re.compile(r'^host\s+(?P[^%]+)\s+.*') + self._rd_host_ptrn = re.compile(r'^host\s+(?P[^%]+)%(?P[0-9]+)\s+.*') + self._ipv4_cidr_ptrn = re.compile(r'^(?P((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|' + r'2[0-4][0-9]|[01]?[0-9][0-9]?))/(?P(3[0-2]|2[0-9]|1[0-9]|[0-9]))') + self._ipv4_cidr_ptrn_rd = re.compile(r'^(?P((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|' + r'2[0-4][0-9]|[01]?[0-9][0-9]?))%(?P[0-9]+)/' + r'(?P(3[0-2]|2[0-9]|1[0-9]|[0-9]))') + self._ipv6_cidr_ptrn = re.compile(r'^(?P^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:)' + r'{1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}' + r'(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|' + r'([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}' + r'(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:' + r'((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}' + r'|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.)' + r'{3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:' + r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}' + r'[0-9]){0,1}[0-9])))/(?P((1(1[0-9]|2[0-8]))|([0-9][0-9])|([0-9])))') + self._ipv6_cidr_ptrn_rd = re.compile(r'^(?P^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|' + r'([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|' + r'([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|' + r'([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|' + r'([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|' + r'([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|' + r'[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|' + r':)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}' + r'|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|' + r'1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|' + r'([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.)' + r'{3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])))%(?P[0-9]+)' + r'/(?P((1(1[0-9]|2[0-8]))|([0-9][0-9])|([0-9])))') + + def encode(self, record): + if isinstance(record, dict): + return self.encode_dict(record) + else: + return self.encode_string(record) + + def encode_dict(self, record): + if self._record_type == 'ip': + return self.encode_address_from_dict(record) + elif self._record_type == 'integer': + return self.encode_integer_from_dict(record) + else: + return self.encode_string_from_dict(record) + + def encode_rd_address(self, record, match, ipv6=False): + if is_valid_ip_interface(match.group('addr')): + key = ip_interface(u"{0}/{1}".format(match.group('addr'), match.group('cidr'))) + else: + raise F5ModuleError( + "When specifying an 'address' type, the value to the left of the separator must be an IP." + ) + if key and 'value' in record: + if ipv6 and key.network.prefixlen == 128: + return self.encode_host(str(key.ip) + '%' + match.group('rd'), record['value']) + else: + if not ipv6 and key.network.prefixlen == 32: + return self.encode_host(str(key.ip) + '%' + match.group('rd'), record['value']) + return self.encode_network( + str(key.network.network_address) + '%' + match.group('rd'), key.network.prefixlen, record['value'] + ) + elif key: + if ipv6 and key.network.prefixlen == 128: + return self.encode_host( + str(key.ip) + '%' + match.group('rd'), str(key.ip) + '%' + match.group('rd') + ) + else: + if not ipv6 and key.network.prefixlen == 32: + return self.encode_host( + str(key.ip) + '%' + match.group('rd'), str(key.ip) + '%' + match.group('rd') + ) + return self.encode_network( + str(key.network.network_address) + '%' + match.group('rd'), key.network.prefixlen, + str(key.network.network_address) + '%' + match.group('rd') + ) + + def encode_address_from_dict(self, record): + rd_match = re.match(self._ipv4_cidr_ptrn_rd, record['key']) + if rd_match: + return self.encode_rd_address(record, rd_match) + rd_match = re.match(self._ipv6_cidr_ptrn_rd, record['key']) + if rd_match: + return self.encode_rd_address(record, rd_match, ipv6=True) + if is_valid_ip_interface(record['key']): + key = ip_interface(u"{0}".format(str(record['key']))) + else: + raise F5ModuleError( + "When specifying an 'address' type, the value to the left of the separator must be an IP." + ) + ipv4_match = re.match(self._ipv4_cidr_ptrn, record['key']) + ipv6_match = re.match(self._ipv6_cidr_ptrn, record['key']) + + if key and 'value' in record: + if (ipv6_match and key.network.prefixlen == 128) or (ipv4_match and key.network.prefixlen == 32): + return self.encode_host(str(key.ip), record['value']) + else: + return self.encode_network( + str(key.network.network_address), key.network.prefixlen, record['value'] + ) + elif key: + if (ipv6_match and key.network.prefixlen == 128) or (ipv4_match and key.network.prefixlen == 32): + return self.encode_host(str(key.ip), str(key.ip)) + else: + return self.encode_network( + str(key.network.network_address), key.network.prefixlen, str(key.network.network_address) + ) + + def encode_integer_from_dict(self, record): + try: + int(record['key']) + except ValueError: + raise F5ModuleError( + "When specifying an 'integer' type, the value to the left of the separator must be a number." + ) + if 'key' in record and 'value' in record: + return '{0} {1} {2}'.format(record['key'], self._separator, record['value']) + elif 'key' in record: + return str(record['key']) + + def encode_string_from_dict(self, record): + if 'key' in record and 'value' in record: + return '{0} {1} {2}'.format(record['key'], self._separator, record['value']) + elif 'key' in record: + return '{0} {1} ""'.format(record['key'], self._separator) + + def encode_string(self, record): + record = record.strip().strip(',') + if self._record_type == 'ip': + return self.encode_address_from_string(record) + elif self._record_type == 'integer': + return self.encode_integer_from_string(record) + else: + return self.encode_string_from_string(record) + + def encode_address_from_string(self, record): + if self._network_pattern.match(record): + # network 192.168.0.0 prefixlen 16 := "Network3" + # network 2402:9400:1000:0:: prefixlen 64 := "Network4" + return record + elif self._host_pattern.match(record): + # host 172.16.1.1/32 := "Host3" + # host 2001:0db8:85a3:0000:0000:8a2e:0370:7334 := "Host4" + return record + elif self._rd_net_prefix_ptrn.match(record) or self._rd_host_ptrn.match(record): + # network 192.168.0.0%11/16 := "Network3" + # network 2402:9400:1000:0::%11/64 := "Network4" + # host 192.168.1.1%11/32 := "Host3" + # host 2001:0db8:85a3:0000:0000:8a2e:0370:7334%11 := "Host4" + return record + elif self._ipv4_cidr_ptrn_rd.match(record) or self._ipv6_cidr_ptrn_rd.match(record): + # 10.0.0.0%12/8 + # 2402:6940::%12/32 := "Network2" + # 192.168.1.1%12/32 := "Host1" + # 2402:9400:1000::%12/128 + parts = [r.strip() for r in record.split(self._separator)] + if parts[0] == '': + return + pattern = re.compile(r'(?P[^%]+)%(?P[0-9]+)/(?P[0-9]+)') + match = pattern.match(parts[0]) + addr = u"{0}/{1}".format(match.group('addr'), match.group('prefix')) + if not is_valid_ip_interface(addr): + raise F5ModuleError( + "When specifying an 'address' type, the value to the left of the separator must be an IP." + ) + key = ip_interface(addr) + ipv4_match = re.match(self._ipv4_cidr_ptrn, addr) + ipv6_match = re.match(self._ipv6_cidr_ptrn, addr) + if len(parts) == 2: + if (ipv4_match and key.network.prefixlen == 32) or (ipv6_match and key.network.prefixlen == 128): + return self.encode_host(str(key.ip) + '%' + str(match.group('rd')), parts[1]) + else: + return self.encode_network( + str(key.network.network_address) + '%' + str(match.group('rd')), + key.network.prefixlen, parts[1] + ) + elif len(parts) == 1 and parts[0] != '': + if (ipv4_match and key.network.prefixlen == 32) or (ipv6_match and key.network.prefixlen == 128): + return self.encode_host( + str(key.ip) + '%' + str(match.group('rd')), str(key.ip) + '%' + str(match.group('rd')) + ) + return self.encode_network( + str(key.network.network_address) + '%' + str(match.group('rd')), + key.network.prefixlen, str(key.network.network_address) + '%' + str(match.group('rd')) + ) + else: + # 192.168.0.0/16 := "Network3" + # 2402:9400:1000:0::/64 := "Network4" + # 10.0.0.0/8 + # 2402:6940::/32 := "Network2" + # 192.168.1.1/32 := "Host1" + # 2402:9400:1000::/128 + parts = [r.strip() for r in record.split(self._separator)] + if parts[0] == '': + return + if len(re.split(' ', parts[0])) == 1: + if not is_valid_ip_interface(parts[0]): + raise F5ModuleError( + "When specifying an 'address' type, the value to the left of the separator must be an IP." + ) + key = ip_interface(u"{0}".format(str(parts[0]))) + ipv4_match = re.match(self._ipv4_cidr_ptrn, str(parts[0])) + ipv6_match = re.match(self._ipv6_cidr_ptrn, str(parts[0])) + + if len(parts) == 2: + if (ipv4_match and key.network.prefixlen == 32) or (ipv6_match and key.network.prefixlen == 128): + return self.encode_host(str(key.ip), parts[1]) + else: + return self.encode_network(str(key.network.network_address), key.network.prefixlen, parts[1]) + elif len(parts) == 1 and parts[0] != '': + if (ipv4_match and key.network.prefixlen == 32) or (ipv6_match and key.network.prefixlen == 128): + return self.encode_host(str(key.ip), str(key.ip)) + return self.encode_network( + str(key.network.network_address), key.network.prefixlen, str(key.network.network_address) + ) + else: + return str(parts[0]) + + def encode_host(self, key, value): + return 'host {0} {1} {2}'.format(str(key), self._separator, str(value)) + + def encode_network(self, key, prefixlen, value): + return 'network {0} prefixlen {1} {2} {3}'.format( + str(key), str(prefixlen), self._separator, str(value) + ) + + def encode_integer_from_string(self, record): + parts = record.split(self._separator) + if len(parts) == 1 and parts[0] == '': + return None + try: + int(parts[0]) + except ValueError: + raise F5ModuleError( + "When specifying an 'integer' type, the value to the left of the separator must be a number." + ) + if len(parts) == 2: + return '{0} {1} {2}'.format(parts[0], self._separator, parts[1]) + elif len(parts) == 1: + return str(parts[0]) + + def encode_string_from_string(self, record): + parts = record.split(self._separator) + if len(parts) == 2: + return '{0} {1} {2}'.format(parts[0], self._separator, parts[1]) + elif len(parts) == 1 and parts[0] != '': + return '{0} {1} ""'.format(parts[0], self._separator) + + +class RecordsDecoder(object): + def __init__(self, record_type=None, separator=None): + self._record_type = record_type + self._separator = separator + self._net_prefix_pattern = re.compile(r'^network\s+(?P[^ ]+)\s+prefixlen\s+(?P\d+)\s+.*') + self._rd_net_prefix_ptrn = re.compile(r'^network\s+(?P[^%]+)%(?P[0-9]+)' + r'\s+prefixlen\s+(?P\d+)\s+.*') + self._host_pattern = re.compile(r'^host\s+(?P[^ ,]+)\s?.*') + self._net_pattern = re.compile(r'^^network\s+(?P[^ ,]+)\s?.*') + self._rd_host_ptrn = re.compile(r'^host\s+(?P[^%]+)%(?P[0-9]+)\s+.*') + + def decode(self, record): + record = record.strip().strip(',') + if self._record_type == 'ip': + return self.decode_address_from_string(record) + else: + return self.decode_from_string(record) + + def decode_address_from_string(self, record): + matches = self._rd_net_prefix_ptrn.match(record) + if matches: + # network 192.168.0.0%11 prefixlen 16 := "Network3", + # network 2402:9400:1000:0::%11 prefixlen 64 := "Network4", + value = record.split(self._separator)[1].strip().strip('"') + addr = "{0}%{1}/{2}".format(matches.group('addr'), matches.group('rd'), matches.group('prefix')) + result = dict(name=addr, data=value) + return result + matches = self._net_prefix_pattern.match(record) + if matches: + # network 192.168.0.0 prefixlen 16 := "Network3", + # network 2402:9400:1000:0:: prefixlen 64 := "Network4", + key = u"{0}/{1}".format(matches.group('addr'), matches.group('prefix')) + addr = ip_network(key) + value = record.split(self._separator)[1].strip().strip('"') + result = dict(name=str(addr), data=value) + return result + matches = self._net_pattern.match(record) + if matches: + # network 192.168.2.0/24, + # network 2402:9400:1000:0:: prefixlen 64 := "Network4", + key = u"{0}".format(matches.group('addr')) + addr = ip_network(key) + if len(record.split(self._separator)) > 1: + value = record.split(self._separator)[1].strip().strip('"') + result = dict(name=str(addr), data=value) + return result + return str(record) + matches = self._rd_host_ptrn.match(record) + if matches: + # host 172.16.1.1%11/32 := "Host3" + # host 2001:0db8:85a3:0000:0000:8a2e:0370:7334%11 := "Host4" + host = ip_interface(u"{0}".format(matches.group('addr'))) + addr = "{0}%{1}/{2}".format(matches.group('addr'), matches.group('rd'), str(host.network.prefixlen)) + value = record.split(self._separator)[1].strip().strip('"') + result = dict(name=addr, data=value) + return result + matches = self._host_pattern.match(record) + if matches: + # host 172.16.1.1/32 := "Host3" + # host 2001:0db8:85a3:0000:0000:8a2e:0370:7334 := "Host4" + key = matches.group('addr') + addr = ip_interface(u"{0}".format(str(key))) + if len(record.split(self._separator)) > 1: + value = record.split(self._separator)[1].strip().strip('"') + result = dict(name=str(addr), data=value) + return result + return str(record) + + raise F5ModuleError( + 'The value "{0}" is not an address'.format(record) + ) + + def decode_from_string(self, record): + parts = record.split(self._separator) + if len(parts) == 2: + return dict(name=parts[0].strip(), data=parts[1].strip().strip('"')) + else: + return dict(name=parts[0].strip(), data="") + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'externalFileName': 'external_file_name', + } + + api_attributes = [ + 'records', + 'type', + 'description', + ] + + returnables = [ + 'type', + 'records', + 'description', + ] + + updatables = [ + 'records', + 'checksum', + 'description', + ] + + @property + def type(self): + if self._values['type'] in ['address', 'addr', 'ip']: + return 'ip' + elif self._values['type'] in ['integer', 'int']: + return 'integer' + elif self._values['type'] in ['string']: + return 'string' + + @property + def records_src(self): + try: + self._values['records_src'].seek(0) + return self._values['records_src'] + except AttributeError: + pass + + if self._values['records_src']: + records = open(self._values['records_src']) + else: + records = self._values['records'] + + if records is None: + return None + + # There is a 98% chance that the user will supply a data group that is < 1MB. + # 99.917% chance it is less than 10 MB. This is well within the range of typical + # memory available on a system. + # + # If this changes, this may need to be changed to use temporary files instead. + self._values['records_src'] = StringIO() + + self._write_records_to_file(records) + return self._values['records_src'] + + def _write_records_to_file(self, records): + bucket_size = 1000000 + bucket = [] + encoder = RecordsEncoder(record_type=self.type, separator=self.separator) + for record in records: + result = encoder.encode(record) + if result: + bucket.append(to_text(result + ",\n")) + if len(bucket) == bucket_size: + self._values['records_src'].writelines(bucket) + bucket = [] + self._values['records_src'].writelines(bucket) + self._values['records_src'].seek(0) + + +class ApiParameters(Parameters): + @property + def checksum(self): + if self._values['checksum'] is None: + return None + result = self._values['checksum'].split(':')[2] + return result + + @property + def records(self): + cleaned_records_list = [] + if self._values['records'] is None: + return None + for record in self._values['records']: + clean_record = {} + for k, v in record.items(): + clean_record[k] = re.sub('\\\\', '', v) + cleaned_records_list.append(clean_record) + return cleaned_records_list + + @property + def records_list(self): + return self._values['records'] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def checksum(self): + if self._values['checksum']: + return self._values['checksum'] + if self.records_src is None: + return None + result = hashlib.sha1() + records = self.records_src + while True: + data = records.read(4096) + if not data: + break + result.update(data.encode('utf-8')) + result = result.hexdigest() + self._values['checksum'] = result + return result + + @property + def external_file_name(self): + if self._values['external_file_name'] is None: + name = self.name + else: + name = self._values['external_file_name'] + if re.search(r'[^a-zA-Z0-9-_.]', name): + raise F5ModuleError( + "'external_file_name' may only contain letters, numbers, underscores, dashes, or a period." + ) + return name + + @property + def records(self): + results = [] + if self.records_src is None: + return None + decoder = RecordsDecoder(record_type=self.type, separator=self.separator) + for record in self.records_src: + result = decoder.decode(record) + if result: + results.append(result) + return results + + @property + def records_list(self): + if self._values['records'] is None: + return None + return self.records + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def _compare_records(self): + want = self.want.records + have = self.have.records + if want == [] and have is None: + return None + if want is None: + return None + w = [] + h = [] + + for x in want: + tmp = tuple((str(k), str(v)) for k, v in iteritems(x)) + w.append(tmp) + for x in have: + tmp = tuple((str(k), str(v)) for k, v in iteritems(x)) + h.append(tmp) + + if set(w) == set(h): + return None + else: + return want + + @property + def records(self): + # External data groups are compared by their checksum, not their records. This + # is because the BIG-IP does not store the actual records in the API. It instead + # stores the checksum of the file. External DGs have the possibility of being huge + # and we would never want to do a comparison of such huge files. + # + # Therefore, comparison is no-op if the DG being worked with is an external DG. + if self.want.internal is False: + return None + if self.have.records is None and self.want.records == []: + return None + if self.have.records is None: + return self.want.records + result = self._compare_records() + return result + + @property + def type(self): + return None + + @property + def checksum(self): + if self.want.internal: + return None + if self.want.checksum is None: + return None + if self.want.checksum != self.have.checksum: + return True + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_changed_options(self): + changed = {} + for key in ApiParameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = ApiParameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + +class InternalManager(BaseManager): + def create(self): + self._set_changed_options() + if size_exceeded(self.want.records_src) or lines_exceeded(self.want.records_src): + raise F5ModuleError( + "The size of the provided data (or file) is too large for an internal data group." + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/data-group/internal/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ExternalManager(BaseManager): + def absent(self): + result = False + if self.exists(): + result = self.remove() + if self.external_file_exists() and self.want.delete_data_group_file: + result = self.remove_data_group_file_from_device() + return result + + def create(self): + if zero_length(self.want.records_src): + raise F5ModuleError( + "An external data group cannot be empty." + ) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.changes.records_src and zero_length(self.want.records_src): + raise F5ModuleError( + "An external data group cannot be empty." + ) + if self.module.check_mode: + return True + self.update_on_device() + return True + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def external_file_exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.external_file_name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def _upload_to_file(self, name, type, remote_path, update=False): + self.upload_file_to_device(self.want.records_src, name) + if update: + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, name) + ) + params = {'sourcePath': 'file:{0}'.format(remote_path)} + resp = self.client.api.patch(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + else: + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + params = dict( + name=name, + type=type, + sourcePath='file:{0}'.format(remote_path), + partition=self.want.partition + ) + resp = self.client.api.post(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response['name'] + raise F5ModuleError(resp.content) + + def remove_file_on_device(self, remote_path): + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs=remote_path + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(response.content) + + def create_on_device(self): + name = self.want.external_file_name + remote_path = '/var/config/rest/downloads/{0}'.format(name) + external_file = self._upload_to_file(name, self.want.type, remote_path, update=False) + + params = dict( + name=self.want.name, + partition=self.want.partition, + externalFileName=external_file, + ) + if self.want.description: + params['description'] = self.want.description + + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(response.content) + + self.remove_file_on_device(remote_path) + + def update_on_device(self): + params = {} + + if self.want.records_src is not None: + name = self.want.external_file_name + remote_path = '/var/config/rest/downloads/{0}'.format(name) + external_file = self._upload_to_file(name, self.have.type, remote_path, update=True) + params['externalFileName'] = external_file + if self.changes.description is not None: + params['description'] = self.changes.description + + if not params: + return + + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + # Remove the remote data group file if asked to + if self.want.delete_data_group_file: + self.remove_data_group_file_from_device() + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def remove_data_group_file_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.external_file_name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + """Reads the current configuration from the device + + For an external data group, we are interested in two things from the + current configuration + + * ``checksum`` + * ``type`` + + The ``checksum`` will allow us to compare the data group value we have + with the data group value being provided. + + The ``type`` will allow us to do validation on the data group value being + provided (if any). + + Returns: + ExternalApiParameters: Attributes of the remote resource. + """ + + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp_dg = self.client.api.get(uri) + + try: + response_dg = resp_dg.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp_dg.status not in [200, 201] or 'code' in response_dg and response_dg['code'] not in [200, 201]: + raise F5ModuleError(resp_dg.content) + + external_file = os.path.basename(response_dg['externalFileName']) + external_file_partition = os.path.dirname(response_dg['externalFileName']).strip('/') + + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(external_file_partition, external_file) + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + result = ApiParameters(params=response) + result.update({'description': response_dg.get('description', None)}) + return result + raise F5ModuleError(resp.content) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + self.module = kwargs.get('module') + + def exec_module(self): + if self.module.params['internal']: + manager = self.get_manager('internal') + else: + manager = self.get_manager('external') + return manager.exec_module() + + def get_manager(self, type): + if type == 'internal': + return InternalManager(**self.kwargs) + elif type == 'external': + return ExternalManager(**self.kwargs) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + type=dict( + choices=['address', 'addr', 'ip', 'string', 'integer', 'int'], + default='string' + ), + delete_data_group_file=dict( + type='bool', + default='no' + ), + internal=dict(type='bool', default='no'), + records=dict( + type='list', + elements='raw', + options=dict( + key=dict(required=True), + value=dict(type='raw') + ) + ), + records_src=dict(type='path'), + external_file_name=dict(), + separator=dict(default=':='), + description=dict(), + state=dict(choices=['absent', 'present'], default='present'), + 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 = [ + ['records', 'records_src'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth.py new file mode 100644 index 00000000..1aaee0c8 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth.py @@ -0,0 +1,826 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_auth +short_description: Manage system authentication on a BIG-IP +description: + - Manage the system authentication configuration. This module can assist in configuring + a number of different system authentication types. Note that this module can not be used + to configure APM authentication types. +version_added: "1.0.0" +options: + type: + description: + - The authentication type to manage with this module. + - Take special note that the parameters supported by this module will vary depending + on the C(type) that you are configuring. + - At this time, this module only supports a subset of the total available auth types. + type: str + required: True + choices: + - tacacs + - local + servers: + description: + - Specifies a list of the IPv4 addresses for servers using the Terminal + Access Controller Access System (TACACS)+ protocol with which the system + communicates to obtain authorization data. + - For each address, an alternate TCP port number may be optionally specified + by specifying the C(port) key. + - If no port number is specified, the default port C(49163) is used. + - This parameter is supported by the C(tacacs) type. + type: raw + suboptions: + address: + description: + - The IP address of the server. + - This field is required, unless you are specifying a simple list of servers. + In that case, the simple list can specify server IPs. See the examples for + more clarification. + port: + description: + - The port of the server. + secret: + description: + - Secret key used to encrypt and decrypt packets sent or received from the + server. + - B(Do not) use the pound/hash sign in the secret for TACACS+ servers. + - When configuring TACACS+ auth for the first time, this value is required. + type: str + service_name: + description: + - Specifies the name of the service the user is requesting to be + authorized to use. + - Identifying what the user is asking to be authorized for enables the + TACACS+ serverc to behave differently for different types of authorization + requests. + - This setting is required when configuring this form of system authentication. + - Note that the majority of TACACS+ implementations are of service type C(ppp), + so try that first. + type: str + choices: + - slip + - ppp + - arap + - shell + - tty-daemon + - connection + - system + - firewall + protocol_name: + description: + - Specifies the protocol associated with the value specified in C(service_name), + which is a subset of the associated service being used for client authorization + or system accounting. + - Note that the majority of TACACS+ implementations are of protocol type C(ip), + so try that first. + type: str + choices: + - lcp + - ip + - ipx + - atalk + - vines + - lat + - xremote + - tn3270 + - telnet + - rlogin + - pad + - vpdn + - ftp + - http + - deccp + - osicp + - unknown + authentication: + description: + - Specifies the process the system employs when sending authentication requests. + - When C(use-first-server), specifies the system sends authentication + attempts only to the first server in the list. + - When C(use-all-servers), specifies the system sends an authentication + request to each server until authentication succeeds, or until the system has + sent a request to all servers in the list. + - This parameter is supported by the C(tacacs) type. + type: str + choices: + - use-first-server + - use-all-servers + accounting: + description: + - Specifies how the system returns accounting information, such as which services + users access and the amount of network resources they consume, to the TACACS+ server. + - When C(send-to-first-server), specifies the system transmits accounting + information back to the first available TACACS+ server in the list. + - When C(send-to-all-servers), specifies the system transmits accounting + information back to all TACACS+ servers in the list. + - This parameter is supported by the C(tacacs) type. + type: str + choices: + - send-to-first-server + - send-to-all-servers + use_for_auth: + description: + - Specifies whether or not this auth source is put in use on the system. + type: bool + state: + description: + - The state of the authentication configuration on the system. + - When C(present), guarantees the system is configured for the specified C(type). + - When C(absent), sets the system auth source back to C(local). + type: str + choices: + - absent + - present + default: present + update_secret: + description: + - C(always) will allow updating secrets if the user chooses to do so. + - C(on_create) will only set the secret when a C(use_auth_source) is C(yes) + and TACACS+ is not currently the auth source. + type: str + choices: + - always + - on_create + default: always +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Nitin Khanna (@nitinthewiz) +''' + +EXAMPLES = r''' +- name: Set the system auth to TACACS+, default server port + bigip_device_auth: + type: tacacs + authentication: use-all-servers + accounting: send-to-all-servers + protocol_name: ip + secret: secret + servers: + - 10.10.10.10 + - 10.10.10.11 + service_name: ppp + state: present + use_for_auth: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set the system auth to TACACS+, override server port + bigip_device_auth: + type: tacacs + authentication: use-all-servers + protocol_name: ip + secret: secret + servers: + - address: 10.10.10.10 + port: 1234 + - 10.10.10.11 + service_name: ppp + use_for_auth: yes + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +servers: + description: List of servers used in TACACS authentication. + returned: changed + type: list + sample: ['1.2.2.1', '4.5.5.4'] +authentication: + description: Process the system uses to serve authentication requests when using TACACS. + returned: changed + type: str + sample: use-all-servers +accounting: + description: Which servers to send information to when using TACACS. + returned: changed + type: str + sample: send-to-all-servers +service_name: + description: Name of the service the user is requesting to be authorized to use. + returned: changed + type: str + sample: ppp +protocol_name: + description: Name of the protocol associated with C(service_name) used for client authentication. + returned: changed + type: str + sample: ip +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class BaseParameters(AnsibleF5Parameters): + @property + def api_map(self): + return {} + + @property + def api_attributes(self): + return [] + + @property + def returnables(self): + return [] + + @property + def updatables(self): + return [] + + +class BaseApiParameters(BaseParameters): + pass + + +class BaseModuleParameters(BaseParameters): + pass + + +class BaseChanges(BaseParameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class BaseUsableChanges(BaseChanges): + pass + + +class BaseReportableChanges(BaseChanges): + pass + + +class TacacsParameters(BaseParameters): + api_map = { + 'protocol': 'protocol_name', + 'service': 'service_name' + } + + api_attributes = [ + 'authentication', + 'accounting', + 'protocol', + 'service', + 'secret', + 'servers' + ] + + returnables = [ + 'servers', + 'secret', + 'authentication', + 'accounting', + 'service_name', + 'protocol_name' + ] + + updatables = [ + 'servers', + 'secret', + 'authentication', + 'accounting', + 'service_name', + 'protocol_name', + 'auth_source', + ] + + +class TacacsApiParameters(TacacsParameters): + pass + + +class TacacsModuleParameters(TacacsParameters): + @property + def servers(self): + if self._values['servers'] is None: + return None + result = [] + for server in self._values['servers']: + if isinstance(server, dict): + if 'address' not in server: + raise F5ModuleError( + "An 'address' field must be provided when specifying separate fields to the 'servers' parameter." + ) + address = server.get('address') + port = server.get('port', None) + elif isinstance(server, string_types): + address = server + port = None + if port is None: + result.append('{0}'.format(address)) + else: + result.append('{0}:{1}'.format(address, port)) + return result + + @property + def auth_source(self): + return 'tacacs' + + +class TacacsChanges(BaseChanges, TacacsParameters): + pass + + +class TacacsUsableChanges(TacacsChanges): + pass + + +class TacacsReportableChanges(TacacsChanges): + @property + def secret(self): + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + want = getattr(self.want, param) + try: + have = getattr(self.have, param) + if want != have: + return want + except AttributeError: + return want + + @property + def secret(self): + if self.want.secret != self.have.secret and self.want.update_secret == 'always': + return self.want.secret + + +class BaseManager(object): + def _set_changed_options(self): + changed = {} + for key in self.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = self.get_usable_changes(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = self.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = self.get_usable_changes(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + 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() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update_auth_source_on_device(self, source): + """Set the system auth source. + + Configuring the authentication source is only one step in the process of setting + up an auth source. The other step is to inform the system of the auth source + you want to use. + + This method is used for situations where + + * The ``use_for_auth`` parameter is set to ``yes`` + * The ``use_for_auth`` parameter is set to ``no`` + * The ``state`` parameter is set to ``absent`` + + When ``state`` equal to ``absent``, before you can delete the TACACS+ configuration, + you must set the system auth to "something else". The system ships with a system + auth called "local", so this is the logical "something else" to use. + + When ``use_for_auth`` is no, the same situation applies as when ``state`` equal + to ``absent`` is done above. + + When ``use_for_auth`` is ``yes``, this method will set the current system auth + state to TACACS+. + + Arguments: + source (string): The source that you want to set on the device. + """ + params = dict( + type=source + ) + uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + 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 read_current_auth_source_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/source".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response['type'] + raise F5ModuleError(resp.content) + + +class LocalManager(BaseManager): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = self.get_module_parameters(params=self.module.params) + self.have = self.get_api_parameters() + self.changes = self.get_usable_changes() + + @property + def returnables(self): + return [] + + @property + def updatables(self): + return [] + + def get_parameters(self, params=None): + return BaseParameters(params=params) + + def get_usable_changes(self, params=None): + return BaseUsableChanges(params=params) + + def get_reportable_changes(self, params=None): + return BaseReportableChanges(params=params) + + def get_module_parameters(self, params=None): + return BaseModuleParameters(params=params) + + def get_api_parameters(self, params=None): + return BaseApiParameters(params=params) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/auth/source".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if response['type'] == 'local': + return True + return False + raise F5ModuleError(resp.content) + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.update_auth_source_on_device('local') + return True + + def present(self): + if not self.exists(): + return self.create() + + def absent(self): + raise F5ModuleError( + "The 'local' type cannot be removed. " + "Instead, specify a 'state' of 'present' on other types." + ) + + +class TacacsManager(BaseManager): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = self.get_module_parameters(params=self.module.params) + self.have = self.get_api_parameters() + self.changes = self.get_usable_changes() + + @property + def returnables(self): + return TacacsParameters.returnables + + @property + def updatables(self): + return TacacsParameters.updatables + + def get_usable_changes(self, params=None): + return TacacsUsableChanges(params=params) + + def get_reportable_changes(self, params=None): + return TacacsReportableChanges(params=params) + + def get_module_parameters(self, params=None): + return TacacsModuleParameters(params=params) + + def get_api_parameters(self, params=None): + return TacacsApiParameters(params=params) + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + if self.want.use_for_auth: + self.update_auth_source_on_device('tacacs') + return True + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + result = False + if self.update_on_device(): + result = True + if self.want.use_for_auth and self.changes.auth_source == 'tacacs': + self.update_auth_source_on_device('tacacs') + result = True + return result + + def remove(self): + if self.module.check_mode: + return True + self.update_auth_source_on_device('local') + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = 'system-auth' + uri = "https://{0}:{1}/mgmt/tm/auth/tacacs".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + if not params: + return False + + uri = 'https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response['auth_source'] = self.read_current_auth_source_from_device() + return self.get_api_parameters(params=response) + raise F5ModuleError(resp.content) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.kwargs = kwargs + + def exec_module(self): + manager = self.get_manager(self.module.params['type']) + return manager.exec_module() + + def get_manager(self, type): + if type == 'tacacs': + return TacacsManager(**self.kwargs) + elif type == 'local': + return LocalManager(**self.kwargs) + else: + raise F5ModuleError( + "The provided 'type' is unknown." + ) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + type=dict( + required=True, + choices=['local', 'tacacs'] + ), + servers=dict(type='raw'), + secret=dict(no_log=True), + service_name=dict( + choices=[ + 'slip', 'ppp', 'arap', 'shell', 'tty-daemon', + 'connection', 'system', 'firewall' + ] + ), + protocol_name=dict( + choices=[ + 'lcp', 'ip', 'ipx', 'atalk', 'vines', 'lat', + 'xremote', 'tn3270', 'telnet', 'rlogin', 'pad', + 'vpdn', 'ftp', 'http', 'deccp', 'osicp', 'unknown' + ] + ), + authentication=dict( + choices=[ + 'use-first-server', + 'use-all-servers' + ] + ), + accounting=dict( + choices=[ + 'send-to-first-server', + 'send-to-all-servers' + ] + ), + use_for_auth=dict(type='bool'), + update_secret=dict( + choices=['always', 'on_create'], + default='always' + ), + state=dict( + default='present', + choices=['present', 'absent'] + ), + + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_ldap.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_ldap.py new file mode 100644 index 00000000..1a915e0f --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_ldap.py @@ -0,0 +1,911 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_auth_ldap +short_description: Manage LDAP device authentication settings on BIG-IP +description: + - Manage LDAP device authentication settings on BIG-IP. +version_added: "1.0.0" +options: + servers: + description: + - Specifies the LDAP servers the system must use to obtain + authentication information. You must specify a server when you + create an LDAP configuration object. + type: list + elements: str + port: + description: + - Specifies the port the system uses for access to the remote host server. + - When configuring LDAP device authentication for the first time, the default + port is C(389) if this parameter is not specified. + type: int + remote_directory_tree: + description: + - Specifies the file location (tree) of the user authentication database on the + server. + type: str + scope: + description: + - Specifies the level of the remote Active Directory or LDAP directory the + system should search for the user authentication. + type: str + choices: + - sub + - one + - base + bind_dn: + description: + - Specifies the distinguished name for the Active Directory or LDAP server user + ID. + - The BIG-IP client authentication module does not support Active Directory or + LDAP servers that do not perform bind referral when authenticating referred + accounts. + - Therefore, if you plan to use Active Directory or LDAP as your authentication + source and want to use referred accounts, make sure your servers perform bind + referral. + type: str + bind_password: + description: + - Specifies a password for the Active Directory or LDAP server user ID. + type: str + user_template: + description: + - Specifies the distinguished name of the user who is logging on. + - You specify the template as a variable that the system replaces with user-specific + information during the logon attempt. + - For example, you could specify a user template such as C(%s@siterequest.com) or + C(uxml:id=%s,ou=people,dc=siterequest,dc=com). + - When a user attempts to log on, the system replaces C(%s) with the name the user + specified in the Basic Authentication dialog box, and passes that as the + distinguished name for the bind operation. + - The system passes the associated password as the password for the bind operation. + - This field can contain only one C(%s) and cannot contain any other format + specifiers. + type: str + check_member_attr: + description: + - Checks the member attribute of the user in the remote LDAP or AD group. + type: bool + ssl: + description: + - Specifies whether the system uses an SSL port to communicate with the LDAP server. + type: str + choices: + - "yes" + - "no" + - start-tls + ca_cert: + description: + - Specifies the name of an SSL certificate from a certificate authority (CA). + - To remove this value, use the reserved value C(none). + type: str + aliases: [ ssl_ca_cert ] + client_key: + description: + - Specifies the name of an SSL client key. + - To remove this value, use the reserved value C(none). + type: str + aliases: [ ssl_client_key ] + client_cert: + description: + - Specifies the name of an SSL client certificate. + - To remove this value, use the reserved value C(none). + type: str + aliases: [ ssl_client_cert ] + validate_certs: + description: + - Specifies whether the system checks an SSL peer, as a result of which the + system requires and verifies the server certificate. + type: bool + aliases: [ ssl_check_peer ] + login_ldap_attr: + description: + - Specifies the LDAP directory attribute containing the local user name that is + associated with the selected directory entry. + - If this parameter is not specified, when configuring LDAP device authentication for the first time, + the default port is C(samaccountname). + type: str + fallback_to_local: + description: + - Specifies the system uses the Local authentication method if the remote + authentication method is not available. + - Option only available on C(TMOS 13.0.0) and above. + type: bool + referrals: + description: + - Specifies whether automatic referral chasing should be enabled. + - Option only available on C(TMOS 15.1.0) and above. + type: bool + version_added: "1.22.0" + state: + description: + - When C(present), ensures the device authentication method exists. + - When C(absent), ensures the device authentication method does not exist. + - When C(state) equal to (absent), before you can delete the LDAP configuration, the system must set auth to + some alternative. The system ships with a system auth called C(local), therefore the system authentication type + is set to that value on the device upon removal of LDAP configuration. + type: str + choices: + - present + - absent + default: present + update_password: + description: + - C(always) always updates the C(bind_password). + - C(on_create) only sets the C(bind_password) for newly created authentication + mechanisms. + type: str + choices: + - always + - on_create + default: always + use_for_auth: + description: + - Specifies whether or not this auth source is put in use on the system. + - If C(yes), the module sets the current system auth type to the value of C(ldap). + - If C(no), the module sets the authentication type to C(local), similar behavior to when C(state) is C(absent), + without removing the configured LDAP resource. + type: bool + source_type: + description: + - Specifies the auth source for user authentication, should be used with C(use_for_auth). + type: str + choices: + - ldap + - active-directory + default: ldap + version_added: "1.13.0" +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an LDAP authentication object + bigip_device_auth_ldap: + name: foo + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +servers: + description: LDAP servers used by the system to obtain authentication information. + returned: changed + type: list + sample: ['192.168.1.1', '192.168.1.2'] +port: + description: The port the system uses for access to the remote LDAP server. + returned: changed + type: int + sample: 389 +remote_directory_tree: + description: File location (tree) of the user authentication database on the server. + returned: changed + type: str + sample: "CN=Users,DC=FOOBAR,DC=LOCAL" +scope: + description: The level of the remote Active Directory or LDAP directory searched for user authentication. + returned: changed + type: str + sample: base +bind_dn: + description: The distinguished name for the Active Directory or LDAP server user ID. + returned: changed + type: str + sample: "user@foobar.local" +user_template: + description: The distinguished name of the user who is logging on. + returned: changed + type: str + sample: "uid=%s,ou=people,dc=foobar,dc=local" +check_member_attr: + description: The user's member attribute in the remote LDAP or AD group. + returned: changed + type: bool + sample: yes +ssl: + description: Specifies whether the system uses an SSL port to communicate with the LDAP server. + returned: changed + type: str + sample: start-tls +ca_cert: + description: The name of an SSL certificate from a certificate authority. + returned: changed + type: str + sample: My-Trusted-CA-Bundle.crt +client_key: + description: The name of an SSL client key. + returned: changed + type: str + sample: MyKey.key +client_cert: + description: The name of an SSL client certificate. + returned: changed + type: str + sample: MyCert.crt +validate_certs: + description: Indicates if the system checks an SSL peer. + returned: changed + type: bool + sample: yes +login_ldap_attr: + description: The LDAP directory attribute containing the local user name associated with the selected directory entry. + returned: changed + type: str + sample: samaccountname +fallback_to_local: + description: Specifies the system uses the Local authentication method as fallback + returned: changed + type: bool + sample: yes +referrals: + description: Specifies whether automatic referral chasing should be enabled + returned: changed + type: bool + sample: yes +''' + +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'bindDn': 'bind_dn', + 'bindPw': 'bind_password', + 'userTemplate': 'user_template', + 'fallback': 'fallback_to_local', + 'referrals': 'referrals', + 'loginAttribute': 'login_ldap_attr', + 'sslCheckPeer': 'validate_certs', + 'sslClientCert': 'client_cert', + 'sslClientKey': 'client_key', + 'sslCaCertFile': 'ca_cert', + 'checkRolesGroup': 'check_member_attr', + 'searchBaseDn': 'remote_directory_tree', + } + + api_attributes = [ + 'bindDn', + 'bindPw', + 'checkRolesGroup', + 'loginAttribute', + 'port', + 'scope', + 'searchBaseDn', + 'servers', + 'ssl', + 'sslCaCertFile', + 'sslCheckPeer', + 'sslClientCert', + 'sslClientKey', + 'userTemplate', + 'referrals', + ] + + returnables = [ + 'bind_dn', + 'bind_password', + 'check_member_attr', + 'fallback_to_local', + 'referrals', + 'login_ldap_attr', + 'port', + 'remote_directory_tree', + 'scope', + 'servers', + 'ssl', + 'ca_cert', + 'validate_certs', + 'client_cert', + 'client_key', + 'user_template', + ] + + updatables = [ + 'bind_dn', + 'bind_password', + 'check_member_attr', + 'fallback_to_local', + 'referrals', + 'login_ldap_attr', + 'port', + 'remote_directory_tree', + 'scope', + 'servers', + 'ssl', + 'ca_cert', + 'validate_certs', + 'client_cert', + 'client_key', + 'user_template', + 'auth_source', + ] + + @property + def ca_cert(self): + if self._values['ca_cert'] is None: + return None + elif self._values['ca_cert'] in ['none', '']: + return '' + return fq_name(self.partition, self._values['ca_cert']) + + @property + def client_key(self): + if self._values['client_key'] is None: + return None + elif self._values['client_key'] in ['none', '']: + return '' + return fq_name(self.partition, self._values['client_key']) + + @property + def client_cert(self): + if self._values['client_cert'] is None: + return None + elif self._values['client_cert'] in ['none', '']: + return '' + return fq_name(self.partition, self._values['client_cert']) + + @property + def validate_certs(self): + return flatten_boolean(self._values['validate_certs']) + + @property + def check_member_attr(self): + return flatten_boolean(self._values['check_member_attr']) + + @property + def login_ldap_attr(self): + if self._values['login_ldap_attr'] is None: + return None + elif self._values['login_ldap_attr'] in ['none', '']: + return '' + return self._values['login_ldap_attr'] + + @property + def user_template(self): + if self._values['user_template'] is None: + return None + elif self._values['user_template'] in ['none', '']: + return '' + return self._values['user_template'] + + @property + def ssl(self): + if self._values['ssl'] is None: + return None + elif self._values['ssl'] == 'start-tls': + return 'start-tls' + return flatten_boolean(self._values['ssl']) + + @property + def fallback_to_local(self): + return flatten_boolean(self._values['fallback_to_local']) + + @property + def referrals(self): + return flatten_boolean(self._values['referrals']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def use_for_auth(self): + return flatten_boolean(self._values['use_for_auth']) + + @property + def auth_source(self): + if self._values['use_for_auth'] is None: + return None + if self.use_for_auth == 'yes': + return self._values['source_type'] + if self.use_for_auth == 'no': + return 'local' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def validate_certs(self): + if self._values['validate_certs'] is None: + return None + elif self._values['validate_certs'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def fallback_to_local(self): + if self._values['fallback_to_local'] is None: + return None + elif self._values['fallback_to_local'] == 'yes': + return 'true' + return 'false' + + @property + def referrals(self): + if self._values['referrals'] is None: + return None + elif self._values['referrals'] == 'yes': + return 'yes' + return 'no' + + @property + def check_member_attr(self): + if self._values['check_member_attr'] is None: + return None + elif self._values['check_member_attr'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def ssl(self): + if self._values['ssl'] is None: + return None + elif self._values['ssl'] == 'start-tls': + return 'start-tls' + elif self._values['ssl'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def bind_password(self): + return None + + @property + def validate_certs(self): + return flatten_boolean(self._values['validate_certs']) + + @property + def referrals(self): + return flatten_boolean(self._values['referrals']) + + @property + def check_member_attr(self): + return flatten_boolean(self._values['check_member_attr']) + + @property + def ssl(self): + if self._values['ssl'] is None: + return None + elif self._values['ssl'] == 'start-tls': + return 'start-tls' + return flatten_boolean(self._values['ssl']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def login_ldap_attr(self): + return cmp_str_with_none(self.want.login_ldap_attr, self.have.login_ldap_attr) + + @property + def user_template(self): + return cmp_str_with_none(self.want.user_template, self.have.user_template) + + @property + def ca_cert(self): + return cmp_str_with_none(self.want.ca_cert, self.have.ca_cert) + + @property + def client_key(self): + return cmp_str_with_none(self.want.client_key, self.have.client_key) + + @property + def client_cert(self): + return cmp_str_with_none(self.want.client_cert, self.have.client_cert) + + @property + def bind_password(self): + if self.want.bind_password != self.have.bind_password and self.want.update_password == 'always': + return self.want.bind_password + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def update_auth_source_on_device(self, source): + """Set the system auth source. + + Configuring the authentication source is only one step in the process of setting + up an auth source. The other step is to inform the system of the auth source + you want to use. + + This method is used for situations where + + * The ``use_for_auth`` parameter is set to ``yes`` + * The ``use_for_auth`` parameter is set to ``no`` + * The ``state`` parameter is set to ``absent`` + + When ``state`` equal to ``absent``, before you can delete the LDAP configuration, + you must set the system auth to "something else". The system ships with a system + auth called "local", so this is the logical "something else" to use. + + When ``use_for_auth`` is no, the same situation applies as when ``state`` equal + to ``absent`` is done above. + + When ``use_for_auth`` is ``yes``, this method will set the current system auth + state to the value of source_type. + + Arguments: + source (string): The source that you want to set on the device. + """ + params = dict( + type=source + ) + uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_fallback_on_device(self, fallback): + params = dict( + fallback=fallback + ) + uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + if self.want.fallback_to_local == 'yes': + self.update_fallback_on_device('true') + elif self.want.fallback_to_local == 'no': + self.update_fallback_on_device('false') + if self.want.use_for_auth and self.changes.auth_source: + self.update_auth_source_on_device(self.changes.auth_source) + return True + + def remove(self): + if self.module.check_mode: + return True + self.update_auth_source_on_device('local') + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + if self.want.fallback_to_local == 'yes': + self.update_fallback_on_device('true') + elif self.want.fallback_to_local == 'no': + self.update_fallback_on_device('false') + if self.want.use_for_auth: + self.update_auth_source_on_device(self.want.auth_source) + return True + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'system-auth') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = 'system-auth' + params['partition'] = 'Common' + uri = "https://{0}:{1}/mgmt/tm/auth/ldap/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + if not params: + return + uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'system-auth') + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'system-auth') + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'system-auth') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response.update(self.read_current_auth_source_from_device()) + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def read_current_auth_source_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/source".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + result = {} + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'fallback' in response: + result['fallback'] = response['fallback'] + if 'type' in response: + result['auth_source'] = response['type'] + return result + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + servers=dict( + type='list', + elements='str', + ), + port=dict(type='int'), + remote_directory_tree=dict(), + scope=dict( + choices=['sub', 'one', 'base'] + ), + bind_dn=dict(), + source_type=dict( + default='ldap', + choices=['ldap', 'active-directory'] + ), + bind_password=dict(no_log=True), + user_template=dict(), + check_member_attr=dict(type='bool'), + ssl=dict( + choices=['yes', 'no', 'start-tls'] + ), + ca_cert=dict(aliases=['ssl_ca_cert']), + client_key=dict(aliases=['ssl_client_key']), + client_cert=dict(aliases=['ssl_client_cert']), + validate_certs=dict(type='bool', aliases=['ssl_check_peer']), + login_ldap_attr=dict(), + fallback_to_local=dict(type='bool'), + referrals=dict(type='bool'), + use_for_auth=dict(type='bool'), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + state=dict(default='present', choices=['absent', 'present']), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_radius.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_radius.py new file mode 100644 index 00000000..b82269d5 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_radius.py @@ -0,0 +1,622 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_auth_radius +short_description: Manages RADIUS auth configuration on a BIG-IP device +description: + - Module creates a RADIUS configuration. +version_added: "1.3.0" +options: + servers: + description: + - Specifies the names of RADIUS servers for use with RADIUS authentication profiles. + type: list + elements: str + accounting_bug: + description: + - Enables or disables validation of the accounting response vector. + - This option should be necessary only on older servers. + type: bool + retries: + description: + - Specifies the number of authentication retries the BIG-IP Local Traffic Management system allows before + authentication fails. + type: int + service_type: + description: + - Specifies the type of service requested from the RADIUS server. The default value is C(authenticate-only). + type: str + choices: + - authenticate-only + - login + - default + - framed + - callback-login + - callback-framed + - outbound + - administrative + - nas-prompt + - callback-nas-prompt + - call-check + - callback-administrative + fallback_to_local: + description: + - Specifies the system uses the Local authentication method if the remote + authentication method is not available. + - Option only available on C(TMOS 13.0.0) and above. + type: bool + use_for_auth: + description: + - Specifies whether or not this auth source is put in use on the system. + - If C(yes), the module sets the current system auth type to the value of C(radius). + - If C(no), the module sets the authentication type to C(local), similar behavior to when C(state) is C(absent), + without removing the configured RADIUS resource. + type: bool + state: + description: + - When C(state) is C(present), ensures the RADIUS server exists. + - When C(state) is C(absent), ensures the RADIUS server is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - This module is based on the command line (TMSH) configuration capabilities of RADIUS authentication, + not the GUI. +author: + - Andrey Kashcheev (@andreykashcheev) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an RADIUS device configuration + bigip_device_auth_radius: + servers: + - "ansible_test1" + - "ansible_test2" + retries: 3 + service_type: authenticate-only + accounting_bug: no + use_for_auth: yes + fallback_to_local: yes + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Update an RADIUS device configuration + bigip_device_auth_radius: + retries: 5 + service_type: administrative + accounting_bug: yes + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Delete RADIUS auth configuration + bigip_device_auth_radius: + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +servers: + description: The servers value of the resource. + returned: changed + type: list + sample: hash/dictionary of values +service_type: + description: Type of service requested from the RADIUS server. + returned: changed + type: str + sample: login +retries: + description: Number of authentication retries before authentication fails. + type: int + returned: changed + sample: 10 +accounting_bug: + description: Enables or disables validation of the accounting response vector. + type: bool + returned: changed + sample: yes +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name, is_empty_list +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'serviceType': 'service_type', + 'accountingBug': 'accounting_bug', + 'fallback': 'fallback_to_local', + } + + api_attributes = [ + 'accountingBug', + 'serviceType', + 'servers', + 'retries', + ] + + returnables = [ + 'accounting_bug', + 'servers', + 'service_type', + 'retries', + 'fallback_to_local', + ] + + updatables = [ + 'accounting_bug', + 'servers', + 'service_type', + 'retries', + 'auth_source', + 'fallback_to_local', + ] + + @property + def fallback_to_local(self): + return flatten_boolean(self._values['fallback_to_local']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def accounting_bug(self): + result = flatten_boolean(self._values['accounting_bug']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def use_for_auth(self): + return flatten_boolean(self._values['use_for_auth']) + + @property + def auth_source(self): + if self._values['use_for_auth'] is None: + return None + if self.use_for_auth == 'yes': + return 'radius' + if self.use_for_auth == 'no': + return 'local' + + @property + def servers(self): + if self._values['servers'] is None: + return None + if is_empty_list(self._values['servers']): + return [] + result = list() + for item in self._values['servers']: + result.append(fq_name('Common', item)) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def fallback_to_local(self): + if self._values['fallback_to_local'] is None: + return None + elif self._values['fallback_to_local'] == 'yes': + return 'true' + return 'false' + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + want = getattr(self.want, param) + try: + have = getattr(self.have, param) + if want != have: + return want + except AttributeError: + return want + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + if self.want.fallback_to_local == 'yes': + self.update_fallback_on_device('true') + elif self.want.fallback_to_local == 'no': + self.update_fallback_on_device('false') + if self.want.use_for_auth and self.changes.auth_source: + self.update_auth_source_on_device(self.changes.auth_source) + return True + + def remove(self): + if self.module.check_mode: + return True + self.update_auth_source_on_device('local') + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + if self.want.fallback_to_local == 'yes': + self.update_fallback_on_device('true') + elif self.want.fallback_to_local == 'no': + self.update_fallback_on_device('false') + if self.want.use_for_auth: + self.update_auth_source_on_device(self.want.auth_source) + return True + + def update_fallback_on_device(self, fallback): + params = dict( + fallback=fallback + ) + uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/auth/radius/~Common~system-auth".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = 'system-auth' + params['partition'] = 'Common' + uri = 'https://{0}:{1}/mgmt/tm/auth/radius'.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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + if not params: + return + uri = "https://{0}:{1}/mgmt/tm/auth/radius/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'system-auth') + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/radius/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'system-auth') + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/radius/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'system-auth') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response.update(self.read_current_auth_source_from_device()) + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def read_current_auth_source_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/source".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + result = {} + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'fallback' in response: + result['fallback'] = response['fallback'] + if 'type' in response: + result['auth_source'] = response['type'] + return result + raise F5ModuleError(resp.content) + + def update_auth_source_on_device(self, source): + """Set the system auth source. + + Configuring the authentication source is only one step in the process of setting + up an auth source. The other step is to inform the system of the auth source + you want to use. + + This method is used for situations where + + * The ``use_for_auth`` parameter is set to ``yes`` + * The ``use_for_auth`` parameter is set to ``no`` + * The ``state`` parameter is set to ``absent`` + + When ``state`` equal to ``absent``, before you can delete the Radius+ configuration, + you must set the system auth to "something else". The system ships with a system + auth called "local", so this is the logical "something else" to use. + + When ``use_for_auth`` is no, the same situation applies as when ``state`` equal + to ``absent`` is done above. + + When ``use_for_auth`` is ``yes``, this method will set the current system auth + state to Radius+. + + Arguments: + source (string): The source that you want to set on the device. + """ + params = dict( + type=source + ) + uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + retries=dict( + type='int' + ), + service_type=dict( + choices=[ + 'authenticate-only', + 'login', + 'default', + 'framed', + 'callback-login', + 'callback-framed', + 'outbound', + 'administrative', + 'nas-prompt', + 'callback-nas-prompt', + 'call-check', + 'callback-administrative' + ] + ), + accounting_bug=dict( + type='bool' + ), + servers=dict( + type='list', + elements='str', + ), + fallback_to_local=dict(type='bool'), + use_for_auth=dict(type='bool'), + state=dict( + default='present', + choices=['absent', 'present'] + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_radius_server.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_radius_server.py new file mode 100644 index 00000000..446ca186 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_auth_radius_server.py @@ -0,0 +1,532 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_auth_radius_server +short_description: Manages the RADIUS server configuration of the device +description: + - Manages a device's RADIUS server configuration. + - Used in tandem with the C(bigip_device_auth_radius) module. +version_added: "1.3.0" +options: + name: + description: + - Specifies the name of the RADIUS server to manage. + type: str + required: True + description: + description: + - The description of the RADIUS server. + type: str + ip: + description: + - The IP address of the server. + - This parameter is mandatory when creating a new resource. + type: str + port: + description: + - The port of the server. + - Valid range of values is between C(0) and C(65535) inclusive. + type: int + secret: + description: + - Specifies the secret used for accessing RADIUS server. + - This parameter is mandatory when creating a new resource. + type: str + timeout: + description: + - Specifies the timeout value in seconds. + - Valid range of values is between C(1) and C(60) inclusive. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures the RADIUS server exists. + - When C(state) is C(absent), ensures the RADIUS server is removed. + type: str + choices: + - present + - absent + default: present + update_secret: + description: + - C(always) will update passwords if the C(secret) is specified. + - C(on_create) will only set the password for newly created servers. + type: str + choices: + - always + - on_create + default: always +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a RADIUS server configuration + bigip_device_auth_radius_server: + name: "ansible_test" + ip: "1.1.1.1" + port: 1812 + secret: "secret" + timeout: 5 + update_secret: on_create + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Update RADIUS server configuration + bigip_device_auth_radius_server: + name: "ansible_test" + ip: "10.10.10.1" + description: "this is a test" + port: 1813 + timeout: 10 + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove RADIUS server configuration + bigip_device_auth_radius_server: + name: "ansible_test" + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +ip: + description: IP address of the RADIUS Server. + returned: changed + type: str + sample: 1.1.1.1 +port: + description: RADIUS service port. + returned: changed + type: int + sample: 1812 +timeout: + description: Timeout value. + returned: changed + type: int + sample: 3 +description: + description: User defined description of the RADIUS server. + returned: changed + type: str + sample: "this is my server" +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'server': 'ip', + } + + api_attributes = [ + 'secret', + 'server', + 'port', + 'timeout', + 'description', + ] + + returnables = [ + 'ip', + 'port', + 'timeout', + 'secret', + 'description', + ] + + updatables = [ + 'secret', + 'ip', + 'port', + 'timeout', + 'description', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def timeout(self): + if self._values['timeout'] is None: + return None + if 1 > self._values['timeout'] > 60: + raise F5ModuleError( + "Timeout value must be between 1 and 60." + ) + return self._values['timeout'] + + @property + def ip(self): + if self._values['ip'] is None: + return None + elif is_valid_ip(self._values['ip']): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + if 0 <= self._values['port'] <= 65535: + return self._values['port'] + raise F5ModuleError( + "Valid ports must be in range 0 - 65535" + ) + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + returnables = [ + 'ip', + 'port', + 'timeout', + 'description', + ] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def secret(self): + if self.want.secret != self.have.secret: + if self.want.update_secret == 'always': + result = self.want.secret + return result + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/auth/radius-server/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/auth/radius-server/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/auth/radius-server/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/radius-server/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/radius-server/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True + ), + description=dict(), + ip=dict(), + port=dict( + type='int' + ), + timeout=dict( + type='int' + ), + secret=dict( + no_log=True, + ), + update_secret=dict( + default='always', + choices=['always', 'on_create'] + ), + state=dict( + default='present', + choices=['absent', 'present'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_certificate.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_certificate.py new file mode 100644 index 00000000..c7974521 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_certificate.py @@ -0,0 +1,622 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_certificate +short_description: Manage self-signed device certificates +description: + - Module used to create and/or renew self-signed device certificates for BIG-IP. +version_added: "1.0.0" +options: + days_valid: + description: + - Specifies the interval for which the self-signed certificate is valid. + - "The maximum value is 25 years: C(9125) days" + type: int + required: True + cert_name: + description: + - Specifies the full name of the certificate file. + - If the name is not default C(server.crt), the module will configure C(httpd) to use them + prior to restarting the C(httpd) daemon. + type: str + default: server.crt + key_name: + description: + - Specifies the full name of the key file. + - If the name is not default C(server.key), the module will configure C(httpd) to use them + prior to restarting the C(httpd) daemon. + type: str + default: server.key + key_size: + description: + - Specifies the desired key size in bits. + - Mandatory option when generating a new certificate. + type: int + choices: + - 512 + - 1024 + - 2048 + - 4096 + default: 2048 + issuer: + description: + - Certificate properties, required when generating new certificates. + suboptions: + country: + description: + - Specifies the Country name attribute for the certificate. + type: str + state: + description: + - Specifies the State or Province attribute for the certificate. + type: str + locality: + description: + - Specifies the city or town name for the certificate. + type: str + organization: + description: + - Specifies the Organization attribute for the certificate. + type: str + division: + description: + - Specifies the department name attribute for the certificate. + type: str + common_name: + description: + - Specifies the Common Name attribute for the certificate. + type: str + email: + description: + - "Specifies the email address of the domain administrator." + type: str + type: dict + add_to_trusted: + description: + - Specified if the certificate should be added to the trusted client and server certificate files. + type: bool + default: no + new_cert: + description: + - Specified if the module should generate a new certificate. + - When C(yes), the device certificate and key will be replaced. + type: bool + default: no + force: + description: + - When C(yes), will update or overwrite the existing certificate when it is not expired on the device. + - When C(no), the certificate will only be updated/overwritten if expired. + - Generally should be C(yes) only in cases where you need to update certificate that is about to expire. + - This option is also needed when generating a new certificate to replace non-expired one. + type: bool + default: no +extends_documentation_fragment: f5networks.f5_modules.f5ssh +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Update expired certificate + bigip_device_certificate: + days_valid: 365 + provider: + password: secret + server: lb.mydomain.com + user: admin + transport: cli + server_port: 22 + delegate_to: localhost + +- name: Update expired certificate non-default names + bigip_device_certificate: + days_valid: 60 + cert_name: custom.crt + key_name: custom.key + provider: + password: secret + server: lb.mydomain.com + user: admin + transport: cli + server_port: 22 + delegate_to: localhost + +- name: Force update not expired certificate + bigip_device_certificate: + days_valid: 365 + force: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + transport: cli + server_port: 22 + delegate_to: localhost + +- name: Create a new certificate to replace expired certificate + bigip_device_certificate: + days_valid: 365 + new_cert: yes + issuer: + country: US + state: WA + common_name: foobar.foo.local + provider: + password: secret + server: lb.mydomain.com + user: admin + transport: cli + server_port: 22 + delegate_to: localhost + +- name: Force create a new custom named certificate to replace not expired certificate + bigip_device_certificate: + days_valid: 365 + cert_name: custom.crt + key_name: custom.key + new_cert: yes + force: yes + issuer: + country: US + state: WA + common_name: foobar.foo.local + key_size: 2048 + provider: + password: secret + server: lb.mydomain.com + user: admin + transport: cli + server_port: 22 + delegate_to: localhost +''' + +RETURN = r''' +days_valid: + description: The interval for which the self-signed certificate is valid. + returned: changed + type: int + sample: 365 +issuer: + description: Specifies certificate properties. + type: complex + returned: changed + contains: + country: + description: The Country name attribute of the certificate. + returned: changed + type: str + sample: US + state: + description: The State or Province attribute of the certificate. + returned: changed + type: str + sample: WA + locality: + description: The city or town name attribute of the certificate. + returned: changed + type: str + sample: Seattle + organization: + description: The Organization attribute of the certificate. + returned: changed + type: str + sample: F5 + division: + description: The department name attribute of the certificate. + returned: changed + type: str + sample: IT + common_name: + description: The Common Name attribute of the certificate. + returned: changed + type: str + sample: foo.bar.local + email: + description: "The domain administrator's email address." + returned: changed + type: str + sample: admin@foo.bar.local +cert_name: + description: The full name of the certificate file. + returned: changed + type: str + sample: common.crt +key_name: + description: The full name of the key file. + returned: changed + type: str + sample: common.key +key_size: + description: The desired key size in bits. + returned: changed + type: int + sample: 2048 +''' +import copy +import os +import ssl +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.connection import exec_command + + +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, is_cli, f5_argument_spec +) + + +class Parameters(AnsibleF5Parameters): + returnables = [ + 'days_valid', + 'issuer', + 'key_size', + 'cert_name', + 'key_name', + ] + + updatables = [ + 'days_valid', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + issuer_map = { + 'country': 'C', + 'state': 'ST', + 'locality': 'L', + 'organization': 'O', + 'division': 'OU', + 'common_name': 'CN', + 'email': 'emailAddress' + } + + @property + def issuer(self): + if self._values['issuer'] is None: + return None + filtered = dict((self.issuer_map[k], v) for k, v in self._values['issuer'].items() if v is not None) + to_parse = ['{0}={1}'.format(k, v) for k, v in filtered.items()] + result = '/' + '/'.join(to_parse) + '/' + return result + + @property + def days_valid(self): + if 1 <= self._values['days_valid'] <= 9125: + return self._values['days_valid'] + raise F5ModuleError( + "Valid 'days_valid' must be in range 1 - 9125 days." + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + issuer_map = { + 'C': 'country', + 'ST': 'state', + 'L': 'locality', + 'O': 'organization', + 'OU': 'division', + 'CN': 'common_name', + 'emailAddress': 'email' + } + + @property + def issuer(self): + if self._values['issuer'] is None: + return None + to_dict = [tuple(item.split('=')) for item in self._values['issuer'].strip('/').split('/')] + result = dict((self.issuer_map[k], v) for k, v in to_dict) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + if not is_cli(self.module): + raise F5ModuleError('Module can only be run via SSH, set the transport property to CLI') + + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + return result + + def present(self): + if self.expired(): + if self.want.new_cert: + self.create() + return True + self.update() + return True + if self.want.force and self.want.new_cert: + self.create() + return True + if self.want.force: + self.update() + return True + return False + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.generate_new() + return True + + def update(self): + self._update_changed_options() + if self.module.check_mode: + return True + self.update_certificate() + if self.want.cert_name != 'server.crt' or self.want.key_name != 'server.key': + self.configure_new_cert() + self.restart_daemon() + if self.want.add_to_trusted: + self.copy_files_to_trusted() + return True + + def expired(self): + self.have = self.read_current_certificate() + current_epoch = int(datetime.now().timestamp()) + if current_epoch > self.have.epoch: + return True + return False + + def generate_new(self): + self.generate_cert_key() + if self.want.cert_name != 'server.crt' or self.want.key_name != 'server.key': + self.configure_new_cert() + self.restart_daemon() + if self.want.add_to_trusted: + self.copy_files_to_trusted() + return True + + def generate_cert_key(self): + cmd = 'openssl req -x509 -nodes -days {3} -newkey rsa:{4} -keyout {0}/ssl.key/{2} ' \ + '-out {0}/ssl.crt/{1} -subj "{5}"'.format('/config/httpd/conf', self.want.cert_name, self.want.key_name, + self.want.days_valid, self.want.key_size, self.want.issuer) + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + raise F5ModuleError(err) + + def create_csr(self): + cmd = 'openssl x509 -x509toreq -in {0}/ssl.crt/{1} -out {0}/ssl.csr/{3}.csr -signkey {0}/ssl.key/{2}'.format( + '/config/httpd/conf', self.want.cert_name, self.want.key_name, os.path.splitext(self.want.cert_name)[0] + ) + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + raise F5ModuleError(err) + + def update_certificate(self): + self.create_csr() + cmd = 'openssl x509 -req -in {0}/ssl.csr/{3}.csr -signkey {0}/ssl.key/{2} -days {4} -out {0}/ssl.crt/{1}'.\ + format('/config/httpd/conf', self.want.cert_name, self.want.key_name, + os.path.splitext(self.want.cert_name)[0], self.want.days_valid) + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + raise F5ModuleError(err) + + def configure_new_cert(self): + cmd1 = 'tmsh modify sys httpd ssl-certkeyfile /config/httpd/conf/ssl.key/{1} ' \ + 'ssl-certfile /config/httpd/conf/ssl.crt/{0}'.format(self.want.cert_name, self.want.key_name) + + cmd2 = 'tmsh save /sys config partitions all' + + rc, out, err = exec_command(self.module, cmd1) + if rc != 0: + raise F5ModuleError(err) + + rc, out, err = exec_command(self.module, cmd2) + if rc != 0: + raise F5ModuleError(err) + + def restart_daemon(self): + cmd = 'tmsh restart /sys service httpd' + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + raise F5ModuleError(err) + + def copy_files_to_trusted(self): + cmd1 = 'cat /config/httpd/conf/ssl.crt/{0} >> /config/big3d/client.crt'.format(self.want.cert_name) + cmd2 = 'cat /config/httpd/conf/ssl.crt/{0} >> /config/gtm/server.crt'.format(self.want.cert_name) + rc, out, err = exec_command(self.module, cmd1) + if rc != 0: + raise F5ModuleError(err) + rc, out, err = exec_command(self.module, cmd2) + if rc != 0: + raise F5ModuleError(err) + + def read_current_certificate(self): + result = dict() + command = 'openssl x509 -in /config/httpd/conf/ssl.crt/{0} -dates -issuer -noout'.format(self.want.cert_name) + rc, out, err = exec_command(self.module, command) + if rc == 0: + result['epoch'] = self._parse_cert_date(out) + return ApiParameters(params=result) + + def _parse_cert_date(self, to_parse): + c_time = to_parse.split('\n')[1].split('=')[1] + result = ssl.cert_time_to_seconds(c_time) + return result + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + key_size=dict( + type='int', + choices=[512, 1024, 2048, 4096], + default=2048 + ), + cert_name=dict( + default='server.crt' + ), + key_name=dict( + default='server.key' + ), + days_valid=dict( + type='int', + required=True + ), + issuer=dict( + type='dict', + options=dict( + country=dict(), + state=dict(), + locality=dict(), + organization=dict(), + division=dict(), + common_name=dict(), + email=dict(), + ), + required_one_of=[ + ['country', 'state', 'locality', 'organization', 'division', 'common_name', 'email'] + ] + ), + + add_to_trusted=dict( + type='bool', + default='no' + ), + new_cert=dict( + type='bool', + default='no' + ), + force=dict( + type='bool', + default='no' + ), + ) + # required to remove REST option from choices and set default to CLI to be in line with docs + provider_update = dict( + transport=dict( + type='str', + default='cli', + choices=['cli'] + ), + ssh_keyfile=dict( + type='path' + ), + server_port=dict( + type='int', + default=22, + fallback=(env_fallback, ['F5_SERVER_PORT']) + ), + ) + new_spec = copy.deepcopy(f5_argument_spec) + self.argument_spec = {} + self.argument_spec.update(new_spec) + self.argument_spec['provider']['options'].update(provider_update) + self.argument_spec.update(argument_spec) + self.required_if = [ + ['new_cert', 'yes', ['days_valid', 'issuer', 'key_size']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_connectivity.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_connectivity.py new file mode 100644 index 00000000..f1dcf0db --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_connectivity.py @@ -0,0 +1,698 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_connectivity +short_description: Manages device IP configuration settings for HA on a BIG-IP. +description: + - Manages device IP configuration settings for High Availability (HA) on a BIG-IP. Each BIG-IP device + has synchronization and failover connectivity information (IP addresses) that + you define as part of HA pairing or clustering. This module allows you to configure + that information. +version_added: "1.0.0" +options: + config_sync_ip: + description: + - Local IP address the system uses for ConfigSync operations. + type: str + mirror_primary_address: + description: + - Specifies the primary IP address for the system to use to mirror + connections. + type: str + mirror_secondary_address: + description: + - Specifies the secondary IP address for the system to use to mirror + connections. + type: str + unicast_failover: + description: + - Addresses to use for failover operations. Options C(address) + and C(port) are supported with dictionary structure, where C(address) is the + local IP address the system uses for failover operations. + - Port specifies the port the system uses for failover operations. If C(port) + is not specified, the default value C(1026) will be used. + - If you are specifying the (recommended) management IP address, use 'management-ip' in + the address field. + - When the value is set to empty list, the parameter value is removed from device. + type: list + elements: dict + failover_multicast: + description: + - When C(yes), ensures the Failover Multicast configuration is enabled + and, if no further multicast configuration is provided, ensures that + C(multicast_interface), C(multicast_address) and C(multicast_port) are + the defaults specified in the description of each option. + - When C(no), ensures that Failover Multicast configuration is disabled. + type: bool + multicast_interface: + description: + - Interface over which the system sends multicast messages associated + with failover. + - When C(failover_multicast) is C(yes) and this option is not provided, a default of C(eth0) will be used. + type: str + multicast_address: + description: + - IP address for the system to send multicast messages associated with + failover. + - When C(failover_multicast) is C(yes) and this option is not provided, a default of C(224.0.0.245) will be used. + type: str + multicast_port: + description: + - Port for the system to send multicast messages associated with + failover. + - When C(failover_multicast) is C(yes) and this option is not provided, a default of C(62960) will be used. + This value must be between 0 and 65535. + type: int + cluster_mirroring: + description: + - Specifies whether mirroring occurs within the same cluster or between + different clusters on a multi-bladed system. + - This parameter is only supported on platforms that have multiple blades, + such as Viprion hardware. It is not supported on Virtual Editions (VEs). + type: str + choices: + - between-clusters + - within-cluster +notes: + - This module is primarily used as a component of configuring HA pairs of + BIG-IP devices. + - Requires BIG-IP >= 12.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Configure device connectivity for standard HA pair + bigip_device_connectivity: + config_sync_ip: 10.1.30.1 + mirror_primary_address: 10.1.30.1 + unicast_failover: + - address: management-ip + - address: 10.1.30.1 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +changed: + description: Denotes if the F5 configuration was updated. + returned: always + type: bool +config_sync_ip: + description: The new value of the C(config_sync_ip) setting. + returned: changed + type: str + sample: 10.1.1.1 +mirror_primary_address: + description: The new value of the C(mirror_primary_address) setting. + returned: changed + type: str + sample: 10.1.1.2 +mirror_secondary_address: + description: The new value of the C(mirror_secondary_address) setting. + returned: changed + type: str + sample: 10.1.1.3 +unicast_failover: + description: The new value of the C(unicast_failover) setting. + returned: changed + type: list + sample: [{'address': '10.1.1.2', 'port': 1026}] +failover_multicast: + description: Whether a failover multicast attribute has been changed or not. + returned: changed + type: bool +multicast_interface: + description: The new value of the C(multicast_interface) setting. + returned: changed + type: str + sample: eth0 +multicast_address: + description: The new value of the C(multicast_address) setting. + returned: changed + type: str + sample: 224.0.0.245 +multicast_port: + description: The new value of the C(multicast_port) setting. + returned: changed + type: int + sample: 1026 +cluster_mirroring: + description: The current cluster-mirroring setting. + returned: changed + type: str + sample: between-clusters +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'unicastAddress': 'unicast_failover', + 'configsyncIp': 'config_sync_ip', + 'multicastInterface': 'multicast_interface', + 'multicastIp': 'multicast_address', + 'multicastPort': 'multicast_port', + 'mirrorIp': 'mirror_primary_address', + 'mirrorSecondaryIp': 'mirror_secondary_address', + 'managementIp': 'management_ip', + } + api_attributes = [ + 'configsyncIp', + 'multicastInterface', + 'multicastIp', + 'multicastPort', + 'mirrorIp', + 'mirrorSecondaryIp', + 'unicastAddress', + ] + returnables = [ + 'config_sync_ip', + 'multicast_interface', + 'multicast_address', + 'multicast_port', + 'mirror_primary_address', + 'mirror_secondary_address', + 'failover_multicast', + 'unicast_failover', + 'cluster_mirroring', + ] + updatables = [ + 'config_sync_ip', + 'multicast_interface', + 'multicast_address', + 'multicast_port', + 'mirror_primary_address', + 'mirror_secondary_address', + 'failover_multicast', + 'unicast_failover', + 'cluster_mirroring', + ] + + @property + def multicast_port(self): + if self._values['multicast_port'] is None: + return None + result = int(self._values['multicast_port']) + if result < 0 or result > 65535: + raise F5ModuleError( + "The specified 'multicast_port' must be between 0 and 65535." + ) + return result + + @property + def multicast_address(self): + if self._values['multicast_address'] is None: + return None + elif self._values['multicast_address'] in ["none", "any6", '']: + return "any6" + elif self._values['multicast_address'] == 'any': + return 'any' + result = self._get_validated_ip_address('multicast_address') + return result + + @property + def mirror_primary_address(self): + if self._values['mirror_primary_address'] is None: + return None + elif self._values['mirror_primary_address'] in ["none", "any6", '']: + return "any6" + result = self._get_validated_ip_address('mirror_primary_address') + return result + + @property + def mirror_secondary_address(self): + if self._values['mirror_secondary_address'] is None: + return None + elif self._values['mirror_secondary_address'] in ["none", "any6", '']: + return "any6" + result = self._get_validated_ip_address('mirror_secondary_address') + return result + + @property + def config_sync_ip(self): + if self._values['config_sync_ip'] is None: + return None + elif self._values['config_sync_ip'] in ["none", '']: + return "none" + result = self._get_validated_ip_address('config_sync_ip') + return result + + def _validate_unicast_failover_port(self, port): + try: + result = int(port) + except ValueError: + raise F5ModuleError( + "The provided 'port' for unicast failover is not a valid number" + ) + except TypeError: + result = 1026 + return result + + def _validate_unicast_failover_address(self, address): + if address != 'management-ip': + if is_valid_ip(address): + return address + else: + raise F5ModuleError( + "'address' field in unicast failover is not a valid IP address" + ) + else: + return address + + def _get_validated_ip_address(self, address): + if is_valid_ip(self._values[address]): + return self._values[address] + raise F5ModuleError( + "The specified '{0}' is not a valid IP address".format(address) + ) + + +class ApiParameters(Parameters): + @property + def cluster_mirroring(self): + if self._values['cluster_mirroring'] is None: + return None + if self._values['cluster_mirroring'] == 'between': + return 'between-clusters' + return 'within-cluster' + + +class ModuleParameters(Parameters): + @property + def unicast_failover(self): + if self._values['unicast_failover'] is None: + return None + if not self._values['unicast_failover']: + return [] + result = [] + for item in self._values['unicast_failover']: + address = item.get('address', None) + port = item.get('port', None) + address = self._validate_unicast_failover_address(address) + port = self._validate_unicast_failover_port(port) + result.append( + dict( + effectiveIp=address, + effectivePort=port, + ip=address, + port=port + ) + ) + if result: + return result + else: + return None + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + change = getattr(self, returnable) + if isinstance(change, dict): + result.update(change) + else: + result[returnable] = change + result = self._filter_params(result) + except Exception: + pass + return result + + +class ReportableChanges(Changes): + returnables = [ + 'config_sync_ip', 'multicast_interface', 'multicast_address', + 'multicast_port', 'mirror_primary_address', 'mirror_secondary_address', + 'failover_multicast', 'unicast_failover' + ] + + @property + def mirror_secondary_address(self): + if self._values['mirror_secondary_address'] in ['none', 'any6']: + return 'none' + return self._values['mirror_secondary_address'] + + @property + def mirror_primary_address(self): + if self._values['mirror_primary_address'] in ['none', 'any6']: + return 'none' + return self._values['mirror_primary_address'] + + @property + def multicast_address(self): + if self._values['multicast_address'] in ['none', 'any6']: + return 'none' + return self._values['multicast_address'] + + +class UsableChanges(Changes): + @property + def mirror_primary_address(self): + if self._values['mirror_primary_address'] == ['any6', 'none', 'any']: + return "any6" + else: + return self._values['mirror_primary_address'] + + @property + def mirror_secondary_address(self): + if self._values['mirror_secondary_address'] == ['any6', 'none', 'any']: + return "any6" + else: + return self._values['mirror_secondary_address'] + + @property + def multicast_address(self): + if self._values['multicast_address'] == ['any6', 'none', 'any']: + return "any" + else: + return self._values['multicast_address'] + + @property + def unicast_failover(self): + if self._values['unicast_failover'] is None: + return None + elif self._values['unicast_failover']: + return self._values['unicast_failover'] + return "none" + + @property + def cluster_mirroring(self): + if self._values['cluster_mirroring'] is None: + return None + elif self._values['cluster_mirroring'] == 'between-clusters': + return 'between' + return 'within' + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def to_tuple(self, failovers): + result = [] + for x in failovers: + for k, v in iteritems(x): + # Have to do this in cases where the BIG-IP stores the word + # "management-ip" when you specify the management IP address. + # + # Otherwise, a difference would be registered. + if v == self.have.management_ip: + v = 'management-ip' + result += [(str(k), str(v))] + return result + + @property + def unicast_failover(self): + if self.want.unicast_failover == [] and self.have.unicast_failover is None: + return None + if self.want.unicast_failover is None: + return None + if self.have.unicast_failover is None: + return self.want.unicast_failover + want = self.to_tuple(self.want.unicast_failover) + have = self.to_tuple(self.have.unicast_failover) + if set(want) == set(have): + return None + else: + return self.want.unicast_failover + + @property + def failover_multicast(self): + values = ['multicast_address', 'multicast_interface', 'multicast_port'] + if self.want.failover_multicast is False: + if self.have.multicast_interface == 'eth0' and self.have.multicast_address == 'any' and self.have.multicast_port == 0: + return None + else: + result = dict( + failover_multicast=True, + multicast_port=0, + multicast_interface='eth0', + multicast_address='any' + ) + return result + else: + if all(self.have._values[x] in [None, 'any6', 'any'] for x in values): + return True + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.update() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + if self.changes.cluster_mirroring: + self.update_cluster_mirroring_on_device() + return True + + def update_on_device(self): + params = self.changes.api_params() + if not params: + return + uri = "https://{0}:{1}/mgmt/tm/cm/device/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + for item in response['items']: + if item['selfDevice'] == 'true': + uri = "https://{0}:{1}/mgmt/tm/cm/device/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(item['partition'], item['name']) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + raise F5ModuleError( + "The host device was not found." + ) + + def update_cluster_mirroring_on_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 'statemirror.clustermirroring' + ) + payload = {"value": self.changes.cluster_mirroring} + resp = self.client.api.patch(uri, json=payload) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + db = self.read_cluster_mirroring_from_device() + uri = "https://{0}:{1}/mgmt/tm/cm/device/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + for item in response['items']: + if item['selfDevice'] == 'true': + uri = "https://{0}:{1}/mgmt/tm/cm/device/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(item['partition'], item['name']) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if db: + response['cluster_mirroring'] = db['value'] + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + raise F5ModuleError( + "The host device was not found." + ) + + def read_cluster_mirroring_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 'statemirror.clustermirroring' + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + multicast_port=dict( + type='int' + ), + multicast_address=dict(), + multicast_interface=dict(), + failover_multicast=dict( + type='bool' + ), + unicast_failover=dict( + type='list', + elements='dict', + ), + mirror_primary_address=dict(), + mirror_secondary_address=dict(), + config_sync_ip=dict(), + cluster_mirroring=dict( + choices=['within-cluster', 'between-clusters'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_together = [ + ['multicast_address', 'multicast_interface', 'multicast_port'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_together=spec.required_together + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_dns.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_dns.py new file mode 100644 index 00000000..61152397 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_dns.py @@ -0,0 +1,536 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_dns +short_description: Manage DNS settings on a BIG-IP +description: + - Manage the DNS settings on a BIG-IP device. +version_added: "1.0.0" +options: + cache: + description: + - Specifies whether the system caches DNS lookups or performs the + operation each time a lookup is needed. Note this applies + only to Access Policy Manager (APM) features, such as ACLs, web application + rewrites, and authentication. + type: str + choices: + - enabled + - disabled + - enable + - disable + name_servers: + description: + - A list of name servers the system uses to validate DNS lookups + type: list + elements: str + search: + description: + - A list of domains the system searches for local domain lookups, + to resolve local host names. + type: list + elements: str + ip_version: + description: + - Specifies whether the DNS specifies IP addresses using IPv4 or IPv6. + type: int + choices: + - 4 + - 6 + state: + description: + - The state of the variable on the system. When C(present), guarantees + an existing variable is set to C(value). + type: str + choices: + - absent + - present + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Set the DNS settings on the BIG-IP + bigip_device_dns: + name_servers: + - 208.67.222.222 + - 208.67.220.220 + search: + - localdomain + - lab.local + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +cache: + description: The new value of the DNS caching. + returned: changed + type: str + sample: enabled +name_servers: + description: List of name servers that were set. + returned: changed + type: list + sample: ['192.0.2.10', '172.17.12.10'] +search: + description: List of search domains that were set. + returned: changed + type: list + sample: ['192.0.2.10', '172.17.12.10'] +ip_version: + description: IP version that was set, DNS will specify IP addresses in this version. + returned: changed + type: int + sample: 4 +warnings: + description: The list of warnings (if any) generated by module based on arguments + returned: always + type: list + sample: ['...', '...'] +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, is_empty_list, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'dns.cache': 'cache', + 'nameServers': 'name_servers', + 'include': 'ip_version', + } + + api_attributes = [ + 'nameServers', 'search', 'include', + ] + + updatables = [ + 'cache', 'name_servers', 'search', 'ip_version', + ] + + returnables = [ + 'cache', 'name_servers', 'search', 'ip_version', + ] + + absentables = [ + 'name_servers', 'search', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def search(self): + search = self._values['search'] + if search is None: + return None + if isinstance(search, str) and search != "": + result = list() + result.append(str(search)) + return result + if is_empty_list(search): + return [] + return search + + @property + def name_servers(self): + name_servers = self._values['name_servers'] + if name_servers is None: + return None + if isinstance(name_servers, str) and name_servers != "": + result = list() + result.append(str(name_servers)) + return result + if is_empty_list(name_servers): + return [] + return name_servers + + @property + def cache(self): + if self._values['cache'] is None: + return None + if str(self._values['cache']) in ['enabled', 'enable']: + return 'enable' + else: + return 'disable' + + @property + def ip_version(self): + if self._values['ip_version'] == 6: + return "options inet6" + elif self._values['ip_version'] == 4: + return "" + else: + return None + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + change = getattr(self, returnable) + if isinstance(change, dict): + result.update(change) + else: + result[returnable] = change + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def ip_version(self): + if self._values['ip_version'] == 'options inet6': + return 6 + elif self._values['ip_version'] == "": + return 4 + else: + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def ip_version(self): + if self.want.ip_version is None: + return None + if self.want.ip_version == "" and self.have.ip_version is None: + return None + if self.want.ip_version == self.have.ip_version: + return None + if self.want.ip_version != self.have.ip_version: + return self.want.ip_version + + @property + def name_servers(self): + state = self.want.state + if self.want.name_servers is None: + return None + if state == 'absent': + if self.have.name_servers is None and self.want.name_servers: + return None + if set(self.want.name_servers) == set(self.have.name_servers): + return [] + if set(self.want.name_servers) != set(self.have.name_servers): + return list(set(self.want.name_servers).difference(self.have.name_servers)) + if not self.want.name_servers: + if self.have.name_servers is None: + return None + if self.have.name_servers is not None: + return self.want.name_servers + if self.have.name_servers is None: + return self.want.name_servers + if set(self.want.name_servers) != set(self.have.name_servers): + return self.want.name_servers + + @property + def search(self): + state = self.want.state + if self.want.search is None: + return None + if not self.want.search: + if self.have.search is None: + return None + if self.have.search is not None: + return self.want.search + if state == 'absent': + if self.have.search is None and self.want.search: + return None + if set(self.want.search) == set(self.have.search): + return [] + if set(self.want.search) != set(self.have.search): + return list(set(self.want.search).difference(self.have.search)) + if self.have.search is None: + return self.want.search + if set(self.want.search) != set(self.have.search): + return self.want.search + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.pop('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _absent_changed_options(self): + diff = Difference(self.want, self.have) + absentables = Parameters.absentables + changed = dict() + for k in absentables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.update() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def should_absent(self): + result = self._absent_changed_options() + if result: + return True + return False + + def absent(self): + self.have = self.read_current_from_device() + if not self.should_absent(): + return False + if self.module.check_mode: + return True + self.absent_on_device() + return True + + def read_dns_cache_setting(self): + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 'dns.cache' + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + cache = self.read_dns_cache_setting() + uri = "https://{0}:{1}/mgmt/tm/sys/dns/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if cache: + response['cache'] = cache['value'] + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + if params: + uri = "https://{0}:{1}/mgmt/tm/sys/dns/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if self.want.cache: + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 'dns.cache' + ) + payload = {"value": self.want.cache} + resp = self.client.api.patch(uri, json=payload) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + return True + + def absent_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/dns/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.patch(uri, json=params) + try: + resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + cache=dict( + choices=['disabled', 'enabled', 'disable', 'enable'] + ), + name_servers=dict( + type='list', + elements='str', + ), + search=dict( + type='list', + elements='str', + ), + ip_version=dict( + choices=[4, 6], + type='int' + ), + state=dict( + default='present', + choices=['absent', 'present'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_one_of = [ + ['name_servers', 'search', 'ip_version', 'cache'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_one_of=spec.required_one_of + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_group.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_group.py new file mode 100644 index 00000000..5be6a409 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_group.py @@ -0,0 +1,620 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_group +short_description: Manage device groups on a BIG-IP +description: + - Managing device groups allows you to create HA pairs and clusters + of BIG-IP devices. Usage of this module should be done in conjunction + with the C(bigip_configsync_actions) to sync the configuration across + the pair or cluster if auto-sync is disabled. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the device group. + type: str + required: True + type: + description: + - Specifies the type of group. + - A C(sync-failover) device group contains devices that synchronize their + configuration data and fail over to one another when a device becomes + unavailable. + - A C(sync-only) device group has no such failover. When creating a new + device group, this option will default to C(sync-only). + - This setting cannot be changed once it has been set. + type: str + choices: + - sync-failover + - sync-only + description: + description: + - Description of the device group. + type: str + auto_sync: + description: + - Indicates whether configuration synchronization occurs manually or + automatically. + - When creating a new device group, this option will default to C(no). + type: bool + asm_sync: + description: + - Specifies whether to synchronize ASM configurations of device group members. + - A device can be a member of only one ASM-enabled device group. + - When creating a new device group, this option will default to C(no). + type: bool + version_added: "1.22.0" + save_on_auto_sync: + description: + - When performing an auto-sync, specifies whether the configuration + will be saved or not. + - When C(no), only the running configuration will be changed on the + device(s) being synced to. + - When creating a new device group, this option will default to C(no). + type: bool + full_sync: + description: + - Specifies whether the system synchronizes the entire configuration + during synchronization operations. + - When C(no), the system performs incremental synchronization operations, + based on the cache size specified in C(max_incremental_sync_size). + - Incremental configuration synchronization is a mechanism for synchronizing + a device-group's configuration among its members, without requiring a + full configuration load for each configuration change. + - In order for this to work, all devices in the device-group must initially + agree on the configuration. Typically this requires at least one full + configuration load to each device. + - When creating a new device group, this option will default to C(no). + type: bool + max_incremental_sync_size: + description: + - Specifies the size of the changes cache for incremental sync. + - For example, using the default, if you make more than 1024 KB worth of + incremental changes, the system performs a full synchronization operation. + - Using incremental synchronization operations can reduce the per-device sync/load + time for configuration changes. + - This setting is relevant only when C(full_sync) is C(no). + type: int + state: + description: + - When C(state) is C(present), ensures the device group exists. + - When C(state) is C(absent), ensures the device group is removed. + type: str + choices: + - present + - absent + default: present + network_failover: + description: + - Indicates whether failover occurs over the network or is hard-wired. + - This parameter is only valid for C(type)s that are C(sync-failover). + type: bool +notes: + - This module is primarily used as a component of configuring HA pairs of + BIG-IP devices. + - Requires BIG-IP >= 12.1.x. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a sync-only device group + bigip_device_group: + name: foo-group + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a sync-only device group with auto-sync enabled + bigip_device_group: + name: foo-group + auto_sync: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a sync-only device group with auto-sync and asm-sync enabled + bigip_device_group: + name: foo-group + auto_sync: yes + asm_sync: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +save_on_auto_sync: + description: The new save_on_auto_sync value of the device group. + returned: changed + type: bool + sample: true +full_sync: + description: The new full_sync value of the device group. + returned: changed + type: bool + sample: false +description: + description: The new description of the device group. + returned: changed + type: str + sample: this is a device group +type: + description: The new type of the device group. + returned: changed + type: str + sample: sync-failover +auto_sync: + description: The new auto_sync value of the device group. + returned: changed + type: bool + sample: true +asm_sync: + description: The new asm_sync value of the device group. + returned: changed + type: bool + sample: true +max_incremental_sync_size: + description: The new sync size of the device group. + returned: changed + type: int + sample: 1000 +network_failover: + description: Whether or not network failover is enabled. + returned: changed + type: bool + sample: yes +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'saveOnAutoSync': 'save_on_auto_sync', + 'fullLoadOnSync': 'full_sync', + 'autoSync': 'auto_sync', + 'asmSync': 'asm_sync', + 'incrementalConfigSyncSizeMax': 'max_incremental_sync_size', + 'networkFailover': 'network_failover', + } + api_attributes = [ + 'saveOnAutoSync', + 'fullLoadOnSync', + 'description', + 'type', + 'autoSync', + 'asmSync', + 'incrementalConfigSyncSizeMax', + 'networkFailover', + ] + returnables = [ + 'save_on_auto_sync', + 'full_sync', + 'description', + 'type', + 'auto_sync', + 'asm_sync', + 'max_incremental_sync_size', + 'network_failover', + ] + updatables = [ + 'save_on_auto_sync', + 'full_sync', + 'description', + 'auto_sync', + 'asm_sync', + 'max_incremental_sync_size', + 'network_failover', + ] + + @property + def max_incremental_sync_size(self): + if not self.full_sync and self._values['max_incremental_sync_size'] is not None: + if self._values['__warnings'] is None: + self._values['__warnings'] = [] + self._values['__warnings'].append( + [ + dict( + msg='"max_incremental_sync_size has no effect if "full_sync" is not true', + version='2.4' + ) + ] + ) + if self._values['max_incremental_sync_size'] is None: + return None + return int(self._values['max_incremental_sync_size']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def asm_sync(self): + result = flatten_boolean(self._values['asm_sync']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def network_failover(self): + result = flatten_boolean(self._values['network_failover']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def auto_sync(self): + result = flatten_boolean(self._values['auto_sync']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def save_on_auto_sync(self): + result = flatten_boolean(self._values['save_on_auto_sync']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + + @property + def full_sync(self): + result = flatten_boolean(self._values['full_sync']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + change = getattr(self, returnable) + if isinstance(change, dict): + result.update(change) + else: + result[returnable] = change + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def network_failover(self): + return flatten_boolean(self._values['network_failover']) + + @property + def auto_sync(self): + return flatten_boolean(self._values['auto_sync']) + + @property + def asm_sync(self): + return flatten_boolean(self._values['asm_sync']) + + @property + def save_on_auto_sync(self): + return flatten_boolean(self._values['save_on_auto_sync']) + + @property + def full_sync(self): + return flatten_boolean(self._values['full_sync']) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + changed = {} + for key in Parameters.updatables: + if getattr(self.want, key) is not None: + attr1 = getattr(self.want, key) + attr2 = getattr(self.have, key) + if attr1 != attr2: + changed[key] = attr1 + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_members_in_group_from_device() + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the device group") + return True + + def create(self): + self._set_changed_options() + if self.want.type == 'sync-only' and self.want.network_failover is not None: + raise F5ModuleError( + "'network_failover' may only be specified when 'type' is 'sync-failover'." + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_members_in_group_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/{2}/devices/".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + for item in response['items']: + new_uri = uri + '{0}'.format(item['name']) + response = self.client.api.delete(new_uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def _set_create_defaults(self, params): + if self.want.auto_sync is None: + params['autoSync'] = 'disabled' + if self.want.full_sync is None: + params['fullLoadOnSync'] = 'false' + if self.want.save_on_auto_sync is None: + params['saveOnAutoSync'] = 'false' + if self.want.asm_sync is None: + params['asmSync'] = 'disabled' + + params['name'] = self.want.name + params['partition'] = self.want.partition + return params + + def create_on_device(self): + params = self._set_create_defaults(self.changes.api_params()) + + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + type=dict( + choices=['sync-failover', 'sync-only'] + ), + description=dict(), + auto_sync=dict( + type='bool' + ), + asm_sync=dict( + type='bool' + ), + save_on_auto_sync=dict( + type='bool', + ), + full_sync=dict( + type='bool' + ), + name=dict( + required=True + ), + max_incremental_sync_size=dict( + type='int' + ), + state=dict( + default='present', + choices=['absent', 'present'] + ), + network_failover=dict(type='bool'), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_group_member.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_group_member.py new file mode 100644 index 00000000..befd8dcd --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_group_member.py @@ -0,0 +1,294 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_group_member +short_description: Manages members in a device group +description: + - Manages members in a device group. Members in a device group can only + be added or removed, never updated. This is because the members are + identified by unique name values and changing that name would invalidate + the uniqueness. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the device that you want to add to the + device group. Often this will be the hostname of the device. + This member must be trusted by the device already. Trusting + can be done with the C(bigip_device_trust) module and the + C(peer_hostname) option to that module. + type: str + required: True + device_group: + description: + - The device group to which you want to add the member. + type: str + required: True + state: + description: + - When C(present), ensures the device group member exists. + - When C(absent), ensures the device group member is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add the current device to the "device_trust_group" device group + bigip_device_group_member: + name: "{{ inventory_hostname }}" + device_group: device_trust_group + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Add the hosts in the current scope to "device_trust_group" + bigip_device_group_member: + name: "{{ item }}" + device_group: device_trust_group + provider: + password: secret + server: lb.mydomain.com + user: admin + loop: "{{ hostvars.keys() }}" + run_once: true + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = {} + + api_attributes = [] + + returnables = [] + + updatables = [] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + change = getattr(self, returnable) + if isinstance(change, dict): + result.update(change) + else: + result[returnable] = change + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + pass + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = Parameters(params=self.module.params) + self.have = None + self.changes = Changes() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Changes(params=changed) + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to remove the member from the device group.") + return True + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/{2}/devices/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.device_group, + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/cm/device-group/{2}/devices/".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.device_group + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/{2}/devices/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.device_group, + self.want.name + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + device_group=dict(required=True), + state=dict( + default='present', + choices=['absent', 'present'] + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_ha_group.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_ha_group.py new file mode 100644 index 00000000..0f693b84 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_ha_group.py @@ -0,0 +1,820 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_ha_group +short_description: Manage HA group settings on a BIG-IP system +description: + - Manage HA (High Availability) group settings on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Name of the HA group to create/manage. + type: str + required: True + enable: + description: + - When set to C(no), the system disables the HA score feature. + type: bool + default: yes + description: + description: + - User created HA group description. + type: str + active_bonus: + description: + - Specifies the extra value to be added to the HA score of the active unit. + - When system creates HA group this value is set to C(10) by the system. + type: int + pools: + description: + - Specifies pools to contribute to the HA score. + - The pools must exist on the BIG-IP, otherwise the operation will fail. + type: list + elements: dict + suboptions: + pool_name: + description: + - The pool name which is used to contribute to the HA score. + - Referencing the pool can be done in the full path format for example, C(/Common/pool_name). + - When the pool is referenced in full path format, the C(partition) parameter is ignored. + type: str + required: True + attribute: + description: + - The pool attribute that contributes to the HA score. + type: str + choices: + - percent-up-members + default: 'percent-up-members' + weight: + description: + - Maximum value the selected pool attribute contributes to the HA score. + type: int + required: True + minimum_threshold: + description: + - Below this value, the selected pool attribute contributes nothing to the HA score. + - This value must be greater than the number of pool members present in the pool. + - In TMOS versions 12.x this attribute is named C(threshold), however it has been deprecated + in versions 13.x and above. + - Specifying this attribute in the module running against v12.x will keep the same behavior + as if C(threshold) option was set. + type: int + partition: + description: + - Device partition where the specified pool exists. + - This parameter is ignored if the C(pool_name) is specified in full path format. + type: str + default: Common + trunks: + description: + - Specifies trunks to contribute to the HA score. + - The trunks must exist on the BIG-IP, otherwise the operation will fail. + type: list + elements: dict + suboptions: + trunk_name: + description: + - The trunk name used to contribute to the HA score. + type: str + required: True + attribute: + description: + - The trunk attribute that contributes to the HA score. + type: str + choices: + - percent-up-members + default: 'percent-up-members' + weight: + description: + - Maximum value the selected trunk attribute contributes to the HA score. + type: int + required: True + minimum_threshold: + description: + - Below this value the selected trunk attribute contributes nothing to the HA score. + - This value must be greater than the number of trunk members. + - In TMOS versions 12.x this attribute is named C(threshold), however it has been deprecated + in versions 13.x and above. + - Specifying this attribute in the module running against v12.x will keep the same behavior + as if C(threshold) option was set. + type: int + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +notes: + - This module does not support atomic removal of HA group objects. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create HA group no members, not active + bigip_device_ha_group: + name: foo_ha + description: empty_foo + active_bonus: 20 + enable: no + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create HA group with pools and trunks + bigip_device_ha_group: + name: baz_ha + description: non_empty_baz + active_bonus: 15 + pools: + - pool_name: foopool + weight: 30 + minimum_threshold: 1 + trunks: + - trunk_name: footrunk + weight: 70 + minimum_threshold: 2 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create HA group pools using full_path format + bigip_device_ha_group: + name: bar_ha + description: non_empty_bar + active_bonus: 12 + pools: + - pool_name: /Baz/foopool + weight: 30 + minimum_threshold: 1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove HA group + bigip_device_ha_group: + name: foo_ha + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +name: + description: Name of the HA group. + returned: changed + type: str + sample: foo_HA +enable: + description: Enables or disables the HA score feature. + returned: changed + type: bool + sample: yes +description: + description: User created HA group description. + returned: changed + type: str + sample: Some Group +active_bonus: + description: The extra value to be added to the HA score of the active unit. + returned: changed + type: int + sample: 20 +pools: + description: The pools to contribute to the HA score. + returned: changed + type: complex + contains: + pool_name: + description: The pool name which is used to contribute to the HA score. + returned: changed + type: str + sample: foo_pool + attribute: + description: The pool attribute that contributes to the HA score. + returned: changed + type: str + sample: percent-up-members + weight: + description: Maximum value the selected pool attribute contributes to the HA score. + returned: changed + type: int + sample: 40 + minimum_threshold: + description: Below this value the selected pool attribute contributes nothing to the HA score. + returned: changed + type: int + sample: 2 + partition: + description: Device partition where the specified pool exists. + returned: changed + type: str + sample: Common + sample: hash/dictionary of values +trunks: + description: The trunks to contribute to the HA score. + returned: changed + type: complex + contains: + trunk_name: + description: The trunk name used to contribute to the HA score. + returned: changed + type: str + sample: foo_trunk + attribute: + description: The trunk attribute that contributes to the HA score. + returned: changed + type: str + sample: percent-up-members + weight: + description: Maximum value the selected trunk attribute contributes to the HA score. + returned: changed + type: int + sample: 40 + minimum_threshold: + description: Below this value, the selected trunk attribute contributes nothing to the HA score. + returned: changed + type: int + sample: 2 + sample: hash/dictionary of values +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback, missing_required_lib +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, fq_name, f5_argument_spec, flatten_boolean +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'activeBonus': 'active_bonus' + } + + api_attributes = [ + 'activeBonus', + 'description', + 'pools', + 'trunks', + 'enabled', + 'disabled', + ] + + returnables = [ + 'name', + 'enabled', + 'disabled', + 'description', + 'active_bonus', + 'pools', + 'trunks', + ] + + updatables = [ + 'enabled', + 'disabled', + 'description', + 'active_bonus', + 'pools', + 'trunks', + ] + + +class ApiParameters(Parameters): + @property + def enabled(self): + result = flatten_boolean(self._values['enabled']) + if result == 'yes': + return True + return None + + @property + def disabled(self): + result = flatten_boolean(self._values['disabled']) + if result == 'yes': + return True + return None + + +class ModuleParameters(Parameters): + @property + def enabled(self): + result = flatten_boolean(self._values['enable']) + if result == 'yes': + return True + return None + + @property + def disabled(self): + result = flatten_boolean(self._values['enable']) + if result == 'no': + return True + return None + + @property + def pools(self): + version_13 = self._is_v13_and_above() + result = list() + if self._values['pools'] is None: + return None + for item in self._values['pools']: + pool = dict() + pool['name'] = fq_name(item['partition'], item['pool_name']) + pool['weight'] = self._handle_weight(item['weight']) + if 'attribute' in item: + pool['attribute'] = item['attribute'] + if 'minimum_threshold' in item: + if version_13: + pool['minimumThreshold'] = item['minimum_threshold'] + else: + pool['threshold'] = item['minimum_threshold'] + result.append(self._filter_params(pool)) + return result + + @property + def trunks(self): + version_13 = self._is_v13_and_above() + result = list() + if self._values['trunks'] is None: + return None + for item in self._values['trunks']: + trunk = dict() + trunk['name'] = item['trunk_name'] + trunk['weight'] = self._handle_weight(item['weight']) + if 'attribute' in item: + trunk['attribute'] = item['attribute'] + if 'minimum_threshold' in item: + if version_13: + trunk['minimumThreshold'] = item['minimum_threshold'] + else: + trunk['threshold'] = item['minimum_threshold'] + result.append(self._filter_params(trunk)) + return result + + def _is_v13_and_above(self): + version = tmos_version(self.client) + if Version(version) >= Version('13.0.0'): + return True + return False + + def _handle_weight(self, weight): + if weight < 10 or weight > 100: + raise F5ModuleError( + "Weight value must be in the range: '10 - 100'." + ) + return weight + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + returnables = [ + 'name', + 'enable', + 'description', + 'active_bonus', + 'pools', + 'trunks', + ] + + @property + def enable(self): + enabled = flatten_boolean(self._values['enabled']) + disabled = flatten_boolean(self._values['disabled']) + if enabled == 'yes': + return 'yes' + if disabled == 'yes': + return 'no' + return None + + @property + def pools(self): + result = list() + if self._values['pools'] is None: + return None + for item in self._values['pools']: + pool = dict() + pool['pool_name'] = item['name'] + pool['weight'] = item['weight'] + if 'attribute' in item: + pool['attribute'] = item['attribute'] + if 'minimumThreshold' in item: + pool['minimum_threshold'] = item['minimumThreshold'] + if 'threshold' in item: + pool['minimum_threshold'] = item['threshold'] + result.append(pool) + return result + + @property + def trunks(self): + result = list() + if self._values['trunks'] is None: + return None + for item in self._values['trunks']: + trunk = dict() + trunk['trunk_name'] = item['name'] + trunk['weight'] = item['weight'] + if 'attribute' in item: + trunk['attribute'] = item['attribute'] + if 'minimumThreshold' in item: + trunk['minimum_threshold'] = item['minimumThreshold'] + if 'threshold' in item: + trunk['minimum_threshold'] = item['threshold'] + result.append(trunk) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def to_tuple(self, items): + result = [] + for x in items: + tmp = [(str(k), str(v)) for k, v in iteritems(x)] + result += tmp + return result + + def _diff_complex_items(self, want, have): + if want == [] and have is None: + return None + if want is None: + return None + if have is None: + return want + w = self.to_tuple(want) + h = self.to_tuple(have) + if set(w).issubset(set(h)): + return None + else: + return want + + @property + def pools(self): + result = self._diff_complex_items(self.want.pools, self.have.pools) + return result + + @property + def trunks(self): + result = self._diff_complex_items(self.want.trunks, self.have.trunks) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params, client=self.client) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/sys/ha-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + uri = "https://{0}:{1}/mgmt/tm/sys/ha-group/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/ha-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/ha-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/ha-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True + ), + enable=dict( + type='bool', + default='yes' + ), + description=dict(), + active_bonus=dict( + type='int' + ), + pools=dict( + type='list', + elements='dict', + options=dict( + pool_name=dict( + required=True + ), + attribute=dict( + choices=[ + 'percent-up-members' + ], + default='percent-up-members' + ), + weight=dict( + required=True, + type='int' + ), + minimum_threshold=dict( + type='int' + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ) + ) + ), + trunks=dict( + type='list', + elements='dict', + options=dict( + trunk_name=dict( + required=True + ), + attribute=dict( + choices=[ + 'percent-up-members' + ], + default='percent-up-members' + ), + weight=dict( + required=True, + type='int' + ), + minimum_threshold=dict( + type='int' + ), + ) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_httpd.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_httpd.py new file mode 100644 index 00000000..87d8c69f --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_httpd.py @@ -0,0 +1,713 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_httpd +short_description: Manage HTTPD related settings on a BIG-IP system +description: + - Manages HTTPD related settings on the BIG-IP. These settings are useful + when you want to set GUI timeouts and other TMUI related settings. +version_added: "1.0.0" +options: + allow: + description: + - If you have enabled HTTPD access, specifies the IP address or address + range for other systems that can communicate with this system. + - To specify all addresses, use the value C(all). + - An IP address can be specified, such as 172.27.1.10. + - IP ranges can be specified, such as 172.27.*.* or 172.27.0.0/255.255.0.0. + type: list + elements: str + auth_name: + description: + - Sets the BIG-IP authentication realm name. + type: str + auth_pam_idle_timeout: + description: + - Sets the GUI timeout for automatic logout, in seconds. + type: int + auth_pam_validate_ip: + description: + - Sets the authPamValidateIp setting. + type: bool + auth_pam_dashboard_timeout: + description: + - Sets whether or not the BIG-IP dashboard will timeout. + type: bool + fast_cgi_timeout: + description: + - Sets the timeout of FastCGI. + type: int + hostname_lookup: + description: + - Sets whether or not to display the hostname, if possible. + type: bool + log_level: + description: + - Sets the minimum HTTPD log level. + type: str + choices: + - alert + - crit + - debug + - emerg + - error + - info + - notice + - warn + max_clients: + description: + - Sets the maximum number of clients that can connect to the GUI at once. + type: int + redirect_http_to_https: + description: + - Whether or not to redirect HTTP requests to the GUI to HTTPS. + type: bool + ssl_port: + description: + - The HTTPS port on which the system should listen. + type: int + ssl_cipher_suite: + description: + - Specifies the ciphers the system uses. + - The values in the suite are separated by colons (:). + - Can be specified in either a string or list form. The list form is the + recommended way to provide the cipher suite. See examples for usage. + - Use the value C(default) to set the cipher suite to the system default. + This value is equivalent to specifying a list of C(ECDHE-RSA-AES128-GCM-SHA256, + ECDHE-RSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-SHA,ECDHE-RSA-AES256-SHA, + ECDHE-RSA-AES128-SHA256,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-AES128-GCM-SHA256, + ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES128-SHA,ECDHE-ECDSA-AES256-SHA, + ECDHE-ECDSA-AES128-SHA256,ECDHE-ECDSA-AES256-SHA384,AES128-GCM-SHA256, + AES256-GCM-SHA384,AES128-SHA,AES256-SHA,AES128-SHA256,AES256-SHA256, + ECDHE-RSA-DES-CBC3-SHA,ECDHE-ECDSA-DES-CBC3-SHA,DES-CBC3-SHA). + type: raw + ssl_protocols: + description: + - The list of SSL protocols to accept on the management console. + - A space-separated list of tokens in the format accepted by the Apache + mod_ssl SSLProtocol directive. + - Can be specified in either a string or list form. The list form is the + recommended way to provide the cipher suite. See examples for usage. + - Use the value C(default) to set the SSL protocols to the system default. + This value is equivalent to specifying a list of C(all,-SSLv2,-SSLv3). + type: raw +notes: + - Requires the B(requests) Python package on the host. This is as easy as + running C(pip install requests). +requirements: + - requests +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Joe Reifel (@JoeReifel) + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Set the BIG-IP authentication realm name + bigip_device_httpd: + auth_name: BIG-IP + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set the auth pam timeout to 3600 seconds + bigip_device_httpd: + auth_pam_idle_timeout: 1200 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set the validate IP settings + bigip_device_httpd: + auth_pam_validate_ip: on + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set SSL cipher suite by list + bigip_device_httpd: + ssl_cipher_suite: + - ECDHE-RSA-AES128-GCM-SHA256 + - ECDHE-RSA-AES256-GCM-SHA384 + - ECDHE-RSA-AES128-SHA + - AES256-SHA256 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set SSL cipher suite by string + bigip_device_httpd: + ssl_cipher_suite: ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA:AES256-SHA256 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set SSL protocols by list + bigip_device_httpd: + ssl_protocols: + - all + - -SSLv2 + - -SSLv3 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set SSL protocols by string + bigip_device_httpd: + ssl_protocols: all -SSLv2 -SSLv3 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +auth_pam_idle_timeout: + description: The new number of seconds for GUI timeout. + returned: changed + type: str + sample: 1200 +auth_name: + description: The new authentication realm name. + returned: changed + type: str + sample: 'foo' +auth_pam_validate_ip: + description: The new authPamValidateIp setting. + returned: changed + type: bool + sample: on +auth_pam_dashboard_timeout: + description: Whether or not the BIG-IP dashboard will timeout. + returned: changed + type: bool + sample: off +fast_cgi_timeout: + description: The new timeout of FastCGI. + returned: changed + type: int + sample: 500 +hostname_lookup: + description: Whether or not to display the hostname, if possible. + returned: changed + type: bool + sample: on +log_level: + description: The new minimum HTTPD log level. + returned: changed + type: str + sample: crit +max_clients: + description: The new maximum number of clients that can connect to the GUI at once. + returned: changed + type: int + sample: 20 +redirect_http_to_https: + description: Whether or not to redirect HTTP requests to the GUI to HTTPS. + returned: changed + type: bool + sample: on +ssl_port: + description: The new HTTPS port to listen on. + returned: changed + type: int + sample: 10443 +ssl_cipher_suite: + description: The new ciphers the system uses. + returned: changed + type: str + sample: ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA +ssl_cipher_suite_list: + description: List of the new ciphers the system uses. + returned: changed + type: str + sample: ['ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES128-SHA'] +ssl_protocols: + description: The new list of SSL protocols to accept on the management console. + returned: changed + type: str + sample: all -SSLv2 -SSLv3 +''' +import time +from datetime import datetime + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'authPamIdleTimeout': 'auth_pam_idle_timeout', + 'authPamValidateIp': 'auth_pam_validate_ip', + 'authName': 'auth_name', + 'authPamDashboardTimeout': 'auth_pam_dashboard_timeout', + 'fastcgiTimeout': 'fast_cgi_timeout', + 'hostnameLookup': 'hostname_lookup', + 'logLevel': 'log_level', + 'maxClients': 'max_clients', + 'redirectHttpToHttps': 'redirect_http_to_https', + 'sslPort': 'ssl_port', + 'sslCiphersuite': 'ssl_cipher_suite', + 'sslProtocol': 'ssl_protocols' + } + + api_attributes = [ + 'authPamIdleTimeout', 'authPamValidateIp', 'authName', 'authPamDashboardTimeout', + 'fastcgiTimeout', 'hostnameLookup', 'logLevel', 'maxClients', 'sslPort', + 'redirectHttpToHttps', 'allow', 'sslCiphersuite', 'sslProtocol' + ] + + returnables = [ + 'auth_pam_idle_timeout', 'auth_pam_validate_ip', 'auth_name', + 'auth_pam_dashboard_timeout', 'fast_cgi_timeout', 'hostname_lookup', + 'log_level', 'max_clients', 'redirect_http_to_https', 'ssl_port', + 'allow', 'ssl_cipher_suite', 'ssl_protocols', 'ssl_cipher_suite_list', + ] + + updatables = [ + 'auth_pam_idle_timeout', 'auth_pam_validate_ip', 'auth_name', + 'auth_pam_dashboard_timeout', 'fast_cgi_timeout', 'hostname_lookup', + 'log_level', 'max_clients', 'redirect_http_to_https', 'ssl_port', + 'allow', 'ssl_cipher_suite', 'ssl_protocols' + ] + + _ciphers = "ECDHE-RSA-AES128-GCM-SHA256:" \ + "ECDHE-RSA-AES256-GCM-SHA384:" \ + "ECDHE-RSA-AES128-SHA:" \ + "ECDHE-RSA-AES256-SHA:" \ + "ECDHE-RSA-AES128-SHA256:" \ + "ECDHE-RSA-AES256-SHA384:" \ + "ECDHE-ECDSA-AES128-GCM-SHA256:" \ + "ECDHE-ECDSA-AES256-GCM-SHA384:" \ + "ECDHE-ECDSA-AES128-SHA:" \ + "ECDHE-ECDSA-AES256-SHA:" \ + "ECDHE-ECDSA-AES128-SHA256:" \ + "ECDHE-ECDSA-AES256-SHA384:" \ + "AES128-GCM-SHA256:" \ + "AES256-GCM-SHA384:" \ + "AES128-SHA:" \ + "AES256-SHA:" \ + "AES128-SHA256:" \ + "AES256-SHA256:" \ + "ECDHE-RSA-DES-CBC3-SHA:" \ + "ECDHE-ECDSA-DES-CBC3-SHA:" \ + "DES-CBC3-SHA" + + _protocols = 'all -SSLv2 -SSLv3' + + @property + def auth_pam_idle_timeout(self): + if self._values['auth_pam_idle_timeout'] is None: + return None + return int(self._values['auth_pam_idle_timeout']) + + @property + def fast_cgi_timeout(self): + if self._values['fast_cgi_timeout'] is None: + return None + return int(self._values['fast_cgi_timeout']) + + @property + def max_clients(self): + if self._values['max_clients'] is None: + return None + return int(self._values['max_clients']) + + @property + def ssl_port(self): + if self._values['ssl_port'] is None: + return None + return int(self._values['ssl_port']) + + +class ModuleParameters(Parameters): + @property + def auth_pam_validate_ip(self): + if self._values['auth_pam_validate_ip'] is None: + return None + if self._values['auth_pam_validate_ip']: + return "on" + return "off" + + @property + def auth_pam_dashboard_timeout(self): + if self._values['auth_pam_dashboard_timeout'] is None: + return None + if self._values['auth_pam_dashboard_timeout']: + return "on" + return "off" + + @property + def hostname_lookup(self): + if self._values['hostname_lookup'] is None: + return None + if self._values['hostname_lookup']: + return "on" + return "off" + + @property + def redirect_http_to_https(self): + if self._values['redirect_http_to_https'] is None: + return None + if self._values['redirect_http_to_https']: + return "enabled" + return "disabled" + + @property + def allow(self): + if self._values['allow'] is None: + return None + if self._values['allow'][0] == 'all': + return 'all' + if self._values['allow'][0] == '': + return '' + allow = self._values['allow'] + result = list(set([str(x) for x in allow])) + result = sorted(result) + return result + + @property + def ssl_cipher_suite(self): + if self._values['ssl_cipher_suite'] is None: + return None + if isinstance(self._values['ssl_cipher_suite'], string_types): + ciphers = self._values['ssl_cipher_suite'].strip() + else: + ciphers = self._values['ssl_cipher_suite'] + if not ciphers: + raise F5ModuleError( + "ssl_cipher_suite may not be set to 'none'" + ) + if ciphers == 'default': + ciphers = ':'.join(Parameters._ciphers.split(':')) + elif isinstance(self._values['ssl_cipher_suite'], string_types): + ciphers = ':'.join(ciphers.split(':')) + else: + ciphers = ':'.join(ciphers) + return ciphers + + @property + def ssl_protocols(self): + if self._values['ssl_protocols'] is None: + return None + if isinstance(self._values['ssl_protocols'], string_types): + protocols = self._values['ssl_protocols'].strip() + else: + protocols = self._values['ssl_protocols'] + if not protocols: + raise F5ModuleError( + "ssl_protocols may not be set to 'none'" + ) + if protocols == 'default': + protocols = ' '.join(Parameters._protocols.split(' ')) + elif isinstance(protocols, string_types): + protocols = ' '.join(protocols.split(' ')) + else: + protocols = ' '.join(protocols) + return protocols + + +class ApiParameters(Parameters): + @property + def allow(self): + if self._values['allow'] is None: + return '' + if self._values['allow'][0] == 'All': + return 'all' + allow = self._values['allow'] + result = list(set([str(x) for x in allow])) + result = sorted(result) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def ssl_cipher_suite(self): + default = ':'.join(Parameters._ciphers.split(':')) + if self._values['ssl_cipher_suite'] == default: + return 'default' + else: + return self._values['ssl_cipher_suite'] + + @property + def ssl_cipher_suite_list(self): + return self._values['ssl_cipher_suite'].split(':') + + @property + def ssl_protocols(self): + default = ' '.join(Parameters._protocols.split(' ')) + if self._values['ssl_protocols'] == default: + return 'default' + else: + return self._values['ssl_protocols'] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def allow(self): + if self.want.allow is None: + return None + if self.want.allow == 'all' and self.have.allow == 'all': + return None + if self.want.allow == 'all': + return ['All'] + if self.want.allow == '' and self.have.allow == '': + return None + if self.want.allow == '': + return [] + if self.want.allow != self.have.allow: + return self.want.allow + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Changes(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + return self.update() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/httpd".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + try: + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + except Exception as ex: + valid = [ + 'Remote end closed connection', + 'Connection aborted', + ] + # BIG-IP will kill your management connection when you change the HTTP + # redirect setting. So this catches that and handles it gracefully. + if 'redirectHttpToHttps' in params: + if any(i for i in valid if i in str(ex)): + # Wait for BIG-IP web server to settle after changing this + time.sleep(2) + return True + raise F5ModuleError(str(ex)) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/httpd".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + allow=dict( + type='list', + elements='str', + ), + auth_name=dict(), + auth_pam_idle_timeout=dict( + type='int' + ), + fast_cgi_timeout=dict( + type='int' + ), + max_clients=dict( + type='int' + ), + ssl_port=dict( + type='int' + ), + auth_pam_validate_ip=dict( + type='bool' + ), + auth_pam_dashboard_timeout=dict( + type='bool' + ), + hostname_lookup=dict( + type='bool' + ), + log_level=dict( + choices=[ + 'alert', 'crit', 'debug', 'emerg', + 'error', 'info', 'notice', 'warn' + ] + ), + redirect_http_to_https=dict( + type='bool' + ), + ssl_cipher_suite=dict(type='raw'), + ssl_protocols=dict(type='raw') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() 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 new file mode 100644 index 00000000..0520b3e3 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_info.py @@ -0,0 +1,18661 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# Copyright: (c) 2013, Matt Hite +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_info +short_description: Collect information from F5 BIG-IP devices +description: + - Collect information from F5 BIG-IP devices. + - This module was called C(bigip_device_facts) before Ansible 2.9. The usage did not change. +version_added: "1.0.0" +options: + partition: + description: + - Specifies the partition to gather the resource information from. + - The default value for the partition is taken as Common. + type: str + default: Common + version_added: "1.14.0" + data_increment: + description: + - Specifies the paging increment at which device data is gathered from the API. + - Allows for greater control of the pace at which data is queried from the device. + - This setting is useful for setups with large configurations which may take a long time with the default values. + - While there is no limit to the value that can be specified, note that putting very large values with + C(gather_subset) set to meta choices like C(all) might lead to module or device API crash. + - F5 recommends using C(data_increment) custom values in tandem with C(partition) and a specific C(gather_subset) + value for best experience. + type: int + default: 10 + version_added: "1.22.0" + gather_subset: + description: + - When supplied, this argument will restrict the information returned to a given subset. + - You can specify a list of values to include a larger subset. + - Values can also be used with an initial C(!) to specify that a specific subset + should not be collected. + type: list + elements: str + required: True + choices: + - all + - monitors + - profiles + - apm-access-profiles + - apm-access-policies + - as3 + - asm-policy-stats + - asm-policies + - asm-server-technologies + - asm-signature-sets + - client-ssl-profiles + - cfe + - devices + - device-groups + - do + - external-monitors + - fasthttp-profiles + - fastl4-profiles + - gateway-icmp-monitors + - gtm-pools + - gtm-servers + - gtm-wide-ips + - gtm-a-pools + - gtm-a-wide-ips + - gtm-aaaa-pools + - gtm-aaaa-wide-ips + - gtm-cname-pools + - gtm-cname-wide-ips + - gtm-mx-pools + - gtm-mx-wide-ips + - gtm-naptr-pools + - gtm-naptr-wide-ips + - gtm-srv-pools + - gtm-srv-wide-ips + - gtm-topology-regions + - http-monitors + - https-monitors + - http-profiles + - iapp-services + - iapplx-packages + - icmp-monitors + - interfaces + - internal-data-groups + - irules + - license + - ltm-pools + - ltm-policies + - management-routes + - nodes + - oneconnect-profiles + - packages + - partitions + - provision-info + - remote-syslog + - route-domains + - self-ips + - server-ssl-profiles + - software-volumes + - software-images + - software-hotfixes + - ssl-certs + - ssl-keys + - sync-status + - system-db + - system-info + - ts + - tcp-monitors + - tcp-half-open-monitors + - tcp-profiles + - traffic-groups + - trunks + - udp-profiles + - users + - ucs + - vcmp-guests + - virtual-addresses + - virtual-servers + - vlans + - "!all" + - "!as3" + - "!do" + - "!ts" + - "!cfe" + - "!monitors" + - "!profiles" + - "!apm-access-profiles" + - "!apm-access-policies" + - "!asm-policy-stats" + - "!asm-policies" + - "!asm-server-technologies" + - "!asm-signature-sets" + - "!client-ssl-profiles" + - "!devices" + - "!device-groups" + - "!external-monitors" + - "!fasthttp-profiles" + - "!fastl4-profiles" + - "!gateway-icmp-monitors" + - "!gtm-pools" + - "!gtm-servers" + - "!gtm-wide-ips" + - "!gtm-a-pools" + - "!gtm-a-wide-ips" + - "!gtm-aaaa-pools" + - "!gtm-aaaa-wide-ips" + - "!gtm-cname-pools" + - "!gtm-cname-wide-ips" + - "!gtm-mx-pools" + - "!gtm-mx-wide-ips" + - "!gtm-naptr-pools" + - "!gtm-naptr-wide-ips" + - "!gtm-srv-pools" + - "!gtm-srv-wide-ips" + - "!gtm-topology-regions" + - "!http-monitors" + - "!https-monitors" + - "!http-profiles" + - "!iapp-services" + - "!iapplx-packages" + - "!icmp-monitors" + - "!interfaces" + - "!internal-data-groups" + - "!irules" + - "!license" + - "!ltm-pools" + - "!ltm-policies" + - "!management-routes" + - "!nodes" + - "!oneconnect-profiles" + - "!packages" + - "!partitions" + - "!provision-info" + - "!remote-syslog" + - "!route-domains" + - "!self-ips" + - "!server-ssl-profiles" + - "!software-volumes" + - "!software-images" + - "!software-hotfixes" + - "!ssl-certs" + - "!ssl-keys" + - "!sync-status" + - "!system-db" + - "!system-info" + - "!tcp-monitors" + - "!tcp-half-open-monitors" + - "!tcp-profiles" + - "!traffic-groups" + - "!trunks" + - "!udp-profiles" + - "!users" + - "!ucs" + - "!vcmp-guests" + - "!virtual-addresses" + - "!virtual-servers" + - "!vlans" + aliases: ['include'] +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Collect BIG-IP information + bigip_device_info: + gather_subset: + - interfaces + - vlans + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Collect all BIG-IP information + bigip_device_info: + gather_subset: + - all + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Collect all BIG-IP information except trunks + bigip_device_info: + gather_subset: + - all + - "!trunks" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Collect BIG-IP information with custom data_increment and specific partition + bigip_device_info: + data_increment: 50 + partition: Foo + gather_subset: + - gtm-a-wide-ips + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +apm_access_profiles: + description: Information about APM Access Profiles. + returned: When C(apm-access-profiles) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/foo_policy + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: foo_policy + access_policy: + description: + - APM Access Policy attached to this Access Profile. + returned: queried + type: str + sample: foo_policy +apm_access_policies: + description: Information about APM Access Policies. + returned: When C(apm-access-policies) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/foo_policy + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: foo_policy +asm_policy_stats: + description: Miscellaneous ASM policy related information. + returned: When C(asm-policy-stats) is specified in C(gather_subset). + type: complex + contains: + policies: + description: + - The total number of ASM policies on the device. + returned: queried + type: int + sample: 3 + parent_policies: + description: + - The total number of ASM parent policies on the device. + returned: queried + type: int + sample: 2 + policies_pending_changes: + description: + - The total number of ASM policies with pending changes on the device. + returned: queried + type: int + sample: 2 + policies_active: + description: + - The number of ASM policies that are marked as active. From TMOS 13.x and above this setting equals + to C(policies_attached). + returned: queried + type: int + sample: 3 + policies_attached: + description: + - The number of ASM policies that are attached to virtual servers. + returned: queried + type: int + sample: 1 + policies_inactive: + description: + - The number of ASM policies that are marked as inactive. From TMOS 13.x and above this setting equals + to C(policies_unattached). + returned: queried + type: int + sample: 0 + policies_unattached: + description: + - The number of ASM policies that are not attached to a virtual server. + returned: queried + type: int + sample: 3 + sample: hash/dictionary of values +asm_policies: + description: Detailed information for ASM policies present on device. + returned: When C(asm-policies) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/foo_policy + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: foo_policy + policy_id: + description: + - Generated ID of the ASM policy resource. + returned: queried + type: str + sample: l0Ckxe-7yHsXp8U5tTgbFQ + active: + description: + - Indicates if an ASM policy is active. In TMOS 13.x and above, + this setting indicates if the policy is bound to any Virtual Server. + returned: queried + type: bool + sample: yes + apply: + description: + - In TMOS 13.x and above, this setting indicates if an ASM policy has pending changes that need to be applied. + returned: queried + type: bool + sample: yes + protocol_independent: + description: + - Indicates if the ASM policy differentiates between HTTP/WS and HTTPS/WSS URLs. + returned: queried + type: bool + sample: no + has_parent: + description: + - Indicates if the ASM policy is a child of another ASM policy. + returned: queried + type: bool + sample: no + type: + description: + - The type of policy, can be C(Security) or C(Parent). + returned: queried + type: str + sample: security + virtual_servers: + description: + - Virtual server or servers which have this policy assigned to them. + returned: queried + type: list + sample: ['/Common/foo_VS/'] + manual_virtual_servers: + description: + - The virtual servers that have Advanced LTM policy configuration which, in turn, have rule(s) built + with ASM control actions enabled. + returned: queried + type: list + sample: ['/Common/test_VS/'] + allowed_response_codes: + description: + - Lists the response status codes between 400 and 599 that the security profile considers legal. + returned: queried + type: list + sample: ['400', '404'] + description: + description: + - Description of the resource. + returned: queried + type: str + sample: Significant Policy Description + learning_mode: + description: + - Determine how the policy is built. + returned: queried + type: str + sample: manual + enforcement_mode: + description: + - Specifies whether blocking is active or inactive for the ASM policy. + returned: queried + type: str + sample: blocking + trust_xff: + description: + - Indicates the system has confidence in an XFF (X-Forwarded-For) header in the request. + returned: queried + type: bool + sample: yes + custom_xff_headers: + description: + - List of custom XFF headers trusted by the system. + returned: queried + type: str + sample: asm-proxy1 + case_insensitive: + description: + - Indicates if the ASM policy treats file types, URLs, and parameters as case sensitive. + returned: queried + type: bool + sample: yes + signature_staging: + description: + - Specifies if the staging feature is active on the ASM policy. + returned: queried + type: bool + sample: yes + place_signatures_in_staging: + description: + - Specifies if the system places new or updated signatures in staging + for the number of days specified in the enforcement readiness period. + returned: queried + type: bool + sample: no + enforcement_readiness_period: + description: + - Period in days both security policy entities and attack signatures + remain in staging mode before the system suggests to enforce them. + returned: queried + type: int + sample: 8 + path_parameter_handling: + description: + - Specifies how the system handles path parameters that are attached to path segments in URIs. + returned: queried + type: str + sample: ignore + trigger_asm_irule_event: + description: + - Indicates if iRule event is enabled. + returned: queried + type: str + sample: disabled + inspect_http_uploads: + description: + - Specifies whether the system should inspect all HTTP uploads. + returned: queried + type: bool + sample: yes + mask_credit_card_numbers_in_request: + description: + - Indicates if the system masks credit card numbers. + returned: queried + type: bool + sample: no + maximum_http_header_length: + description: + - Maximum length of an HTTP header name and value that the system processes. + returned: queried + type: int + sample: 8192 + use_dynamic_session_id_in_url: + description: + - Specifies how the security policy processes URLs that use dynamic sessions. + returned: queried + type: bool + sample: no + maximum_cookie_header_length: + description: + - Maximum length of a cookie header name and value that the system processes. + returned: queried + type: int + sample: 8192 + application_language: + description: + - The language encoding for the web application. + returned: queried + type: str + sample: utf-8 + disallowed_geolocations: + description: + - Displays countries that may not access the web application. + returned: queried + type: str + sample: Argentina + csrf_protection_enabled: + description: + - Specifies if CSRF protection is active on the ASM policy. + returned: queried + type: bool + sample: yes + csrf_protection_ssl_only: + description: + - Specifies that only HTTPS URLs will be checked for CSRF protection. + returned: queried + type: bool + sample: yes + csrf_protection_expiration_time_in_seconds: + description: + - Specifies how long, in seconds, a configured CSRF token is valid before it expires. + returned: queried + type: int + sample: 600 + csrf_urls: + description: + - Specifies a list of URLs for CSRF token verification. + - In version 13.0.0 and later, this has become a sub-collection and a list of dictionaries. + - In version 12.x, this is a list of simple strings. + returned: queried + type: complex + contains: + csrf_url_required_parameters: + description: + - Indicates whether to ignore or require one of the specified parameters is present + in a request when checking if the URL entry matches the request. + returned: queried + type: str + sample: ignore + csrf_url_parameters_list: + description: + - List of parameters to look for in a request when checking if the URL entry matches the request. + returned: queried + type: list + sample: ['fooparam'] + csrf_url: + description: + - Specifies an URL to protect. + returned: queried + type: str + sample: ['/foo.html'] + csrf_url_method: + description: + - Method for the specified URL. + returned: queried + type: str + sample: POST + csrf_url_enforcement_action: + description: + - Indicates the action specified for the system to take when the URL entry matches. + returned: queried + type: str + sample: none + csrf_url_id: + description: + - Specifies the generated ID for the configured CSRF URL resource. + returned: queried + type: str + sample: l0Ckxe-7yHsXp8U5tTgbFQ + csrf_url_wildcard_order: + description: + - Specifies the order in which the wildcard URLs are enforced. + returned: queried + type: str + sample: 1 + sample: hash/dictionary of values +asm_server_technologies: + description: Detailed information for ASM server technologies present on the device. + returned: When C(asm-server-technologies) is specified in C(gather_subset). + type: complex + contains: + id: + description: + - Displays the generated ID for the server technology resource. + returned: queried + type: str + sample: l0Ckxe-7yHsXp8U5tTgbFQ + server_technology_name: + description: + - Friendly name of the server technology resource. + returned: queried + type: str + sample: Wordpress + server_technology_references: + description: + - List of dictionaries containing API self links of the associated technology resources. + returned: queried + type: complex + contains: + link: + description: + - A self link to an associated server technology. + returned: queried + type: str + sample: https://localhost/mgmt/tm/asm/server-technologies/NQG7CT02OBC2cQWbnP7T-A?ver=13.1.0 + sample: hash/dictionary of values +asm_signature_sets: + description: Detailed information for ASM signature sets present on device. + returned: When C(asm-signature-sets) is specified in C(gather_subset). + type: complex + contains: + name: + description: + - Name of the signature set. + returned: queried + type: str + sample: WebSphere signatures + id: + description: + - Displays the generated ID for the signature set resource. + returned: queried + type: str + sample: l0Ckxe-7yHsXp8U5tTgbFQ + type: + description: + - The method used to select signatures to be a part of the signature set. + returned: queried + type: str + sample: filter-based + category: + description: + - Displays the category of the signature set. + returned: queried + type: str + sample: filter-based + is_user_defined: + description: + - Specifies this signature set was added by a user. + returned: queried + type: bool + sample: no + assign_to_policy_by_default: + description: + - Indicates whether the system assigns this signature set to a new created security policy by default. + returned: queried + type: bool + sample: yes + default_alarm: + description: + - Displays whether the security policy logs the request data in the Statistics + screen if a request matches a signature that is included in the signature set. + returned: queried + type: bool + sample: yes + default_block: + description: + - When the security policy enforcement mode is Blocking, displays + how the system treats requests that match a signature included in the signature set. + returned: queried + type: bool + sample: yes + default_learn: + description: + - Displays whether the security policy learns all requests that match a signature + that is included in the signature set. + returned: queried + type: bool + sample: yes + sample: hash/dictionary of values +client_ssl_profiles: + description: Client SSL Profile related information. + returned: When C(client-ssl-profiles) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/bigip02.internal + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: bigip02.internal + alert_timeout: + description: + - Maximum time period, in seconds, to keep the SSL session active after an alert + message is sent, or indefinite. + returned: queried + type: int + sample: 0 + allow_non_ssl: + description: + - Enables or disables non-SSL connections. + returned: queried + type: bool + sample: yes + authenticate_depth: + description: + - Specifies the authenticate depth. This is the client certificate chain maximum traversal depth. + returned: queried + type: int + sample: 9 + authenticate_frequency: + description: + - Specifies how often the system authenticates a user. + returned: queried + type: str + sample: once + ca_file: + description: + - Specifies the certificate authority (CA) file name. + returned: queried + type: str + sample: /Common/default-ca.crt + cache_size: + description: + - Specifies the SSL session cache size. + returned: queried + type: int + sample: 262144 + cache_timeout: + description: + - Specifies the SSL session cache timeout value. + returned: queried + type: int + sample: 3600 + certificate_file: + description: + - Specifies the name of the certificate installed on the traffic + management system for the purpose of terminating or initiating + an SSL connection. + returned: queried + type: str + sample: /Common/default.crt + key_file: + description: + - Specifies the name of the key installed on the traffic + management system for the purpose of terminating or initiating + an SSL connection. + returned: queried + type: str + sample: /Common/default.key + chain_file: + description: + - Specifies or builds a certificate chain file that a client can + use to authenticate the profile. + returned: queried + type: str + sample: /Common/ca-chain.crt + ciphers: + description: + - Specifies a list of cipher names. + returned: queried + type: str + sample: ['DEFAULT'] + crl_file: + description: + - Specifies the certificate revocation list file name. + returned: queried + type: str + sample: /Common/default.crl + parent: + description: + - Parent of the profile + returned: queried + type: str + sample: /Common/clientssl + description: + description: + - Description of the profile. + returned: queried + type: str + sample: My profile + modssl_methods: + description: + - Enables or disables ModSSL method emulation. + returned: queried + type: bool + sample: no + peer_certification_mode: + description: + - Specifies the peer certificate mode. + returned: queried + type: str + sample: ignore + sni_require: + description: + - When this option is C(yes), a client connection that does not + specify a known server name or does not support SNI extension will + be rejected. + returned: queried + type: bool + sample: no + sni_default: + description: + - When C(yes), this profile is the default SSL profile when the server + name in a client connection does not match any configured server + names, or a client connection does not specify any server name at + all. + returned: queried + type: bool + sample: yes + strict_resume: + description: + - Enables or disables strict-resume. + returned: queried + type: bool + sample: yes + profile_mode_enabled: + description: + - Specifies the profile mode, which enables or disables SSL + processing. + returned: queried + type: bool + sample: yes + renegotiation_maximum_record_delay: + description: + - Maximum number of SSL records that the traffic + management system can receive before it renegotiates an SSL + session. + returned: queried + type: int + sample: 0 + renegotiation_period: + description: + - Number of seconds required to renegotiate an SSL + session. + returned: queried + type: int + sample: 0 + renegotiation: + description: + - Specifies whether renegotiations are enabled. + returned: queried + type: bool + sample: yes + server_name: + description: + - Specifies the server names to be matched with SNI (server name + indication) extension information in ClientHello from a client + connection. + returned: queried + type: str + sample: bigip01 + session_ticket: + description: + - Enables or disables session-ticket. + returned: queried + type: bool + sample: no + unclean_shutdown: + description: + - Whether to force the SSL profile to perform a clean shutdown of all SSL + connections or not + returned: queried + type: bool + sample: no + retain_certificate: + description: + - APM module requires storing certificate in SSL session. When + C(no), certificate will not be stored in SSL session. + returned: queried + type: bool + sample: yes + secure_renegotiation_mode: + description: + - Specifies the secure renegotiation mode. + returned: queried + type: str + sample: require + handshake_timeout: + description: + - Specifies the handshake timeout in seconds. + returned: queried + type: int + sample: 10 + forward_proxy_certificate_extension_include: + description: + - Specifies the extensions of the web server certificates to be + included in the generated certificates using SSL Forward Proxy. + returned: queried + type: list + sample: ["basic-constraints", "subject-alternative-name"] + forward_proxy_certificate_lifespan: + description: + - Specifies the lifespan of the certificate generated using the SSL + forward proxy feature. + returned: queried + type: int + sample: 30 + forward_proxy_lookup_by_ipaddr_port: + description: + - Specifies whether to perform certificate look up by IP address and + port number. + returned: queried + type: bool + sample: no + forward_proxy_enabled: + description: + - Enables or disables SSL forward proxy feature. + returned: queried + type: bool + sample: yes + forward_proxy_ca_passphrase: + description: + - Specifies the passphrase of the key file that is used as the + certification authority key when SSL forward proxy feature is + enabled. + returned: queried + type: str + forward_proxy_ca_certificate_file: + description: + - Specifies the name of the certificate file that is used as the + certification authority certificate when SSL forward proxy feature + is enabled. + returned: queried + type: str + forward_proxy_ca_key_file: + description: + - Specifies the name of the key file that is used as the + certification authority key when SSL forward proxy feature is + enabled. + returned: queried + type: str + sample: hash/dictionary of values +devices: + description: Device related information. + returned: When C(devices) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/bigip02.internal + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: bigip02.internal + active_modules: + description: + - The currently licensed and provisioned modules on the device. + returned: queried + type: list + sample: ["DNS Services (LAB)", "PSM, VE"] + base_mac_address: + description: + - Media Access Control address (MAC address) of the device. + returned: queried + type: str + sample: "fa:16:3e:c3:42:6f" + build: + description: + - The minor version information of the total product version. + returned: queried + type: str + sample: 0.0.1 + chassis_id: + description: + - Serial number of the device. + returned: queried + type: str + sample: 11111111-2222-3333-444444444444 + chassis_type: + description: + - Displays the chassis type. The possible values are C(individual) and C(viprion). + returned: queried + type: str + sample: individual + comment: + description: + - User comments about the device. + returned: queried + type: str + sample: My device + configsync_address: + description: + - IP address used for configuration synchronization. + returned: queried + type: str + sample: 10.10.10.10 + contact: + description: + - Administrator contact information. + returned: queried + type: str + sample: The User + description: + description: + - Description of the device. + returned: queried + type: str + sample: My device + edition: + description: + - Displays the software edition. + returned: queried + type: str + sample: Point Release 7 + failover_state: + description: + - Device failover state. + returned: queried + type: str + sample: active + hostname: + description: + - Device hostname + returned: queried + type: str + sample: bigip02.internal + location: + description: + - Specifies the physical location of the device. + returned: queried + type: str + sample: London + management_address: + description: + - IP address of the management interface. + returned: queried + type: str + sample: 3.3.3.3 + marketing_name: + description: + - Marketing name of the device platform. + returned: queried + type: str + sample: BIG-IP Virtual Edition + multicast_address: + description: + - Specifies the multicast IP address used for failover. + returned: queried + type: str + sample: 4.4.4.4 + optional_modules: + description: + - Modules that are available for the current platform, but are not currently licensed. + returned: queried + type: list + sample: ["App Mode (TMSH Only, No Root/Bash)", "BIG-IP VE, Multicast Routing"] + platform_id: + description: + - Displays the device platform identifier. + returned: queried + type: str + sample: Z100 + primary_mirror_address: + description: + - Specifies the IP address used for state mirroring. + returned: queried + type: str + sample: 5.5.5.5 + product: + description: + - Displays the software product name. + returned: queried + type: str + sample: BIG-IP + secondary_mirror_address: + description: + - Secondary IP address used for state mirroring. + returned: queried + type: str + sample: 2.2.2.2 + self: + description: + - Whether or not this device is the one that was queried for information. + returned: queried + type: bool + sample: yes + software_version: + description: + - Displays the software version number. + returned: queried + type: str + sample: 13.1.0.7 + timelimited_modules: + description: + - Displays the licensed modules that are time-limited. + returned: queried + type: list + sample: ["IP Intelligence, 3Yr, ...", "PEM URL Filtering, 3Yr, ..."] + timezone: + description: + - Displays the time zone configured on the device. + returned: queried + type: str + sample: UTC + unicast_addresses: + description: + - Specifies the entire set of unicast addresses used for failover. + returned: queried + type: complex + contains: + effective_ip: + description: + - The IP address that peers can use to reach this unicast address IP. + returned: queried + type: str + sample: 5.4.3.5 + effective_port: + description: + - The port that peers can use to reach this unicast address. + returned: queried + type: int + sample: 1026 + ip: + description: + - The IP address the failover daemon will listen on for packets from its peers. + returned: queried + type: str + sample: 5.4.3.5 + port: + description: + - The IP port the failover daemon uses to accept packets from its peers. + returned: queried + type: int + sample: 1026 + sample: hash/dictionary of values +device_groups: + description: Device group related information. + returned: When C(device-groups) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/fasthttp + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: fasthttp + autosync_enabled: + description: + - Whether the device group automatically synchronizes configuration data to its members. + returned: queried + type: bool + sample: no + description: + description: + - Description of the device group. + returned: queried + type: str + sample: My device group + devices: + description: + - List of devices in the group. Devices are listed by their C(full_path). + returned: queried + type: list + sample: [/Common/bigip02.internal] + full_load_on_sync: + description: + - Specifies the entire configuration for a device group is sent when configuration + synchronization is performed. + returned: queried + type: bool + sample: yes + incremental_config_sync_size_maximum: + description: + - Specifies the maximum size (in KB) to devote to incremental config sync cached transactions. + returned: queried + type: int + sample: 1024 + network_failover_enabled: + description: + - Specifies whether network failover is used. + returned: queried + type: bool + sample: yes + type: + description: + - Specifies the type of device group. + returned: queried + type: str + sample: sync-only + asm_sync_enabled: + description: + - Specifies whether to synchronize ASM configurations of device group members. + returned: queried + type: bool + sample: yes + sample: hash/dictionary of values +external_monitors: + description: External monitor related information. + returned: When C(external-monitors) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/external + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: external + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: external + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My monitor + destination: + description: + - Specifies the IP address and service port of the resource that is + the destination of this monitor. + returned: queried + type: str + sample: "*:*" + args: + description: + - Specifies any command-line arguments the script requires. + returned: queried + type: str + sample: arg1 arg2 arg3 + external_program: + description: + - Specifies the name of the file for the monitor to use. + returned: queried + type: str + sample: /Common/arg_example + variables: + description: + - Specifies any variables the script requires. + type: dict + sample: { "key1": "val", "key_2": "val 2" } + interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when either the resource is down or the status + of the resource is unknown. + returned: queried + type: int + sample: 5 + manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to B(up) at the next successful monitor check. + returned: queried + type: bool + sample: yes + time_until_up: + description: + - Specifies the amount of time, in seconds, after the first + successful response before a node is marked up. + returned: queried + type: int + sample: 0 + timeout: + description: + - Specifies the number of seconds the target has in which to respond + to the monitor request. + returned: queried + type: int + sample: 16 + up_interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when the resource is up. + returned: queried + type: int + sample: 0 + sample: hash/dictionary of values +fasthttp_profiles: + description: FastHTTP profile related information. + returned: When C(fasthttp-profiles) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/fasthttp + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: fasthttp + client_close_timeout: + description: + - Number of seconds after which the system closes a client connection, when + the system either receives a client FIN packet or sends a FIN packet to the client. + returned: queried + type: int + sample: 5 + oneconnect_idle_timeout_override: + description: + - Number of seconds after which a server-side connection in a OneConnect pool + is eligible for deletion, when the connection has no traffic. + returned: queried + type: int + sample: 0 + oneconnect_maximum_reuse: + description: + - Maximum number of times the system can re-use a current connection. + returned: queried + type: int + sample: 0 + oneconnect_maximum_pool_size: + description: + - Maximum number of connections to a load balancing pool. + returned: queried + type: int + sample: 2048 + oneconnect_minimum_pool_size: + description: + - Minimum number of connections to a load balancing pool. + returned: queried + type: int + sample: 0 + oneconnect_replenish': + description: + - When C(yes), specifies the system will not keep a steady-state maximum of + connections to the back-end, unless the number of connections to the pool have + dropped beneath the C(minimum_pool_size) specified in the profile. + returned: queried + type: bool + sample: yes + oneconnect_ramp_up_increment: + description: + - The increment in which the system makes additional connections available, when + all available connections are in use. + returned: queried + type: int + sample: 4 + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: fasthttp + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My profile + force_http_1_0_response: + description: + - When C(yes), specifies the server sends responses to clients in the HTTP/1.0 + format. + returned: queried + type: bool + sample: no + request_header_insert: + description: + - A string the system inserts as a header in an HTTP request. If the header + already exists, the system does not replace it. + returned: queried + type: str + sample: "X-F5-Authentication: foo" + http_1_1_close_workarounds: + description: + - When C(yes), specifies the server uses workarounds for HTTP 1.1 close issues. + returned: queried + type: bool + sample: no + idle_timeout: + description: + - Length of time that a connection is idle (has no traffic) before the connection + is eligible for deletion. + returned: queried + type: int + sample: 300 + insert_xforwarded_for: + description: + - Whether the system inserts the X-Forwarded-For header in an HTTP request with the + client IP address, to use with connection pooling. + returned: queried + type: bool + sample: no + maximum_header_size: + description: + - Maximum amount of HTTP header data the system buffers before making a load + balancing decision. + returned: queried + type: int + sample: 32768 + maximum_requests: + description: + - Maximum number of requests the system can receive on a client-side connection, + before the system closes the connection. + returned: queried + type: int + sample: 0 + maximum_segment_size_override: + description: + - Maximum segment size (MSS) override for server-side connections. + returned: queried + type: int + sample: 0 + receive_window_size: + description: + - Amount of data the BIG-IP system can accept without acknowledging the server. + returned: queried + type: int + sample: 0 + reset_on_timeout: + description: + - When C(yes), specifies the system sends a reset packet (RST) in addition to + deleting the connection, when a connection exceeds the idle timeout value. + returned: queried + type: bool + sample: yes + server_close_timeout: + description: + - Number of seconds after which the system closes a client connection, when the system + either receives a server FIN packet or sends a FIN packet to the server. + returned: queried + type: int + sample: 5 + server_sack: + description: + - Whether the BIG-IP system processes Selective ACK (Sack) packets in cookie responses + from the server. + returned: queried + type: bool + sample: no + server_timestamp: + description: + - Whether the BIG-IP system processes timestamp request packets in cookie responses + from the server. + returned: queried + type: bool + sample: no + unclean_shutdown: + description: + - How the system handles closing connections. Values provided may be C(enabled), C(disabled), + or C(fast). + returned: queried + type: str + sample: enabled + sample: hash/dictionary of values +fastl4_profiles: + description: FastL4 profile related information. + returned: When C(fastl4-profiles) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/fastl4 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: fastl4 + client_timeout: + description: + - Specifies late binding client timeout in seconds. + - This is the number of seconds allowed for a client to transmit enough data to + select a server pool. + - If this timeout expires, the timeout-recovery option dictates whether + to drop the connection or fallback to the normal FastL4 load balancing method + to pick a server pool. + returned: queried + type: int + sample: 30 + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: fastl4 + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My profile + explicit_flow_migration: + description: + - Specifies whether to have the iRule code determine exactly when + the FIX stream drops down to the ePVA hardware. + returned: queried + type: bool + sample: yes + hardware_syn_cookie: + description: + - Enables or disables hardware SYN cookie support when PVA10 is present on the system. + - This option is deprecated in version 13.0.0 and is replaced by C(syn-cookie-enable). + returned: queried + type: bool + sample: no + idle_timeout: + description: + - Specifies the number of seconds a connection is idle before the connection is + eligible for deletion. + - Values are in the range of 0 to 4294967295 (inclusive). + - C(0) is equivalent to the TMUI value "immediate". + - C(4294967295) is equivalent to the TMUI value "indefinite". + returned: queried + type: int + sample: 300 + dont_fragment_flag: + description: + - Describes the Don't Fragment (DF) bit setting in the IP Header of + the outgoing TCP packet. + - When C(pmtu), sets the outgoing IP Header DF bit based on the IP pmtu + setting(tm.pathmtudiscovery). + - When C(preserve), sets the outgoing packet's IP Header DF bit to be the same as + the incoming IP Header DF bit. + - When C(set), sets the outgoing packet's IP Header DF bit. + - When C(clear), clears the outgoing packet's IP Header DF bit. + returned: queried + type: str + sample: pmtu + ip_tos_to_client: + description: + - Specifies an IP Type of Service (ToS) number for the client-side. + - This option specifies the ToS level the traffic management + system assigns to IP packets when sending them to clients. + returned: queried + type: str + sample: 200 + ip_tos_to_server: + description: + - Specifies an IP ToS number for the server side. + - This option specifies the ToS level the traffic management system assigns + to IP packets when sending them to servers. + returned: queried + type: str + sample: pass-through + ttl_mode: + description: + - Describes the outgoing TCP packet's IP Header TTL mode. + - When C(proxy), sets the outgoing IP Header TTL value to 255/64 for IPv4/IPv6 + respectively. + - When C(preserve), sets the outgoing IP Header TTL value to be same as the + incoming IP Header TTL value. + - When C(decrement), sets the outgoing IP Header TTL value to be one less than + the incoming TTL value. + - When C(set), sets the outgoing IP Header TTL value to a specific value (as + specified by C(ttl_v4) or C(ttl_v6). + returned: queried + type: str + sample: preserve + ttl_v4: + description: + - Specifies the outgoing packet's IP Header TTL value for IPv4 traffic. + - Maximum value is 255. + returned: queried + type: int + sample: 200 + ttl_v6: + description: + - Specify the outgoing packet's IP Header TTL value for IPv6. + traffic. + - Maximum value is 255. + returned: queried + type: int + sample: 300 + keep_alive_interval: + description: + - Specifies the keep-alive probe interval, in seconds. + - A value of 0 indicates keep-alive is disabled. + returned: queried + type: int + sample: 10 + late_binding: + description: + - Specifies whether to enable or disable the intelligent selection of a + back-end server pool. + returned: queried + type: bool + sample: yes + link_qos_to_client: + description: + - Specifies a Link Quality of Service (QoS) (VLAN priority) number + for the client side. + - This option specifies the QoS level the system assigns to packets + when sending them to clients. + returned: queried + type: int + sample: 7 + link_qos_to_server: + description: + - Specifies a Link QoS (VLAN priority) number for the server side. + - This option specifies the QoS level the system assigns to + packets when sending them to servers. + returned: queried + type: int + sample: 5 + loose_close: + description: + - Specifies the system closes a loosely-initiated connection + when it receives the first FIN packet from either the + client or the server. + returned: queried + type: bool + sample: no + loose_init: + description: + - Specifies the system initializes a connection when it + receives any Transmission Control Protocol (TCP) packet, rather + than requiring a SYN packet for connection initiation. + returned: queried + type: bool + sample: yes + mss_override: + description: + - Specifies a maximum segment size (MSS) override for server + connections. Note this is also the MSS advertised to a client + when a client first connects. + - C(0) (zero), means the option is disabled. Otherwise, the value will be + between 256 and 9162. + returned: queried + type: int + sample: 500 + priority_to_client: + description: + - Specifies the internal packet priority for the client side. + - This option specifies the internal packet priority the system + assigns to packets when sending them to clients. + returned: queried + type: int + sample: 300 + priority_to_server: + description: + - Specifies the internal packet priority for the server side. + - This option specifies the internal packet priority the system + assigns to packets when sending them to servers. + returned: queried + type: int + sample: 200 + pva_acceleration: + description: + - Specifies the Packet Velocity(r) ASIC acceleration policy. + returned: queried + type: str + sample: full + pva_dynamic_client_packets: + description: + - Specifies the number of client packets before dynamic ePVA + hardware re-offloading occurs. + - Values are between 0 and 10. + returned: queried + type: int + sample: 8 + pva_dynamic_server_packets: + description: + - Specifies the number of server packets before dynamic ePVA + hardware re-offloading occurs. + - Values are between 0 and 10. + returned: queried + type: int + sample: 5 + pva_flow_aging: + description: + - Specifies if automatic aging from ePVA flow cache is enabled or not. + returned: queried + type: bool + sample: yes + pva_flow_evict: + description: + - Specifies if this flow can be evicted upon hash collision with a + new flow learn snoop request. + returned: queried + type: bool + sample: no + pva_offload_dynamic: + description: + - Specifies whether PVA flow dynamic offloading is enabled or not. + returned: queried + type: bool + sample: yes + pva_offload_state: + description: + - Specifies at what stage the ePVA performs hardware offload. + - When C(embryonic), applies at TCP CSYN or the first client UDP packet. + - When C(establish), applies TCP 3WAY handshaking or UDP CS round trip are + confirmed. + returned: queried + type: str + sample: embryonic + reassemble_fragments: + description: + - Specifies whether to reassemble fragments. + returned: queried + type: bool + sample: yes + receive_window: + description: + - Specifies the window size to use, in bytes. + - The maximum is 2^31 for window scale enabling. + returned: queried + type: int + sample: 1000 + reset_on_timeout: + description: + - Specifies whether you want to reset connections on timeout. + returned: queried + type: bool + sample: yes + rtt_from_client: + description: + - Enables or disables the TCP timestamp options to measure the round + trip time to the client. + returned: queried + type: bool + sample: no + rtt_from_server: + description: + - Enables or disables the TCP timestamp options to measure the round + trip time to the server. + returned: queried + type: bool + sample: yes + server_sack: + description: + - Specifies whether to support the server sack option in cookie responses + by default. + returned: queried + type: bool + sample: no + server_timestamp: + description: + - Specifies whether to support the server timestamp option in cookie + responses by default. + returned: queried + type: bool + sample: yes + software_syn_cookie: + description: + - Enables or disables software SYN cookie support when PVA10 is not present + on the system. + - This option is deprecated in version 13.0.0 and is replaced by + C(syn_cookie_enabled). + returned: queried + type: bool + sample: yes + syn_cookie_enabled: + description: + - Enables syn-cookies capability on this virtual server. + returned: queried + type: bool + sample: no + syn_cookie_mss: + description: + - Specifies a maximum segment size (MSS) for server connections when + SYN Cookie is enabled. + returned: queried + type: int + sample: 2000 + syn_cookie_whitelist: + description: + - Specifies whether or not to use a SYN Cookie WhiteList when doing + software SYN Cookies. + returned: queried + type: bool + sample: no + tcp_close_timeout: + description: + - Specifies a TCP close timeout in seconds. + returned: queried + type: int + sample: 100 + generate_init_seq_number: + description: + - Specifies whether you want to generate TCP sequence numbers on all + SYNs that conform with RFC1948, and allow timestamp recycling. + returned: queried + type: bool + sample: yes + tcp_handshake_timeout: + description: + - Specifies a TCP handshake timeout in seconds. + returned: queried + type: int + sample: 5 + strip_sack: + description: + - Specifies whether you want to block the TCP SackOK option from + passing to the server on an initiating SYN. + returned: queried + type: bool + sample: yes + tcp_time_wait_timeout: + description: + - Specifies a TCP time_wait timeout in milliseconds. + returned: queried + type: int + sample: 60 + tcp_timestamp_mode: + description: + - Specifies how you want to handle the TCP timestamp. + returned: queried + type: str + sample: preserve + tcp_window_scale_mode: + description: + - Specifies how you want to handle the TCP window scale. + returned: queried + type: str + sample: preserve + timeout_recovery: + description: + - Specifies late binding timeout recovery mode. This is the action + to take when late binding timeout occurs on a connection. + - When C(disconnect), only the L7 iRule actions are acceptable to + pick a server. + - When C(fallback), the normal FastL4 load balancing methods are acceptable + to pick a server. + returned: queried + type: str + sample: fallback + sample: hash/dictionary of values +gateway_icmp_monitors: + description: Gateway ICMP monitor related information. + returned: When C(gateway-icmp-monitors) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/gateway_icmp + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: gateway_icmp + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: gateway_icmp + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My monitor + adaptive: + description: + - Whether adaptive response time monitoring is enabled for this monitor. + returned: queried + type: bool + sample: no + adaptive_divergence_type: + description: + - Specifies whether the adaptive-divergence-value is C(relative) or + C(absolute). + returned: queried + type: str + sample: relative + adaptive_divergence_value: + description: + - Specifies how far from mean latency each monitor probe is allowed + to be. + returned: queried + type: int + sample: 25 + adaptive_limit: + description: + - Specifies the hard limit, in milliseconds, which the probe is not + allowed to exceed, regardless of the divergence value. + returned: queried + type: int + sample: 200 + adaptive_sampling_timespan: + description: + - Specifies the size of the sliding window, in seconds, which + records probe history. + returned: queried + type: int + sample: 300 + destination: + description: + - Specifies the IP address and service port of the resource that is + the destination of this monitor. + returned: queried + type: str + sample: "*:*" + interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when either the resource is down or the status + of the resource is unknown. + returned: queried + type: int + sample: 5 + manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to (B)up at the next successful monitor check. + returned: queried + type: bool + sample: yes + time_until_up: + description: + - Specifies the amount of time, in seconds, after the first + successful response before a node is marked up. + returned: queried + type: int + sample: 0 + timeout: + description: + - Specifies the number of seconds the target has in which to respond + to the monitor request. + returned: queried + type: int + sample: 16 + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + returned: queried + type: bool + sample: no + up_interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when the resource is up. + returned: queried + type: int + sample: 0 + sample: hash/dictionary of values +gtm_pools: + description: + - GTM pool related information. + - Every "type" of pool has the exact same list of possible information. Therefore, + the list of information here is presented once instead of 6 times. + returned: When any of C(gtm-pools) or C(gtm-*-pools) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/pool1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: pool1 + alternate_mode: + description: + - The load balancing mode the system uses to load balance name resolution + requests among the members of the pool. + returned: queried + type: str + sample: drop-packet + dynamic_ratio: + description: + - Specifies whether the dynamic ratio load balancing algorithm is enabled for this + pool. + returned: queried + type: bool + sample: yes + enabled: + description: + - Specifies the pool is enabled. + returned: queried + type: bool + disabled: + description: + - Specifies the pool is disabled. + returned: queried + type: bool + fallback_mode: + description: + - Specifies the load balancing mode the system uses to load balance + name resolution among the pool members if the preferred and alternate + modes are unsuccessful in picking a pool. + returned: queried + type: str + load_balancing_mode: + description: + - Specifies the preferred load balancing mode the system uses to load + balance requests across pool members. + returned: queried + type: str + manual_resume: + description: + - Whether manual resume is enabled for this pool. + returned: queried + type: bool + max_answers_returned: + description: + - Maximum number of available virtual servers the system lists in a + response. + returned: queried + type: int + members: + description: + - Lists of members (and their configurations) in the pool. + returned: queried + type: dict + partition: + description: + - Partition on which the pool exists. + returned: queried + type: str + qos_hit_ratio: + description: + - Weight of the Hit Ratio performance factor for the QoS dynamic load + balancing method. + returned: queried + type: int + qos_hops: + description: + - Weight of the Hops performance factor when load balancing mode or fallback mode + is QoS. + returned: queried + type: int + qos_kilobytes_second: + description: + - Weight assigned to the Kilobytes per Second performance factor when the load + balancing option is QoS. + returned: queried + type: int + qos_lcs: + description: + - Weight assigned to the Link Capacity performance factor when the load balacing + option is QoS. + returned: queried + type: int + qos_packet_rate: + description: + - Weight assigned to the Packet Rate performance factor when the load balacing + option is QoS. + returned: queried + type: int + qos_rtt: + description: + - Weight assigned to the Round Trip Time performance factor when the load balacing + option is QoS. + returned: queried + type: int + qos_topology: + description: + - Weight assigned to the Topology performance factor when the load balacing option + is QoS. + returned: queried + type: int + qos_vs_capacity: + description: + - Weight assigned to the Virtual Server performance factor when the load balacing + option is QoS. + returned: queried + type: int + qos_vs_score: + description: + - Weight assigned to the Virtual Server Score performance factor when the load balacing + option is QoS. + returned: queried + type: int + ttl: + description: + - Number of seconds the IP address, once found, is valid. + returned: queried + type: int + verify_member_availability: + description: + - Whether or not the system verifies the availability of the members before + sending a connection to them. + returned: queried + type: bool + sample: hash/dictionary of values +gtm_servers: + description: + - GTM server related information. + returned: When C(gtm-servers) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/server1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: server1 + datacenter: + description: + - Full name of the datacenter to which this server belongs. + returned: queried + type: str + enabled: + description: + - Specifies the server is enabled. + returned: queried + type: bool + disabled: + description: + - Specifies the server is disabled. + returned: queried + type: bool + expose_route_domains: + description: + - Allow the GTM server to auto-discover the LTM virtual servers from all + route domains. + returned: queried + type: bool + iq_allow_path: + description: + - Whether the GTM uses this BIG-IP system to conduct a path probe before + delegating traffic to it. + returned: queried + type: bool + iq_allow_service_check: + description: + - Whether the GTM uses this BIG-IP system to conduct a service check probe + before delegating traffic to it. + returned: queried + type: bool + iq_allow_snmp: + description: + - Whether the GTM uses this BIG-IP system to conduct an SNMP probe + before delegating traffic to it. + returned: queried + type: bool + limit_cpu_usage: + description: + - For a server configured as a generic host, specifies the percent of CPU + usage, otherwise this has no effect. + returned: queried + type: int + limit_cpu_usage_status: + description: + - Whether C(limit_cpu_usage) is enabled for this server. + returned: queried + type: bool + limit_max_bps: + description: + - Maximum allowable data throughput rate in bits per second for this server. + returned: queried + type: int + limit_max_bps_status: + description: + - Whether C(limit_max_bps) is enabled for this server. + returned: queried + type: bool + limit_max_connections: + description: + - Maximum number of concurrent connections, combined, for this server. + returned: queried + type: int + limit_max_connections_status: + description: + - Whether C(limit_max_connections) is enabled for this server. + type: bool + limit_max_pps: + description: + - Maximum allowable data transfer rate for this server, in packets per second. + returned: queried + type: int + limit_max_pps_status: + description: + - Whether C(limit_max_pps) is enabled for this server. + returned: queried + type: bool + limit_mem_available: + description: + - For a server configured as a generic host, specifies the available memory + required by the virtual servers on the server. + - If available memory falls below this limit, the system marks the server as + unavailable. + returned: queried + type: int + limit_mem_available_status: + description: + - Whether C(limit_mem_available) is enabled for this server. + returned: queried + type: bool + link_discovery: + description: + - Specifies whether the system auto-discovers the links for this server. + returned: queried + type: str + monitors: + description: + - Specifies health monitors that the system uses to determine whether this + server is available for load balancing. + returned: queried + type: list + sample: ['/Common/https_443', '/Common/icmp'] + monitor_type: + description: + - Whether one or more monitors need to pass, or all monitors need to pass. + returned: queried + type: str + sample: and_list + product: + description: + - Specifies the server type. + returned: queried + type: str + prober_fallback: + description: + - The type of prober to use to monitor this server's resources when the + preferred type is not available. + returned: queried + type: str + prober_preference: + description: + - Specifies the type of prober to use to monitor this server's resources. + returned: queried + type: str + virtual_server_discovery: + description: + - Whether the system auto-discovers the virtual servers for this server. + returned: queried + type: str + addresses: + description: + - Specifies the server IP addresses. + returned: queried + type: dict + devices: + description: + - Specifies the names of the devices that represent this server. + returned: queried + type: dict + virtual_servers: + description: + - Specifies the virtual servers that are resources for this server. + returned: queried + type: dict + sample: hash/dictionary of values +gtm_wide_ips: + description: + - GTM Wide IP related information. + - Every "type" of Wide IP has the exact same list of possible information. Therefore, + the list of information here is presented once instead of 6 times. + returned: When any of C(gtm-wide-ips) or C(gtm-*-wide-ips) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/wide1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: wide1 + description: + description: + - Description of the Wide IP. + returned: queried + type: str + enabled: + description: + - Whether the Wide IP is enabled. + returned: queried + type: bool + disabled: + description: + - Whether the Wide IP is disabled. + returned: queried + type: bool + failure_rcode: + description: + - Specifies the DNS RCODE used when C(failure_rcode_response) is C(yes). + returned: queried + type: int + failure_rcode_response: + description: + - When C(yes), specifies the system returns a RCODE response to + Wide IP requests after exhausting all load balancing methods. + returned: queried + type: bool + failure_rcode_ttl: + description: + - Specifies the negative caching TTL of the SOA for the RCODE response. + returned: queried + type: int + last_resort_pool: + description: + - Specifies which pool, as listed in Pool List, for the system to use as + the last resort pool for the Wide IP. + returned: queried + type: str + minimal_response: + description: + - Specifies the system forms the smallest allowable DNS response to + a query. + returned: queried + type: str + persist_cidr_ipv4: + description: + - Specifies the number of bits the system uses to identify IPv4 addresses + when persistence is enabled. + returned: queried + type: int + persist_cidr_ipv6: + description: + - Specifies the number of bits the system uses to identify IPv6 addresses + when persistence is enabled. + returned: queried + type: int + pool_lb_mode: + description: + - Specifies the load balancing method used to select a pool in this Wide IP. + returned: queried + type: str + ttl_persistence: + description: + - Specifies, in seconds, the length of time for which the persistence + entry is valid. + returned: queried + type: int + pools: + description: + - Specifies the pools this Wide IP uses for load balancing. + returned: queried + type: dict + sample: hash/dictionary of values +gtm_topology_regions: + description: GTM regions related information. + returned: When C(gtm-topology-regions) is specified in C(gather_subset) + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/region1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: region1 + region_members: + description: + - The list of region members. + type: complex + contains: + negate: + description: + - Indicates if the region member is a C(IS-NOT) negative. In a BIG-IP configuration, the + region members can be C(IS) or C(IS-NOT). + returned: when configured for the region member. + type: bool + sample: yes + subnet: + description: + - An IP address and network mask in the CIDR format. + type: str + returned: when configured for the region member. + sample: 10.10.10.0/24 + region: + description: + - The name of region already defined in the configuration. + type: str + returned: when configured for the region member. + sample: /Common/region1 + continent: + description: + - The name of one of the seven continents in ISO format, along with the C(Unknown) setting. + type: str + returned: when configured for the region member. + sample: AF + country: + description: + - The country name returned as an ISO country code. + - Valid country codes can be found here https://countrycode.org/. + type: str + returned: when configured for the region member. + sample: US + state: + description: + - The state in a given country. + type: str + returned: when configured for the region member. + sample: "AD/Sant Julia de Loria" + pool: + description: + - The name of a GTM pool already defined in the configuration. + type: str + returned: when configured for the region member. + sample: /Common/pool1 + datacenter: + description: + - The name of a GTM data center already defined in the configuration. + type: str + returned: when configured for the region member. + sample: /Common/dc1 + isp: + description: + - Specifies an Internet service provider. + type: str + returned: when configured for the region member. + sample: /Common/AOL + geo_isp: + description: + - Specifies a geolocation ISP. + type: str + returned: when configured for the region member. + sample: /Common/FOO_ISP + sample: hash/dictionary of values + sample: hash/dictionary of values +http_monitors: + description: HTTP monitor related information. + returned: When C(http-monitors) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/http + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: http + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: http + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My monitor + adaptive: + description: + - Whether adaptive response time monitoring is enabled for this monitor. + returned: queried + type: bool + sample: no + adaptive_divergence_type: + description: + - Specifies whether the adaptive-divergence-value is C(relative) or + C(absolute). + returned: queried + type: str + sample: relative + adaptive_divergence_value: + description: + - Specifies how far from mean latency each monitor probe is allowed + to be. + returned: queried + type: int + sample: 25 + adaptive_limit: + description: + - Specifies the hard limit, in milliseconds, which the probe is not + allowed to exceed, regardless of the divergence value. + returned: queried + type: int + sample: 200 + adaptive_sampling_timespan: + description: + - Specifies the size of the sliding window, in seconds, which + records probe history. + returned: queried + type: int + sample: 300 + destination: + description: + - Specifies the IP address and service port of the resource that is + the destination of this monitor. + returned: queried + type: str + sample: "*:*" + interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when either the resource is down or the status + of the resource is unknown. + returned: queried + type: int + sample: 5 + ip_dscp: + description: + - Specifies the differentiated services code point (DSCP). + returned: queried + type: int + sample: 0 + manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to (B)up at the next successful monitor check. + returned: queried + type: bool + sample: yes + receive_string: + description: + - Specifies the text string the monitor looks for in the + returned resource. + returned: queried + type: str + sample: check string + receive_disable_string: + description: + - Specifies a text string the monitor looks for in the returned + resource. If the text string is matched in the returned resource, + the corresponding node or pool member is marked session disabled. + returned: queried + type: str + sample: check disable string + reverse: + description: + - Specifies whether the monitor operates in reverse mode. When the + monitor is in reverse mode, a successful check marks the monitored + object down instead of up. + returned: queried + type: bool + sample: no + send_string: + description: + - Specifies the text string the monitor sends to the target + object. + returned: queried + type: str + sample: "GET /\\r\\n" + time_until_up: + description: + - Specifies the amount of time, in seconds, after the first + successful response before a node is marked up. + returned: queried + type: int + sample: 0 + timeout: + description: + - Specifies the number of seconds the target has in which to respond + to the monitor request. + returned: queried + type: int + sample: 16 + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + returned: queried + type: bool + sample: no + up_interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when the resource is up. + returned: queried + type: int + sample: 0 + username: + description: + - Specifies the username, if the monitored target requires + authentication. + returned: queried + type: str + sample: user1 + sample: hash/dictionary of values +https_monitors: + description: HTTPS monitor related information. + returned: When C(https-monitors) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/http + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: http + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: http + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My monitor + adaptive: + description: + - Whether adaptive response time monitoring is enabled for this monitor. + returned: queried + type: bool + sample: no + adaptive_divergence_type: + description: + - Specifies whether the adaptive-divergence-value is C(relative) or + C(absolute). + returned: queried + type: str + sample: relative + adaptive_divergence_value: + description: + - Specifies how far from mean latency each monitor probe is allowed + to be. + returned: queried + type: int + sample: 25 + adaptive_limit: + description: + - Specifies the hard limit, in milliseconds, which the probe is not + allowed to exceed, regardless of the divergence value. + returned: queried + type: int + sample: 200 + adaptive_sampling_timespan: + description: + - Specifies the size of the sliding window, in seconds, which + records probe history. + returned: queried + type: int + sample: 300 + destination: + description: + - Specifies the IP address and service port of the resource that is + the destination of this monitor. + returned: queried + type: str + sample: "*:*" + interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when either the resource is down or the status + of the resource is unknown. + returned: queried + type: int + sample: 5 + ip_dscp: + description: + - Specifies the differentiated services code point (DSCP). + returned: queried + type: int + sample: 0 + manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to up at the next successful monitor check. + returned: queried + type: bool + sample: yes + receive_string: + description: + - Specifies the text string the monitor looks for in the + returned resource. + returned: queried + type: str + sample: check string + receive_disable_string: + description: + - Specifies a text string the monitor looks for in the returned + resource. If the text string is matched in the returned resource, + the corresponding node or pool member is marked session disabled. + returned: queried + type: str + sample: check disable string + reverse: + description: + - Specifies whether the monitor operates in reverse mode. When the + monitor is in reverse mode, a successful check marks the monitored + object down instead of up. + returned: queried + type: bool + sample: no + send_string: + description: + - Specifies the text string the monitor sends to the target + object. + returned: queried + type: str + sample: "GET /\\r\\n" + ssl_profile: + description: + - Specifies the SSL profile to use for the HTTPS monitor. + returned: queried + type: str + sample: /Common/serverssl + time_until_up: + description: + - Specifies the amount of time, in seconds, after the first + successful response before a node is marked up. + returned: queried + type: int + sample: 0 + timeout: + description: + - Specifies the number of seconds the target has in which to respond + to the monitor request. + returned: queried + type: int + sample: 16 + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + returned: queried + type: bool + sample: no + up_interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when the resource is up. + returned: queried + type: int + sample: 0 + username: + description: + - Specifies the username, if the monitored target requires + authentication. + returned: queried + type: str + sample: user1 + sample: hash/dictionary of values +http_profiles: + description: HTTP profile related information. + returned: When C(http-profiles) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/http + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: http + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: http + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My profile + accept_xff: + description: + - Enables or disables trusting the client IP address, and statistics + from the client IP address, based on the request's X-Forwarded-For + (XFF) headers, if they exist. + returned: queried + type: bool + sample: yes + allow_truncated_redirects: + description: + - Specifies the pass-through behavior when a redirect lacking the + trailing carriage-return and line feed pair at the end of the headers + is parsed. + - When C(no), the system will silently drop the invalid HTTP. + returned: queried + type: bool + sample: no + excess_client_headers: + description: + - Specifies the pass-through behavior when the C(max_header_count) value is + exceeded by the client. + - When C(reject), the system rejects the connection. + returned: queried + type: str + sample: reject + excess_server_headers: + description: + - Specifies the pass-through behavior when C(max_header_count) value is + exceeded by the server. + - When C(reject), the system rejects the connection. + returned: queried + type: str + sample: reject + known_methods: + description: + - Optimizes the behavior of a known HTTP method in the list. + - The default methods include the following HTTP/1.1 methods. CONNECT, + DELETE, GET, HEAD, LOCK, OPTIONS, POST, PROPFIND, PUT, TRACE, UNLOCK. + - If a known method is deleted from the C(known_methods) list, the + BIG-IP system applies the C(unknown_method) setting to manage that traffic. + returned: queried + type: list + sample: ['CONNECT', 'DELETE', ...] + max_header_count: + description: + - Specifies the maximum number of headers the system supports. + returned: queried + type: int + sample: 64 + max_header_size: + description: + - Specifies the maximum size, in bytes, the system allows for all HTTP + request headers combined, including the request line. + returned: queried + type: int + sample: 32768 + max_requests: + description: + - Specifies the number of requests the system accepts on a per-connection + basis. + returned: queried + type: int + sample: 0 + oversize_client_headers: + description: + - Specifies the pass-through behavior when the C(max_header_size) value + is exceeded by the client. + returned: queried + type: str + sample: reject + oversize_server_headers: + description: + - Specifies the pass-through behavior when the C(max_header_size) value + is exceeded by the server. + returned: queried + type: str + sample: reject + pipeline_action: + description: + - Enables or disables HTTP/1.1 pipelining. + returned: queried + type: str + sample: allow + unknown_method: + description: + - Specifies the behavior (allow, reject, or pass through) when an unknown + HTTP method is parsed. + returned: queried + type: str + sample: allow + default_connect_handling: + description: + - Specifies the behavior of the proxy service when handling outbound requests. + returned: queried + type: str + sample: deny + hsts_include_subdomains: + description: + - When C(yes), applies the HSTS policy to the HSTS host and its subdomains. + returned: queried + type: bool + sample: yes + hsts_enabled: + description: + - When C(yes), enables the HTTP Strict Transport Security settings. + returned: queried + type: bool + sample: yes + insert_xforwarded_for: + description: + - When C(yes), specifies the system inserts an X-Forwarded-For header in + an HTTP request with the client IP address, to use with connection pooling. + returned: queried + type: bool + sample: no + lws_max_columns: + description: + - Specifies the maximum column width for any given line, when inserting an HTTP + header in an HTTP request. + returned: queried + type: int + sample: 80 + onconnect_transformations: + description: + - When C(yes), specifies the system performs HTTP header transformations + for the purpose of keeping connections open. + returned: queried + type: bool + sample: yes + proxy_mode: + description: + - Specifies the proxy mode for this profile. Either reverse, explicit, or transparent. + returned: queried + type: str + sample: reverse + redirect_rewrite: + description: + - Specifies whether the system rewrites the URIs that are part of HTTP + redirect (3XX) responses. + returned: queried + type: str + sample: none + request_chunking: + description: + - Specifies how the system handles HTTP content that is chunked by a client. + returned: queried + type: str + sample: preserve + response_chunking: + description: + - Specifies how the system handles HTTP content that is chunked by a server. + returned: queried + type: str + sample: selective + server_agent_name: + description: + - Specifies the string used as the server name in traffic generated by LTM. + returned: queried + type: str + sample: BigIP + sflow_poll_interval: + description: + - The maximum interval in seconds between two pollings. + returned: queried + type: int + sample: 0 + sflow_sampling_rate: + description: + - Specifies the ratio of packets observed to the samples generated. + returned: queried + type: int + sample: 0 + via_request: + description: + - Specifies whether to Remove, Preserve, or Append Via headers included in + a client request to an origin web server. + returned: queried + type: str + sample: preserve + via_response: + description: + - Specifies whether to Remove, Preserve, or Append Via headers included in + an origin web server response to a client. + returned: queried + type: str + sample: preserve + sample: hash/dictionary of values +iapp_services: + description: iApp v1 service related information. + returned: When C(iapp-services) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/service1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: service1 + device_group: + description: + - The device group the iApp service is part of. + returned: queried + type: str + sample: /Common/dg1 + inherited_device_group: + description: + - Whether the device group is inherited or not. + returned: queried + type: bool + sample: yes + inherited_traffic_group: + description: + - Whether the traffic group is inherited or not. + returned: queried + type: bool + sample: yes + strict_updates: + description: + - Whether strict updates are enabled or not. + returned: queried + type: bool + sample: yes + template_modified: + description: + - Whether template the service is based on is modified from its + default value, or not. + returned: queried + type: bool + sample: yes + traffic_group: + description: + - Traffic group the service is a part of. + returned: queried + type: str + sample: /Common/tg + tables: + description: + - List of the tabular data used to create the service. + returned: queried + type: list + sample: [{"name": "basic__snatpool_members"},...] + variables: + description: + - List of the variable data used to create the service. + returned: queried + type: list + sample: [{"name": "afm__policy"},{"encrypted": "no"},{"value": "/#no_not_use#"},...] + metadata: + description: + - List of the metadata data used to create the service. + returned: queried + type: list + sample: [{"name": "var1"},{"persist": "true"},...] + lists: + description: + - List of the lists data used to create the service. + returned: queried + type: list + sample: [{"name": "irules__irules"},{"value": []},...] + description: + description: + - Description of the service. + returned: queried + type: str + sample: My service + sample: hash/dictionary of values +icmp_monitors: + description: ICMP monitor related information. + returned: When C(icmp-monitors) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/icmp + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: icmp + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: icmp + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My monitor + adaptive: + description: + - Whether adaptive response time monitoring is enabled for this monitor. + returned: queried + type: bool + sample: no + adaptive_divergence_type: + description: + - Specifies whether the adaptive-divergence-value is C(relative) or + C(absolute). + returned: queried + type: str + sample: relative + adaptive_divergence_value: + description: + - Specifies how far from mean latency each monitor probe is allowed + to be. + returned: queried + type: int + sample: 25 + adaptive_limit: + description: + - Specifies the hard limit, in milliseconds, which the probe is not + allowed to exceed, regardless of the divergence value. + returned: queried + type: int + sample: 200 + adaptive_sampling_timespan: + description: + - Specifies the size of the sliding window, in seconds, which + records probe history. + returned: queried + type: int + sample: 300 + destination: + description: + - Specifies the IP address and service port of the resource that is + the destination of this monitor. + returned: queried + type: str + sample: "*:*" + interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when either the resource is down or the status + of the resource is unknown. + returned: queried + type: int + sample: 5 + manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to (B)up at the next successful monitor check. + type: bool + sample: yes + time_until_up: + description: + - Specifies the amount of time, in seconds, after the first + successful response before a node is marked up. + returned: queried + type: int + sample: 0 + timeout: + description: + - Specifies the number of seconds the target has in which to respond + to the monitor request. + returned: queried + type: int + sample: 16 + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + returned: queried + type: bool + sample: no + up_interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when the resource is up. + returned: queried + type: int + sample: 0 + sample: hash/dictionary of values +interfaces: + description: Interface related information. + returned: When C(interfaces) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/interface1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: interface1 + active_media_type: + description: + - Displays the current media setting for the interface. + returned: queried + type: str + sample: 100TX-FD + flow_control: + description: + - Specifies how the system controls the sending of PAUSE frames for + flow control. + returned: queried + type: str + sample: tx-rx + description: + description: + - Description of the interface. + returned: queried + type: str + sample: My interface + bundle: + description: + - The bundle capability on the port. + returned: queried + type: str + sample: not-supported + bundle_speed: + description: + - The bundle-speed on the port when bundle capability is + enabled. + returned: queried + type: str + sample: 100G + enabled: + description: + - Whether the interface is enabled or not. + returned: queried + type: bool + sample: yes + if_index: + description: + - The index assigned to this interface. + returned: queried + type: int + sample: 32 + mac_address: + description: + - Displays the 6-byte ethernet address in non-case-sensitive + hexadecimal colon notation. + returned: queried + type: str + sample: "00:0b:09:88:00:9a" + media_sfp: + description: + - The settings for an SFP (pluggable) interface. + returned: queried + type: str + sample: auto + lldp_admin: + description: + - Sets the sending or receiving of LLDP packets on that interface. + Should be one of C(disable), C(txonly), C(rxonly) or C(txrx). + returned: queried + type: str + sample: txonly + mtu: + description: + - Displays the Maximum Transmission Unit (MTU) of the interface, + which is the maximum number of bytes in a frame without IP + fragmentation. + returned: queried + type: int + sample: 1500 + prefer_port: + description: + - Indicates which side of a combo port the interface uses, if both + sides of the port have the potential for external links. + returned: queried + type: str + sample: sfp + sflow_poll_interval: + description: + - Specifies the maximum interval in seconds between two + pollings. + returned: queried + type: int + sample: 0 + sflow_poll_interval_global: + description: + - Specifies whether the global interface poll-interval setting + overrides the object-level poll-interval setting. + returned: queried + type: bool + sample: yes + stp_auto_edge_port: + description: + - STP edge port detection. + returned: queried + type: bool + sample: yes + stp_enabled: + description: + - Whether STP is enabled or not. + returned: queried + type: bool + sample: no + stp_link_type: + description: + - Specifies the STP link type for the interface. + returned: queried + type: str + sample: auto + sample: hash/dictionary of values +irules: + description: iRule related information. + returned: When C(irules) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/irule1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: irule1 + ignore_verification: + description: + - Whether the verification of the iRule should be ignored or not. + returned: queried + type: bool + sample: no + checksum: + description: + - Checksum of the iRule as calculated by BIG-IP. + returned: queried + type: str + sample: d41d8cd98f00b204e9800998ecf8427e + definition: + description: + - The actual definition of the iRule. + returned: queried + type: str + sample: when HTTP_REQUEST ... + signature: + description: + - The calculated signature of the iRule. + returned: queried + type: str + sample: WsYy2M6xMqvosIKIEH/FSsvhtWMe6xKOA6i7f... + sample: hash/dictionary of values +license: + description: License related info. + returned: When C(license) is specified in C(gather_subset). + type: complex + contains: + license_start_date: + description: + - Specifies the date on whichthe license was issued. + returned: queried + type: str + sample: 2022/11/21 + license_end_date: + description: + - Specifies the date on which the license is no longer valid. + returned: queried + type: str + sample: 2022/12/30 + licensed_on_date: + description: + - Specifies the date on which the license was activated. + returned: queried + type: str + sample: 2022/11/22 + licensed_version: + description: + - Specifies the version of the device that is using this license. + returned: queried + type: str + sample: 15.1.2 + max_permitted_version: + description: + - Specifies the maximum version to which this license can be applied. + returned: queried + type: str + sample: 18.*.* + min_permitted_version: + description: + - Specifies the minimum version to which this license can be applied. + returned: queried + type: str + sample: 5.*.* + platform_id: + description: + - Specifies the platform id for which the license was activated. + returned: queried + type: str + sample: Z100k + registration_key: + description: + - Specifies the registration key associated with the license. + returned: queried + type: str + sample: XXXX-XXXX-XXXX-XXXX-XXXX + service_check_date: + description: + - Specifies the last date the license was last activated. + returned: queried + type: str + sample: 2022/11/30 + active_modules: + description: + - Specifies the modules that are activated by the license. + returned: queried + type: list + sample: [{"key":"A123456-9876543", "featureModules":"{}"}, ...] + sample: hash/dictionary of values +ltm_pools: + description: List of LTM (Local Traffic Manager) pools. + returned: When C(ltm-pools) is specified in C(gather_subset). + type: complex + contains: + active_member_count: + description: + - The number of active pool members in the pool. + returned: queried + type: int + sample: 3 + all_avg_queue_entry_age: + description: + - Average queue entry age, for both the pool and its members. + returned: queried + type: int + sample: 5 + all_max_queue_entry_age_ever: + description: + - Maximum queue entry age ever, for both the pool and its members. + returned: queried + type: int + sample: 2 + all_max_queue_entry_age_recently: + description: + - Maximum queue entry age recently, for both the pool and its members. + returned: queried + type: int + sample: 5 + all_num_connections_queued_now: + description: + - Number of connections queued now, for both the pool and its members. + returned: queried + type: int + sample: 20 + all_num_connections_serviced: + description: + - Number of connections serviced, for both the pool and its members. + returned: queried + type: int + sample: 15 + all_queue_head_entry_age: + description: + - Queue head entry age, for both the pool and its members. + returned: queried + type: int + sample: 4 + available_member_count: + description: + - The number of available pool members in the pool. + returned: queried + type: int + sample: 4 + availability_status: + description: + - The availability of the pool. + returned: queried + type: str + sample: offline + allow_nat: + description: + - Whether NATs are automatically enabled or disabled for any connections using this pool. + returned: queried + type: bool + sample: yes + allow_snat: + description: + - Whether SNATs are automatically enabled or disabled for any connections using this pool. + returned: queried + type: bool + sample: yes + client_ip_tos: + description: + - Whether the system sets a Type of Service (ToS) level within a packet sent to the client, + based on the targeted pool. + - Values can range from C(0) to C(255), or be set to C(pass-through) or C(mimic). + returned: queried + type: str + sample: pass-through + client_link_qos: + description: + - Whether the system sets a Quality of Service (QoS) level within a packet sent to the client, + based on the targeted pool. + - Values can range from C(0) to C(7), or be set to C(pass-through). + returned: queried + type: str + sample: pass-through + current_sessions: + description: + - Current sessions. + returned: queried + type: int + sample: 2 + description: + description: + - Description of the pool. + returned: queried + type: str + sample: my pool + enabled_status: + description: + - The enabled status of the pool. + returned: queried + type: str + sample: enabled + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/pool1 + ignore_persisted_weight: + description: + - Specifies not to count the weight of persisted connections on pool members when making load balancing decisions. + returned: queried + type: bool + sample: no + lb_method: + description: + - Load balancing method used by the pool. + returned: queried + type: str + sample: round-robin + member_count: + description: + - Total number of members in the pool. + returned: queried + type: int + sample: 50 + metadata: + description: + - Dictionary of arbitrary key/value pairs set on the pool. + returned: queried + type: dict + sample: hash/dictionary of values + minimum_active_members: + description: + - Whether the system load balances traffic according to the priority number assigned to the pool member. + - This parameter is identical to C(priority_group_activation) and is just an alias for it. + returned: queried + type: int + sample: 2 + minimum_up_members: + description: + - The minimum number of pool members that must be up. + returned: queried + type: int + sample: 1 + minimum_up_members_action: + description: + - The action to take if the C(minimum_up_members_checking) is enabled and the number of active pool + members falls below the number specified in C(minimum_up_members). + returned: queried + type: str + sample: failover + minimum_up_members_checking: + description: + - Enables or disables the C(minimum_up_members) feature. + returned: queried + type: bool + sample: no + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: pool1 + pool_avg_queue_entry_age: + description: + - Average queue entry age, for the pool only. + returned: queried + type: int + sample: 5 + pool_max_queue_entry_age_ever: + description: + - Maximum queue entry age ever, for the pool only. + returned: queried + type: int + sample: 2 + pool_max_queue_entry_age_recently: + description: + - Maximum queue entry age recently, for the pool only. + returned: queried + type: int + sample: 5 + pool_num_connections_queued_now: + description: + - Number of connections queued now, for the pool only. + returned: queried + type: int + sample: 20 + pool_num_connections_serviced: + description: + - Number of connections serviced, for the pool only. + returned: queried + type: int + sample: 15 + pool_queue_head_entry_age: + description: + - Queue head entry age, for the pool only. + returned: queried + type: int + sample: 4 + priority_group_activation: + description: + - Whether the system load balances traffic according to the priority number assigned to the pool member. + - This parameter is identical to C(minimum_active_members) and is just an alias for it. + returned: queried + type: int + sample: 2 + queue_depth_limit: + description: + - The maximum number of connections that may simultaneously be queued to go to any member of this pool. + returned: queried + type: int + sample: 3 + queue_on_connection_limit: + description: + - Enable or disable queuing connections when pool member or node connection limits are reached. + returned: queried + type: bool + sample: yes + queue_time_limit: + description: + - Specifies the maximum time, in milliseconds, a connection will remain queued. + returned: queried + type: int + sample: 0 + real_session: + description: + - The actual REST API value for the C(session) attribute. + - This is different from the C(state) return value, as the return value + can be considered a generalization of all available sessions, instead of the + specific value of the session. + returned: queried + type: str + sample: monitor-enabled + real_state: + description: + - The actual REST API value for the C(state) attribute. + - This is different from the C(state) return value, as the return value + can be considered a generalization of all available states, instead of the + specific value of the state. + returned: queried + type: str + sample: up + reselect_tries: + description: + - The number of times the system tries to contact a pool member after a passive failure. + returned: queried + type: int + sample: 0 + server_ip_tos: + description: + - The Type of Service (ToS) level to use when sending packets to a server. + returned: queried + type: str + sample: pass-through + server_link_qos: + description: + - The Quality of Service (QoS) level to use when sending packets to a server. + returned: queried + type: str + sample: pass-through + service_down_action: + description: + - The action to take if the service specified in the pool is marked down. + returned: queried + type: str + sample: none + server_side_bits_in: + description: + - Number of server-side ingress bits. + returned: queried + type: int + sample: 1000 + server_side_bits_out: + description: + - Number of server-side egress bits. + returned: queried + type: int + sample: 200 + server_side_current_connections: + description: + - Number of current connections server-side. + returned: queried + type: int + sample: 300 + server_side_max_connections: + description: + - Maximum number of connections server-side. + returned: queried + type: int + sample: 40 + server_side_pkts_in: + description: + - Number of server-side ingress packets. + returned: queried + type: int + sample: 1098384 + server_side_pkts_out: + description: + - Number of server-side egress packets. + returned: queried + type: int + sample: 3484734 + server_side_total_connections: + description: + - Total number of server-side connections. + returned: queried + type: int + sample: 24 + slow_ramp_time: + description: + - The ramp time for the pool. + - This provides the ability for a pool member that is newly enabled or marked up + to receive proportionally less traffic than other members in the pool. + returned: queried + type: int + sample: 10 + status_reason: + description: + - If there is a problem with the status of the pool, it is reported here. + returned: queried + type: str + sample: The children pool member(s) are down. + members: + description: List of LTM (Local Traffic Manager) pools. + returned: when members exist in the pool. + type: complex + contains: + address: + description: IP address of the pool member. + returned: queried + type: str + sample: 1.1.1.1 + connection_limit: + description: The maximum number of concurrent connections allowed for a pool member. + returned: queried + type: int + sample: 0 + description: + description: The description of the pool member. + returned: queried + type: str + sample: pool member 1 + dynamic_ratio: + description: + - A range of numbers you want the system to use in conjunction with the ratio load balancing method. + returned: queried + type: int + sample: 1 + ephemeral: + description: + - Whether the node backing the pool member is ephemeral or not. + returned: queried + type: bool + sample: yes + fqdn_autopopulate: + description: + - Whether the node should scale to the IP address set returned by DNS. + returned: queried + type: bool + sample: yes + full_path: + description: + - Full name of the resource as known to the BIG-IP. + - Includes the port in the name. + returned: queried + type: str + sample: "/Common/member:80" + inherit_profile: + description: + - Whether the pool member inherits the encapsulation profile from the parent pool. + returned: queried + type: bool + sample: no + logging: + description: + - Whether the monitor applied should log its actions. + returned: queried + type: bool + sample: no + monitors: + description: + - The Monitors active on the pool member. Monitor names are in their "full_path" form. + returned: queried + type: list + sample: ['/Common/http'] + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: "member:80" + partition: + description: + - Partition the member exists on. + returned: queried + type: str + sample: Common + priority_group: + description: + - The priority group within the pool for this pool member. + returned: queried + type: int + sample: 0 + encapsulation_profile: + description: + - The encapsulation profile to use for the pool member. + returned: queried + type: str + sample: ip4ip4 + rate_limit: + description: + - The maximum number of connections per second allowed for a pool member. + returned: queried + type: bool + sample: no + ratio: + description: + - The weight of the pool for load balancing purposes. + returned: queried + type: int + sample: 1 + session: + description: + - Enables or disables the pool member for new sessions. + returned: queried + type: str + sample: monitor-enabled + state: + description: + - Controls the state of the pool member, overriding any monitors. + returned: queried + type: str + sample: down + total_requests: + description: + - Total requests. + returned: queried + type: int + sample: 8 + sample: hash/dictionary of values +ltm_policies: + description: List of LTM (Local Traffic Manager) policies. + returned: When C(ltm-policies) is specified in C(gather_subset). + type: complex + contains: + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: policy1 + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/policy1 + description: + description: + - Description of the policy. + returned: queried + type: str + sample: My policy + strategy: + description: + - The match strategy for the policy. + returned: queried + type: str + sample: /Common/first-match + requires: + description: + - Aspects of the system required by this policy. + returned: queried + type: list + sample: ['http'] + controls: + description: + - Aspects of the system controlled by this policy. + returned: queried + type: list + sample: ['forwarding'] + status: + description: + - Indicates published or draft policy status. + returned: queried + type: str + sample: draft + rules: + description: + - List of LTM (Local Traffic Manager) policy rules. + returned: when rules are defined in the policy. + type: complex + contains: + actions: + description: + - The actions the policy will take when a match is encountered. + returned: when actions are defined in the rule. + type: complex + contains: + http_reply: + description: + - Indicates if the action affects a reply to a given HTTP request. + returned: when defined in the action. + type: bool + sample: yes + redirect: + description: + - This action will redirect a request. + returned: when defined in the action. + type: bool + sample: no + request: + description: + - This policy action is performed on connection requests. + returned: when defined in the action. + type: bool + sample: no + location: + description: + - This action will come from the given location. + returned: when defined in the action. + type: str + sample: "tcl:https://[getfield [HTTP::host] \\\":\\\" 1][HTTP::uri]" + sample: hash/dictionary of values + conditions: + description: + - The conditions a policy will match on. + returned: when conditions are defined in the rule. + type: complex + contains: + case_insensitive: + description: + - Specifies the value matched on is case insensitive. + returned: when defined in the condition. + type: bool + sample: no + case_sensitive: + description: + - Specifies the value matched on is case sensitive. + returned: when defined in the condition. + type: bool + sample: yes + contains_string: + description: + - Specifies the value matches if it contains a certain string. + returned: when defined in the condition. + type: bool + sample: yes + external: + description: + - Specifies the value matched on is from the external side of a connection. + returned: when defined in the condition. + type: bool + sample: yes + http_basic_auth: + description: + - This condition matches on basic HTTP authorization. + returned: when defined in the condition. + type: bool + sample: no + http_host: + description: + - This condition matches on an HTTP host. + returned: when defined in the condition. + type: bool + sample: yes + http_uri: + description: + - This condition matches on an HTTP URI. + returned: when defined in the condition. + type: bool + sample: no + datagroup: + description: + - This condition matches on an HTTP URI. + returned: when defined in the condition. + type: str + sample: /Common/policy_using_datagroup + tcp: + description: + - This condition matches on TCP parameters. + returned: when defined in the condition. + type: bool + sample: no + address: + description: + - This condition matches on a TCP address. + returned: when defined in the condition. + type: bool + sample: no + matches: + description: + - This condition matches on an address. + returned: when defined in the condition. + type: bool + sample: no + proxy_connect: + description: + - Specifies the value matched on is proxyConnect. + returned: when defined in the condition. + type: bool + sample: no + proxy_request: + description: + - Specifies the value matched on is proxyRequest. + returned: when defined in the condition. + type: bool + sample: no + remote: + description: + - Specifies the value matched on is remote. + returned: when defined in the condition. + type: bool + sample: no + request: + description: + - This policy matches on a request. + returned: when defined in the condition. + type: bool + sample: yes + username: + description: + - Matches on a username. + returned: when defined in the condition. + type: bool + sample: yes + all: + description: + - Matches all. + returned: when defined in the condition. + type: bool + sample: yes + values: + description: + - The specified values will be matched on. + returned: when defined in the condition. + type: list + sample: ['foo.bar.com', 'baz.cool.com'] + sample: hash/dictionary of values + sample: hash/dictionary of values + sample: hash/dictionary of values +management_routes: + description: Management route related information. + returned: When C(management-routes) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/default + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: default + description: + description: + - User defined description of the route. + returned: queried + type: str + sample: route-1-external + gateway: + description: + - The gateway IP address through which the system forwards packets to the destination. + returned: queried + type: str + sample: 192.168.0.1 + mtu: + description: + - The maximum transmission unit for the management interface. + returned: queried + type: str + sample: 0 + network: + description: + - The destination subnet and netmask, also specified as default or default-inet6. + returned: queried + type: str + sample: default + sample: hash/dictionary of values +nodes: + description: Node related information. + returned: When C(nodes) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/5.6.7.8 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: 5.6.7.8 + ratio: + description: + - Fixed size ratio used for node during C(Ratio) load balancing. + returned: queried + type: int + sample: 10 + description: + description: + - Description of the node. + returned: queried + type: str + sample: My node + connection_limit: + description: + - Maximum number of connections the node can handle. + returned: queried + type: int + sample: 100 + address: + description: + - IP address of the node. + returned: queried + type: str + sample: 2.3.4.5 + dynamic_ratio: + description: + - Dynamic ratio number for the node used when doing C(Dynamic Ratio) load balancing. + returned: queried + type: int + sample: 200 + rate_limit: + description: + - Maximum number of connections per second allowed for the node. + returned: queried + type: int + sample: 1000 + monitor_status: + description: + - Status of the node as reported by the monitor(s) associated with it. + - This value is also used in determining node C(state). + returned: queried + type: str + sample: down + session_status: + description: + - This value is also used in determining node C(state). + returned: queried + type: str + sample: enabled + availability_status: + description: + - The availability of the node. + returned: queried + type: str + sample: offline + enabled_status: + description: + - The enabled status of the node. + returned: queried + type: str + sample: enabled + status_reason: + description: + - If there is a problem with the status of the node, it is reported here. + returned: queried + type: str + sample: /Common/https_443 No successful responses received... + monitor_rule: + description: + - A string representation of the full monitor rule. + returned: queried + type: str + sample: /Common/https_443 and /Common/icmp + monitors: + description: + - A list of the monitors identified in the C(monitor_rule). + returned: queried + type: list + sample: ['/Common/https_443', '/Common/icmp'] + monitor_type: + description: + - The C(monitor_type) field related to the C(bigip_node) module, for this nodes + monitors. + returned: queried + type: str + sample: and_list + fqdn_name: + description: + - FQDN name of the node. + returned: queried + type: str + sample: sample.host.foo.com + fqdn_auto_populate: + description: + - Indicates if the system automatically creates ephemeral nodes using DNS discovered IPs. + returned: queried + type: bool + sample: yes + fqdn_address_type: + description: + - The address family of the automatically created ephemeral nodes. + returned: queried + type: str + sample: ipv4 + fqdn_up_interval: + description: + - The interval at which a query occurs when the DNS server is up. + returned: queried + type: int + sample: 3600 + fqdn_down_interval: + description: + - The interval in which a query occurs when the DNS server is down. + returned: queried + type: int + sample: 15 + sample: hash/dictionary of values +oneconnect_profiles: + description: OneConnect profile related information. + returned: When C(oneconnect-profiles) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/oneconnect + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: oneconnect + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: oneconnect + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My profile + idle_timeout_override: + description: + - Specifies the number of seconds that a connection is idle before + the connection flow is eligible for deletion. + returned: queried + type: int + sample: 1000 + limit_type: + description: + - When C(none), simultaneous in-flight requests and responses over TCP + connections to a pool member are counted toward the limit. + - When C(idle), idle connections will be dropped as the TCP connection + limit is reached. + - When C(strict), the TCP connection limit is honored with no + exceptions. This means idle connections will prevent new TCP + connections from being made until they expire, even if they could + otherwise be reused. + returned: queried + type: str + sample: idle + max_age: + description: + - Specifies the maximum age, in seconds, of a connection + in the connection reuse pool. + returned: queried + type: int + sample: 100 + max_reuse: + description: + - Specifies the maximum number of times a server connection can + be reused. + returned: queried + type: int + sample: 1000 + max_size: + description: + - Specifies the maximum number of connections the system holds + in the connection reuse pool. + - If the pool is already full, then the server connection closes after + the response is completed. + returned: queried + type: int + sample: 1000 + share_pools: + description: + - Indicates connections may be shared not only within a virtual + server, but also among similar virtual servers. + returned: queried + type: bool + sample: yes + source_mask: + description: + - Specifies a source IP mask. + - If no mask is provided, the value C(any6) is used. + returned: queried + type: str + sample: 255.255.255.0 + sample: hash/dictionary of values +partitions: + description: Partition related information. + returned: When C(partitions) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: Common + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: Common + description: + description: + - Description of the partition. + returned: queried + type: str + sample: Tenant 1 + default_route_domain: + description: + - ID of the route domain that is associated with the IP addresses that reside + in the partition. + returned: queried + type: int + sample: 0 + sample: hash/dictionary of values +provision_info: + description: Module provisioning related information. + returned: When C(provision-info) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: asm + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: asm + cpu_ratio: + description: + - Ratio of CPU allocated to this module. + - Only relevant if C(level) was specified as C(custom). Otherwise, this value + will be reported as C(0). + returned: queried + type: int + sample: 0 + disk_ratio: + description: + - Ratio of disk allocated to this module. + - Only relevant if C(level) was specified as C(custom). Otherwise, this value + will be reported as C(0). + returned: queried + type: int + sample: 0 + memory_ratio: + description: + - Ratio of memory allocated to this module. + - Only relevant if C(level) was specified as C(custom). Otherwise, this value + will be reported as C(0). + returned: queried + type: int + sample: 0 + level: + description: + - Provisioned level of the module on BIG-IP. + - Valid return values can include C(none), C(minimum), C(nominal), C(dedicated) + and C(custom). + returned: queried + type: int + sample: 0 + sample: hash/dictionary of values +remote_syslog: + description: Remote Syslog related information. + returned: When C(remote-syslog) is specified in C(gather_subset). + type: complex + contains: + servers: + description: Configured remote syslog servers. + returned: queried + type: complex + contains: + name: + description: Name of remote syslog server as configured on the system. + returned: queried + type: str + sample: /Common/foobar1 + remote_port: + description: Remote port of the remote syslog server. + returned: queried + type: int + sample: 514 + local_ip: + description: The local IP address of the remote syslog server. + returned: queried + type: str + sample: 10.10.10.10 + remote_host: + description: The IP address or hostname of the remote syslog server. + returned: queried + type: str + sample: 192.168.1.1 + sample: hash/dictionary of values + sample: hash/dictionary of values +route_domains: + description: Route domain related information. + returned: When C(self-ips) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/rd1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: rd1 + description: + description: + - Description of the Route Domain. + returned: queried + type: str + sample: My route domain + id: + description: + - The unique identifying integer representing the route domain. + returned: queried + type: int + sample: 10 + parent: + description: + - The route domain the system searches when it cannot find a route in the configured domain. + returned: queried + type: str + sample: 0 + bwc_policy: + description: + - The bandwidth controller for the route domain. + returned: queried + type: str + sample: /Common/foo + connection_limit: + description: + - The new connection limit for the route domain. + returned: queried + type: int + sample: 100 + flow_eviction_policy: + description: + - The new eviction policy to use with this route domain. + returned: queried + type: str + sample: /Common/default-eviction-policy + service_policy: + description: + - The new service policy to use with this route domain. + returned: queried + type: str + sample: /Common-my-service-policy + strict: + description: + - The new strict isolation setting. + returned: queried + type: str + sample: enabled + routing_protocol: + description: + - List of routing protocols applied to the route domain. + returned: queried + type: list + sample: ['bfd', 'bgp'] + vlans: + description: + - List of new VLANs the route domain is applied to. + returned: queried + type: list + sample: ['/Common/http-tunnel', '/Common/socks-tunnel'] + sample: hash/dictionary of values +self_ips: + description: Self IP related information. + returned: When C(self-ips) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/self1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: self1 + description: + description: + - Description of the Self IP. + returned: queried + type: str + sample: My self-ip + netmask: + description: + - Netmask portion of the IP address, in dotted notation. + returned: queried + type: str + sample: 255.255.255.0 + netmask_cidr: + description: + - Netmask portion of the IP address, in CIDR notation. + returned: queried + type: int + sample: 24 + floating: + description: + - Whether the Self IP is a floating address or not. + returned: queried + type: bool + sample: yes + traffic_group: + description: + - Traffic group the Self IP is associated with. + returned: queried + type: str + sample: /Common/traffic-group-local-only + service_policy: + description: + - Service policy assigned to the Self IP. + returned: queried + type: str + sample: /Common/service1 + vlan: + description: + - VLAN associated with the Self IP. + returned: queried + type: str + sample: /Common/vlan1 + allow_access_list: + description: + - List of protocols, and optionally their ports, that are allowed to access the + Self IP. Also known as port-lockdown in the web interface. + - Items in the list are in the format of "protocol:port". Some items may not + have a port associated with them and in those cases the port is C(0). + returned: queried + type: list + sample: ['tcp:80', 'egp:0'] + traffic_group_inherited: + description: + - Whether or not the traffic group is inherited. + returned: queried + type: bool + sample: no + sample: hash/dictionary of values +server_ssl_profiles: + description: Server SSL related information. + returned: When C(server-ssl-profiles) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: serverssl + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: serverssl + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My profile + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: serverssl + alert_timeout: + description: + - Maximum time period in seconds to keep the SSL + session active after an alert message is sent, or indefinite. + returned: queried + type: str + sample: 100 + allow_expired_crl: + description: + - Use the specified CRL file, even if it has expired. + returned: queried + type: bool + sample: yes + authentication_frequency: + description: + - Specifies the frequency of authentication. + returned: queried + type: str + sample: once + authenticate_depth: + description: + - The client certificate chain maximum traversal depth + returned: queried + type: int + sample: 9 + authenticate_name: + description: + - Common Name (CN) embedded in a server certificate. + - The system authenticates a server based on the specified CN. + returned: queried + type: str + sample: foo + bypass_on_client_cert_fail: + description: + - Enables or disables SSL forward proxy bypass on failing to get + client certificate that the server asks for. + returned: queried + type: bool + sample: yes + bypass_on_handshake_alert: + description: + - Enables or disables SSL forward proxy bypass on receiving + handshake_failure, protocol_version or unsupported_extension alert + message during the serverside SSL handshake. + returned: queried + type: bool + sample: no + c3d_ca_cert: + description: + - Name of the certificate file used as the + certification authority certificate when SSL client certificate + constrained delegation is enabled. + returned: queried + type: str + sample: /Common/cacert.crt + c3d_ca_key: + description: + - Name of the key file used as the + certification authority key when SSL client certificate + constrained delegation is enabled. + returned: queried + type: str + sample: /Common/default.key + c3d_cert_extension_includes: + description: + - Extensions of the client certificates to be included + in the generated certificates using SSL client certificate + constrained delegation. + returned: queried + type: list + sample: [ "basic-constraints", "extended-key-usage", ... ] + c3d_cert_lifespan: + description: + - Lifespan of the certificate generated using the SSL + client certificate constrained delegation. + returned: queried + type: int + sample: 24 + ca_file: + description: + - Certificate authority file name. + returned: queried + type: str + sample: default.crt + cache_size: + description: + - The SSL session cache size. + returned: queried + type: int + sample: 262144 + cache_timeout: + description: + - The SSL session cache timeout value, which is the usable + lifetime seconds of negotiated SSL session IDs. + returned: queried + type: int + sample: 86400 + cert: + description: + - The name of the certificate installed on the traffic + management system for the purpose of terminating or initiating an + SSL connection. + returned: queried + type: str + sample: /Common/default.crt + chain: + description: + - Specifies or builds a certificate chain file that a client can use + to authenticate the profile. + returned: queried + type: str + sample: /Common/default.crt + cipher_group: + description: + - Specifies a cipher group. + returned: queried + type: str + ciphers: + description: + - Specifies a cipher name. + returned: queried + type: str + sample: DEFAULT + crl_file: + description: + - Specifies the certificate revocation list file name. + returned: queried + type: str + expire_cert_response_control: + description: + - Specifies the BIGIP action when the server certificate has + expired. + returned: queried + type: str + sample: drop + handshake_timeout: + description: + - Specifies the handshake timeout in seconds. + returned: queried + type: str + sample: 10 + key: + description: + - Specifies the name of the key + installed on the traffic management system for the purpose of + terminating or initiating an SSL connection. + returned: queried + type: str + sample: /Common/default.key + max_active_handshakes: + description: + - Specifies the maximum number of allowed active SSL handshakes. + returned: queried + type: str + sample: 100 + mod_ssl_methods: + description: + - Enables or disables ModSSL methods. + returned: queried + type: bool + sample: yes + mode: + description: + - Enables or disables SSL processing. + returned: queried + type: bool + sample: no + ocsp: + description: + - Specifies the name of the OCSP profile for validating + the status of the server certificate. + returned: queried + type: str + options: + description: + - Enables options, including some industry-related workarounds. + returned: queried + type: list + sample: [ "netscape-reuse-cipher-change-bug", "dont-insert-empty-fragments" ] + peer_cert_mode: + description: + - Specifies the peer certificate mode. + returned: queried + type: str + sample: ignore + proxy_ssl: + description: + - Allows further modification of application traffic within + an SSL tunnel while still allowing the server to perform necessary + authorization, authentication, auditing steps. + returned: queried + type: bool + sample: yes + proxy_ssl_passthrough: + description: + - Allows Proxy SSL to passthrough the traffic when ciphersuite negotiated + between the client and server is not supported. + returned: queried + type: bool + sample: yes + renegotiate_period: + description: + - Number of seconds from the initial connect time + after which the system renegotiates an SSL session. + returned: queried + type: str + sample: indefinite + renegotiate_size: + description: + - Specifies a throughput size of SSL renegotiation, in megabytes. + returned: queried + type: str + sample: indefinite + renegotiation: + description: + - Whether renegotiations are enabled. + returned: queried + type: bool + sample: yes + retain_certificate: + description: + - APM module requires storing certificates in the SSL session. When C(no), + a certificate will not be stored in the SSL session. + returned: queried + type: bool + sample: no + generic_alert: + description: + - Enables or disables generic-alert. + returned: queried + type: bool + sample: yes + secure_renegotiation: + description: + - Specifies the secure renegotiation mode. + returned: queried + type: str + sample: require + server_name: + description: + - Server name to be included in the SNI (server name + indication) extension during SSL handshake in ClientHello. + returned: queried + type: str + session_mirroring: + description: + - Enables or disables the mirroring of sessions to the high availability + peer. + returned: queried + type: bool + sample: yes + session_ticket: + description: + - Enables or disables session-ticket. + returned: queried + type: bool + sample: no + sni_default: + description: + - When C(yes), this profile is the default SSL profile when the server + name in a client connection does not match any configured server + names, or a client connection does not specify any server name at + all. + returned: queried + type: bool + sample: yes + sni_require: + description: + - When C(yes), connections to a server that do not support SNI + extension will be rejected. + returned: queried + type: bool + sample: no + ssl_c3d: + description: + - Enables or disables SSL Client certificate constrained delegation. + returned: queried + type: bool + sample: yes + ssl_forward_proxy_enabled: + description: + - Enables or disables the ssl-forward-proxy feature. + returned: queried + type: bool + sample: no + ssl_sign_hash: + description: + - Specifies the SSL sign hash algorithm which is used to sign and verify + SSL Server Key Exchange and Certificate Verify messages for the + specified SSL profiles. + returned: queried + type: str + sample: sha1 + ssl_forward_proxy_bypass: + description: + - Enables or disables the ssl-forward-proxy-bypass feature. + returned: queried + type: bool + sample: yes + strict_resume: + description: + - Enables or disables the resumption of SSL sessions after an + unclean shutdown. + returned: queried + type: bool + sample: no + unclean_shutdown: + description: + - Specifies, when C(yes), that the SSL profile performs unclean + shutdowns of all SSL connections. This means underlying TCP + connections are closed without exchanging the required SSL + shutdown alerts. + returned: queried + type: bool + sample: yes + untrusted_cert_response_control: + description: + - Specifies the BIG-IP action when the server certificate has + an untrusted CA. + returned: queried + type: str + sample: drop + sample: hash/dictionary of values +software_hotfixes: + description: List of software hotfixes. + returned: When C(software-hotfixes) is specified in C(gather_subset). + type: complex + contains: + name: + description: + - Name of the image. + returned: queried + type: str + sample: Hotfix-BIGIP-13.0.0.3.0.1679-HF3.iso + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: Hotfix-BIGIP-13.0.0.3.0.1679-HF3.iso + build: + description: + - Build number of the image. + - This is usually a sub-string of the C(name). + returned: queried + type: str + sample: 3.0.1679 + checksum: + description: + - MD5 checksum of the image. + - Note that this is the checksum stored inside the ISO. It is not + the actual checksum of the ISO. + returned: queried + type: str + sample: df1ec715d2089d0fa54c0c4284656a98 + product: + description: + - Product contained in the ISO. + returned: queried + type: str + sample: BIG-IP + id: + description: + - ID component of the image. + - This is usually a sub-string of the C(name). + returned: queried + type: str + sample: HF3 + title: + description: + - Human friendly name of the image. + returned: queried + type: str + sample: Hotfix Version 3.0.1679 + verified: + description: + - Specifies whether the system has verified this image. + returned: queried + type: bool + sample: yes + version: + description: + - Version of software contained in the image. + - This is a sub-string of the C(name). + returned: queried + type: str + sample: 13.0.0 + sample: hash/dictionary of values +software_images: + description: List of software images. + returned: When C(software-images) is specified in C(gather_subset). + type: complex + contains: + name: + description: + - Name of the image. + returned: queried + type: str + sample: BIGIP-13.1.0.7-0.0.1.iso + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: BIGIP-13.1.0.7-0.0.1.iso + build: + description: + - Build number of the image. + - This is usually a sub-string of the C(name). + returned: queried + type: str + sample: 0.0.1 + build_date: + description: + - Date of the build. + returned: queried + type: str + sample: "2018-05-05T15:26:30" + checksum: + description: + - MD5 checksum of the image. + - Note that this is the checksum stored inside the ISO. It is not + the actual checksum of the ISO. + returned: queried + type: str + sample: df1ec715d2089d0fa54c0c4284656a98 + file_size: + description: + - Size of the image, in megabytes. + returned: queried + type: int + sample: 1938 + last_modified: + description: + - Last modified date of the ISO. + returned: queried + type: str + sample: "2018-05-05T15:26:30" + product: + description: + - Product contained in the ISO. + returned: queried + type: str + sample: BIG-IP + verified: + description: + - Whether or not the system has verified this image. + returned: queried + type: bool + sample: yes + version: + description: + - Version of software contained in the image. + - This is a sub-string of the C(name). + returned: queried + type: str + sample: 13.1.0.7 + sample: hash/dictionary of values +software_volumes: + description: List of software volumes. + returned: When C(software-volumes) is specified in C(gather_subset). + type: complex + contains: + active: + description: + - Whether the volume is currently active or not. + - An active volume contains the currently running version of software. + returned: queried + type: bool + sample: yes + base_build: + description: + - Base build version of the software installed in the volume. + - When a hotfix is installed, this refers to the base version of software + that the hotfix requires. + returned: queried + type: str + sample: 0.0.6 + build: + description: + - Build version of the software installed in the volume. + returned: queried + type: str + sample: 0.0.6 + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: HD1.1 + default_boot_location: + description: + - Whether this volume is the default boot location or not. + returned: queried + type: bool + sample: yes + name: + description: + - Relative name of the resource in the BIG-IP. + - This usually matches the C(full_name). + returned: queried + type: str + sample: HD1.1 + product: + description: + - The F5 product installed in this slot. + - This should always be BIG-IP. + returned: queried + type: str + sample: BIG-IP + status: + description: + - Status of the software installed, or being installed, in the volume. + - When C(complete), indicates the software has completed installing. + returned: queried + type: str + sample: complete + version: + description: + - Version of software installed in the volume, excluding the C(build) number. + returned: queried + type: str + sample: 13.1.0.4 + sample: hash/dictionary of values +ssl_certs: + description: SSL certificate related information. + returned: When C(ssl-certs) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/cert1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: cert1 + key_type: + description: + - Specifies the type of cryptographic key associated with this certificate. + returned: queried + type: str + sample: rsa-private + key_size: + description: + - Specifies the size (in bytes) of the file associated with this file object. + returned: queried + type: int + sample: 2048 + system_path: + description: + - Path on the BIG-IP where the cert can be found. + returned: queried + type: str + sample: /config/ssl/ssl.crt/f5-irule.crt + sha1_checksum: + description: + - SHA1 checksum of the certificate. + returned: queried + type: str + sample: 1306e84e1e6a2da53816cefe1f684b80d6be1e3e + subject: + description: + - Specifies X509 information of the certificate's subject. + returned: queried + type: str + sample: "emailAddress=support@f5.com,CN=..." + last_update_time: + description: + - Specifies the last time the file-object was + updated/modified. + returned: queried + type: str + sample: "2018-05-15T21:11:15Z" + issuer: + description: + - Specifies X509 information of the certificate's issuer. + returned: queried + type: str + sample: "emailAddress=support@f5.com,...CN=support.f5.com," + is_bundle: + description: + - Specifies whether the certificate file is a bundle (that is, + whether it contains more than one certificate). + returned: queried + type: bool + sample: no + fingerprint: + description: + - Displays the SHA-256 fingerprint of the certificate. + returned: queried + type: str + sample: "SHA256/88:A3:05:...:59:01:EA:5D:B0" + expiration_date: + description: + - Specifies a string representation of the expiration date of the + certificate. + returned: queried + type: str + sample: "Aug 13 21:21:29 2031 GMT" + expiration_timestamp: + description: + - Specifies the date this certificate expires. Stored as a + POSIX time. + returned: queried + type: int + sample: 1944422489 + create_time: + description: + - Specifies the time the file-object was created. + returned: queried + type: str + sample: "2018-05-15T21:11:15Z" + serial_no: + description: + - Specifies certificate's serial number + returned: queried + type: str + sample: "1234567890" + subject_alternative_name: + description: + - Displays the Subject Alternative Name for the certificate. + - The X509v3 Subject Alternative Name is embedded in the certificate for X509 extension purposes. + returned: queried + type: str + sample: "DNS:www.example.com, DNS:www.example.internal.net" + sample: hash/dictionary of values +ssl_keys: + description: SSL certificate related information. + returned: When C(ssl-keys) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/key1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: key1 + key_type: + description: + - Specifies the cryptographic type of the key. That is, + which algorithm this key is compatible with. + returned: queried + type: str + sample: rsa-private + key_size: + description: + - Specifies the size of the cryptographic key associated with this + file object, in bits. + returned: queried + type: int + sample: 2048 + security_type: + description: + - Specifies the type of security used to handle or store the key. + returned: queried + type: str + sample: normal + system_path: + description: + - The path on the filesystem where the key is stored. + returned: queried + type: str + sample: /config/ssl/ssl.key/default.key + sha1_checksum: + description: + - The SHA1 checksum of the key. + returned: queried + type: str + sample: 1fcf7de3dd8e834d613099d8e10b2060cd9ecc9f + sample: hash/dictionary of values +sync_status: + description: + - Configuration Synchronization Status across all Device Groups. + - Note that the sync-status works across all device groups - a specific device group cannot be queried for its sync-status. + - In general the device-group with the 'worst' sync-status will be shown. + returned: When C(sync-status) is specified in C(gather_subset). + type: complex + contains: + color: + description: + - Sync status color. + - Eg. red, blue, green, yellow + returned: queried + type: str + sample: red + details: + description: + - A list of all details provided for the current sync-status of the device + returned: queried + type: list + sample: + - Optional action: Add a device to the trust domain + mode: + description: + - Device operation mode (high-availability, standalone) + returned: queried + type: str + sample: + - high-availability + - standalone + recommended_action: + description: + - The next recommended action to take on the current sync-status. + - This field might be empty. + returned: queried + type: str + sample: Synchronize bigip-a.example.com to group some-device-group + status: + description: + - Synchronization Status + returned: queried + type: str + sample: + - Changes Pending + - In Sync + - Standalone + - Disconnected + summary: + description: The configuration synchronization status summary + returned: queried + type: str + sample: + - The device group is awaiting the initial config sync + - There is a possible change conflict between ... + sample: hash/dictionary of values +system_db: + description: System DB related information. + returned: When C(system-db) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: vendor.wwwurl + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: vendor.wwwurl + default: + description: + - Default value of the key. + returned: queried + type: str + sample: www.f5.com + scf_config: + description: + - Whether the database key would be found in an SCF config or not. + returned: queried + type: str + sample: false + value: + description: + - The value of the key. + returned: queried + type: str + sample: www.f5.com + value_range: + description: + - The accepted range of values for the key. + returned: queried + type: str + sample: string + sample: hash/dictionary of values +system_info: + description: Traffic group related information. + returned: When C(traffic-groups) is specified in C(gather_subset). + type: complex + contains: + base_mac_address: + description: + - Media Access Control address (MAC address) of the device. + returned: queried + type: str + sample: "fa:16:3e:c3:42:6f" + marketing_name: + description: + - Marketing name of the device platform. + returned: queried + type: str + sample: BIG-IP Virtual Edition + time: + description: + - Mapping of the current time information to specific time-named keys. + returned: queried + type: complex + contains: + day: + description: + - The current day of the month, in numeric form. + returned: queried + type: int + sample: 7 + hour: + description: + - The current hour of the day in 24-hour format. + returned: queried + type: int + sample: 18 + minute: + description: + - The current minute of the hour. + returned: queried + type: int + sample: 16 + month: + description: + - The current month, in numeric form. + returned: queried + type: int + sample: 6 + second: + description: + - The current second of the minute. + returned: queried + type: int + sample: 51 + year: + description: + - The current year in 4-digit format. + returned: queried + type: int + sample: 2018 + hardware_information: + description: + - Information related to the hardware (drives and CPUs) of the system. + type: complex + returned: queried + contains: + model: + description: + - The model of the hardware. + returned: queried + type: str + sample: Virtual Disk + name: + description: + - The name of the hardware. + returned: queried + type: str + sample: HD1 + type: + description: + - The type of hardware. + returned: queried + type: str + sample: physical-disk + versions: + description: + - Hardware specific properties. + returned: queried + type: complex + contains: + name: + description: + - Name of the property. + returned: queried + type: str + sample: Size + version: + description: + - Value of the property. + returned: queried + type: str + sample: 154.00G + package_edition: + description: + - Displays the software edition. + returned: queried + type: str + sample: Point Release 7 + package_version: + description: + - A string combining the C(product_build) and C(product_build_date). + returned: queried + type: str + sample: "Build 0.0.1 - Tue May 15 15:26:30 PDT 2018" + product_code: + description: + - Code identifying the product. + returned: queried + type: str + sample: BIG-IP + product_build: + description: + - Build version of the release version. + returned: queried + type: str + sample: 0.0.1 + product_version: + description: + - Major product version of the running software. + returned: queried + type: str + sample: 13.1.0.7 + product_built: + description: + - UNIX timestamp of when the product was built. + returned: queried + type: int + sample: 180515152630 + product_build_date: + description: + - Human readable build date. + returned: queried + type: str + sample: "Tue May 15 15:26:30 PDT 2018" + product_changelist: + description: + - Changelist the product branches from. + returned: queried + type: int + sample: 2557198 + product_jobid: + description: + - ID of the job that built the product version. + returned: queried + type: int + sample: 1012030 + chassis_serial: + description: + - Serial of the chassis. + returned: queried + type: str + sample: 11111111-2222-3333-444444444444 + host_board_part_revision: + description: + - Revision of the host board. + returned: queried + type: str + host_board_serial: + description: + - Serial of the host board. + returned: queried + type: str + platform: + description: + - Platform identifier. + returned: queried + type: str + sample: Z100 + switch_board_part_revision: + description: + - Switch board revision. + returned: queried + type: str + switch_board_serial: + description: + - Serial of the switch board. + returned: queried + type: str + uptime: + description: + - Time since the system booted, in seconds. + returned: queried + type: int + sample: 603202 + sample: hash/dictionary of values +tcp_monitors: + description: TCP monitor related information. + returned: When C(tcp-monitors) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/tcp + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: tcp + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: tcp + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My monitor + adaptive: + description: + - Whether adaptive response time monitoring is enabled for this monitor. + returned: queried + type: bool + sample: no + adaptive_divergence_type: + description: + - Specifies whether the adaptive-divergence-value is C(relative) or + C(absolute). + returned: queried + type: str + sample: relative + adaptive_divergence_value: + description: + - Specifies how far from mean latency each monitor probe is allowed + to be. + returned: queried + type: int + sample: 25 + adaptive_limit: + description: + - Specifies the hard limit, in milliseconds, which the probe is not + allowed to exceed, regardless of the divergence value. + returned: queried + type: int + sample: 200 + adaptive_sampling_timespan: + description: + - Specifies the size of the sliding window, in seconds, which + records probe history. + returned: queried + type: int + sample: 300 + destination: + description: + - Specifies the IP address and service port of the resource that is + the destination of this monitor. + returned: queried + type: str + sample: "*:*" + interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when either the resource is down or the status + of the resource is unknown. + returned: queried + type: int + sample: 5 + ip_dscp: + description: + - Specifies the differentiated services code point (DSCP). + returned: queried + type: int + sample: 0 + manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to up at the next successful monitor check. + returned: queried + type: bool + sample: yes + reverse: + description: + - Specifies whether the monitor operates in reverse mode. When the + monitor is in reverse mode, a successful check marks the monitored + object down instead of up. + returned: queried + type: bool + sample: no + time_until_up: + description: + - Specifies the amount of time, in seconds, after the first + successful response before a node is marked up. + returned: queried + type: int + sample: 0 + timeout: + description: + - Specifies the number of seconds the target has in which to respond + to the monitor request. + returned: queried + type: int + sample: 16 + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + returned: queried + type: bool + sample: no + up_interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when the resource is up. + returned: queried + type: int + sample: 0 + sample: hash/dictionary of values +tcp_half_open_monitors: + description: TCP Half-open monitor related information. + returned: When C(tcp-half-open-monitors) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/tcp + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: tcp + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: tcp + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My monitor + destination: + description: + - Specifies the IP address and service port of the resource that is + the destination of this monitor. + returned: queried + type: str + sample: "*:*" + interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when either the resource is down or the status + of the resource is unknown. + returned: queried + type: int + sample: 5 + manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to up at the next successful monitor check. + returned: queried + type: bool + sample: yes + time_until_up: + description: + - Specifies the amount of time, in seconds, after the first + successful response before a node is marked up. + returned: queried + type: int + sample: 0 + timeout: + description: + - Specifies the number of seconds the target has in which to respond + to the monitor request. + returned: queried + type: int + sample: 16 + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + returned: queried + type: bool + sample: no + up_interval: + description: + - Specifies, in seconds, the frequency at which the system issues + the monitor check when the resource is up. + returned: queried + type: int + sample: 0 + sample: hash/dictionary of values +tcp_profiles: + description: TCP profile related information. + returned: When C(tcp-profiles) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: tcp + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: /Common/tcp + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: tcp + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My profile + abc: + description: + - Appropriate Byte Counting (RFC 3465) + - When C(yes), increases the congestion window by basing the amount to + increase on the number of previously unacknowledged bytes that each ACK covers. + returned: queried + type: bool + sample: yes + ack_on_push: + description: + - When C(yes), specifies significantly improved performance to Microsoft + Windows and MacOS peers who are writing out on a very small send buffer. + returned: queried + type: bool + sample: no + auto_proxy_buffer: + description: + - When C(yes), specifies the system uses the network measurements to set + the optimal proxy buffer size. + returned: queried + type: bool + sample: yes + auto_receive_window: + description: + - When C(yes), specifies the system uses the network measurements to + set the optimal receive window size. + returned: queried + type: bool + sample: no + auto_send_buffer: + description: + - When C(yes), specifies the system uses the network measurements to + set the optimal send buffer size. + returned: queried + type: bool + sample: yes + close_wait: + description: + - Specifies the length of time a TCP connection remains in the LAST-ACK + state before quitting. + - In addition to a numeric value, the value of this fact may also be one of + C(immediate) or C(indefinite). + - When C(immediate), specifies the TCP connection closes immediately + after entering the LAST-ACK state. + - When C(indefinite), specifies that TCP connections in the LAST-ACK state + do not close until they meet the maximum retransmissions timeout. + returned: queried + type: str + sample: indefinite + congestion_metrics_cache: + description: + - When C(yes), specifies the system uses a cache for storing congestion + metrics. + - Subsequently, because these metrics are already known and cached, the initial + slow-start ramp for previously-encountered peers improves. + returned: queried + type: bool + sample: yes + congestion_metrics_cache_timeout: + description: + - Specifies the number of seconds for which entries in the congestion metrics + cache are valid. + returned: queried + type: int + sample: 0 + congestion_control: + description: + - Specifies the algorithm to use to share network resources among competing + users to reduce congestion. + - Return values may include, C(high-speed), C(cdg), C(chd), C(none), C(cubic), + C(illinois), C(new-reno), C(reno), C(scalable), C(vegas), C(westwood), and + C(woodside). + returned: queried + type: str + sample: high-speed + deferred_accept: + description: + - When C(yes), specifies the system defers allocation of the connection + chain context until the system has received the payload from the client. + - Enabling this setting is useful in dealing with 3-way handshake denial-of-service + attacks. + returned: queried + type: bool + sample: yes + delay_window_control: + description: + - Specifies the system uses an estimate of queuing delay as a measure of + congestion to control, in addition to the normal loss-based control, the amount + of data sent. + returned: queried + type: bool + sample: yes + delayed_acks: + description: + - When checked (enabled), specifies the system can send fewer than one ACK + (acknowledgment) segment per data segment received. + returned: queried + type: bool + sample: yes + dsack: + description: + - D-SACK (RFC 2883) + - When C(yes), specifies the use of the selective ACK (SACK) option to acknowledge + duplicate segments. + returned: queried + type: bool + sample: yes + early_retransmit: + description: + - When C(yes), specifies the system uses early retransmit (as specified in + RFC 5827) to reduce the recovery time for connections that are receive- buffer + or user-data limited. + returned: queried + type: bool + sample: yes + explicit_congestion_notification: + description: + - When C(yes), specifies the system uses the TCP flags CWR (congestion window + reduction) and ECE (ECN-Echo) to notify its peer of congestion and congestion + counter-measures. + returned: queried + type: bool + sample: yes + enhanced_loss_recovery: + description: + - Specifies whether the system uses enhanced loss recovery to recover from random + packet losses more effectively. + returned: queried + type: bool + sample: yes + fast_open: + description: + - When C(yes), specifies, the system supports TCP Fast Open, which reduces + latency by allowing a client to include the first packet of data with the SYN + returned: queried + type: bool + sample: yes + fast_open_cookie_expiration: + description: + - Specifies the number of seconds that a Fast Open Cookie delivered to a client + is valid for SYN packets from that client. + returned: queried + type: int + sample: 1000 + fin_wait_1: + description: + - Specifies the length of time that a TCP connection is in the FIN-WAIT-1 or + CLOSING state before quitting. + returned: queried + type: str + sample: indefinite + fin_wait_2: + description: + - Specifies the length of time a TCP connection is in the FIN-WAIT-2 state + before quitting. + returned: queried + type: str + sample: 100 + idle_timeout: + description: + - Specifies the length of time a connection is idle (has no traffic) before + the connection is eligible for deletion. + returned: queried + type: str + sample: 300 + initial_congestion_window_size: + description: + - Specifies the initial congestion window size for connections to this destination. + returned: queried + type: int + sample: 3 + initial_receive_window_size: + description: + - Specifies the initial receive window size for connections to this destination. + returned: queried + type: int + sample: 5 + dont_fragment_flag: + description: + - Specifies the Don't Fragment (DF) bit setting in the IP Header of the outgoing + TCP packet. + returned: queried + type: str + sample: pmtu + ip_tos: + description: + - Specifies the L3 Type of Service (ToS) level the system inserts in TCP + packets destined for clients. + returned: queried + type: str + sample: mimic + time_to_live: + description: + - Specifies the outgoing TCP packet's IP Header TTL mode. + returned: queried + type: str + sample: proxy + time_to_live_v4: + description: + - Specifies the outgoing packet's IP Header TTL value for IPv4 traffic. + returned: queried + type: int + sample: 255 + time_to_live_v6: + description: + - Specifies the outgoing packet's IP Header TTL value for IPv6 traffic. + returned: queried + type: int + sample: 64 + keep_alive_interval: + description: + - Specifies how frequently the system sends data over an idle TCP + connection, to determine whether the connection is still valid. + returned: queried + type: str + sample: 50 + limited_transmit_recovery: + description: + - When C(yes), specifies the system uses limited transmit recovery + revisions for fast retransmits (as specified in RFC 3042) to reduce + the recovery time for connections on a lossy network. + returned: queried + type: bool + sample: yes + link_qos: + description: + - Specifies the L2 Quality of Service (QoS) level the system inserts + in TCP packets destined for clients. + returned: queried + type: str + sample: 200 + max_segment_retrans: + description: + - Specifies the maximum number of times that the system resends data segments. + returned: queried + type: int + sample: 8 + max_syn_retrans: + description: + - Specifies the maximum number of times the system resends a SYN + packet when it does not receive a corresponding SYN-ACK. + returned: queried + type: int + sample: 3 + max_segment_size: + description: + - Specifies the largest amount of data the system can receive in a + single TCP segment, not including the TCP and IP headers. + returned: queried + type: int + sample: 1460 + md5_signature: + description: + - When C(yes), specifies to use RFC2385 TCP-MD5 signatures to protect + TCP traffic against intermediate tampering. + returned: queried + type: bool + sample: yes + minimum_rto: + description: + - Specifies the minimum length of time the system waits for + acknowledgements of data sent before resending the data. + returned: queried + type: int + sample: 1000 + multipath_tcp: + description: + - When C(yes), specifies the system accepts Multipath TCP (MPTCP) + connections, which allow multiple client-side flows to connect to a + single server-side flow. + returned: queried + type: bool + sample: yes + mptcp_checksum: + description: + - When C(yes), specifies the system calculates the checksum for + MPTCP connections. + returned: queried + type: bool + sample: no + mptcp_checksum_verify: + description: + - When C(yes), specifies the system verifies the checksum for + MPTCP connections. + returned: queried + type: bool + sample: no + mptcp_fallback: + description: + - Specifies an action on fallback, that is, when MPTCP transitions + to regular TCP, because something prevents MPTCP from working correctly. + returned: queried + type: str + sample: reset + mptcp_fast_join: + description: + - When C(yes), specifies a FAST join, allowing data to be sent on the + MP_JOIN_SYN, which can allow a server response to occur in parallel + with the JOIN. + returned: queried + type: bool + sample: no + mptcp_idle_timeout: + description: + - Specifies the number of seconds that an MPTCP connection is idle + before the connection is eligible for deletion. + returned: queried + type: int + sample: 300 + mptcp_join_max: + description: + - Specifies the highest number of MPTCP connections that can join to + a given connection. + returned: queried + type: int + sample: 5 + mptcp_make_after_break: + description: + - Specifies make-after-break functionality is supported, allowing + for long-lived MPTCP sessions. + returned: queried + type: bool + sample: no + mptcp_no_join_dss_ack: + description: + - When checked (enabled), specifies no DSS option is sent on the + JOIN ACK. + returned: queried + type: bool + sample: no + mptcp_rto_max: + description: + - Specifies the number of RTOs (retransmission timeouts) before declaring + the subflow dead. + returned: queried + type: int + sample: 5 + mptcp_retransmit_min: + description: + - Specifies the minimum value (in msec) of the retransmission timer for + these MPTCP flows. + returned: queried + type: int + sample: 1000 + mptcp_subflow_max: + description: + - Specifies the maximum number of MPTCP subflows for a single flow. + returned: queried + type: int + sample: 6 + mptcp_timeout: + description: + - Specifies, in seconds, the timeout value to discard long-lived sessions + that do not have an active flow. + returned: queried + type: int + sample: 3600 + nagle_algorithm: + description: + - Specifies whether the system applies Nagle's algorithm to reduce the + number of short segments on the network. + returned: queried + type: bool + sample: no + pkt_loss_ignore_burst: + description: + - Specifies the probability of performing congestion control when + multiple packets are lost, even if the Packet Loss Ignore Rate was + not exceeded. + returned: queried + type: int + sample: 0 + pkt_loss_ignore_rate: + description: + - Specifies the threshold of packets lost per million at which the + system performs congestion control. + returned: queried + type: int + sample: 0 + proxy_buffer_high: + description: + - Specifies the proxy buffer level, in bytes, at which the receive window + is closed. + returned: queried + type: int + sample: 49152 + proxy_buffer_low: + description: + - Specifies the proxy buffer level, in bytes, at which the receive window + is opened. + returned: queried + type: int + sample: 32768 + proxy_max_segment: + description: + - When C(yes), specifies the system attempts to advertise the same + maximum segment size (MSS) to the server-side connection as that of the + client-side connection. + returned: queried + type: bool + sample: yes + proxy_options: + description: + - When C(yes), specifies the system advertises an option (such as + time stamps) to the server only when the option is negotiated with the + client. + returned: queried + type: bool + sample: no + push_flag: + description: + - Specifies how the BIG-IP system receives ACKs. + returned: queried + type: str + sample: default + rate_pace: + description: + - When C(yes), specifies the system paces the egress packets to + avoid dropping packets, allowing for optimum goodput. + returned: queried + type: bool + sample: yes + rate_pace_max_rate: + description: + - Specifies the maximum rate in bytes per second to which the system + paces TCP data transmission. + returned: queried + type: int + sample: 0 + receive_window: + description: + - Specifies the maximum advertised RECEIVE window size. + returned: queried + type: int + sample: 65535 + reset_on_timeout: + description: + - When C(yes), specifies the system sends a reset packet (RST) + in addition to deleting the connection, when a connection exceeds + the idle timeout value. + returned: queried + type: bool + sample: yes + retransmit_threshold: + description: + - Specifies the number of duplicate ACKs (retransmit threshold) to start + fast recovery. + returned: queried + type: int + sample: 3 + selective_acks: + description: + - When C(yes), specifies the system processes data using + selective ACKs (SACKs) whenever possible, to improve system performance. + returned: queried + type: bool + sample: yes + selective_nack: + description: + - When C(yes), specifies the system processes data using a selective + negative acknowledgment (SNACK) whenever possible, to improve system + performance. + returned: queried + type: bool + sample: yes + send_buffer: + description: + - Specifies the SEND window size. + returned: queried + type: int + sample: 65535 + slow_start: + description: + - When C(yes), specifies the system uses Slow-Start Congestion + Avoidance as described in RFC3390 in order to ramp up traffic without + causing excessive congestion on the link. + returned: queried + type: bool + sample: yes + syn_cookie_enable: + description: + - Specifies the default (if no DoS profile is associated) number of + embryonic connections that are allowed on any virtual server, + before SYN Cookie challenges are enabled for that virtual server. + returned: queried + type: bool + sample: yes + syn_cookie_white_list: + description: + - Specifies whether or not to use a SYN Cookie WhiteList when doing + software SYN Cookies. + returned: queried + type: bool + sample: no + syn_retrans_to_base: + description: + - Specifies the initial RTO (Retransmission TimeOut) base multiplier + for SYN retransmissions. + returned: queried + type: int + sample: 3000 + tail_loss_probe: + description: + - When C(yes), specifies the system uses Tail Loss Probe to + reduce the number of retransmission timeouts. + returned: queried + type: bool + sample: yes + time_wait_recycle: + description: + - When C(yes), specifies that connections in a TIME-WAIT state are + reused when the system receives a SYN packet, indicating a request + for a new connection. + returned: queried + type: bool + sample: yes + time_wait: + description: + - Specifies the length of time that a TCP connection remains in the + TIME-WAIT state before entering the CLOSED state. + returned: queried + type: str + sample: 2000 + timestamps: + description: + - When C(yes), specifies the system uses the timestamps extension + for TCP (as specified in RFC 1323) to enhance high-speed network performance. + returned: queried + type: bool + sample: yes + verified_accept: + description: + - When C(yes), specifies the system can actually communicate with + the server before establishing a client connection. + returned: queried + type: bool + sample: yes + zero_window_timeout: + description: + - Specifies the timeout in milliseconds for terminating a connection + with an effective zero length TCP transmit window. + returned: queried + type: str + sample: 2000 + sample: hash/dictionary of values +traffic_groups: + description: Traffic group related information. + returned: When C(traffic-groups) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/tg1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: tg1 + description: + description: + - Description of the traffic group. + returned: queried + type: str + sample: My traffic group + auto_failback_enabled: + description: + - Specifies whether the traffic group fails back to the default + device. + returned: queried + type: bool + sample: yes + auto_failback_time: + description: + - Specifies the time required to fail back. + returned: queried + type: int + sample: 60 + ha_load_factor: + description: + - Specifies a number for this traffic group that represents the load + this traffic group presents to the system relative to other + traffic groups. + returned: queried + type: int + sample: 1 + ha_order: + description: + - This list of devices specifies the order in which the devices will + become active for the traffic group when a failure occurs. + returned: queried + type: list + sample: ['/Common/device1', '/Common/device2'] + is_floating: + description: + - Indicates whether the traffic group can fail over to other devices + in the device group. + returned: queried + type: bool + sample: no + mac_masquerade_address: + description: + - Specifies a MAC address for the traffic group. + returned: queried + type: str + sample: "00:98:76:54:32:10" + sample: hash/dictionary of values +trunks: + description: Trunk related information. + returned: When C(trunks) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/trunk1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: trunk1 + description: + description: + - Description of the Trunk. + returned: queried + type: str + sample: My trunk + media_speed: + description: + - Speed of the media attached to the trunk. + returned: queried + type: int + sample: 10000 + lacp_mode: + description: + - The operation mode for LACP. + returned: queried + type: str + sample: passive + lacp_enabled: + description: + - Whether LACP is enabled or not. + returned: queried + type: bool + sample: yes + stp_enabled: + description: + - Whether Spanning Tree Protocol (STP) is enabled or not. + returned: queried + type: bool + sample: yes + operational_member_count: + description: + - Number of working members associated with the trunk. + returned: queried + type: int + sample: 1 + media_status: + description: + - Whether the media that is part of the trunk is up or not. + returned: queried + type: bool + sample: yes + link_selection_policy: + description: + - The LACP policy the trunk uses to determine which member link can handle + new traffic. + returned: queried + type: str + sample: maximum-bandwidth + lacp_timeout: + description: + - The rate at which the system sends the LACP control packets. + returned: queried + type: int + sample: 10 + interfaces: + description: + - The list of interfaces that are part of the trunk. + returned: queried + type: list + sample: ['1.2', '1.3'] + distribution_hash: + description: + - The basis for the hash that the system uses as the frame distribution algorithm. + - The system uses this hash to determine which interface to use for forwarding + traffic. + returned: queried + type: str + sample: src-dst-ipport + configured_member_count: + description: + - The number of configured members that are associated with the trunk. + returned: queried + type: int + sample: 1 + sample: hash/dictionary of values + +ucs: + description: UCS backup related information + returned: When C(ucs) is specified in C(gather_subset) + type: complex + contains: + file_name: + description: + - Name of the UCS backup file. + returned: queried + type: str + sample: backup.ucs + encrypted: + description: + - Whether the file is encrypted or not. + returned: queried + type: bool + sample: no + file_size: + description: + - Size of the UCS file in bytes. + returned: queried + type: str + sample: "3" + file_created_date: + description: + - Date and time when the ucs file was created. + returned: queried + type: str + sample: "2022-03-10T09:30:19Z" + sample: hash/dictionary of values + version_added: "1.15.0" + +udp_profiles: + description: UDP profile related information. + returned: When C(udp-profiles) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: udp + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: /Common/udp + parent: + description: + - Profile from which this profile inherits settings. + returned: queried + type: str + sample: udp + description: + description: + - Description of the resource. + returned: queried + type: str + sample: My profile + allow_no_payload: + description: + - Allow the passage of datagrams that contain header information, but no essential data. + returned: queried + type: bool + sample: yes + buffer_max_bytes: + description: + - Ingress buffer byte limit. Maximum allowed value is 16777215. + returned: queried + type: int + sample: 655350 + buffer_max_packets: + description: + - Ingress buffer packet limit. Maximum allowed value is 255. + returned: queried + type: int + sample: 0 + datagram_load_balancing: + description: + - Load balance UDP datagram by datagram + returned: queried + type: bool + sample: yes + idle_timeout: + description: + - Number of seconds that a connection is idle before + the connection is eligible for deletion. + - In addition to a number, may be one of the values C(indefinite) or + C(immediate). + returned: queried + type: bool + sample: 200 + ip_df_mode: + description: + - Describes the Don't Fragment (DF) bit setting in the outgoing UDP + packet. + - May be one of C(pmtu), C(preserve), C(set), or C(clear). + - When C(pmtu), sets the outgoing UDP packet DF big based on the ip + pmtu setting. + - When C(preserve), preserves the incoming UDP packet Don't Fragment bit. + - When C(set), sets the outgoing UDP packet DF bit. + - When C(clear), clears the outgoing UDP packet DF bit. + returned: queried + type: str + sample: pmtu + ip_tos_to_client: + description: + - The Type of Service level the traffic management + system assigns to UDP packets when sending them to clients. + - May be numeric, or the values C(pass-through) or C(mimic). + returned: queried + type: str + sample: mimic + ip_ttl_mode: + description: + - The outgoing UDP packet's TTL mode. + - Valid modes are C(proxy), C(preserve), C(decrement), and C(set). + - When C(proxy), sets the IP TTL of IPv4 to the default value of 255 and + IPv6 to the default value of 64. + - When C(preserve), sets the IP TTL to the original packet TTL value. + - When C(decrement), sets the IP TTL to the original packet TTL value minus 1. + - When C(set), sets the IP TTL with the specified values in C(ip_ttl_v4) and + C(ip_ttl_v6) values in the same profile. + returned: queried + type: str + sample: proxy + ip_ttl_v4: + description: + - IPv4 TTL. + returned: queried + type: int + sample: 10 + ip_ttl_v6: + description: + - IPv6 TTL. + returned: queried + type: int + sample: 100 + link_qos_to_client: + description: + - The Quality of Service level the system assigns to + UDP packets when sending them to clients. + - May be either numberic or the value C(pass-through). + returned: queried + type: str + sample: pass-through + no_checksum: + description: + - Whether checksum processing is enabled or disabled. + - Note that if the datagram is IPv6, the system always performs + checksum processing. + returned: queried + type: bool + sample: yes + proxy_mss: + description: + - When C(yes), specifies the system advertises the same mss + to the server as was negotiated with the client. + returned: queried + type: bool + sample: yes + sample: hash/dictionary of values +users: + description: Details of the users on the system. + returned: When C(users) is specified in C(gather_subset). + type: complex + contains: + description: + description: + - Description of the resource. + returned: queried + type: str + sample: Admin user + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: admin + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: admin + partition_access: + description: + - Partition that user has access to, including user role. + returned: queried + type: complex + contains: + name: + description: + - Name of partition. + returned: queried + type: str + sample: all-partitions + role: + description: + - Role allowed to user on partition. + returned: queried + type: str + sample: auditor + shell: + description: + - The shell assigned to the user account. + returned: queried + type: str + sample: tmsh +vcmp_guests: + description: vCMP related information. + returned: When C(vcmp-guests) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: guest1 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: guest1 + allowed_slots: + description: + - List of slots the guest is allowed to be assigned to. + returned: queried + type: list + sample: [0, 1, 3] + assigned_slots: + description: + - Slots the guest is assigned to. + returned: queried + type: list + sample: [0] + boot_priority: + description: + - Specifies the boot priority of the guest. A lower number means earlier to boot. + returned: queried + type: int + sample: 65535 + cores_per_slot: + description: + - Number of cores the system allocates to the guest. + returned: queried + type: int + sample: 2 + hostname: + description: + - FQDN assigned to the guest. + returned: queried + type: str + sample: guest1.localdomain + hotfix_image: + description: + - Hotfix image to install onto any of this guest's newly created virtual disks. + returned: queried + type: str + sample: Hotfix-BIGIP-12.1.3.4-0.0.2-hf1.iso + initial_image: + description: + - Software image to install onto any of this guest's newly created virtual disks. + returned: queried + type: str + sample: BIGIP-12.1.3.4-0.0.2.iso + mgmt_route: + description: + - Management gateway IP address for the guest. + returned: queried + type: str + sample: 2.2.2.1 + mgmt_address: + description: + - Management IP address configuration for the guest. + returned: queried + type: str + sample: 2.3.2.3 + mgmt_network: + description: + - Accessibility of this vCMP guest's management network. + returned: queried + type: str + sample: bridged + vlans: + description: + - List of VLANs on which the guest is either enabled or disabled. + returned: queried + type: list + sample: ['/Common/vlan1', '/Common/vlan2'] + min_number_of_slots: + description: + - Specifies the minimum number of slots the guest must be assigned to. + returned: queried + type: int + sample: 2 + number_of_slots: + description: + - Specifies the number of slots the guest should be assigned to. + - This number is always greater than, or equal to, C(min_number_of_slots). + returned: queried + type: int + sample: 2 + ssl_mode: + description: + - The SSL hardware allocation mode for the guest. + returned: queried + type: str + sample: shared + state: + description: + - Specifies the state of the guest. + - May be one of C(configured), C(provisioned), or C(deployed). + - Each state implies the actions of all states before it. + returned: queried + type: str + sample: provisioned + virtual_disk: + description: + - The filename of the virtual disk to use for this guest. + returned: queried + type: str + sample: guest1.img + sample: hash/dictionary of values +virtual_addresses: + description: Virtual address related information. + returned: When C(virtual-addresses) is specified in C(gather_subset). + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/2.3.4.5 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: 2.3.4.5 + address: + description: + - The virtual IP address. + returned: queried + type: str + sample: 2.3.4.5 + arp_enabled: + description: + - Whether or not ARP is enabled for the specified virtual address. + returned: queried + type: bool + sample: yes + auto_delete_enabled: + description: + - Indicates if the virtual address will be deleted automatically on + deletion of the last associated virtual server or not. + returned: queried + type: bool + sample: no + connection_limit: + description: + - Concurrent connection limit for one or more virtual + servers. + returned: queried + type: int + sample: 0 + description: + description: + - The description of the virtual address. + returned: queried + type: str + sample: My virtual address + enabled: + description: + - Whether the virtual address is enabled or not. + returned: queried + type: bool + sample: yes + icmp_echo: + description: + - Whether the virtual address should reply to ICMP echo requests. + returned: queried + type: bool + sample: yes + floating: + description: + - Property derived from the traffic group. A floating virtual + address is a virtual address for a VLAN that serves as a shared + address by all devices of a BIG-IP traffic-group. + returned: queried + type: bool + sample: yes + netmask: + description: + - Netmask of the virtual address. + returned: queried + type: str + sample: 255.255.255.255 + route_advertisement: + description: + - Specifies the route advertisement setting for the virtual address. + returned: queried + type: bool + sample: no + traffic_group: + description: + - Traffic group on which the virtual address is active. + returned: queried + type: str + sample: /Common/traffic-group-1 + spanning: + description: + - Whether or not spanning is enabled for the specified virtual address. + returned: queried + type: bool + sample: no + inherited_traffic_group: + description: + - Indicates if the traffic group is inherited from the parent folder. + returned: queried + type: bool + sample: no + sample: hash/dictionary of values +virtual_servers: + description: Virtual address related information. + returned: When C(virtual-addresses) is specified in C(gather_subset). + type: complex + contains: + availability_status: + description: + - The availability of the virtual server. + returned: queried + type: str + sample: offline + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/2.3.4.5 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: 2.3.4.5 + auto_lasthop: + description: + - When enabled, allows the system to send return traffic to the MAC address + that transmitted the request, even if the routing table points to a different + network or interface. + returned: queried + type: str + sample: default + bw_controller_policy: + description: + - The bandwidth controller for the system to use to enforce a throughput policy + for incoming network traffic. + returned: queried + type: str + sample: /Common/bw1 + client_side_bits_in: + description: + - Number of client-side ingress bits. + returned: queried + type: int + sample: 1000 + client_side_bits_out: + description: + - Number of client-side egress bits. + returned: queried + type: int + sample: 200 + client_side_current_connections: + description: + - Number of current connections client-side. + returned: queried + type: int + sample: 300 + client_side_evicted_connections: + description: + - Number of evicted connections client-side. + returned: queried + type: int + sample: 100 + client_side_max_connections: + description: + - Maximum number of connections client-side. + returned: queried + type: int + sample: 40 + client_side_pkts_in: + description: + - Number of client-side ingress packets. + returned: queried + type: int + sample: 1098384 + client_side_pkts_out: + description: + - Number of client-side egress packets. + returned: queried + type: int + sample: 3484734 + client_side_slow_killed: + description: + - Number of slow connections killed, client-side. + returned: queried + type: int + sample: 234 + client_side_total_connections: + description: + - Total number of connections. + returned: queried + type: int + sample: 24 + cmp_enabled: + description: + - Whether or not clustered multi-processor (CMP) acceleration is enabled. + returned: queried + type: bool + sample: yes + cmp_mode: + description: + - The clustered-multiprocessing mode. + returned: queried + type: str + sample: all-cpus + connection_limit: + description: + - Maximum number of concurrent connections you want to allow for the virtual server. + returned: queried + type: int + sample: 100 + description: + description: + - The description of the virtual server. + returned: queried + type: str + sample: My virtual + enabled: + description: + - Whether or not the virtual is enabled. + returned: queried + type: bool + sample: yes + ephemeral_bits_in: + description: + - Number of ephemeral ingress bits. + returned: queried + type: int + sample: 1000 + ephemeral_bits_out: + description: + - Number of ephemeral egress bits. + returned: queried + type: int + sample: 200 + ephemeral_current_connections: + description: + - Number of ephemeral current connections. + returned: queried + type: int + sample: 300 + ephemeral_evicted_connections: + description: + - Number of ephemeral evicted connections. + returned: queried + type: int + sample: 100 + ephemeral_max_connections: + description: + - Maximum number of ephemeral connections. + returned: queried + type: int + sample: 40 + ephemeral_pkts_in: + description: + - Number of ephemeral ingress packets. + returned: queried + type: int + sample: 1098384 + ephemeral_pkts_out: + description: + - Number of ephemeral egress packets. + returned: queried + type: int + sample: 3484734 + ephemeral_slow_killed: + description: + - Number of ephemeral slow connections killed. + returned: queried + type: int + sample: 234 + ephemeral_total_connections: + description: + - Total number of ephemeral connections. + returned: queried + type: int + sample: 24 + total_software_accepted_syn_cookies: + description: + - SYN Cookies Total Software Accepted. + returned: queried + type: int + sample: 0 + total_hardware_accepted_syn_cookies: + description: + - SYN Cookies Total Hardware Accepted. + returned: queried + type: int + sample: 0 + total_hardware_syn_cookies: + description: + - SYN Cookies Total Hardware. + returned: queried + type: int + sample: 0 + hardware_syn_cookie_instances: + description: + - Hardware SYN Cookie Instances. + returned: queried + type: int + sample: 0 + total_software_rejected_syn_cookies: + description: + - Total Software Rejected. + returned: queried + type: int + sample: 0 + software_syn_cookie_instances: + description: + - Software SYN Cookie Instances. + returned: queried + type: int + sample: 0 + current_syn_cache: + description: + - Current SYN Cache. + returned: queried + type: int + sample: 0 + max_conn_duration: + description: + - Max Conn Duration/msec. + returned: queried + type: int + sample: 0 + mean_conn_duration: + description: + - Mean Conn Duration/msec. + returned: queried + type: int + sample: 0 + min_conn_duration: + description: + - Min Conn Duration/msec. + returned: queried + type: int + sample: 0 + cpu_usage_ratio_last_5_min: + description: + - CPU Usage Ratio (%) Last 5 Minutes. + returned: queried + type: int + sample: 0 + cpu_usage_ratio_last_5_sec: + description: + - CPU Usage Ratio (%) Last 5 Seconds. + returned: queried + type: int + sample: 0 + cpu_usage_ratio_last_1_min: + description: + - CPU Usage Ratio (%) Last 1 Minute. + returned: queried + type: int + sample: 0 + syn_cache_overflow: + description: + - SYN Cache Overflow. + returned: queried + type: int + sample: 0 + total_software_syn_cookies: + description: + - Total Software SYN Cookies + returned: queried + type: int + sample: 0 + syn_cookies_status: + description: + - SYN Cookies Status. + returned: queried + type: str + sample: not-activated + fallback_persistence_profile: + description: + - Fallback persistence profile for the virtual server to use + when the default persistence profile is not available. + returned: queried + type: str + sample: /Common/fallback1 + persistence_profile: + description: + - The persistence profile you want the system to use as the default + for this virtual server. + returned: queried + type: str + sample: /Common/persist1 + translate_port: + description: + - Enables or disables port translation. + returned: queried + type: bool + sample: yes + translate_address: + description: + - Enables or disables address translation for the virtual server. + returned: queried + type: bool + sample: yes + vlans: + description: + - List of VLANs on which the virtual server is either enabled or disabled. + returned: queried + type: list + sample: ['/Common/vlan1', '/Common/vlan2'] + destination: + description: + - Name of the virtual address and service on which the virtual server + listens for connections. + returned: queried + type: str + sample: /Common/2.2.3.3%1:76 + last_hop_pool: + description: + - Name of the last hop pool you want the virtual + server to use to direct reply traffic to the last hop router. + returned: queried + type: str + sample: /Common/pool1 + nat64_enabled: + description: + - Whether or not NAT64 is enabled. + returned: queried + type: bool + sample: yes + source_port_behavior: + description: + - Specifies whether the system preserves the source port of the connection. + returned: queried + type: str + sample: preserve + ip_intelligence_policy: + description: + - IP Intelligence policy assigned to the virtual. + returned: queried + type: str + sample: /Common/ip1 + protocol: + description: + - IP protocol for which you want the virtual server to direct traffic. + returned: queried + type: str + sample: tcp + default_pool: + description: + - Pool name you want the virtual server to use as the default pool. + returned: queried + type: str + sample: /Common/pool1 + rate_limit_mode: + description: + - Indicates whether the rate limit is applied per virtual object, + per source address, per destination address, or some combination + thereof. + returned: queried + type: str + sample: object + rate_limit_source_mask: + description: + - Specifies a mask, in bits, to be applied to the source address as + part of the rate limiting. + returned: queried + type: int + sample: 0 + rate_limit: + description: + - Maximum number of connections per second allowed for a virtual server. + returned: queried + type: int + sample: 34 + snat_type: + description: + - Specifies the type of source address translation associated + with the specified virtual server. + returned: queried + type: str + sample: none + snat_pool: + description: + - Specifies the name of a LSN or SNAT pool used by the specified virtual server. + returned: queried + type: str + sample: /Common/pool1 + status_reason: + description: + - If there is a problem with the status of the virtual, it is reported here. + returned: queried + type: str + sample: The children pool member(s) either don't have service checking... + gtm_score: + description: + - Specifies a score that is associated with the virtual server. + returned: queried + type: int + sample: 0 + rate_class: + description: + - Name of an existing rate class you want the + virtual server to use to enforce a throughput policy for incoming + network traffic. + returned: queried + type: str + rate_limit_destination_mask: + description: + - Specifies a mask, in bits, to be applied to the destination + address as part of the rate limiting. + returned: queried + type: int + sample: 32 + source_address: + description: + - Specifies an IP address or network from which the virtual server + will accept traffic. + returned: queried + type: str + sample: 0.0.0./0 + authentication_profile: + description: + - Specifies a list of authentication profile names, separated by + spaces, that the virtual server uses to manage authentication. + returned: queried + type: list + sample: ['/Common/ssl_drldp'] + connection_mirror_enabled: + description: + - Whether or not connection mirroring is enabled. + returned: queried + type: bool + sample: yes + irules: + description: + - List of iRules that customize the virtual server to direct and manage traffic. + returned: queried + type: list + sample: ['/Common/rule1', /Common/rule2'] + policies: + description: + - List of LTM policies attached to the virtual server. + returned: queried + type: list + sample: ['/Common/policy1', /Common/policy2'] + security_log_profiles: + description: + - Specifies the log profile applied to the virtual server. + returned: queried + type: list + sample: ['/Common/global-network', '/Common/local-dos'] + type: + description: + - Virtual server type. + returned: queried + type: str + sample: standard + destination_address: + description: + - Address portion of the C(destination). + returned: queried + type: str + sample: 2.3.3.2 + destination_port: + description: + - Port potion of the C(destination). + returned: queried + type: int + sample: 80 + profiles: + description: + - List of the profiles attached to the virtual. + type: complex + contains: + context: + description: + - Which side of the connection the profile affects; either C(all), + C(client-side) or C(server-side). + returned: queried + type: str + sample: client-side + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: /Common/tcp + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: tcp + total_requests: + description: + - Total requests. + returned: queried + type: int + sample: 8 + sample: hash/dictionary of values +vlans: + description: List of VLAN information. + returned: When C(vlans) is specified in C(gather_subset). + type: complex + contains: + auto_lasthop: + description: + - Allows the system to send return traffic to the MAC address that transmitted the + request, even if the routing table points to a different network or interface. + returned: queried + type: str + sample: enabled + cmp_hash_algorithm: + description: + - Specifies how the traffic on the VLAN will be disaggregated. + returned: queried + type: str + sample: default + description: + description: + - Description of the VLAN. + returned: queried + type: str + sample: My vlan + failsafe_action: + description: + - Action for the system to take when the fail-safe mechanism is triggered. + returned: queried + type: str + sample: reboot + failsafe_enabled: + description: + - Whether failsafe is enabled or not. + returned: queried + type: bool + sample: yes + failsafe_timeout: + description: + - Number of seconds that an active unit can run without detecting network traffic + on this VLAN before it starts a failover. + returned: queried + type: int + sample: 90 + if_index: + description: + - Index assigned to this VLAN. It is a unique identifier assigned for all objects + displayed in the SNMP IF-MIB. + returned: queried + type: int + sample: 176 + learning_mode: + description: + - Whether switch ports placed in the VLAN are configured for switch learning, + forwarding only, or dropped. + returned: queried + type: str + sample: enable-forward + interfaces: + description: + - List of tagged or untagged interfaces and trunks that you want to configure for the VLAN. + returned: queried + type: complex + contains: + full_path: + description: + - Full name of the resource as known to the BIG-IP. + returned: queried + type: str + sample: 1.3 + name: + description: + - Relative name of the resource in the BIG-IP. + returned: queried + type: str + sample: 1.3 + tagged: + description: + - Whether the interface is tagged or not. + returned: queried + type: bool + sample: no + mtu: + description: + - Specific maximum transition unit (MTU) for the VLAN. + returned: queried + type: int + sample: 1500 + sflow_poll_interval: + description: + - Maximum interval in seconds between two pollings. + returned: queried + type: int + sample: 0 + sflow_poll_interval_global: + description: + - Whether the global VLAN poll-interval setting overrides the object-level + poll-interval setting. + returned: queried + type: bool + sample: no + sflow_sampling_rate: + description: + - Ratio of packets observed to the samples generated. + returned: queried + type: int + sample: 0 + sflow_sampling_rate_global: + description: + - Whether the global VLAN sampling-rate setting overrides the object-level + sampling-rate setting. + returned: queried + type: bool + sample: yes + source_check_enabled: + description: + - Specifies that only connections that have a return route in the routing table are accepted. + returned: queried + type: bool + sample: yes + true_mac_address: + description: + - Media access control (MAC) address for the lowest-numbered interface assigned to this VLAN. + returned: queried + type: str + sample: "fa:16:3e:10:da:ff" + tag: + description: + - Tag number for the VLAN. + returned: queried + type: int + sample: 30 + sample: hash/dictionary of values +''' + +import datetime +import math +import re +import time +import traceback +from collections import namedtuple +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) +from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE +from ansible.module_utils.six import ( + iteritems, string_types +) +from ansible.module_utils.urls import urlparse + +from ipaddress import ip_interface + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.urls import parseStats +from ..module_utils.icontrol import ( + tmos_version, modules_provisioned, packages_installed +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.kwargs = kwargs + + # A list of modules currently provisioned on the device. + # + # This list is used by different fact managers to check to see + # if they should even attempt to gather information. If the module is + # not provisioned, then it is likely that the REST API will not + # return valid data. + # + # For example, ASM (at the time of this writing 13.x/14.x) will + # raise an exception if you attempt to query its APIs if it is + # not provisioned. An example error message is shown below. + # + # { + # "code": 400, + # "message": "java.net.ConnectException: Connection refused (Connection refused)", + # "referer": "172.18.43.40", + # "restOperationId": 18164160, + # "kind": ":resterrorresponse" + # } + # + # This list is provided to the specific fact manager by the + # master ModuleManager of this module. + self.provisioned_modules = [] + + # A list of packages currently installed on the device. + # + # This list is used by different fact managers to check to see + # if they should even attempt to gather information. If the package is + # not provisioned, then it is likely that the REST API will not + # return valid data. + # + # This list is provided to the specific fact manager by the + # master ModuleManager of this module. + self.installed_packages = [] + + def exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + return results + + +class Parameters(AnsibleF5Parameters): + @property + def gather_subset(self): + if isinstance(self._values['gather_subset'], string_types): + self._values['gather_subset'] = [self._values['gather_subset']] + elif not isinstance(self._values['gather_subset'], list): + raise F5ModuleError( + "The specified gather_subset must be a list." + ) + tmp = list(set(self._values['gather_subset'])) + tmp.sort() + self._values['gather_subset'] = tmp + + return self._values['gather_subset'] + + +class BaseParameters(Parameters): + @property + def enabled(self): + return flatten_boolean(self._values['enabled']) + + @property + def disabled(self): + return flatten_boolean(self._values['disabled']) + + def _remove_internal_keywords(self, resource): + resource.pop('kind', None) + resource.pop('generation', None) + resource.pop('selfLink', None) + resource.pop('isSubcollection', None) + resource.pop('fullPath', None) + + def to_return(self): + result = {} + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + + +class ApmAccessProfileFactParameters(BaseParameters): + api_map = { + 'accessPolicy': 'access_policy', + 'fullPath': 'full_path', + } + + returnables = [ + 'access_policy', + 'full_path', + 'name', + ] + + +class ApmAccessProfileFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(ApmAccessProfileFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(apm_access_profiles=facts) + return result + + def _exec_module(self): + if 'apm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = ApmAccessProfileFactParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/apm/profile/access".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class ApmAccessPolicyFactParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + } + + returnables = [ + 'full_path', + 'name', + ] + + +class ApmAccessPolicyFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(ApmAccessPolicyFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(apm_access_policies=facts) + return result + + def _exec_module(self): + if 'apm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = ApmAccessProfileFactParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/apm/policy/access-policy".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class As3Parameters(BaseParameters): + api_map = { + } + + returnables = [ + + ] + + +class As3FactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + self.installed_packages = packages_installed(self.client) + super(As3FactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(as3_config=facts) + return result + + def _exec_module(self): + if 'as3' not in self.installed_packages: + return [] + facts = self.read_facts() + return facts + + def read_facts(self): + collection = self.read_collection_from_device() + return collection + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/shared/appsvcs/declare".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status == 204 or 'code' in response and response['code'] == 204: + return [] + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'class' not in response: + return [] + result = dict() + result['declaration'] = response + return result + + +class AsmPolicyStatsParameters(BaseParameters): + api_map = { + + } + + returnables = [ + 'policies', + 'parent_policies', + 'policies_pending_changes', + 'policies_active', + 'policies_attached', + 'policies_inactive', + 'policies_unattached', + ] + + @property + def policies(self): + if self._values['policies'] is None or len(self._values['policies']) == 0: + return None + return len(self._values['policies']) + + @property + def parent_policies(self): + if self._values['policies'] is None or len(self._values['policies']) == 0: + return None + return len([x for x in self._values['policies'] if 'type' in x and x['type'] == "parent"]) + + @property + def policies_pending_changes(self): + if self._values['policies'] is None or len(self._values['policies']) == 0: + return None + return len([x for x in self._values['policies'] if x['isModified'] is True]) + + +class AsmPolicyStatsParametersv13(AsmPolicyStatsParameters): + @property + def policies_active(self): + if self._values['policies'] is None or len(self._values['policies']) == 0: + return None + return len([x for x in self._values['policies'] if 'active' in x and x['active']]) + + @property + def policies_inactive(self): + if self._values['policies'] is None or len(self._values['policies']) == 0: + return None + return len([x for x in self._values['policies'] if 'active' in x and x['active'] is not True]) + + @property + def policies_attached(self): + return self.policies_active + + @property + def policies_unattached(self): + return self.policies_inactive + + +class AsmPolicyStatsParametersv12(AsmPolicyStatsParameters): + @property + def policies_active(self): + if self._values['policies'] is None or len(self._values['policies']) == 0: + return None + return len([x for x in self._values['policies'] if x['active'] is True]) + + @property + def policies_inactive(self): + if self._values['policies'] is None or len(self._values['policies']) == 0: + return None + return len([x for x in self._values['policies'] if x['active'] is not True]) + + @property + def policies_attached(self): + if self._values['policies'] is None or len(self._values['policies']) == 0: + return None + return len([x for x in self._values['policies'] + if x['active'] is True and len(x['virtualServers']) > 0]) + + @property + def policies_unattached(self): + if self._values['policies'] is None or len(self._values['policies']) == 0: + return None + return len([x for x in self._values['policies'] + if x['active'] is False and len(x['virtualServers']) == 0]) + + +class AsmPolicyStatsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(AsmPolicyStatsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(asm_policy_stats=facts) + return result + + def _exec_module(self): + if 'asm' not in self.provisioned_modules: + return [] + facts = self.read_facts() + results = facts.to_return() + return results + + def version_is_less_than_13(self): + version = tmos_version(self.client) + if Version(version) < Version('13.0.0'): + return True + else: + return False + + def read_facts(self): + collection = self.read_collection_from_device() + if self.version_is_less_than_13(): + params = AsmPolicyStatsParametersv12(params=collection) + else: + params = AsmPolicyStatsParametersv13(params=collection) + return params + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/asm/policies".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + return dict( + policies=response['items'] + ) + + +class AsmPolicyFactParameters(BaseParameters): + api_map = { + 'hasParent': 'has_parent', + 'protocolIndependent': 'protocol_independent', + 'virtualServers': 'virtual_servers', + 'manualVirtualServers': 'manual_virtual_servers', + 'allowedResponseCodes': 'allowed_response_codes', + 'learningMode': 'learning_mode', + 'enforcementMode': 'enforcement_mode', + 'customXffHeaders': 'custom_xff_headers', + 'caseInsensitive': 'case_insensitive', + 'stagingSettings': 'staging_settings', + 'applicationLanguage': 'application_language', + 'trustXff': 'trust_xff', + 'geolocation-enforcement': 'geolocation_enforcement', + 'disallowedLocations': 'disallowed_locations', + 'signature-settings': 'signature_settings', + 'header-settings': 'header_settings', + 'cookie-settings': 'cookie_settings', + 'policy-builder': 'policy_builder', + 'disallowed-geolocations': 'disallowed_geolocations', + 'whitelist-ips': 'whitelist_ips', + 'fullPath': 'full_path', + 'csrf-protection': 'csrf_protection', + 'isModified': 'apply', + } + + returnables = [ + 'full_path', + 'name', + 'policy_id', + 'active', + 'protocol_independent', + 'has_parent', + 'type', + 'virtual_servers', + 'allowed_response_codes', + 'description', + 'learning_mode', + 'enforcement_mode', + 'custom_xff_headers', + 'case_insensitive', + 'signature_staging', + 'place_signatures_in_staging', + 'enforcement_readiness_period', + 'path_parameter_handling', + 'trigger_asm_irule_event', + 'inspect_http_uploads', + 'mask_credit_card_numbers_in_request', + 'maximum_http_header_length', + 'use_dynamic_session_id_in_url', + 'maximum_cookie_header_length', + 'application_language', + 'trust_xff', + 'disallowed_geolocations', + 'csrf_urls', + 'csrf_protection_enabled', + 'csrf_protection_ssl_only', + 'csrf_protection_expiration_time_in_seconds', + 'apply', + ] + + def _morph_keys(self, key_map, item): + for k, v in iteritems(key_map): + item[v] = item.pop(k, None) + result = self._filter_params(item) + return result + + @property + def active(self): + return flatten_boolean(self._values['active']) + + @property + def apply(self): + return flatten_boolean(self._values['apply']) + + @property + def case_insensitive(self): + return flatten_boolean(self._values['case_insensitive']) + + @property + def has_parent(self): + return flatten_boolean(self._values['has_parent']) + + @property + def policy_id(self): + if self._values['id'] is None: + return None + return self._values['id'] + + @property + def manual_virtual_servers(self): + if 'manual_virtual_servers' in self._values: + if self._values['manual_virtual_servers'] is None: + return None + return self._values['manual_virtual_servers'] + + @property + def signature_staging(self): + if 'staging_settings' in self._values: + if self._values['staging_settings'] is None: + return None + if 'signatureStaging' in self._values['staging_settings']: + return flatten_boolean(self._values['staging_settings']['signatureStaging']) + if 'signature_settings' in self._values: + if self._values['signature_settings'] is None: + return None + if 'signatureStaging' in self._values['signature_settings']: + return flatten_boolean(self._values['signature_settings']['signatureStaging']) + + @property + def place_signatures_in_staging(self): + if 'staging_settings' in self._values: + if self._values['staging_settings'] is None: + return None + if 'placeSignaturesInStaging' in self._values['staging_settings']: + return flatten_boolean(self._values['staging_settings']['placeSignaturesInStaging']) + if 'signature_settings' in self._values: + if self._values['signature_settings'] is None: + return None + if 'signatureStaging' in self._values['signature_settings']: + return flatten_boolean(self._values['signature_settings']['placeSignaturesInStaging']) + + @property + def enforcement_readiness_period(self): + if 'staging_settings' in self._values: + if self._values['staging_settings'] is None: + return None + if 'enforcementReadinessPeriod' in self._values['staging_settings']: + return self._values['staging_settings']['enforcementReadinessPeriod'] + if 'general' in self._values: + if self._values['general'] is None: + return None + if 'signatureStaging' in self._values['general']: + return self._values['general']['enforcementReadinessPeriod'] + + @property + def path_parameter_handling(self): + if 'attributes' in self._values: + if self._values['attributes'] is None: + return None + if 'pathParameterHandling' in self._values['attributes']: + return self._values['attributes']['pathParameterHandling'] + if 'general' in self._values: + if self._values['general'] is None: + return None + if 'pathParameterHandling' in self._values['general']: + return self._values['general']['pathParameterHandling'] + + @property + def trigger_asm_irule_event(self): + if 'attributes' in self._values: + if self._values['attributes'] is None: + return None + if 'triggerAsmIruleEvent' in self._values['attributes']: + return self._values['attributes']['triggerAsmIruleEvent'] + if 'general' in self._values: + if self._values['general'] is None: + return None + if 'triggerAsmIruleEvent' in self._values['general']: + return self._values['general']['triggerAsmIruleEvent'] + + @property + def inspect_http_uploads(self): + if 'attributes' in self._values: + if self._values['attributes'] is None: + return None + if 'inspectHttpUploads' in self._values['attributes']: + return flatten_boolean(self._values['attributes']['inspectHttpUploads']) + if 'antivirus' in self._values: + if self._values['antivirus'] is None: + return None + if 'inspectHttpUploads' in self._values['antivirus']: + return flatten_boolean(self._values['antivirus']['inspectHttpUploads']) + + @property + def mask_credit_card_numbers_in_request(self): + if 'attributes' in self._values: + if self._values['attributes'] is None: + return None + if 'maskCreditCardNumbersInRequest' in self._values['attributes']: + return flatten_boolean(self._values['attributes']['maskCreditCardNumbersInRequest']) + if 'general' in self._values: + if self._values['general'] is None: + return None + if 'maskCreditCardNumbersInRequest' in self._values['general']: + return flatten_boolean(self._values['general']['maskCreditCardNumbersInRequest']) + + @property + def maximum_http_header_length(self): + if 'attributes' in self._values: + if self._values['attributes'] is None: + return None + if 'maximumHttpHeaderLength' in self._values['attributes']: + if self._values['attributes']['maximumHttpHeaderLength'] == 'any': + return 'any' + return int(self._values['attributes']['maximumHttpHeaderLength']) + + if 'header_settings' in self._values: + if self._values['header_settings'] is None: + return None + if 'maximumHttpHeaderLength' in self._values['header_settings']: + if self._values['header_settings']['maximumHttpHeaderLength'] == 'any': + return 'any' + return int(self._values['header_settings']['maximumHttpHeaderLength']) + + @property + def use_dynamic_session_id_in_url(self): + if 'attributes' in self._values: + if self._values['attributes'] is None: + return None + if 'useDynamicSessionIdInUrl' in self._values['attributes']: + return flatten_boolean(self._values['attributes']['useDynamicSessionIdInUrl']) + if 'general' in self._values: + if self._values['general'] is None: + return None + if 'useDynamicSessionIdInUrl' in self._values['general']: + return flatten_boolean(self._values['general']['useDynamicSessionIdInUrl']) + + @property + def maximum_cookie_header_length(self): + if 'attributes' in self._values: + if self._values['attributes'] is None: + return None + if 'maximumCookieHeaderLength' in self._values['attributes']: + if self._values['attributes']['maximumCookieHeaderLength'] == 'any': + return 'any' + return int(self._values['attributes']['maximumCookieHeaderLength']) + if 'cookie_settings' in self._values: + if self._values['cookie_settings'] is None: + return None + if 'maximumCookieHeaderLength' in self._values['cookie_settings']: + if self._values['cookie_settings']['maximumCookieHeaderLength'] == 'any': + return 'any' + return int(self._values['cookie_settings']['maximumCookieHeaderLength']) + + @property + def trust_xff(self): + if 'trust_xff' in self._values: + if self._values['trust_xff'] is None: + return None + return flatten_boolean(self._values['trust_xff']) + if 'general' in self._values: + if self._values['general'] is None: + return None + if 'trustXff' in self._values['general']: + return flatten_boolean(self._values['general']['trustXff']) + + @property + def custom_xff_headers(self): + if 'custom_xff_headers' in self._values: + if self._values['custom_xff_headers'] is None: + return None + return self._values['custom_xff_headers'] + if 'general' in self._values: + if self._values['general'] is None: + return None + if 'customXffHeaders' in self._values['general']: + return self._values['general']['customXffHeaders'] + + @property + def allowed_response_codes(self): + if 'allowed_response_codes' in self._values: + if self._values['allowed_response_codes'] is None: + return None + return self._values['allowed_response_codes'] + if 'general' in self._values: + if self._values['general'] is None: + return None + if 'allowedResponseCodes' in self._values['general']: + return self._values['general']['allowedResponseCodes'] + + @property + def learning_mode(self): + if 'policy_builder' in self._values: + if self._values['policy_builder'] is None: + return None + if 'learningMode' in self._values['policy_builder']: + return self._values['policy_builder']['learningMode'] + + @property + def disallowed_locations(self): + if 'geolocation_enforcement' in self._values: + if self._values['geolocation_enforcement'] is None: + return None + return self._values['geolocation_enforcement']['disallowedLocations'] + + @property + def disallowed_geolocations(self): + if 'disallowed_geolocations' in self._values: + if self._values['disallowed_geolocations'] is None: + return None + return self._values['disallowed_geolocations'] + + @property + def csrf_protection_enabled(self): + if 'csrf_protection' in self._values: + return flatten_boolean(self._values['csrf_protection']['enabled']) + + @property + def csrf_protection_ssl_only(self): + if 'csrf_protection' in self._values: + if 'sslOnly' in self._values['csrf_protection']: + return flatten_boolean(self._values['csrf_protection']['sslOnly']) + + @property + def csrf_protection_expiration_time_in_seconds(self): + if 'csrf_protection' in self._values: + if 'expirationTimeInSeconds' in self._values['csrf_protection']: + if self._values['csrf_protection']['expirationTimeInSeconds'] is None: + return None + if self._values['csrf_protection']['expirationTimeInSeconds'] == 'disabled': + return 'disabled' + return int(self._values['csrf_protection']['expirationTimeInSeconds']) + + def format_csrf_collection(self, items): + result = list() + key_map = { + 'requiredParameters': 'csrf_url_required_parameters', + 'url': 'csrf_url', + 'method': 'csrf_url_method', + 'enforcementAction': 'csrf_url_enforcement_action', + 'id': 'csrf_url_id', + 'wildcardOrder': 'csrf_url_wildcard_order', + 'parametersList': 'csrf_url_parameters_list' + } + for item in items: + self._remove_internal_keywords(item) + item.pop('lastUpdateMicros') + output = self._morph_keys(key_map, item) + result.append(output) + return result + + @property + def csrf_urls(self): + if 'csrfUrls' in self._values: + if self._values['csrfUrls'] is None: + return None + return self._values['csrfUrls'] + if 'csrf-urls' in self._values: + if self._values['csrf-urls'] is None: + return None + return self.format_csrf_collection(self._values['csrf-urls']) + + @property + def protocol_independent(self): + return flatten_boolean(self._values['protocol_independent']) + + +# TODO include: web-scraping,ip-intelligence,session-tracking, +# TODO login-enforcement,data-guard,redirection-protection,vulnerability-assessment, parentPolicyReference + + +class AsmPolicyFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(AsmPolicyFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(asm_policies=facts) + return result + + def _exec_module(self): + if 'asm' not in self.provisioned_modules: + return [] + manager = self.get_manager() + return manager._exec_module() + + def get_manager(self): + if self.version_is_less_than_13(): + return AsmPolicyFactManagerV12(**self.kwargs) + else: + return AsmPolicyFactManagerV13(**self.kwargs) + + def version_is_less_than_13(self): + version = tmos_version(self.client) + if Version(version) < Version('13.0.0'): + return True + else: + return False + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = AsmPolicyFactParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + 10 + return result + + +class AsmPolicyFactManagerV12(AsmPolicyFactManager): + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/asm/policies".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + to_expand = 'policy-builder,geolocation-enforcement,csrf-protection' + query = '?$top=10&$skip={0}&$expand={1}&$filter=partition+eq+{2}'.format( + skip, + to_expand, + self.module.params['partition'] + ) + + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + return response['items'] + + +class AsmPolicyFactManagerV13(AsmPolicyFactManager): + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/asm/policies".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + to_expand = 'general,signature-settings,header-settings,cookie-settings,antivirus,' \ + 'policy-builder,csrf-protection,csrf-urls' + query = '?$top=10&$skip={0}&$expand={1}&$filter=partition+eq+{2}'.format( + skip, + to_expand, + self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + return response['items'] + + +class AsmServerTechnologyFactParameters(BaseParameters): + api_map = { + 'serverTechnologyName': 'server_technology_name', + 'serverTechnologyReferences': 'server_technology_references', + } + + returnables = [ + 'id', + 'server_technology_name', + 'server_technology_references', + ] + + +class AsmServerTechnologyFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(AsmServerTechnologyFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(asm_server_technologies=facts) + return result + + def _exec_module(self): + results = [] + if 'asm' not in self.provisioned_modules: + return results + if self.version_is_less_than_13(): + return results + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['server_technology_name']) + return results + + def version_is_less_than_13(self): + version = tmos_version(self.client) + if Version(version) < Version('13.0.0'): + return True + else: + return False + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = AsmServerTechnologyFactParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/asm/server-technologies".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class AsmSignatureSetsFactParameters(BaseParameters): + api_map = { + 'isUserDefined': 'is_user_defined', + 'assignToPolicyByDefault': 'assign_to_policy_by_default', + 'defaultAlarm': 'default_alarm', + 'defaultBlock': 'default_block', + 'defaultLearn': 'default_learn', + } + + returnables = [ + 'name', + 'id', + 'type', + 'category', + 'is_user_defined', + 'assign_to_policy_by_default', + 'default_alarm', + 'default_block', + 'default_learn', + ] + + @property + def is_user_defined(self): + return flatten_boolean(self._values['is_user_defined']) + + @property + def assign_to_policy_by_default(self): + return flatten_boolean(self._values['assign_to_policy_by_default']) + + @property + def default_alarm(self): + return flatten_boolean(self._values['default_alarm']) + + @property + def default_block(self): + return flatten_boolean(self._values['default_block']) + + @property + def default_learn(self): + return flatten_boolean(self._values['default_learn']) + + +# TODO: add the following: filter, systems, signatureReferences + + +class AsmSignatureSetsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(AsmSignatureSetsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(asm_signature_sets=facts) + return result + + def _exec_module(self): + results = [] + if 'asm' not in self.provisioned_modules: + return results + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['name']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = AsmSignatureSetsFactParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/asm/signature-sets".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = '?$top={0}&$skip={1}'.format(self.module.params['data_increment'], skip) + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return None + + return response['items'] + + +class ClientSslProfilesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'alertTimeout': 'alert_timeout', + 'allowNonSsl': 'allow_non_ssl', + 'authenticateDepth': 'authenticate_depth', + 'authenticate': 'authenticate_frequency', + 'caFile': 'ca_file', + 'cacheSize': 'cache_size', + 'cacheTimeout': 'cache_timeout', + 'cert': 'certificate_file', + 'key': 'key_file', + 'chain': 'chain_file', + 'crlFile': 'crl_file', + 'defaultsFrom': 'parent', + 'modSslMethods': 'modssl_methods', + 'peerCertMode': 'peer_certification_mode', + 'sniRequire': 'sni_require', + 'strictResume': 'strict_resume', + 'mode': 'profile_mode_enabled', + 'renegotiateMaxRecordDelay': 'renegotiation_maximum_record_delay', + 'renegotiatePeriod': 'renegotiation_period', + 'serverName': 'server_name', + 'sessionTicket': 'session_ticket', + 'sniDefault': 'sni_default', + 'uncleanShutdown': 'unclean_shutdown', + 'retainCertificate': 'retain_certificate', + 'secureRenegotiation': 'secure_renegotiation_mode', + 'handshakeTimeout': 'handshake_timeout', + 'certExtensionIncludes': 'forward_proxy_certificate_extension_include', + 'certLifespan': 'forward_proxy_certificate_lifespan', + 'certLookupByIpaddrPort': 'forward_proxy_lookup_by_ipaddr_port', + 'sslForwardProxy': 'forward_proxy_enabled', + 'proxyCaPassphrase': 'forward_proxy_ca_passphrase', + 'proxyCaCert': 'forward_proxy_ca_certificate_file', + 'proxyCaKey': 'forward_proxy_ca_key_file' + } + + returnables = [ + 'full_path', + 'name', + 'alert_timeout', + 'allow_non_ssl', + 'authenticate_depth', + 'authenticate_frequency', + 'ca_file', + 'cache_size', + 'cache_timeout', + 'certificate_file', + 'key_file', + 'chain_file', + 'ciphers', + 'crl_file', + 'parent', + 'description', + 'modssl_methods', + 'peer_certification_mode', + 'sni_require', + 'sni_default', + 'strict_resume', + 'profile_mode_enabled', + 'renegotiation_maximum_record_delay', + 'renegotiation_period', + 'renegotiation', + 'server_name', + 'session_ticket', + 'unclean_shutdown', + 'retain_certificate', + 'secure_renegotiation_mode', + 'handshake_timeout', + 'forward_proxy_certificate_extension_include', + 'forward_proxy_certificate_lifespan', + 'forward_proxy_lookup_by_ipaddr_port', + 'forward_proxy_enabled', + 'forward_proxy_ca_passphrase', + 'forward_proxy_ca_certificate_file', + 'forward_proxy_ca_key_file' + ] + + @property + def alert_timeout(self): + if self._values['alert_timeout'] is None: + return None + if self._values['alert_timeout'] == 'indefinite': + return 0 + return int(self._values['alert_timeout']) + + @property + def renegotiation_maximum_record_delay(self): + if self._values['renegotiation_maximum_record_delay'] is None: + return None + if self._values['renegotiation_maximum_record_delay'] == 'indefinite': + return 0 + return int(self._values['renegotiation_maximum_record_delay']) + + @property + def renegotiation_period(self): + if self._values['renegotiation_period'] is None: + return None + if self._values['renegotiation_period'] == 'indefinite': + return 0 + return int(self._values['renegotiation_period']) + + @property + def handshake_timeout(self): + if self._values['handshake_timeout'] is None: + return None + if self._values['handshake_timeout'] == 'indefinite': + return 0 + return int(self._values['handshake_timeout']) + + @property + def allow_non_ssl(self): + if self._values['allow_non_ssl'] is None: + return None + if self._values['allow_non_ssl'] == 'disabled': + return 'no' + return 'yes' + + @property + def forward_proxy_enabled(self): + if self._values['forward_proxy_enabled'] is None: + return None + if self._values['forward_proxy_enabled'] == 'disabled': + return 'no' + return 'yes' + + @property + def renegotiation(self): + if self._values['renegotiation'] is None: + return None + if self._values['renegotiation'] == 'disabled': + return 'no' + return 'yes' + + @property + def forward_proxy_lookup_by_ipaddr_port(self): + if self._values['forward_proxy_lookup_by_ipaddr_port'] is None: + return None + if self._values['forward_proxy_lookup_by_ipaddr_port'] == 'disabled': + return 'no' + return 'yes' + + @property + def unclean_shutdown(self): + if self._values['unclean_shutdown'] is None: + return None + if self._values['unclean_shutdown'] == 'disabled': + return 'no' + return 'yes' + + @property + def session_ticket(self): + if self._values['session_ticket'] is None: + return None + if self._values['session_ticket'] == 'disabled': + return 'no' + return 'yes' + + @property + def retain_certificate(self): + if self._values['retain_certificate'] is None: + return None + if self._values['retain_certificate'] == 'true': + return 'yes' + return 'no' + + @property + def server_name(self): + if self._values['server_name'] in [None, 'none']: + return None + return self._values['server_name'] + + @property + def forward_proxy_ca_certificate_file(self): + if self._values['forward_proxy_ca_certificate_file'] in [None, 'none']: + return None + return self._values['forward_proxy_ca_certificate_file'] + + @property + def forward_proxy_ca_key_file(self): + if self._values['forward_proxy_ca_key_file'] in [None, 'none']: + return None + return self._values['forward_proxy_ca_key_file'] + + @property + def authenticate_frequency(self): + if self._values['authenticate_frequency'] is None: + return None + return self._values['authenticate_frequency'] + + @property + def ca_file(self): + if self._values['ca_file'] in [None, 'none']: + return None + return self._values['ca_file'] + + @property + def certificate_file(self): + if self._values['certificate_file'] in [None, 'none']: + return None + return self._values['certificate_file'] + + @property + def key_file(self): + if self._values['key_file'] in [None, 'none']: + return None + return self._values['key_file'] + + @property + def chain_file(self): + if self._values['chain_file'] in [None, 'none']: + return None + return self._values['chain_file'] + + @property + def crl_file(self): + if self._values['crl_file'] in [None, 'none']: + return None + return self._values['crl_file'] + + @property + def ciphers(self): + if self._values['ciphers'] is None: + return None + if self._values['ciphers'] == 'none': + return 'none' + return self._values['ciphers'].split(' ') + + @property + def modssl_methods(self): + if self._values['modssl_methods'] is None: + return None + if self._values['modssl_methods'] == 'disabled': + return 'no' + return 'yes' + + @property + def strict_resume(self): + if self._values['strict_resume'] is None: + return None + if self._values['strict_resume'] == 'disabled': + return 'no' + return 'yes' + + @property + def profile_mode_enabled(self): + if self._values['profile_mode_enabled'] is None: + return None + if self._values['profile_mode_enabled'] == 'disabled': + return 'no' + return 'yes' + + @property + def sni_require(self): + if self._values['sni_require'] is None: + return None + if self._values['sni_require'] == 'false': + return 'no' + return 'yes' + + @property + def sni_default(self): + if self._values['sni_default'] is None: + return None + if self._values['sni_default'] == 'false': + return 'no' + return 'yes' + + +class ClientSslProfilesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(ClientSslProfilesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(client_ssl_profiles=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = ClientSslProfilesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/client-ssl".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class CFEParameters(BaseParameters): + api_map = { + } + + returnables = [ + + ] + + +class CFEFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + self.installed_packages = packages_installed(self.client) + super(CFEFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(cfe_config=facts) + return result + + def _exec_module(self): + if 'cfe' not in self.installed_packages: + return [] + facts = self.read_facts() + return facts + + def read_facts(self): + collection = self.read_collection_from_device() + return collection + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/shared/cloud-failover/declare".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = {} + result['declaration'] = response['declaration'] + return result + + +class DeviceGroupsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'autoSync': 'autosync_enabled', + 'asmSync': 'asm_sync_enabled', + 'devicesReference': 'devices', + 'fullLoadOnSync': 'full_load_on_sync', + 'incrementalConfigSyncSizeMax': 'incremental_config_sync_size_maximum', + 'networkFailover': 'network_failover_enabled' + } + + returnables = [ + 'full_path', + 'name', + 'autosync_enabled', + 'description', + 'devices', + 'full_load_on_sync', + 'incremental_config_sync_size_maximum', + 'network_failover_enabled', + 'type', + 'asm_sync_enabled' + ] + + @property + def network_failover_enabled(self): + if self._values['network_failover_enabled'] is None: + return None + if self._values['network_failover_enabled'] == 'enabled': + return 'yes' + return 'no' + + @property + def asm_sync_enabled(self): + if self._values['asm_sync_enabled'] is None: + return None + if self._values['asm_sync_enabled'] == 'disabled': + return 'no' + return 'yes' + + @property + def autosync_enabled(self): + if self._values['autosync_enabled'] is None: + return None + if self._values['autosync_enabled'] == 'disabled': + return 'no' + return 'yes' + + @property + def full_load_on_sync(self): + if self._values['full_load_on_sync'] is None: + return None + if self._values['full_load_on_sync'] == 'true': + return 'yes' + return 'no' + + @property + def devices(self): + if self._values['devices'] is None or 'items' not in self._values['devices']: + return None + result = [x['fullPath'] for x in self._values['devices']['items']] + result.sort() + return result + + +class DeviceGroupsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(DeviceGroupsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(device_groups=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = DeviceGroupsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/cm/device-group/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class DevicesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'activeModules': 'active_modules', + 'baseMac': 'base_mac_address', + 'chassisId': 'chassis_id', + 'chassisType': 'chassis_type', + 'configsyncIp': 'configsync_address', + 'failoverState': 'failover_state', + 'managementIp': 'management_address', + 'marketingName': 'marketing_name', + 'multicastIp': 'multicast_address', + 'optionalModules': 'optional_modules', + 'platformId': 'platform_id', + 'mirrorIp': 'primary_mirror_address', + 'mirrorSecondaryIp': 'secondary_mirror_address', + 'version': 'software_version', + 'timeLimitedModules': 'timelimited_modules', + 'timeZone': 'timezone', + 'unicastAddress': 'unicast_addresses', + 'selfDevice': 'self' + } + + returnables = [ + 'full_path', + 'name', + 'active_modules', + 'base_mac_address', + 'build', + 'chassis_id', + 'chassis_type', + 'comment', + 'configsync_address', + 'contact', + 'description', + 'edition', + 'failover_state', + 'hostname', + 'location', + 'management_address', + 'marketing_name', + 'multicast_address', + 'optional_modules', + 'platform_id', + 'primary_mirror_address', + 'product', + 'secondary_mirror_address', + 'self', + 'software_version', + 'timelimited_modules', + 'timezone', + 'unicast_addresses', + ] + + @property + def active_modules(self): + if self._values['active_modules'] is None: + return None + result = [] + for x in self._values['active_modules']: + parts = x.split('|') + result += parts[2:] + return list(set(result)) + + @property + def self(self): + result = flatten_boolean(self._values['self']) + return result + + @property + def configsync_address(self): + if self._values['configsync_address'] in [None, 'none']: + return None + return self._values['configsync_address'] + + @property + def primary_mirror_address(self): + if self._values['primary_mirror_address'] in [None, 'any6']: + return None + return self._values['primary_mirror_address'] + + @property + def secondary_mirror_address(self): + if self._values['secondary_mirror_address'] in [None, 'any6']: + return None + return self._values['secondary_mirror_address'] + + @property + def unicast_addresses(self): + if self._values['unicast_addresses'] is None: + return None + result = [] + + for addr in self._values['unicast_addresses']: + tmp = {} + for key in ['effectiveIp', 'effectivePort', 'ip', 'port']: + if key in addr: + renamed_key = self.convert(key) + tmp[renamed_key] = addr.get(key, None) + if tmp: + result.append(tmp) + if result: + return result + + def convert(self, name): + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +class DevicesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(DevicesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(devices=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = DevicesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/cm/device".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class DOParameters(BaseParameters): + api_map = { + } + + returnables = [ + + ] + + +class DOFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + self.installed_packages = packages_installed(self.client) + super(DOFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(do_config=facts) + return result + + def _exec_module(self): + if 'do' not in self.installed_packages: + return [] + facts = self.read_facts() + return facts + + def read_facts(self): + collection = self.read_collection_from_device() + return collection + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/shared/declarative-onboarding/inspect".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = {} + result['declaration'] = response[0]['declaration'] + return result + + +class ExternalMonitorsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultsFrom': 'parent', + 'adaptiveDivergenceType': 'adaptive_divergence_type', + 'adaptiveDivergenceValue': 'adaptive_divergence_value', + 'adaptiveLimit': 'adaptive_limit', + 'adaptiveSamplingTimespan': 'adaptive_sampling_timespan', + 'manualResume': 'manual_resume', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + 'run': 'external_program', + 'apiRawValues': 'variables', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'args', + 'destination', + 'external_program', + 'interval', + 'manual_resume', + 'time_until_up', + 'timeout', + 'up_interval', + 'variables', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def variables(self): + if self._values['variables'] is None: + return None + result = {} + for k, v in iteritems(self._values['variables']): + k = k.replace('userDefined ', '').strip() + result[k] = v + return result + + +class ExternalMonitorsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(ExternalMonitorsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(external_monitors=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = ExternalMonitorsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/external".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class FastHttpProfilesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'clientCloseTimeout': 'client_close_timeout', + 'connpoolIdleTimeoutOverride': 'oneconnect_idle_timeout_override', + 'connpoolMaxReuse': 'oneconnect_maximum_reuse', + 'connpoolMaxSize': 'oneconnect_maximum_pool_size', + 'connpoolMinSize': 'oneconnect_minimum_pool_size', + 'connpoolReplenish': 'oneconnect_replenish', + 'connpoolStep': 'oneconnect_ramp_up_increment', + 'defaultsFrom': 'parent', + 'forceHttp_10Response': 'force_http_1_0_response', + 'headerInsert': 'request_header_insert', + 'http_11CloseWorkarounds': 'http_1_1_close_workarounds', + 'idleTimeout': 'idle_timeout', + 'insertXforwardedFor': 'insert_xforwarded_for', + 'maxHeaderSize': 'maximum_header_size', + 'maxRequests': 'maximum_requests', + 'mssOverride': 'maximum_segment_size_override', + 'receiveWindowSize': 'receive_window_size', + 'resetOnTimeout': 'reset_on_timeout', + 'serverCloseTimeout': 'server_close_timeout', + 'serverSack': 'server_sack', + 'serverTimestamp': 'server_timestamp', + 'uncleanShutdown': 'unclean_shutdown' + } + + returnables = [ + 'full_path', + 'name', + 'client_close_timeout', + 'oneconnect_idle_timeout_override', + 'oneconnect_maximum_reuse', + 'oneconnect_maximum_pool_size', + 'oneconnect_minimum_pool_size', + 'oneconnect_replenish', + 'oneconnect_ramp_up_increment', + 'parent', + 'description', + 'force_http_1_0_response', + 'request_header_insert', + 'http_1_1_close_workarounds', + 'idle_timeout', + 'insert_xforwarded_for', + 'maximum_header_size', + 'maximum_requests', + 'maximum_segment_size_override', + 'receive_window_size', + 'reset_on_timeout', + 'server_close_timeout', + 'server_sack', + 'server_timestamp', + 'unclean_shutdown' + ] + + @property + def request_header_insert(self): + if self._values['request_header_insert'] in [None, 'none']: + return None + return self._values['request_header_insert'] + + @property + def server_timestamp(self): + return flatten_boolean(self._values['server_timestamp']) + + @property + def server_sack(self): + return flatten_boolean(self._values['server_sack']) + + @property + def reset_on_timeout(self): + return flatten_boolean(self._values['reset_on_timeout']) + + @property + def insert_xforwarded_for(self): + return flatten_boolean(self._values['insert_xforwarded_for']) + + @property + def http_1_1_close_workarounds(self): + return flatten_boolean(self._values['http_1_1_close_workarounds']) + + @property + def force_http_1_0_response(self): + return flatten_boolean(self._values['force_http_1_0_response']) + + @property + def oneconnect_replenish(self): + return flatten_boolean(self._values['oneconnect_replenish']) + + @property + def idle_timeout(self): + if self._values['idle_timeout'] is None: + return None + elif self._values['idle_timeout'] == 'immediate': + return 0 + elif self._values['idle_timeout'] == 'indefinite': + return 4294967295 + return int(self._values['idle_timeout']) + + +class FastHttpProfilesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(FastHttpProfilesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(fasthttp_profiles=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = FastHttpProfilesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fasthttp".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class FastL4ProfilesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'clientTimeout': 'client_timeout', + 'defaultsFrom': 'parent', + 'explicitFlowMigration': 'explicit_flow_migration', + 'hardwareSynCookie': 'hardware_syn_cookie', + 'idleTimeout': 'idle_timeout', + 'ipDfMode': 'dont_fragment_flag', + 'ipTosToClient': 'ip_tos_to_client', + 'ipTosToServer': 'ip_tos_to_server', + 'ipTtlMode': 'ttl_mode', + 'ipTtlV4': 'ttl_v4', + 'ipTtlV6': 'ttl_v6', + 'keepAliveInterval': 'keep_alive_interval', + 'lateBinding': 'late_binding', + 'linkQosToClient': 'link_qos_to_client', + 'linkQosToServer': 'link_qos_to_server', + 'looseClose': 'loose_close', + 'looseInitialization': 'loose_init', + 'mssOverride': 'mss_override', + 'priorityToClient': 'priority_to_client', + 'priorityToServer': 'priority_to_server', + 'pvaAcceleration': 'pva_acceleration', + 'pvaDynamicClientPackets': 'pva_dynamic_client_packets', + 'pvaDynamicServerPackets': 'pva_dynamic_server_packets', + 'pvaFlowAging': 'pva_flow_aging', + 'pvaFlowEvict': 'pva_flow_evict', + 'pvaOffloadDynamic': 'pva_offload_dynamic', + 'pvaOffloadState': 'pva_offload_state', + 'reassembleFragments': 'reassemble_fragments', + 'receiveWindowSize': 'receive_window', + 'resetOnTimeout': 'reset_on_timeout', + 'rttFromClient': 'rtt_from_client', + 'rttFromServer': 'rtt_from_server', + 'serverSack': 'server_sack', + 'serverTimestamp': 'server_timestamp', + 'softwareSynCookie': 'software_syn_cookie', + 'synCookieEnable': 'syn_cookie_enabled', + 'synCookieMss': 'syn_cookie_mss', + 'synCookieWhitelist': 'syn_cookie_whitelist', + 'tcpCloseTimeout': 'tcp_close_timeout', + 'tcpGenerateIsn': 'generate_init_seq_number', + 'tcpHandshakeTimeout': 'tcp_handshake_timeout', + 'tcpStripSack': 'strip_sack', + 'tcpTimeWaitTimeout': 'tcp_time_wait_timeout', + 'tcpTimestampMode': 'tcp_timestamp_mode', + 'tcpWscaleMode': 'tcp_window_scale_mode', + 'timeoutRecovery': 'timeout_recovery', + } + + returnables = [ + 'full_path', + 'name', + 'client_timeout', + 'parent', + 'description', + 'explicit_flow_migration', + 'hardware_syn_cookie', + 'idle_timeout', + 'dont_fragment_flag', + 'ip_tos_to_client', + 'ip_tos_to_server', + 'ttl_mode', + 'ttl_v4', + 'ttl_v6', + 'keep_alive_interval', + 'late_binding', + 'link_qos_to_client', + 'link_qos_to_server', + 'loose_close', + 'loose_init', + 'mss_override', # Maximum Segment Size Override + 'priority_to_client', + 'priority_to_server', + 'pva_acceleration', + 'pva_dynamic_client_packets', + 'pva_dynamic_server_packets', + 'pva_flow_aging', + 'pva_flow_evict', + 'pva_offload_dynamic', + 'pva_offload_state', + 'reassemble_fragments', + 'receive_window', + 'reset_on_timeout', + 'rtt_from_client', + 'rtt_from_server', + 'server_sack', + 'server_timestamp', + 'software_syn_cookie', + 'syn_cookie_enabled', + 'syn_cookie_mss', + 'syn_cookie_whitelist', + 'tcp_close_timeout', + 'generate_init_seq_number', + 'tcp_handshake_timeout', + 'strip_sack', + 'tcp_time_wait_timeout', + 'tcp_timestamp_mode', + 'tcp_window_scale_mode', + 'timeout_recovery', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def strip_sack(self): + return flatten_boolean(self._values['strip_sack']) + + @property + def generate_init_seq_number(self): + return flatten_boolean(self._values['generate_init_seq_number']) + + @property + def syn_cookie_whitelist(self): + return flatten_boolean(self._values['syn_cookie_whitelist']) + + @property + def syn_cookie_enabled(self): + return flatten_boolean(self._values['syn_cookie_enabled']) + + @property + def software_syn_cookie(self): + return flatten_boolean(self._values['software_syn_cookie']) + + @property + def server_timestamp(self): + return flatten_boolean(self._values['server_timestamp']) + + @property + def server_sack(self): + return flatten_boolean(self._values['server_sack']) + + @property + def rtt_from_server(self): + return flatten_boolean(self._values['rtt_from_server']) + + @property + def rtt_from_client(self): + return flatten_boolean(self._values['rtt_from_client']) + + @property + def reset_on_timeout(self): + return flatten_boolean(self._values['reset_on_timeout']) + + @property + def explicit_flow_migration(self): + return flatten_boolean(self._values['explicit_flow_migration']) + + @property + def reassemble_fragments(self): + return flatten_boolean(self._values['reassemble_fragments']) + + @property + def pva_flow_aging(self): + return flatten_boolean(self._values['pva_flow_aging']) + + @property + def pva_flow_evict(self): + return flatten_boolean(self._values['pva_flow_evict']) + + @property + def pva_offload_dynamic(self): + return flatten_boolean(self._values['pva_offload_dynamic']) + + @property + def hardware_syn_cookie(self): + return flatten_boolean(self._values['hardware_syn_cookie']) + + @property + def loose_close(self): + return flatten_boolean(self._values['loose_close']) + + @property + def loose_init(self): + return flatten_boolean(self._values['loose_init']) + + @property + def late_binding(self): + return flatten_boolean(self._values['late_binding']) + + @property + def tcp_handshake_timeout(self): + if self._values['tcp_handshake_timeout'] is None: + return None + elif self._values['tcp_handshake_timeout'] == 'immediate': + return 0 + elif self._values['tcp_handshake_timeout'] == 'indefinite': + return 4294967295 + return int(self._values['tcp_handshake_timeout']) + + @property + def idle_timeout(self): + if self._values['idle_timeout'] is None: + return None + elif self._values['idle_timeout'] == 'immediate': + return 0 + elif self._values['idle_timeout'] == 'indefinite': + return 4294967295 + return int(self._values['idle_timeout']) + + @property + def tcp_close_timeout(self): + if self._values['tcp_close_timeout'] is None: + return None + elif self._values['tcp_close_timeout'] == 'immediate': + return 0 + elif self._values['tcp_close_timeout'] == 'indefinite': + return 4294967295 + return int(self._values['tcp_close_timeout']) + + @property + def keep_alive_interval(self): + if self._values['keep_alive_interval'] is None: + return None + elif self._values['keep_alive_interval'] == 'disabled': + return 0 + return int(self._values['keep_alive_interval']) + + @property + def ip_tos_to_client(self): + if self._values['ip_tos_to_client'] is None: + return None + try: + return int(self._values['ip_tos_to_client']) + except ValueError: + return self._values['ip_tos_to_client'] + + @property + def ip_tos_to_server(self): + if self._values['ip_tos_to_server'] is None: + return None + try: + return int(self._values['ip_tos_to_server']) + except ValueError: + return self._values['ip_tos_to_server'] + + @property + def link_qos_to_client(self): + if self._values['link_qos_to_client'] is None: + return None + try: + return int(self._values['link_qos_to_client']) + except ValueError: + return self._values['link_qos_to_client'] + + @property + def link_qos_to_server(self): + if self._values['link_qos_to_server'] is None: + return None + try: + return int(self._values['link_qos_to_server']) + except ValueError: + return self._values['link_qos_to_server'] + + @property + def priority_to_client(self): + if self._values['priority_to_client'] is None: + return None + try: + return int(self._values['priority_to_client']) + except ValueError: + return self._values['priority_to_client'] + + @property + def priority_to_server(self): + if self._values['priority_to_server'] is None: + return None + try: + return int(self._values['priority_to_server']) + except ValueError: + return self._values['priority_to_server'] + + +class FastL4ProfilesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(FastL4ProfilesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(fastl4_profiles=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = FastL4ProfilesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GatewayIcmpMonitorsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultsFrom': 'parent', + 'adaptiveDivergenceType': 'adaptive_divergence_type', + 'adaptiveDivergenceValue': 'adaptive_divergence_value', + 'adaptiveLimit': 'adaptive_limit', + 'adaptiveSamplingTimespan': 'adaptive_sampling_timespan', + 'manualResume': 'manual_resume', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'adaptive', + 'adaptive_divergence_type', + 'adaptive_divergence_value', + 'adaptive_limit', + 'adaptive_sampling_timespan', + 'destination', + 'interval', + 'manual_resume', + 'time_until_up', + 'timeout', + 'transparent', + 'up_interval', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def transparent(self): + return flatten_boolean(self._values['transparent']) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def adaptive(self): + return flatten_boolean(self._values['adaptive']) + + +class GatewayIcmpMonitorsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GatewayIcmpMonitorsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gateway_icmp_monitors=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GatewayIcmpMonitorsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/gateway-icmp".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmXPoolsParameters(BaseParameters): + api_map = { + 'alternateMode': 'alternate_mode', + 'dynamicRatio': 'dynamic_ratio', + 'fallbackMode': 'fallback_mode', + 'fullPath': 'full_path', + 'loadBalancingMode': 'load_balancing_mode', + 'manualResume': 'manual_resume', + 'maxAnswersReturned': 'max_answers_returned', + 'qosHitRatio': 'qos_hit_ratio', + 'qosHops': 'qos_hops', + 'qosKilobytesSecond': 'qos_kilobytes_second', + 'qosLcs': 'qos_lcs', + 'qosPacketRate': 'qos_packet_rate', + 'qosRtt': 'qos_rtt', + 'qosTopology': 'qos_topology', + 'qosVsCapacity': 'qos_vs_capacity', + 'qosVsScore': 'qos_vs_score', + 'verifyMemberAvailability': 'verify_member_availability', + 'membersReference': 'members' + } + + returnables = [ + 'alternate_mode', + 'dynamic_ratio', + 'enabled', + 'disabled', + 'fallback_mode', + 'full_path', + 'load_balancing_mode', + 'manual_resume', + 'max_answers_returned', + 'members', + 'name', + 'partition', + 'qos_hit_ratio', + 'qos_hops', + 'qos_kilobytes_second', + 'qos_lcs', + 'qos_packet_rate', + 'qos_rtt', + 'qos_topology', + 'qos_vs_capacity', + 'qos_vs_score', + 'ttl', + 'verify_member_availability', + ] + + @property + def verify_member_availability(self): + return flatten_boolean(self._values['verify_member_availability']) + + @property + def dynamic_ratio(self): + return flatten_boolean(self._values['dynamic_ratio']) + + @property + def max_answers_returned(self): + if self._values['max_answers_returned'] is None: + return None + return int(self._values['max_answers_returned']) + + @property + def members(self): + result = [] + if self._values['members'] is None or 'items' not in self._values['members']: + return result + for item in self._values['members']['items']: + self._remove_internal_keywords(item) + if 'disabled' in item: + item['disabled'] = flatten_boolean(item['disabled']) + item['enabled'] = flatten_boolean(not item['disabled']) + if 'enabled' in item: + item['enabled'] = flatten_boolean(item['enabled']) + item['disabled'] = flatten_boolean(not item['enabled']) + if 'fullPath' in item: + item['full_path'] = item.pop('fullPath') + if 'memberOrder' in item: + item['member_order'] = int(item.pop('memberOrder')) + # Cast some attributes to integer + for x in ['order', 'preference', 'ratio', 'service']: + if x in item: + item[x] = int(item[x]) + result.append(item) + return result + + @property + def qos_hit_ratio(self): + if self._values['qos_hit_ratio'] is None: + return None + return int(self._values['qos_hit_ratio']) + + @property + def qos_hops(self): + if self._values['qos_hops'] is None: + return None + return int(self._values['qos_hops']) + + @property + def qos_kilobytes_second(self): + if self._values['qos_kilobytes_second'] is None: + return None + return int(self._values['qos_kilobytes_second']) + + @property + def qos_lcs(self): + if self._values['qos_lcs'] is None: + return None + return int(self._values['qos_lcs']) + + @property + def qos_packet_rate(self): + if self._values['qos_packet_rate'] is None: + return None + return int(self._values['qos_packet_rate']) + + @property + def qos_rtt(self): + if self._values['qos_rtt'] is None: + return None + return int(self._values['qos_rtt']) + + @property + def qos_topology(self): + if self._values['qos_topology'] is None: + return None + return int(self._values['qos_topology']) + + @property + def qos_vs_capacity(self): + if self._values['qos_vs_capacity'] is None: + return None + return int(self._values['qos_vs_capacity']) + + @property + def qos_vs_score(self): + if self._values['qos_vs_score'] is None: + return None + return int(self._values['qos_vs_score']) + + @property + def availability_state(self): + if self._values['stats'] is None: + return None + try: + result = self._values['stats']['status']['availabilityState'] + return result['description'] + except AttributeError: + return None + + @property + def enabled_state(self): + if self._values['stats'] is None: + return None + try: + result = self._values['stats']['status']['enabledState'] + return result['description'] + except AttributeError: + return None + + @property + def availability_status(self): + # This fact is a combination of the availability_state and enabled_state + # + # The purpose of the fact is to give a higher-level view of the availability + # of the pool, that can be used in playbooks. If you need further detail, + # consider using the following facts together. + # + # - availability_state + # - enabled_state + # + if self.enabled_state == 'enabled': + if self.availability_state == 'offline': + return 'red' + elif self.availability_state == 'available': + return 'green' + elif self.availability_state == 'unknown': + return 'blue' + else: + return 'none' + else: + # disabled + return 'black' + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + +class GtmAPoolsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmAPoolsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_a_pools=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXPoolsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/a".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmAaaaPoolsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmAaaaPoolsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_aaaa_pools=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXPoolsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/aaaa".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmCnamePoolsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmCnamePoolsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_cname_pools=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXPoolsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/cname".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmMxPoolsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmMxPoolsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_mx_pools=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXPoolsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/mx".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmNaptrPoolsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmNaptrPoolsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_naptr_pools=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXPoolsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/naptr".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmSrvPoolsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmSrvPoolsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_srv_pools=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXPoolsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/srv".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmServersParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'exposeRouteDomains': 'expose_route_domains', + 'iqAllowPath': 'iq_allow_path', + 'iqAllowServiceCheck': 'iq_allow_service_check', + 'iqAllowSnmp': 'iq_allow_snmp', + 'limitCpuUsage': 'limit_cpu_usage', + 'limitCpuUsageStatus': 'limit_cpu_usage_status', + 'limitMaxBps': 'limit_max_bps', + 'limitMaxBpsStatus': 'limit_max_bps_status', + 'limitMaxConnections': 'limit_max_connections', + 'limitMaxConnectionsStatus': 'limit_max_connections_status', + 'limitMaxPps': 'limit_max_pps', + 'limitMaxPpsStatus': 'limit_max_pps_status', + 'limitMemAvail': 'limit_mem_available', + 'limitMemAvailStatus': 'limit_mem_available_status', + 'linkDiscovery': 'link_discovery', + 'proberFallback': 'prober_fallback', + 'proberPreference': 'prober_preference', + 'virtualServerDiscovery': 'virtual_server_discovery', + 'devicesReference': 'devices', + 'virtualServersReference': 'virtual_servers', + 'monitor': 'monitors', + } + + returnables = [ + 'datacenter', + 'enabled', + 'disabled', + 'expose_route_domains', + 'iq_allow_path', + 'full_path', + 'iq_allow_service_check', + 'iq_allow_snmp', + 'limit_cpu_usage', + 'limit_cpu_usage_status', + 'limit_max_bps', + 'limit_max_bps_status', + 'limit_max_connections', + 'limit_max_connections_status', + 'limit_max_pps', + 'limit_max_pps_status', + 'limit_mem_available', + 'limit_mem_available_status', + 'link_discovery', + 'monitors', + 'monitor_type', + 'name', + 'product', + 'prober_fallback', + 'prober_preference', + 'virtual_server_discovery', + 'addresses', + 'devices', + 'virtual_servers', + ] + + def _remove_internal_keywords(self, resource, stats=False): + if stats: + resource.pop('kind', None) + resource.pop('generation', None) + resource.pop('isSubcollection', None) + resource.pop('fullPath', None) + else: + resource.pop('kind', None) + resource.pop('generation', None) + resource.pop('selfLink', None) + resource.pop('isSubcollection', None) + resource.pop('fullPath', None) + + def _read_virtual_stats_from_device(self, url): + uri = "https://{0}:{1}{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + url + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + result = parseStats(response) + try: + return result['stats'] + except KeyError: + return {} + + def _process_vs_stats(self, link): + result = dict() + item = self._read_virtual_stats_from_device(urlparse(link).path) + if not item: + return result + result['status'] = item['status']['availabilityState'] + result['status_reason'] = item['status']['statusReason'] + result['state'] = item['status']['enabledState'] + result['bits_per_sec_in'] = item['metrics']['bitsPerSecIn'] + result['bits_per_sec_in'] = item['metrics']['bitsPerSecOut'] + result['pkts_per_sec_in'] = item['metrics']['pktsPerSecIn'] + result['pkts_per_sec_out'] = item['metrics']['pktsPerSecOut'] + result['connections'] = item['metrics']['connections'] + result['picks'] = item['picks'] + result['virtual_server_score'] = item['metrics']['vsScore'] + result['uptime'] = item['uptime'] + return result + + @property + def monitors(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + return result + except Exception: + return [self._values['monitors']] + + @property + def monitor_type(self): + if self._values['monitors'] is None: + return None + pattern = r'min\s+\d+\s+of' + matches = re.search(pattern, self._values['monitors']) + if matches: + return 'm_of_n' + else: + return 'and_list' + + @property + def limit_mem_available_status(self): + return flatten_boolean(self._values['limit_mem_available_status']) + + @property + def limit_max_pps_status(self): + return flatten_boolean(self._values['limit_max_pps_status']) + + @property + def limit_max_connections_status(self): + return flatten_boolean(self._values['limit_max_connections_status']) + + @property + def limit_max_bps_status(self): + return flatten_boolean(self._values['limit_max_bps_status']) + + @property + def limit_cpu_usage_status(self): + return flatten_boolean(self._values['limit_cpu_usage_status']) + + @property + def iq_allow_service_check(self): + return flatten_boolean(self._values['iq_allow_service_check']) + + @property + def iq_allow_snmp(self): + return flatten_boolean(self._values['iq_allow_snmp']) + + @property + def expose_route_domains(self): + return flatten_boolean(self._values['expose_route_domains']) + + @property + def iq_allow_path(self): + return flatten_boolean(self._values['iq_allow_path']) + + @property + def product(self): + if self._values['product'] is None: + return None + if self._values['product'] in ['single-bigip', 'redundant-bigip']: + return 'bigip' + return self._values['product'] + + @property + def devices(self): + result = [] + if self._values['devices'] is None or 'items' not in self._values['devices']: + return result + for item in self._values['devices']['items']: + self._remove_internal_keywords(item) + if 'fullPath' in item: + item['full_path'] = item.pop('fullPath') + result.append(item) + return result + + @property + def virtual_servers(self): + result = [] + if self._values['virtual_servers'] is None or 'items' not in self._values['virtual_servers']: + return result + for item in self._values['virtual_servers']['items']: + self._remove_internal_keywords(item, stats=True) + stats = self._process_vs_stats(item['selfLink']) + self._remove_internal_keywords(item) + item['stats'] = stats + if 'disabled' in item: + if item['disabled'] in BOOLEANS_TRUE: + item['disabled'] = flatten_boolean(item['disabled']) + item['enabled'] = flatten_boolean(not item['disabled']) + if 'enabled' in item: + if item['enabled'] in BOOLEANS_TRUE: + item['enabled'] = flatten_boolean(item['enabled']) + item['disabled'] = flatten_boolean(not item['enabled']) + if 'fullPath' in item: + item['full_path'] = item.pop('fullPath') + if 'limitMaxBps' in item: + item['limit_max_bps'] = int(item.pop('limitMaxBps')) + if 'limitMaxBpsStatus' in item: + item['limit_max_bps_status'] = item.pop('limitMaxBpsStatus') + if 'limitMaxConnections' in item: + item['limit_max_connections'] = int(item.pop('limitMaxConnections')) + if 'limitMaxConnectionsStatus' in item: + item['limit_max_connections_status'] = item.pop('limitMaxConnectionsStatus') + if 'limitMaxPps' in item: + item['limit_max_pps'] = int(item.pop('limitMaxPps')) + if 'limitMaxPpsStatus' in item: + item['limit_max_pps_status'] = item.pop('limitMaxPpsStatus') + if 'translationAddress' in item: + item['translation_address'] = item.pop('translationAddress') + if 'translationPort' in item: + item['translation_port'] = int(item.pop('translationPort')) + result.append(item) + return result + + @property + def limit_cpu_usage(self): + if self._values['limit_cpu_usage'] is None: + return None + return int(self._values['limit_cpu_usage']) + + @property + def limit_max_bps(self): + if self._values['limit_max_bps'] is None: + return None + return int(self._values['limit_max_bps']) + + @property + def limit_max_connections(self): + if self._values['limit_max_connections'] is None: + return None + return int(self._values['limit_max_connections']) + + @property + def limit_max_pps(self): + if self._values['limit_max_pps'] is None: + return None + return int(self._values['limit_max_pps']) + + @property + def limit_mem_available(self): + if self._values['limit_mem_available'] is None: + return None + return int(self._values['limit_mem_available']) + + +class GtmServersFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmServersFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_servers=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmServersParameters(client=self.client, params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/server".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmXWideIpsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'failureRcode': 'failure_rcode', + 'failureRcodeResponse': 'failure_rcode_response', + 'failureRcodeTtl': 'failure_rcode_ttl', + 'lastResortPool': 'last_resort_pool', + 'minimalResponse': 'minimal_response', + 'persistCidrIpv4': 'persist_cidr_ipv4', + 'persistCidrIpv6': 'persist_cidr_ipv6', + 'poolLbMode': 'pool_lb_mode', + 'ttlPersistence': 'ttl_persistence' + } + + returnables = [ + 'full_path', + 'description', + 'enabled', + 'disabled', + 'failure_rcode', + 'failure_rcode_response', + 'failure_rcode_ttl', + 'last_resort_pool', + 'minimal_response', + 'name', + 'persist_cidr_ipv4', + 'persist_cidr_ipv6', + 'pool_lb_mode', + 'ttl_persistence', + 'pools', + ] + + @property + def pools(self): + result = [] + if self._values['pools'] is None: + return [] + for pool in self._values['pools']: + del pool['nameReference'] + for x in ['order', 'ratio']: + if x in pool: + pool[x] = int(pool[x]) + result.append(pool) + return result + + @property + def failure_rcode_response(self): + return flatten_boolean(self._values['failure_rcode_response']) + + @property + def failure_rcode_ttl(self): + if self._values['failure_rcode_ttl'] is None: + return None + return int(self._values['failure_rcode_ttl']) + + @property + def persist_cidr_ipv4(self): + if self._values['persist_cidr_ipv4'] is None: + return None + return int(self._values['persist_cidr_ipv4']) + + @property + def persist_cidr_ipv6(self): + if self._values['persist_cidr_ipv6'] is None: + return None + return int(self._values['persist_cidr_ipv6']) + + @property + def ttl_persistence(self): + if self._values['ttl_persistence'] is None: + return None + return int(self._values['ttl_persistence']) + + +class GtmAWideIpsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmAWideIpsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_a_wide_ips=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXWideIpsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/a".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmAaaaWideIpsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmAaaaWideIpsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_aaaa_wide_ips=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXWideIpsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/aaaa".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmCnameWideIpsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmCnameWideIpsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_cname_wide_ips=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXWideIpsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/cname".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmMxWideIpsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmMxWideIpsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_mx_wide_ips=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXWideIpsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/mx".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmNaptrWideIpsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmNaptrWideIpsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_naptr_wide_ips=facts) + return result + + def _exec_module(self): + results = [] + if 'gtm' not in self.provisioned_modules: + return [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXWideIpsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/naptr".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmSrvWideIpsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmSrvWideIpsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_srv_wide_ips=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmXWideIpsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/srv".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class GtmTopologyRegionParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'regionMembers': 'region_members', + } + + returnables = [ + 'name', + 'full_path', + 'region_members', + ] + + def _string_to_dict(self, member): + result = dict() + item = member['name'].split(' ', 2) + if len(item) > 2: + result['negate'] = 'yes' + if item[1] == 'geoip-isp': + result['geo_isp'] = item[2] + else: + result[item[1]] = item[2] + return result + else: + if item[0] == 'geoip-isp': + result['geo_isp'] = item[1] + else: + result[item[0]] = item[1] + return result + + @property + def region_members(self): + result = [] + if self._values['region_members'] is None: + return [] + for member in self._values['region_members']: + result.append(self._string_to_dict(member)) + return result + + +class GtmTopologyRegionFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(GtmTopologyRegionFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(gtm_topology_regions=facts) + return result + + def _exec_module(self): + if 'gtm' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = GtmTopologyRegionParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/gtm/region".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class HttpMonitorsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultsFrom': 'parent', + 'adaptiveDivergenceType': 'adaptive_divergence_type', + 'adaptiveDivergenceValue': 'adaptive_divergence_value', + 'adaptiveLimit': 'adaptive_limit', + 'adaptiveSamplingTimespan': 'adaptive_sampling_timespan', + 'ipDscp': 'ip_dscp', + 'manualResume': 'manual_resume', + 'recv': 'receive_string', + 'recvDisable': 'receive_disable_string', + 'send': 'send_string', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'adaptive', + 'adaptive_divergence_type', + 'adaptive_divergence_value', + 'adaptive_limit', + 'adaptive_sampling_timespan', + 'destination', + 'interval', + 'ip_dscp', + 'manual_resume', + 'receive_string', + 'receive_disable_string', + 'reverse', + 'send_string', + 'time_until_up', + 'timeout', + 'transparent', + 'up_interval', + 'username', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def transparent(self): + return flatten_boolean(self._values['transparent']) + + @property + def reverse(self): + return flatten_boolean(self._values['reverse']) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def adaptive(self): + return flatten_boolean(self._values['adaptive']) + + +class HttpMonitorsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(HttpMonitorsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(http_monitors=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = HttpMonitorsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/http".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = response['items'] + return result + + +class HttpsMonitorsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultsFrom': 'parent', + 'adaptiveDivergenceType': 'adaptive_divergence_type', + 'adaptiveDivergenceValue': 'adaptive_divergence_value', + 'adaptiveLimit': 'adaptive_limit', + 'adaptiveSamplingTimespan': 'adaptive_sampling_timespan', + 'ipDscp': 'ip_dscp', + 'manualResume': 'manual_resume', + 'recv': 'receive_string', + 'recvDisable': 'receive_disable_string', + 'send': 'send_string', + 'sslProfile': 'ssl_profile', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'adaptive', + 'adaptive_divergence_type', + 'adaptive_divergence_value', + 'adaptive_limit', + 'adaptive_sampling_timespan', + 'destination', + 'interval', + 'ip_dscp', + 'manual_resume', + 'receive_string', + 'receive_disable_string', + 'reverse', + 'send_string', + 'ssl_profile', + 'time_until_up', + 'timeout', + 'transparent', + 'up_interval', + 'username', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def transparent(self): + return flatten_boolean(self._values['transparent']) + + @property + def reverse(self): + return flatten_boolean(self._values['reverse']) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def adaptive(self): + return flatten_boolean(self._values['adaptive']) + + +class HttpsMonitorsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(HttpsMonitorsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(https_monitors=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = HttpsMonitorsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/https".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = response['items'] + return result + + +class HttpProfilesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultsFrom': 'parent', + 'acceptXff': 'accept_xff', + 'explicitProxy': 'explicit_proxy', + 'insertXforwardedFor': 'insert_xforwarded_for', + 'lwsWidth': 'lws_max_columns', + 'oneconnectTransformations': 'onconnect_transformations', + 'proxyType': 'proxy_mode', + 'redirectRewrite': 'redirect_rewrite', + 'requestChunking': 'request_chunking', + 'responseChunking': 'response_chunking', + 'serverAgentName': 'server_agent_name', + 'viaRequest': 'via_request', + 'viaResponse': 'via_response', + 'pipeline': 'pipeline_action', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'accept_xff', + 'allow_truncated_redirects', + 'excess_client_headers', + 'excess_server_headers', + 'known_methods', + 'max_header_count', + 'max_header_size', + 'max_requests', + 'oversize_client_headers', + 'oversize_server_headers', + 'pipeline_action', + 'unknown_method', + 'default_connect_handling', + 'hsts_include_subdomains', + 'hsts_enabled', + 'insert_xforwarded_for', + 'lws_max_columns', + 'onconnect_transformations', + 'proxy_mode', + 'redirect_rewrite', + 'request_chunking', + 'response_chunking', + 'server_agent_name', + 'sflow_poll_interval', + 'sflow_sampling_rate', + 'via_request', + 'via_response', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def accept_xff(self): + return flatten_boolean(self._values['accept_xff']) + + @property + def excess_client_headers(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['excessClientHeaders'] is None: + return None + return self._values['enforcement']['excessClientHeaders'] + + @property + def excess_server_headers(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['excessServerHeaders'] is None: + return None + return self._values['enforcement']['excessServerHeaders'] + + @property + def known_methods(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['knownMethods'] is None: + return None + return self._values['enforcement']['knownMethods'] + + @property + def max_header_count(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['maxHeaderCount'] is None: + return None + return self._values['enforcement']['maxHeaderCount'] + + @property + def max_header_size(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['maxHeaderSize'] is None: + return None + return self._values['enforcement']['maxHeaderSize'] + + @property + def max_requests(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['maxRequests'] is None: + return None + return self._values['enforcement']['maxRequests'] + + @property + def oversize_client_headers(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['oversizeClientHeaders'] is None: + return None + return self._values['enforcement']['oversizeClientHeaders'] + + @property + def oversize_server_headers(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['oversizeServerHeaders'] is None: + return None + return self._values['enforcement']['oversizeServerHeaders'] + + @property + def allow_truncated_redirects(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['truncatedRedirects'] is None: + return None + return flatten_boolean(self._values['enforcement']['truncatedRedirects']) + + @property + def unknown_method(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['unknownMethod'] is None: + return None + return self._values['enforcement']['unknownMethod'] + + @property + def default_connect_handling(self): + if self._values['explicit_proxy'] is None: + return None + if self._values['explicit_proxy']['defaultConnectHandling'] is None: + return None + return self._values['explicit_proxy']['defaultConnectHandling'] + + @property + def hsts_include_subdomains(self): + if self._values['hsts'] is None: + return None + if self._values['hsts']['includeSubdomains'] is None: + return None + return flatten_boolean(self._values['hsts']['includeSubdomains']) + + @property + def hsts_enabled(self): + if self._values['hsts'] is None: + return None + if self._values['hsts']['mode'] is None: + return None + return flatten_boolean(self._values['hsts']['mode']) + + @property + def hsts_max_age(self): + if self._values['hsts'] is None: + return None + if self._values['hsts']['mode'] is None: + return None + return self._values['hsts']['maximumAge'] + + @property + def insert_xforwarded_for(self): + if self._values['insert_xforwarded_for'] is None: + return None + return flatten_boolean(self._values['insert_xforwarded_for']) + + @property + def onconnect_transformations(self): + if self._values['onconnect_transformations'] is None: + return None + return flatten_boolean(self._values['onconnect_transformations']) + + @property + def sflow_poll_interval(self): + if self._values['sflow'] is None: + return None + if self._values['sflow']['pollInterval'] is None: + return None + return self._values['sflow']['pollInterval'] + + @property + def sflow_sampling_rate(self): + if self._values['sflow'] is None: + return None + if self._values['sflow']['samplingRate'] is None: + return None + return self._values['sflow']['samplingRate'] + + +class HttpProfilesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(HttpProfilesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(http_profiles=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = HttpProfilesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class IappServicesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'deviceGroup': 'device_group', + 'inheritedDevicegroup': 'inherited_device_group', + 'inheritedTrafficGroup': 'inherited_traffic_group', + 'strictUpdates': 'strict_updates', + 'templateModified': 'template_modified', + 'trafficGroup': 'traffic_group', + } + + returnables = [ + 'full_path', + 'name', + 'device_group', + 'inherited_device_group', + 'inherited_traffic_group', + 'strict_updates', + 'template_modified', + 'traffic_group', + 'tables', + 'variables', + 'metadata', + 'lists', + 'description', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def inherited_device_group(self): + return flatten_boolean(self._values['inherited_device_group']) + + @property + def inherited_traffic_group(self): + return flatten_boolean(self._values['inherited_traffic_group']) + + @property + def strict_updates(self): + return flatten_boolean(self._values['strict_updates']) + + @property + def template_modified(self): + return flatten_boolean(self._values['template_modified']) + + +class IappServicesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(IappServicesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(iapp_services=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = IappServicesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/sys/application/service".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class IapplxPackagesParameters(BaseParameters): + api_map = { + 'packageName': 'package_name', + } + + returnables = [ + 'name', + 'version', + 'release', + 'arch', + 'package_name', + 'tags', + ] + + +class IapplxPackagesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(IapplxPackagesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(iapplx_packages=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['name']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = IapplxPackagesParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + params = dict(operation='QUERY') + uri = "https://{0}:{1}/mgmt/shared/iapp/package-management-tasks".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 resp.status not in [200, 201, 202] or 'code' in response and response['code'] not in [200, 201, 202]: + raise F5ModuleError(resp.content) + + status = self.wait_for_task(response['id']) + if status == 'FINISHED': + uri = "https://{0}:{1}/mgmt/shared/iapp/package-management-tasks/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + response['id'] + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status not in [200, 201, 202] or 'code' in response and response['code'] not in [200, 201, 202]: + raise F5ModuleError(resp.content) + + else: + raise F5ModuleError( + "An error occurred querying iAppLX packages." + ) + result = response['queryResponse'] + return result + + def wait_for_task(self, task_id): + uri = "https://{0}:{1}/mgmt/shared/iapp/package-management-tasks/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + task_id + ) + for x in range(0, 60): + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if response['status'] in ['FINISHED', 'FAILED']: + return response['status'] + time.sleep(1) + return response['status'] + + +class IcmpMonitorsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultsFrom': 'parent', + 'adaptiveDivergenceType': 'adaptive_divergence_type', + 'adaptiveDivergenceValue': 'adaptive_divergence_value', + 'adaptiveLimit': 'adaptive_limit', + 'adaptiveSamplingTimespan': 'adaptive_sampling_timespan', + 'manualResume': 'manual_resume', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'adaptive', + 'adaptive_divergence_type', + 'adaptive_divergence_value', + 'adaptive_limit', + 'adaptive_sampling_timespan', + 'destination', + 'interval', + 'manual_resume', + 'time_until_up', + 'timeout', + 'transparent', + 'up_interval', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def transparent(self): + return flatten_boolean(self._values['transparent']) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def adaptive(self): + return flatten_boolean(self._values['adaptive']) + + +class IcmpMonitorsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(IcmpMonitorsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(icmp_monitors=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = IcmpMonitorsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/icmp".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class InterfacesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'mediaActive': 'active_media_type', + 'flowControl': 'flow_control', + 'bundleSpeed': 'bundle_speed', + 'ifIndex': 'if_index', + 'macAddress': 'mac_address', + 'mediaSfp': 'media_sfp', + 'lldpAdmin': 'lldp_admin', + 'preferPort': 'prefer_port', + 'stpAutoEdgePort': 'stp_auto_edge_port', + 'stp': 'stp_enabled', + 'stpLinkType': 'stp_link_type' + } + + returnables = [ + 'full_path', + 'name', + 'active_media_type', + 'flow_control', + 'description', + 'bundle', + 'bundle_speed', + 'enabled', + 'if_index', + 'mac_address', + 'media_sfp', + 'lldp_admin', + 'mtu', + 'prefer_port', + 'sflow_poll_interval', + 'sflow_poll_interval_global', + 'stp_auto_edge_port', + 'stp_enabled', + 'stp_link_type' + ] + + @property + def stp_auto_edge_port(self): + return flatten_boolean(self._values['stp_auto_edge_port']) + + @property + def stp_enabled(self): + return flatten_boolean(self._values['stp_enabled']) + + @property + def sflow_poll_interval_global(self): + if self._values['sflow'] is None: + return None + if 'pollIntervalGlobal' in self._values['sflow']: + return self._values['sflow']['pollIntervalGlobal'] + + @property + def sflow_poll_interval(self): + if self._values['sflow'] is None: + return None + if 'pollInterval' in self._values['sflow']: + return self._values['sflow']['pollInterval'] + + @property + def mac_address(self): + if self._values['mac_address'] in [None, 'none']: + return None + return self._values['mac_address'] + + @property + def enabled(self): + return flatten_boolean(self._values['enabled']) + + +class InterfacesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(InterfacesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(interfaces=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = InterfacesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/net/interface".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}".format(self.module.params['data_increment'], skip) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class InternalDataGroupsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path' + } + + returnables = [ + 'full_path', + 'name', + 'type', + 'records' + ] + + +class InternalDataGroupsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(InternalDataGroupsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(internal_data_groups=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = InternalDataGroupsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class IrulesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'ignoreVerification': 'ignore_verification', + } + + returnables = [ + 'full_path', + 'name', + 'ignore_verification', + 'checksum', + 'definition', + 'signature' + ] + + @property + def checksum(self): + if self._values['apiAnonymous'] is None: + return None + pattern = r'definition-checksum\s(?P\w+)' + matches = re.search(pattern, self._values['apiAnonymous']) + if matches: + return matches.group('checksum') + + @property + def definition(self): + if self._values['apiAnonymous'] is None: + return None + pattern = r'(definition-(checksum|signature)\s[\w=\/+]+)' + result = re.sub(pattern, '', self._values['apiAnonymous']).strip() + if result: + return result + + @property + def signature(self): + if self._values['apiAnonymous'] is None: + return None + pattern = r'definition-signature\s(?P[\w=\/+]+)' + matches = re.search(pattern, self._values['apiAnonymous']) + if matches: + return matches.group('signature') + + @property + def ignore_verification(self): + if self._values['ignore_verification'] is None: + return 'no' + return 'yes' + + +class IrulesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(IrulesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(irules=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = IrulesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/rule".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class LicenseParameters(BaseParameters): + api_map = { + + } + + returnables = [ + 'license_start_date', + 'license_end_date', + 'licensed_on_date', + 'licensed_version', + 'max_permitted_version', + 'min_permitted_version', + 'platform_id', + 'registration_key', + 'service_check_date', + 'active_modules' + ] + + @property + def license_start_date(self): + if self._values['license'] is None: + return None + return self._values['license']['licenseStartDate']['description'] + + @property + def license_end_date(self): + if self._values['license'] is None: + return None + return self._values['license']['licenseEndDate']['description'] + + @property + def licensed_on_date(self): + if self._values['license'] is None: + return None + return self._values['license']['licensedOnDate']['description'] + + @property + def licensed_version(self): + if self._values['license'] is None: + return None + return self._values['license']['licensedVersion']['description'] + + @property + def max_permitted_version(self): + if self._values['license'] is None: + return None + return self._values['license']['maxPermittedVersion']['description'] + + @property + def min_permitted_version(self): + if self._values['license'] is None: + return None + return self._values['license']['minPermittedVersion']['description'] + + @property + def platform_id(self): + if self._values['license'] is None: + return None + return self._values['license']['platformId']['description'] + + @property + def registration_key(self): + if self._values['license'] is None: + return None + return self._values['license']['registrationKey']['description'] + + @property + def service_check_date(self): + if self._values['license'] is None: + return None + return self._values['license']['serviceCheckDate']['description'] + + @property + def active_modules(self): + if self._values['license'] is None: + return None + result = list() + license = self._values['license'] + for key in license: + if key.startswith("http"): + v = license[key]['nestedStats']['entries'] + for k in v.keys(): + addons = { + k2: v2['description'] + for k2, v2 in + v[k]['nestedStats']['entries'].items() + } + result.append(addons) + return result + + +class LicenseFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(LicenseFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(license=facts) + return result + + def _exec_module(self): + facts = self.read_facts() + result = facts.to_return() + return result + + def read_facts(self): + resource = self.read_collection_from_device() + params = LicenseParameters(params=resource) + return params + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/license/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = dict() + try: + result['license'] = response['entries']['https://localhost/mgmt/tm/sys/license/0']['nestedStats']['entries'] + return result + except KeyError: + return None + + +class LtmPoolsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'allowNat': 'allow_nat', + 'allowSnat': 'allow_snat', + 'ignorePersistedWeight': 'ignore_persisted_weight', + 'ipTosToClient': 'client_ip_tos', + 'ipTosToServer': 'server_ip_tos', + 'linkQosToClient': 'client_link_qos', + 'linkQosToServer': 'server_link_qos', + 'loadBalancingMode': 'lb_method', + 'minActiveMembers': 'minimum_active_members', + 'minUpMembers': 'minimum_up_members', + 'minUpMembersAction': 'minimum_up_members_action', + 'minUpMembersChecking': 'minimum_up_members_checking', + 'queueDepthLimit': 'queue_depth_limit', + 'queueOnConnectionLimit': 'queue_on_connection_limit', + 'queueTimeLimit': 'queue_time_limit', + 'reselectTries': 'reselect_tries', + 'serviceDownAction': 'service_down_action', + 'slowRampTime': 'slow_ramp_time', + 'monitor': 'monitors', + } + + returnables = [ + 'full_path', + 'name', + 'allow_nat', + 'allow_snat', + 'description', + 'ignore_persisted_weight', + 'client_ip_tos', + 'server_ip_tos', + 'client_link_qos', + 'server_link_qos', + 'lb_method', + 'minimum_active_members', + 'minimum_up_members', + 'minimum_up_members_action', + 'minimum_up_members_checking', + 'monitors', + 'queue_depth_limit', + 'queue_on_connection_limit', + 'queue_time_limit', + 'reselect_tries', + 'service_down_action', + 'slow_ramp_time', + 'priority_group_activation', + 'members', + 'metadata', + 'active_member_count', + 'available_member_count', + 'availability_status', + 'enabled_status', + 'status_reason', + 'all_max_queue_entry_age_ever', + 'all_avg_queue_entry_age', + 'all_queue_head_entry_age', + 'all_max_queue_entry_age_recently', + 'all_num_connections_queued_now', + 'all_num_connections_serviced', + 'pool_max_queue_entry_age_ever', + 'pool_avg_queue_entry_age', + 'pool_queue_head_entry_age', + 'pool_max_queue_entry_age_recently', + 'pool_num_connections_queued_now', + 'pool_num_connections_serviced', + 'current_sessions', + 'member_count', + 'total_requests', + 'server_side_bits_in', + 'server_side_bits_out', + 'server_side_current_connections', + 'server_side_max_connections', + 'server_side_pkts_in', + 'server_side_pkts_out', + 'server_side_total_connections', + ] + + @property + def active_member_count(self): + if 'availableMemberCnt' in self._values['stats']: + return int(self._values['stats']['activeMemberCnt']) + return None + + @property + def available_member_count(self): + if 'availableMemberCnt' in self._values['stats']: + return int(self._values['stats']['availableMemberCnt']) + return None + + @property + def all_max_queue_entry_age_ever(self): + return self._values['stats']['connqAll']['ageEdm'] + + @property + def all_avg_queue_entry_age(self): + return self._values['stats']['connqAll']['ageEma'] + + @property + def all_queue_head_entry_age(self): + return self._values['stats']['connqAll']['ageHead'] + + @property + def all_max_queue_entry_age_recently(self): + return self._values['stats']['connqAll']['ageMax'] + + @property + def all_num_connections_queued_now(self): + return self._values['stats']['connqAll']['depth'] + + @property + def all_num_connections_serviced(self): + return self._values['stats']['connqAll']['serviced'] + + @property + def availability_status(self): + return self._values['stats']['status']['availabilityState'] + + @property + def enabled_status(self): + return self._values['stats']['status']['enabledState'] + + @property + def status_reason(self): + return self._values['stats']['status']['statusReason'] + + @property + def pool_max_queue_entry_age_ever(self): + return self._values['stats']['connq']['ageEdm'] + + @property + def pool_avg_queue_entry_age(self): + return self._values['stats']['connq']['ageEma'] + + @property + def pool_queue_head_entry_age(self): + return self._values['stats']['connq']['ageHead'] + + @property + def pool_max_queue_entry_age_recently(self): + return self._values['stats']['connq']['ageMax'] + + @property + def pool_num_connections_queued_now(self): + return self._values['stats']['connq']['depth'] + + @property + def pool_num_connections_serviced(self): + return self._values['stats']['connq']['serviced'] + + @property + def current_sessions(self): + return self._values['stats']['curSessions'] + + @property + def member_count(self): + if 'memberCnt' in self._values['stats']: + return self._values['stats']['memberCnt'] + return None + + @property + def total_requests(self): + return self._values['stats']['totRequests'] + + @property + def server_side_bits_in(self): + return self._values['stats']['serverside']['bitsIn'] + + @property + def server_side_bits_out(self): + return self._values['stats']['serverside']['bitsOut'] + + @property + def server_side_current_connections(self): + return self._values['stats']['serverside']['curConns'] + + @property + def server_side_max_connections(self): + return self._values['stats']['serverside']['maxConns'] + + @property + def server_side_pkts_in(self): + return self._values['stats']['serverside']['pktsIn'] + + @property + def server_side_pkts_out(self): + return self._values['stats']['serverside']['pktsOut'] + + @property + def server_side_total_connections(self): + return self._values['stats']['serverside']['totConns'] + + @property + def ignore_persisted_weight(self): + return flatten_boolean(self._values['ignore_persisted_weight']) + + @property + def minimum_up_members_checking(self): + return flatten_boolean(self._values['minimum_up_members_checking']) + + @property + def queue_on_connection_limit(self): + return flatten_boolean(self._values['queue_on_connection_limit']) + + @property + def priority_group_activation(self): + """Returns the TMUI value for "Priority Group Activation" + + This value is identified as ``minActiveMembers`` in the REST API, so this + is just a convenience key for users of Ansible (where the ``bigip_virtual_server`` + parameter is called ``priority_group_activation``. + + Returns: + int: Priority number assigned to the pool members. + """ + return self._values['minimum_active_members'] + + @property + def metadata(self): + """Returns metadata associated with a pool + + An arbitrary amount of metadata may be associated with a pool. You typically + see this used in situations where the user wants to annotate a resource, maybe + in cases where an automation system is responsible for creating the resource. + + The metadata in the API is always stored as a list of dictionaries. We change + this to be a simple dictionary before it is returned to the user. + + Returns: + dict: A dictionary of key/value pairs where the key is the metadata name + and the value is the metadata value. + """ + if self._values['metadata'] is None: + return None + result = dict([(k['name'], k['value']) for k in self._values['metadata']]) + return result + + @property + def members(self): + if not self._values['members']: + return None + result = [] + for member in self._values['members']: + member['connection_limit'] = member.pop('connectionLimit', None) + member['dynamic_ratio'] = member.pop('dynamicRatio', None) + member['full_path'] = member.pop('fullPath', None) + member['inherit_profile'] = member.pop('inheritProfile', None) + member['priority_group'] = member.pop('priorityGroup', None) + member['rate_limit'] = member.pop('rateLimit', None) + + if 'fqdn' in member and 'autopopulate' in member['fqdn']: + if member['fqdn']['autopopulate'] == 'enabled': + member['fqdn_autopopulate'] = 'yes' + elif member['fqdn']['autopopulate'] == 'disabled': + member['fqdn_autopopulate'] = 'no' + del member['fqdn'] + + for key in ['ephemeral', 'inherit_profile', 'logging', 'rate_limit']: + tmp = flatten_boolean(member[key]) + member[key] = tmp + + if 'profiles' in member: + # Even though the ``profiles`` is a list, there is only ever 1 + member['encapsulation_profile'] = [x['name'] for x in member['profiles']][0] + del member['profiles'] + + if 'monitor' in member: + monitors = member.pop('monitor') + if monitors is not None: + try: + member['monitors'] = re.findall(r'/[\w-]+/[^\s}]+', monitors) + except Exception: + member['monitors'] = [monitors.strip()] + + session = member.pop('session') + state = member.pop('state') + + member['real_session'] = session + member['real_state'] = state + + if state in ['user-up', 'unchecked', 'fqdn-up-no-addr', 'fqdn-up'] and session in ['user-enabled']: + member['state'] = 'present' + elif state in ['user-down'] and session in ['user-disabled']: + member['state'] = 'forced_offline' + elif state in ['up', 'checking'] and session in ['monitor-enabled']: + member['state'] = 'present' + elif state in ['down'] and session in ['monitor-enabled']: + member['state'] = 'offline' + else: + member['state'] = 'disabled' + self._remove_internal_keywords(member) + member = dict([(k, v) for k, v in iteritems(member) if v is not None]) + result.append(member) + return result + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + try: + result = re.findall(r'/[\w-]+/[^\s}]+', self._values['monitors']) + return result + except Exception: + return [self._values['monitors'].strip()] + + +class LtmPoolsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(LtmPoolsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(ltm_pools=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + attrs = resource + members = self.read_member_from_device(attrs['fullPath']) + attrs['members'] = members + attrs['stats'] = self.read_stats_from_device(attrs['fullPath']) + params = LtmPoolsParameters(params=attrs) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + """Read the LTM pools collection from the device + + Note that sub-collection expansion does not work with LTM pools. Therefore, + one needs to query the ``members`` endpoint separately and add that to the + list of ``attrs`` before the full set of attributes is sent to the ``Parameters`` + class. + + Returns: + list: List of ``Pool`` objects + """ + uri = "https://{0}:{1}/mgmt/tm/ltm/pool".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + def read_member_from_device(self, full_path): + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=full_path) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + def read_stats_from_device(self, full_path): + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=full_path) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = parseStats(response) + try: + return result['stats'] + except KeyError: + return {} + + +class LtmPolicyParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'rulesReference': 'rules', + } + + returnables = [ + 'full_path', + 'name', + 'status', + 'description', + 'strategy', + 'rules', + 'requires', + 'controls', + ] + + def _handle_conditions(self, conditions): + result = [] + if conditions is None or 'items' not in conditions: + return result + for condition in conditions['items']: + tmp = dict() + tmp['case_insensitive'] = flatten_boolean(condition.pop('caseInsensitive', None)) + tmp['case_sensitive'] = flatten_boolean(condition.pop('caseSensitive', None)) + tmp['contains_string'] = flatten_boolean(condition.pop('contains', None)) + tmp['external'] = flatten_boolean(condition.pop('external', None)) + tmp['http_basic_auth'] = flatten_boolean(condition.pop('httpBasicAuth', None)) + tmp['http_host'] = flatten_boolean(condition.pop('httpHost', None)) + tmp['datagroup'] = condition.pop('datagroup', None) + tmp['tcp'] = flatten_boolean(condition.pop('tcp', None)) + tmp['remote'] = flatten_boolean(condition.pop('remote', None)) + tmp['matches'] = flatten_boolean(condition.pop('matches', None)) + tmp['address'] = flatten_boolean(condition.pop('address', None)) + tmp['present'] = flatten_boolean(condition.pop('present', None)) + tmp['proxy_connect'] = flatten_boolean(condition.pop('proxyConnect', None)) + tmp['proxy_request'] = flatten_boolean(condition.pop('proxyRequest', None)) + tmp['host'] = flatten_boolean(condition.pop('host', None)) + tmp['http_uri'] = flatten_boolean(condition.pop('httpUri', None)) + tmp['request'] = flatten_boolean(condition.pop('request', None)) + tmp['username'] = flatten_boolean(condition.pop('username', None)) + tmp['external'] = flatten_boolean(condition.pop('external', None)) + tmp['values'] = condition.pop('values', None) + tmp['all'] = flatten_boolean(condition.pop('all', None)) + result.append(self._filter_params(tmp)) + return result + + def _handle_actions(self, actions): + result = [] + if actions is None or 'items' not in actions: + return result + for action in actions['items']: + tmp = dict() + tmp['httpReply'] = flatten_boolean(action.pop('http_reply', None)) + tmp['redirect'] = flatten_boolean(action.pop('redirect', None)) + tmp['request'] = flatten_boolean(action.pop('request', None)) + tmp['location'] = action.pop('location', None) + result.append(self._filter_params(tmp)) + return result + + @property + def rules(self): + result = [] + if self._values['rules'] is None or 'items' not in self._values['rules']: + return result + for item in self._values['rules']['items']: + self._remove_internal_keywords(item) + item['conditions'] = self._handle_conditions(item.pop('conditionsReference', None)) + item['actions'] = self._handle_actions(item.pop('actionsReference', None)) + result.append(item) + return result + + +class LtmPolicyFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(LtmPolicyFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(ltm_policies=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = LtmPolicyParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class NodesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'connectionLimit': 'connection_limit', + 'dynamicRatio': 'dynamic_ratio', + 'rateLimit': 'rate_limit', + 'monitor': 'monitors' + } + + returnables = [ + 'full_path', + 'name', + 'ratio', + 'description', + 'connection_limit', + 'address', + 'dynamic_ratio', + 'rate_limit', + 'monitor_status', + 'session_status', + 'availability_status', + 'enabled_status', + 'status_reason', + 'monitor_rule', + 'monitors', + 'monitor_type', + 'fqdn_name', + 'fqdn_auto_populate', + 'fqdn_address_type', + 'fqdn_up_interval', + 'fqdn_down_interval', + ] + + @property + def fqdn_name(self): + if self._values['fqdn'] is None: + return None + return self._values['fqdn'].get('tmName', None) + + @property + def fqdn_auto_populate(self): + if self._values['fqdn'] is None: + return None + return flatten_boolean(self._values['fqdn'].get('autopopulate', None)) + + @property + def fqdn_address_type(self): + if self._values['fqdn'] is None: + return None + return self._values['fqdn'].get('addressFamily', None) + + @property + def fqdn_up_interval(self): + if self._values['fqdn'] is None: + return None + result = self._values['fqdn'].get('interval', None) + if result: + return int(result) + + @property + def fqdn_down_interval(self): + if self._values['fqdn'] is None: + return None + result = self._values['fqdn'].get('downInterval', None) + if result: + return int(result) + + @property + def monitors(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + return result + except Exception: + return [self._values['monitors']] + + @property + def monitor_type(self): + if self._values['monitors'] is None: + return None + pattern = r'min\s+\d+\s+of' + matches = re.search(pattern, self._values['monitors']) + if matches: + return 'm_of_n' + else: + return 'and_list' + + @property + def rate_limit(self): + if self._values['rate_limit'] is None: + return None + elif self._values['rate_limit'] == 'disabled': + return 0 + else: + return int(self._values['rate_limit']) + + @property + def monitor_status(self): + return self._values['stats']['monitorStatus'] + + @property + def session_status(self): + return self._values['stats']['sessionStatus'] + + @property + def availability_status(self): + return self._values['stats']['status']['availabilityState'] + + @property + def enabled_status(self): + return self._values['stats']['status']['enabledState'] + + @property + def status_reason(self): + return self._values['stats']['status']['statusReason'] + + @property + def monitor_rule(self): + return self._values['stats']['monitorRule'] + + +class NodesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(NodesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(nodes=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + attrs = resource + attrs['stats'] = self.read_stats_from_device(attrs['fullPath']) + params = NodesParameters(params=attrs) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/node".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + def read_stats_from_device(self, full_path): + uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=full_path) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = parseStats(response) + try: + return result['stats'] + except KeyError: + return {} + + +class OneConnectProfilesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'clientTimeout': 'client_timeout', + 'defaultsFrom': 'parent', + 'idleTimeoutOverride': 'idle_timeout_override', + 'limitType': 'limit_type', + 'maxAge': 'max_age', + 'maxReuse': 'max_reuse', + 'maxSize': 'max_size', + 'sharePools': 'share_pools', + 'sourceMask': 'source_mask', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'idle_timeout_override', + 'limit_type', + 'max_age', + 'max_reuse', + 'max_size', + 'share_pools', + 'source_mask', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def idle_timeout_override(self): + if self._values['idle_timeout_override'] is None: + return None + elif self._values['idle_timeout_override'] == 'disabled': + return 0 + elif self._values['idle_timeout_override'] == 'indefinite': + return 4294967295 + return int(self._values['idle_timeout_override']) + + @property + def share_pools(self): + return flatten_boolean(self._values['share_pools']) + + +class OneConnectProfilesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(OneConnectProfilesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(oneconnect_profiles=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = OneConnectProfilesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class PartitionParameters(BaseParameters): + api_map = { + 'defaultRouteDomain': 'default_route_domain', + 'fullPath': 'full_path', + } + + returnables = [ + 'name', + 'full_path', + 'description', + 'default_route_domain' + ] + + +class PartitionFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(PartitionFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(partitions=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = PartitionParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/partition".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class ProvisionInfoParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'cpuRatio': 'cpu_ratio', + 'diskRatio': 'disk_ratio', + 'memoryRatio': 'memory_ratio', + } + + returnables = [ + 'full_path', + 'name', + 'cpu_ratio', + 'disk_ratio', + 'memory_ratio', + 'level' + ] + + +class ProvisionInfoFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(ProvisionInfoFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(provision_info=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = ProvisionInfoParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/provision".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class RouteDomainParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'bwcPolicy': 'bwc_policy', + 'connectionLimit': 'connection_limit', + 'flowEvictionPolicy': 'flow_eviction_policy', + 'servicePolicy': 'service_policy', + 'routingProtocol': 'routing_protocol', + } + + returnables = [ + 'name', + 'id', + 'full_path', + 'parent', + 'bwc_policy', + 'connection_limit', + 'description', + 'flow_eviction_policy', + 'service_policy', + 'strict', + 'routing_protocol', + 'vlans', + ] + + @property + def strict(self): + return flatten_boolean(self._values['strict']) + + @property + def connection_limit(self): + if self._values['connection_limit'] is None: + return None + return int(self._values['connection_limit']) + + @property + def id(self): + if self._values['id'] is None: + return None + return int(self._values['id']) + + +class RouteDomainFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(RouteDomainFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(route_domains=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = RouteDomainParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/net/route-domain".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class SelfIpsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'trafficGroup': 'traffic_group', + 'servicePolicy': 'service_policy', + 'allowService': 'allow_access_list', + 'inheritedTrafficGroup': 'traffic_group_inherited' + } + + returnables = [ + 'full_path', + 'name', + 'address', + 'description', + 'netmask', + 'netmask_cidr', + 'floating', + 'traffic_group', + 'service_policy', + 'vlan', + 'allow_access_list', + 'traffic_group_inherited' + ] + + @property + def address(self): + parts = self._values['address'].split('/') + return parts[0] + + @property + def netmask(self): + result = None + parts = self._values['address'].split('/') + if is_valid_ip(parts[0]): + ip = ip_interface(u'{0}'.format(self._values['address'])) + result = ip.netmask + return str(result) + + @property + def netmask_cidr(self): + parts = self._values['address'].split('/') + return int(parts[1]) + + @property + def traffic_group_inherited(self): + if self._values['traffic_group_inherited'] is None: + return None + elif self._values['traffic_group_inherited'] in [False, 'false']: + # BIG-IP appears to store this as a string. This is a bug, so we handle both + # cases here. + return 'no' + else: + return 'yes' + + @property + def floating(self): + if self._values['floating'] is None: + return None + elif self._values['floating'] == 'disabled': + return 'no' + else: + return 'yes' + + +class SelfIpsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SelfIpsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(self_ips=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = SelfIpsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/net/self".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class ServerSslProfilesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'alertTimeout': 'alert_timeout', + 'allowExpiredCrl': 'allow_expired_crl', + 'authenticate': 'authentication_frequency', + 'authenticateDepth': 'authenticate_depth', + 'authenticateName': 'authenticate_name', + 'bypassOnClientCertFail': 'bypass_on_client_cert_fail', + 'bypassOnHandshakeAlert': 'bypass_on_handshake_alert', + 'c3dCaCert': 'c3d_ca_cert', + 'c3dCaKey': 'c3d_ca_key', + 'c3dCertExtensionIncludes': 'c3d_cert_extension_includes', + 'c3dCertLifespan': 'c3d_cert_lifespan', + 'caFile': 'ca_file', + 'cacheSize': 'cache_size', + 'cacheTimeout': 'cache_timeout', + 'cipherGroup': 'cipher_group', + 'crlFile': 'crl_file', + 'defaultsFrom': 'parent', + 'expireCertResponseControl': 'expire_cert_response_control', + 'genericAlert': 'generic_alert', + 'handshakeTimeout': 'handshake_timeout', + 'maxActiveHandshakes': 'max_active_handshakes', + 'modSslMethods': 'mod_ssl_methods', + 'tmOptions': 'options', + 'peerCertMode': 'peer_cert_mode', + 'proxySsl': 'proxy_ssl', + 'proxySslPassthrough': 'proxy_ssl_passthrough', + 'renegotiatePeriod': 'renegotiate_period', + 'renegotiateSize': 'renegotiate_size', + 'retainCertificate': 'retain_certificate', + 'secureRenegotiation': 'secure_renegotiation', + 'serverName': 'server_name', + 'sessionMirroring': 'session_mirroring', + 'sessionTicket': 'session_ticket', + 'sniDefault': 'sni_default', + 'sniRequire': 'sni_require', + 'sslC3d': 'ssl_c3d', + 'sslForwardProxy': 'ssl_forward_proxy_enabled', + 'sslForwardProxyBypass': 'ssl_forward_proxy_bypass', + 'sslSignHash': 'ssl_sign_hash', + 'strictResume': 'strict_resume', + 'uncleanShutdown': 'unclean_shutdown', + 'untrustedCertResponseControl': 'untrusted_cert_response_control' + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'unclean_shutdown', + 'strict_resume', + 'ssl_forward_proxy_enabled', + 'ssl_forward_proxy_bypass', + 'sni_default', + 'sni_require', + 'ssl_c3d', + 'session_mirroring', + 'session_ticket', + 'mod_ssl_methods', + 'allow_expired_crl', + 'retain_certificate', + 'mode', + 'bypass_on_client_cert_fail', + 'bypass_on_handshake_alert', + 'generic_alert', + 'renegotiation', + 'proxy_ssl', + 'proxy_ssl_passthrough', + 'peer_cert_mode', + 'untrusted_cert_response_control', + 'ssl_sign_hash', + 'server_name', + 'secure_renegotiation', + 'renegotiate_size', + 'renegotiate_period', + 'options', + 'ocsp', + 'max_active_handshakes', + 'key', + 'handshake_timeout', + 'expire_cert_response_control', + 'cert', + 'chain', + 'authentication_frequency', + 'ciphers', + 'cipher_group', + 'crl_file', + 'cache_timeout', + 'cache_size', + 'ca_file', + 'c3d_cert_lifespan', + 'alert_timeout', + 'c3d_ca_key', + 'authenticate_depth', + 'authenticate_name', + 'c3d_ca_cert', + 'c3d_cert_extension_includes', + ] + + @property + def c3d_cert_extension_includes(self): + if self._values['c3d_cert_extension_includes'] is None: + return None + if len(self._values['c3d_cert_extension_includes']) == 0: + return None + self._values['c3d_cert_extension_includes'].sort() + return self._values['c3d_cert_extension_includes'] + + @property + def options(self): + if self._values['options'] is None: + return None + if len(self._values['options']) == 0: + return None + self._values['options'].sort() + return self._values['options'] + + @property + def c3d_ca_cert(self): + if self._values['c3d_ca_cert'] in [None, 'none']: + return None + return self._values['c3d_ca_cert'] + + @property + def ocsp(self): + if self._values['ocsp'] in [None, 'none']: + return None + return self._values['ocsp'] + + @property + def server_name(self): + if self._values['server_name'] in [None, 'none']: + return None + return self._values['server_name'] + + @property + def cipher_group(self): + if self._values['cipher_group'] is None: + return None + if self._values['cipher_group'] == 'none': + return 'none' + return self._values['cipher_group'] + + @property + def authenticate_name(self): + if self._values['authenticate_name'] in [None, 'none']: + return None + return self._values['authenticate_name'] + + @property + def c3d_ca_key(self): + if self._values['c3d_ca_key'] in [None, 'none']: + return None + return self._values['c3d_ca_key'] + + @property + def ca_file(self): + if self._values['ca_file'] in [None, 'none']: + return None + return self._values['ca_file'] + + @property + def crl_file(self): + if self._values['crl_file'] in [None, 'none']: + return None + return self._values['crl_file'] + + @property + def authentication_frequency(self): + if self._values['authentication_frequency'] in [None, 'none']: + return None + return self._values['authentication_frequency'] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def proxy_ssl_passthrough(self): + return flatten_boolean(self._values['proxy_ssl_passthrough']) + + @property + def proxy_ssl(self): + return flatten_boolean(self._values['proxy_ssl']) + + @property + def generic_alert(self): + return flatten_boolean(self._values['generic_alert']) + + @property + def renegotiation(self): + return flatten_boolean(self._values['renegotiation']) + + @property + def bypass_on_handshake_alert(self): + return flatten_boolean(self._values['bypass_on_handshake_alert']) + + @property + def bypass_on_client_cert_fail(self): + return flatten_boolean(self._values['bypass_on_client_cert_fail']) + + @property + def mode(self): + return flatten_boolean(self._values['mode']) + + @property + def retain_certificate(self): + return flatten_boolean(self._values['retain_certificate']) + + @property + def allow_expired_crl(self): + return flatten_boolean(self._values['allow_expired_crl']) + + @property + def mod_ssl_methods(self): + return flatten_boolean(self._values['mod_ssl_methods']) + + @property + def session_ticket(self): + return flatten_boolean(self._values['session_ticket']) + + @property + def session_mirroring(self): + return flatten_boolean(self._values['session_mirroring']) + + @property + def unclean_shutdown(self): + return flatten_boolean(self._values['unclean_shutdown']) + + @property + def strict_resume(self): + return flatten_boolean(self._values['strict_resume']) + + @property + def ssl_forward_proxy_enabled(self): + return flatten_boolean(self._values['ssl_forward_proxy_enabled']) + + @property + def ssl_forward_proxy_bypass(self): + return flatten_boolean(self._values['ssl_forward_proxy_bypass']) + + @property + def sni_default(self): + return flatten_boolean(self._values['sni_default']) + + @property + def sni_require(self): + return flatten_boolean(self._values['sni_require']) + + @property + def ssl_c3d(self): + return flatten_boolean(self._values['ssl_c3d']) + + +class ServerSslProfilesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(ServerSslProfilesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(server_ssl_profiles=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = ServerSslProfilesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/server-ssl".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class SoftwareVolumesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'basebuild': 'base_build', + } + + returnables = [ + 'full_path', + 'name', + 'active', + 'base_build', + 'build', + 'product', + 'status', + 'version', + 'install_volume', + 'default_boot_location' + ] + + @property + def install_volume(self): + if self._values['media'] is None: + return None + return self._values['media'].get('name', None) + + @property + def default_boot_location(self): + if self._values['media'] is None: + return None + return flatten_boolean(self._values['media'].get('defaultBootLocation', None)) + + @property + def active(self): + if self._values['active'] is True: + return 'yes' + return 'no' + + +class SoftwareVolumesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SoftwareVolumesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(software_volumes=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = SoftwareVolumesParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/software/volume".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class SoftwareHotfixesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + } + + returnables = [ + 'name', + 'full_path', + 'build', + 'checksum', + 'id', + 'product', + 'title', + 'verified', + 'version', + ] + + +class SoftwareHotfixesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SoftwareHotfixesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(software_hotfixes=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = SoftwareHotfixesParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/software/hotfix".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class SoftwareImagesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'buildDate': 'build_date', + 'fileSize': 'file_size', + 'lastModified': 'last_modified', + } + + returnables = [ + 'name', + 'full_path', + 'build', + 'build_date', + 'checksum', + 'file_size', + 'last_modified', + 'product', + 'verified', + 'version', + ] + + @property + def file_size(self): + if self._values['file_size'] is None: + return None + matches = re.match(r'\d+', self._values['file_size']) + if matches: + return int(matches.group(0)) + + @property + def build_date(self): + """Normalizes the build_date string + + The ISOs usually ship with a broken format + + ex: Tue May 15 15 26 30 PDT 2018 + + This will re-format that time so that it looks like ISO 8601 without + microseconds + + ex: 2018-05-15T15:26:30 + + :return: + """ + if self._values['build_date'] is None: + return None + + d = self._values['build_date'].split(' ') + + # This removes the timezone portion from the string. This is done + # because Python has awfule tz parsing and strptime doesnt work with + # all timezones in %Z; it only uses the timezones found in time.tzname + d.pop(6) + + result = datetime.datetime.strptime(' '.join(d), '%a %b %d %H %M %S %Y').isoformat() + return result + + @property + def last_modified(self): + """Normalizes the last_modified string + + The strings that the system reports look like the following + + ex: Tue May 15 15:26:30 2018 + + This property normalizes this value to be isoformat + + ex: 2018-05-15T15:26:30 + + :return: + """ + if self._values['last_modified'] is None: + return None + result = datetime.datetime.strptime(self._values['last_modified'], '%a %b %d %H:%M:%S %Y').isoformat() + return result + + +class SoftwareImagesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SoftwareImagesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(software_images=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = SoftwareImagesParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/software/image".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class SslCertificatesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'keyType': 'key_type', + 'certificateKeySize': 'key_size', + 'systemPath': 'system_path', + 'checksum': 'sha1_checksum', + 'lastUpdateTime': 'last_update_time', + 'isBundle': 'is_bundle', + 'expirationString': 'expiration_date', + 'expirationDate': 'expiration_timestamp', + 'createTime': 'create_time', + 'subjectAlternativeName': 'subject_alternative_name', + 'serialNumber': 'serial_no', + } + + returnables = [ + 'full_path', + 'name', + 'key_type', + 'key_size', + 'system_path', + 'sha1_checksum', + 'subject', + 'last_update_time', + 'issuer', + 'is_bundle', + 'fingerprint', + 'expiration_date', + 'expiration_timestamp', + 'create_time', + 'subject_alternative_name', + 'serial_no', + ] + + @property + def sha1_checksum(self): + if self._values['sha1_checksum'] is None: + return None + parts = self._values['sha1_checksum'].split(':') + return parts[2] + + @property + def is_bundle(self): + if self._values['sha1_checksum'] is None: + return None + if self._values['is_bundle'] in BOOLEANS_TRUE: + return 'yes' + return 'no' + + +class SslCertificatesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SslCertificatesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(ssl_certs=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = SslCertificatesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class SslKeysParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'keyType': 'key_type', + 'keySize': 'key_size', + 'securityType': 'security_type', + 'systemPath': 'system_path', + 'checksum': 'sha1_checksum', + } + + returnables = [ + 'full_path', + 'name', + 'key_type', + 'key_size', + 'security_type', + 'system_path', + 'sha1_checksum', + ] + + @property + def sha1_checksum(self): + if self._values['sha1_checksum'] is None: + return None + parts = self._values['sha1_checksum'].split(':') + return parts[2] + + +class SslKeysFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SslKeysFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(ssl_keys=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = SslKeysParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class SystemDbParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultValue': 'default', + 'scfConfig': 'scf_config', + 'valueRange': 'value_range' + } + + returnables = [ + 'name', + 'full_path', + 'default', + 'scf_config', + 'value', + 'value_range' + ] + + +class SyncStatusParameters(BaseParameters): + api_map = { + } + + returnables = [ + 'color', + 'details', + 'mode', + 'recommended_action', + 'status', + 'summary', + ] + + @property + def color(self): + result = self._values.get('color', {}).get('description', "") + if result.strip(): + return result + return "" + + @property + def details(self): + result = [] + details = (self._values.get('https://localhost/mgmt/tm/cm/syncStatus/0/details', {}) + .get('nestedStats', {}) + .get('entries', {})) + for entry in details.keys(): + result.append( + details[entry].get('nestedStats', {}) + .get('entries', {}) + .get('details', {}) + .get('description', "") + ) + result.reverse() + return result + + @property + def mode(self): + result = self._values.get('mode', {}).get('description', "") + if result.strip(): + return result + return "" + + @property + def status(self): + result = self._values.get('status', {}).get('description', "") + if result.strip(): + return result + return "" + + @property + def summary(self): + result = self._values.get('summary', {}).get('description', "") + if result.strip(): + return result + return "" + + @property + def recommended_action(self): + for entry in self.details: + match = re.match(r".*[Rr]ecommended action:\s(.*)$", entry) + if match: + return match.group(1) + return "" + + +class SyncStatusFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SyncStatusFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(sync_status=facts) + return result + + def _exec_module(self): + facts = self.read_facts() + attrs = facts.to_return() + result = [attrs] + return result + + def read_facts(self): + collection = self.read_collection_from_device() + return SyncStatusParameters(params=collection) + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cm/sync-status".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = response.get('entries', {}) \ + .get('https://localhost/mgmt/tm/cm/sync-status/0', {}) \ + .get('nestedStats', {}) \ + .get('entries') + return result + + +class SystemDbFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SystemDbFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(system_db=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = SystemDbParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/db".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class SystemInfoParameters(BaseParameters): + api_map = { + + } + + returnables = [ + 'base_mac_address', + 'marketing_name', + 'time', + 'hardware_information', + 'product_information', + 'package_edition', + 'package_version', + 'product_code', + 'product_build', + 'product_built', + 'product_build_date', + 'product_changelist', + 'product_jobid', + 'product_version', + 'uptime', + 'chassis_serial', + 'host_board_part_revision', + 'host_board_serial', + 'platform', + 'switch_board_part_revision', + 'switch_board_serial' + ] + + @property + def chassis_serial(self): + if self._values['system-info'] is None: + return None + if 'bigipChassisSerialNum' not in self._values['system-info'][0]: + return None + return self._values['system-info'][0]['bigipChassisSerialNum'] + + @property + def switch_board_serial(self): + if self._values['system-info'] is None: + return None + if 'switchBoardSerialNum' not in self._values['system-info'][0]: + return None + if self._values['system-info'][0]['switchBoardSerialNum'].strip() == '': + return None + return self._values['system-info'][0]['switchBoardSerialNum'] + + @property + def switch_board_part_revision(self): + if self._values['system-info'] is None: + return None + if 'switchBoardPartRevNum' not in self._values['system-info'][0]: + return None + if self._values['system-info'][0]['switchBoardPartRevNum'].strip() == '': + return None + return self._values['system-info'][0]['switchBoardPartRevNum'] + + @property + def platform(self): + if self._values['system-info'] is None: + return None + return self._values['system-info'][0]['platform'] + + @property + def host_board_serial(self): + if self._values['system-info'] is None: + return None + if 'hostBoardSerialNum' not in self._values['system-info'][0]: + return None + if self._values['system-info'][0]['hostBoardSerialNum'].strip() == '': + return None + return self._values['system-info'][0]['hostBoardSerialNum'] + + @property + def host_board_part_revision(self): + if self._values['system-info'] is None: + return None + if 'hostBoardPartRevNum' not in self._values['system-info'][0]: + return None + if self._values['system-info'][0]['hostBoardPartRevNum'].strip() == '': + return None + return self._values['system-info'][0]['hostBoardPartRevNum'] + + @property + def package_edition(self): + return self._values['Edition'] + + @property + def package_version(self): + return 'Build {0} - {1}'.format(self._values['Build'], self._values['Date']) + + @property + def product_build(self): + return self._values['Build'] + + @property + def product_build_date(self): + return self._values['Date'] + + @property + def product_built(self): + if 'Built' in self._values['version_info']: + return int(self._values['version_info']['Built']) + + @property + def product_changelist(self): + if 'Changelist' in self._values['version_info']: + return int(self._values['version_info']['Changelist']) + + @property + def product_jobid(self): + if 'JobID' in self._values['version_info']: + return int(self._values['version_info']['JobID']) + + @property + def product_code(self): + return self._values['Product'] + + @property + def product_version(self): + return self._values['Version'] + + @property + def hardware_information(self): + if self._values['hardware-version'] is None: + return None + self._transform_name_attribute(self._values['hardware-version']) + result = [v for k, v in iteritems(self._values['hardware-version'])] + return result + + def _transform_name_attribute(self, entry): + if isinstance(entry, dict): + for k, v in list(entry.items()): + if k == 'tmName': + entry['name'] = entry.pop('tmName') + self._transform_name_attribute(v) + elif isinstance(entry, list): + for k in entry: + if k == 'tmName': + entry['name'] = entry.pop('tmName') + self._transform_name_attribute(k) + else: + return + + @property + def time(self): + if self._values['fullDate'] is None: + return None + date = datetime.datetime.strptime(self._values['fullDate'], "%Y-%m-%dT%H:%M:%SZ") + result = dict( + day=date.day, + hour=date.hour, + minute=date.minute, + month=date.month, + second=date.second, + year=date.year + ) + return result + + @property + def marketing_name(self): + if self._values['platform'] is None: + return None + return self._values['platform'][0]['marketingName'] + + @property + def base_mac_address(self): + if self._values['platform'] is None: + return None + return self._values['platform'][0]['baseMac'] + + +class SystemInfoFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SystemInfoFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(system_info=facts) + return result + + def _exec_module(self): + facts = self.read_facts() + results = facts.to_return() + return results + + def read_facts(self): + collection = self.read_collection_from_device() + params = SystemInfoParameters(params=collection) + return params + + def read_collection_from_device(self): + result = dict() + tmp = self.read_hardware_info_from_device() + if tmp: + result.update(tmp) + + tmp = self.read_clock_info_from_device() + if tmp: + result.update(tmp) + + tmp = self.read_version_info_from_device() + if tmp: + result.update(tmp) + + tmp = self.read_uptime_info_from_device() + if tmp: + result.update(tmp) + + tmp = self.read_version_file_info_from_device() + if tmp: + result.update(tmp) + + return result + + def read_version_file_info_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "cat /VERSION"' + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + try: + pattern = r'^(?P(Product|Build|Sequence|BaseBuild|Edition|Date|Built|Changelist|JobID))\:(?P.*)' + result = response['commandResult'].strip() + except KeyError: + return None + + if 'No such file or directory' in result: + return None + + lines = response['commandResult'].split("\n") + result = dict() + for line in lines: + if not line: + continue + matches = re.match(pattern, line) + if matches: + result[matches.group('key')] = matches.group('value').strip() + + if result: + return dict( + version_info=result + ) + + def read_uptime_info_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "cat /proc/uptime"' + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + try: + parts = response['commandResult'].strip().split(' ') + return dict( + uptime=math.floor(float(parts[0])) + ) + except KeyError: + pass + + def read_hardware_info_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/hardware".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = parseStats(response) + return result + + def read_clock_info_from_device(self): + """Parses clock info from the REST API + + The clock stat returned from the REST API (at the time of 13.1.0.7) + is similar to the following. + + { + "kind": "tm:sys:clock:clockstats", + "selfLink": "https://localhost/mgmt/tm/sys/clock?ver=13.1.0.4", + "entries": { + "https://localhost/mgmt/tm/sys/clock/0": { + "nestedStats": { + "entries": { + "fullDate": { + "description": "2018-06-05T13:38:33Z" + } + } + } + } + } + } + + Parsing this data using the ``parseStats`` method, yields a list of + the clock stats in a format resembling that below. + + [{'fullDate': '2018-06-05T13:41:05Z'}] + + Therefore, this method cherry-picks the first entry from this list + and returns it. There can be no other items in this list. + + Returns: + A dict mapping keys to the corresponding clock stats. For + example: + + {'fullDate': '2018-06-05T13:41:05Z'} + + There should never not be a clock stat, unless by chance it + is removed from the API in the future, or changed to a different + API endpoint. + + Raises: + F5ModuleError: A non-successful HTTP code was returned or a JSON + response was not found. + """ + uri = "https://{0}:{1}/mgmt/tm/sys/clock".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = parseStats(response) + return result[0] + + def read_version_info_from_device(self): + """Parses version info from the REST API + + The version stat returned from the REST API (at the time of 13.1.0.7) + is similar to the following. + + { + "kind": "tm:sys:version:versionstats", + "selfLink": "https://localhost/mgmt/tm/sys/version?ver=13.1.0.4", + "entries": { + "https://localhost/mgmt/tm/sys/version/0": { + "nestedStats": { + "entries": { + "Build": { + "description": "0.0.6" + }, + "Date": { + "description": "Tue Mar 13 20:10:42 PDT 2018" + }, + "Edition": { + "description": "Point Release 4" + }, + "Product": { + "description": "BIG-IP" + }, + "Title": { + "description": "Main Package" + }, + "Version": { + "description": "13.1.0.4" + } + } + } + } + } + } + + Parsing this data using the ``parseStats`` method, yields a list of + the clock stats in a format resembling that below. + + [{'Build': '0.0.6', 'Date': 'Tue Mar 13 20:10:42 PDT 2018', + 'Edition': 'Point Release 4', 'Product': 'BIG-IP', 'Title': 'Main Package', + 'Version': '13.1.0.4'}] + + Therefore, this method cherry-picks the first entry from this list + and returns it. There can be no other items in this list. + + Returns: + A dict mapping keys to the corresponding clock stats. For + example: + + {'Build': '0.0.6', 'Date': 'Tue Mar 13 20:10:42 PDT 2018', + 'Edition': 'Point Release 4', 'Product': 'BIG-IP', 'Title': 'Main Package', + 'Version': '13.1.0.4'} + + There should never not be a version stat, unless by chance it + is removed from the API in the future, or changed to a different + API endpoint. + + Raises: + F5ModuleError: A non-successful HTTP code was returned or a JSON + response was not found. + """ + uri = "https://{0}:{1}/mgmt/tm/sys/version".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = parseStats(response) + return result[0] + + +class TSParameters(BaseParameters): + api_map = { + } + + returnables = [ + + ] + + +class TSFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + self.installed_packages = packages_installed(self.client) + super(TSFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(ts_config=facts) + return result + + def _exec_module(self): + if 'ts' not in self.installed_packages: + return [] + facts = self.read_facts() + return facts + + def read_facts(self): + collection = self.read_collection_from_device() + return collection + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/shared/telemetry/declare".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'message' not in response: + return [] + result = dict() + result['declaration'] = response['declaration'] + return result + + +class TcpMonitorsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultsFrom': 'parent', + 'adaptiveDivergenceType': 'adaptive_divergence_type', + 'adaptiveDivergenceValue': 'adaptive_divergence_value', + 'adaptiveLimit': 'adaptive_limit', + 'adaptiveSamplingTimespan': 'adaptive_sampling_timespan', + 'ipDscp': 'ip_dscp', + 'manualResume': 'manual_resume', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'adaptive', + 'adaptive_divergence_type', + 'adaptive_divergence_value', + 'adaptive_limit', + 'adaptive_sampling_timespan', + 'destination', + 'interval', + 'ip_dscp', + 'manual_resume', + 'reverse', + 'time_until_up', + 'timeout', + 'transparent', + 'up_interval', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def transparent(self): + return flatten_boolean(self._values['transparent']) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def adaptive(self): + return flatten_boolean(self._values['adaptive']) + + @property + def reverse(self): + return flatten_boolean(self._values['reverse']) + + +class TcpMonitorsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(TcpMonitorsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(tcp_monitors=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = TcpMonitorsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class TcpHalfOpenMonitorsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultsFrom': 'parent', + 'manualResume': 'manual_resume', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'destination', + 'interval', + 'manual_resume', + 'time_until_up', + 'timeout', + 'transparent', + 'up_interval', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def transparent(self): + return flatten_boolean(self._values['transparent']) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + +class TcpHalfOpenMonitorsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(TcpHalfOpenMonitorsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(tcp_half_open_monitors=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = TcpHalfOpenMonitorsParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp-half-open".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class TcpProfilesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'defaultsFrom': 'parent', + 'ackOnPush': 'ack_on_push', + 'autoProxyBufferSize': 'auto_proxy_buffer', + 'autoReceiveWindowSize': 'auto_receive_window', + 'autoSendBufferSize': 'auto_send_buffer', + 'closeWaitTimeout': 'close_wait', + 'cmetricsCache': 'congestion_metrics_cache', + 'cmetricsCacheTimeout': 'congestion_metrics_cache_timeout', + 'congestionControl': 'congestion_control', + 'deferredAccept': 'deferred_accept', + 'delayWindowControl': 'delay_window_control', + 'delayedAcks': 'delayed_acks', + 'earlyRetransmit': 'early_retransmit', + 'ecn': 'explicit_congestion_notification', + 'enhancedLossRecovery': 'enhanced_loss_recovery', + 'fastOpen': 'fast_open', + 'fastOpenCookieExpiration': 'fast_open_cookie_expiration', + 'finWaitTimeout': 'fin_wait_1', + 'finWait_2Timeout': 'fin_wait_2', + 'idleTimeout': 'idle_timeout', + 'initCwnd': 'initial_congestion_window_size', + 'initRwnd': 'initial_receive_window_size', + 'ipDfMode': 'dont_fragment_flag', + 'ipTosToClient': 'ip_tos', + 'ipTtlMode': 'time_to_live', + 'ipTtlV4': 'time_to_live_v4', + 'ipTtlV6': 'time_to_live_v6', + 'keepAliveInterval': 'keep_alive_interval', + 'limitedTransmit': 'limited_transmit_recovery', + 'linkQosToClient': 'link_qos', + 'maxRetrans': 'max_segment_retrans', + 'synMaxRetrans': 'max_syn_retrans', + 'rexmtThresh': 'retransmit_threshold', + 'maxSegmentSize': 'max_segment_size', + 'md5Signature': 'md5_signature', + 'minimumRto': 'minimum_rto', + 'mptcp': 'multipath_tcp', + 'mptcpCsum': 'mptcp_checksum', + 'mptcpCsumVerify': 'mptcp_checksum_verify', + 'mptcpFallback': 'mptcp_fallback', + 'mptcpFastjoin': 'mptcp_fast_join', + 'mptcpIdleTimeout': 'mptcp_idle_timeout', + 'mptcpJoinMax': 'mptcp_join_max', + 'mptcpMakeafterbreak': 'mptcp_make_after_break', + 'mptcpNojoindssack': 'mptcp_no_join_dss_ack', + 'mptcpRtomax': 'mptcp_rto_max', + 'mptcpRxmitmin': 'mptcp_retransmit_min', + 'mptcpSubflowmax': 'mptcp_subflow_max', + 'mptcpTimeout': 'mptcp_timeout', + 'nagle': 'nagle_algorithm', + 'pktLossIgnoreBurst': 'pkt_loss_ignore_burst', + 'pktLossIgnoreRate': 'pkt_loss_ignore_rate', + 'proxyBufferHigh': 'proxy_buffer_high', + 'proxyBufferLow': 'proxy_buffer_low', + 'proxyMss': 'proxy_max_segment', + 'proxyOptions': 'proxy_options', + 'pushFlag': 'push_flag', + 'ratePace': 'rate_pace', + 'ratePaceMaxRate': 'rate_pace_max_rate', + 'receiveWindowSize': 'receive_window', + 'resetOnTimeout': 'reset_on_timeout', + 'selectiveAcks': 'selective_acks', + 'selectiveNack': 'selective_nack', + 'sendBufferSize': 'send_buffer', + 'slowStart': 'slow_start', + 'synCookieEnable': 'syn_cookie_enable', + 'synCookieWhitelist': 'syn_cookie_white_list', + 'synRtoBase': 'syn_retrans_to_base', + 'tailLossProbe': 'tail_loss_probe', + 'timeWaitRecycle': 'time_wait_recycle', + 'timeWaitTimeout': 'time_wait', + 'verifiedAccept': 'verified_accept', + 'zeroWindowTimeout': 'zero_window_timeout', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'abc', + 'ack_on_push', + 'auto_proxy_buffer', + 'auto_receive_window', + 'auto_send_buffer', + 'close_wait', + 'congestion_metrics_cache', + 'congestion_metrics_cache_timeout', + 'congestion_control', + 'deferred_accept', + 'delay_window_control', + 'delayed_acks', + 'dsack', + 'early_retransmit', + 'explicit_congestion_notification', + 'enhanced_loss_recovery', + 'fast_open', + 'fast_open_cookie_expiration', + 'fin_wait_1', + 'fin_wait_2', + 'idle_timeout', + 'initial_congestion_window_size', + 'initial_receive_window_size', + 'dont_fragment_flag', + 'ip_tos', + 'time_to_live', + 'time_to_live_v4', + 'time_to_live_v6', + 'keep_alive_interval', + 'limited_transmit_recovery', + 'link_qos', + 'max_segment_retrans', + 'max_syn_retrans', + 'max_segment_size', + 'md5_signature', + 'minimum_rto', + 'multipath_tcp', + 'mptcp_checksum', + 'mptcp_checksum_verify', + 'mptcp_fallback', + 'mptcp_fast_join', + 'mptcp_idle_timeout', + 'mptcp_join_max', + 'mptcp_make_after_break', + 'mptcp_no_join_dss_ack', + 'mptcp_rto_max', + 'mptcp_retransmit_min', + 'mptcp_subflow_max', + 'mptcp_timeout', + 'nagle_algorithm', + 'pkt_loss_ignore_burst', + 'pkt_loss_ignore_rate', + 'proxy_buffer_high', + 'proxy_buffer_low', + 'proxy_max_segment', + 'proxy_options', + 'push_flag', + 'rate_pace', + 'rate_pace_max_rate', + 'receive_window', + 'reset_on_timeout', + 'retransmit_threshold', + 'selective_acks', + 'selective_nack', + 'send_buffer', + 'slow_start', + 'syn_cookie_enable', + 'syn_cookie_white_list', + 'syn_retrans_to_base', + 'tail_loss_probe', + 'time_wait_recycle', + 'time_wait', + 'timestamps', + 'verified_accept', + 'zero_window_timeout', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def time_wait(self): + if self._values['time_wait'] is None: + return None + if self._values['time_wait'] == 0: + return "immediate" + if self._values['time_wait'] == 4294967295: + return 'indefinite' + return self._values['time_wait'] + + @property + def close_wait(self): + if self._values['close_wait'] is None: + return None + if self._values['close_wait'] == 0: + return "immediate" + if self._values['close_wait'] == 4294967295: + return 'indefinite' + return self._values['close_wait'] + + @property + def fin_wait_1(self): + if self._values['fin_wait_1'] is None: + return None + if self._values['fin_wait_1'] == 0: + return "immediate" + if self._values['fin_wait_1'] == 4294967295: + return 'indefinite' + return self._values['fin_wait_1'] + + @property + def fin_wait_2(self): + if self._values['fin_wait_2'] is None: + return None + if self._values['fin_wait_2'] == 0: + return "immediate" + if self._values['fin_wait_2'] == 4294967295: + return 'indefinite' + return self._values['fin_wait_2'] + + @property + def zero_window_timeout(self): + if self._values['zero_window_timeout'] is None: + return None + if self._values['zero_window_timeout'] == 4294967295: + return 'indefinite' + return self._values['zero_window_timeout'] + + @property + def idle_timeout(self): + if self._values['idle_timeout'] is None: + return None + if self._values['idle_timeout'] == 4294967295: + return 'indefinite' + return self._values['idle_timeout'] + + @property + def keep_alive_interval(self): + if self._values['keep_alive_interval'] is None: + return None + if self._values['keep_alive_interval'] == 4294967295: + return 'indefinite' + return self._values['keep_alive_interval'] + + @property + def verified_accept(self): + return flatten_boolean(self._values['verified_accept']) + + @property + def timestamps(self): + return flatten_boolean(self._values['timestamps']) + + @property + def time_wait_recycle(self): + return flatten_boolean(self._values['time_wait_recycle']) + + @property + def tail_loss_probe(self): + return flatten_boolean(self._values['tail_loss_probe']) + + @property + def syn_cookie_white_list(self): + return flatten_boolean(self._values['syn_cookie_white_list']) + + @property + def syn_cookie_enable(self): + return flatten_boolean(self._values['syn_cookie_enable']) + + @property + def slow_start(self): + return flatten_boolean(self._values['slow_start']) + + @property + def selective_nack(self): + return flatten_boolean(self._values['selective_nack']) + + @property + def selective_acks(self): + return flatten_boolean(self._values['selective_acks']) + + @property + def reset_on_timeout(self): + return flatten_boolean(self._values['reset_on_timeout']) + + @property + def rate_pace(self): + return flatten_boolean(self._values['rate_pace']) + + @property + def proxy_options(self): + return flatten_boolean(self._values['proxy_options']) + + @property + def proxy_max_segment(self): + return flatten_boolean(self._values['proxy_max_segment']) + + @property + def nagle_algorithm(self): + return flatten_boolean(self._values['nagle_algorithm']) + + @property + def mptcp_no_join_dss_ack(self): + return flatten_boolean(self._values['mptcp_no_join_dss_ack']) + + @property + def mptcp_make_after_break(self): + return flatten_boolean(self._values['mptcp_make_after_break']) + + @property + def mptcp_fast_join(self): + return flatten_boolean(self._values['mptcp_fast_join']) + + @property + def mptcp_checksum_verify(self): + return flatten_boolean(self._values['mptcp_checksum_verify']) + + @property + def mptcp_checksum(self): + return flatten_boolean(self._values['mptcp_checksum']) + + @property + def multipath_tcp(self): + return flatten_boolean(self._values['multipath_tcp']) + + @property + def md5_signature(self): + return flatten_boolean(self._values['md5_signature']) + + @property + def limited_transmit_recovery(self): + return flatten_boolean(self._values['limited_transmit_recovery']) + + @property + def fast_open(self): + return flatten_boolean(self._values['fast_open']) + + @property + def enhanced_loss_recovery(self): + return flatten_boolean(self._values['enhanced_loss_recovery']) + + @property + def explicit_congestion_notification(self): + return flatten_boolean(self._values['explicit_congestion_notification']) + + @property + def early_retransmit(self): + return flatten_boolean(self._values['early_retransmit']) + + @property + def dsack(self): + return flatten_boolean(self._values['dsack']) + + @property + def delayed_acks(self): + return flatten_boolean(self._values['delayed_acks']) + + @property + def delay_window_control(self): + return flatten_boolean(self._values['delay_window_control']) + + @property + def deferred_accept(self): + return flatten_boolean(self._values['deferred_accept']) + + @property + def congestion_metrics_cache(self): + return flatten_boolean(self._values['congestion_metrics_cache']) + + @property + def auto_send_buffer(self): + return flatten_boolean(self._values['auto_send_buffer']) + + @property + def auto_receive_window(self): + return flatten_boolean(self._values['auto_receive_window']) + + @property + def auto_proxy_buffer(self): + return flatten_boolean(self._values['auto_proxy_buffer']) + + @property + def abc(self): + return flatten_boolean(self._values['abc']) + + @property + def ack_on_push(self): + return flatten_boolean(self._values['ack_on_push']) + + +class TcpProfilesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(TcpProfilesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(tcp_profiles=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = TcpProfilesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/tcp".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class TrafficGroupsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'autoFailbackEnabled': 'auto_failback_enabled', + 'autoFailbackTime': 'auto_failback_time', + 'haLoadFactor': 'ha_load_factor', + 'haOrder': 'ha_order', + 'isFloating': 'is_floating', + 'mac': 'mac_masquerade_address' + } + + returnables = [ + 'full_path', + 'name', + 'description', + 'auto_failback_enabled', + 'auto_failback_time', + 'ha_load_factor', + 'ha_order', + 'is_floating', + 'mac_masquerade_address' + ] + + @property + def auto_failback_time(self): + if self._values['auto_failback_time'] is None: + return None + return int(self._values['auto_failback_time']) + + @property + def auto_failback_enabled(self): + if self._values['auto_failback_enabled'] is None: + return None + elif self._values['auto_failback_enabled'] == 'false': + # Yes, the REST API stores this as a string + return 'no' + return 'yes' + + @property + def is_floating(self): + if self._values['is_floating'] is None: + return None + elif self._values['is_floating'] == 'true': + # Yes, the REST API stores this as a string + return 'yes' + return 'no' + + @property + def mac_masquerade_address(self): + if self._values['mac_masquerade_address'] in [None, 'none']: + return None + return self._values['mac_masquerade_address'] + + +class TrafficGroupsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(TrafficGroupsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(traffic_groups=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + attrs = resource + attrs['stats'] = self.read_stats_from_device(attrs['fullPath']) + params = TrafficGroupsParameters(params=attrs) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/cm/traffic-group".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + def read_stats_from_device(self, full_path): + uri = "https://{0}:{1}/mgmt/tm/cm/traffic-group/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=full_path) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = parseStats(response) + try: + return result['stats'] + except KeyError: + return {} + + +class TrunksParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'media': 'media_speed', + 'lacpMode': 'lacp_mode', + 'lacp': 'lacp_state', + 'lacpTimeout': 'lacp_timeout', + 'stp': 'stp_enabled', + 'workingMbrCount': 'operational_member_count', + 'linkSelectPolicy': 'link_selection_policy', + 'distributionHash': 'distribution_hash', + 'cfgMbrCount': 'configured_member_count' + } + + returnables = [ + 'full_path', + 'name', + 'description', + 'media_speed', + 'lacp_mode', # 'active' or 'passive' + 'lacp_enabled', + 'stp_enabled', + 'operational_member_count', + 'media_status', + 'link_selection_policy', + 'lacp_timeout', + 'interfaces', + 'distribution_hash', + 'configured_member_count' + ] + + @property + def lacp_enabled(self): + if self._values['lacp_enabled'] is None: + return None + elif self._values['lacp_enabled'] == 'disabled': + return 'no' + return 'yes' + + @property + def stp_enabled(self): + if self._values['stp_enabled'] is None: + return None + elif self._values['stp_enabled'] == 'disabled': + return 'no' + return 'yes' + + @property + def media_status(self): + return self._values['stats']['status'] + + +class TrunksFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(TrunksFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(trunks=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + attrs = resource + attrs['stats'] = self.read_stats_from_device(attrs['fullPath']) + params = TrunksParameters(params=attrs) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/net/trunk".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}".format(self.module.params['data_increment'], skip) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + def read_stats_from_device(self, full_path): + uri = "https://{0}:{1}/mgmt/tm/net/trunk/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=full_path) + ) + 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) + result = parseStats(response) + try: + return result['stats'] + except KeyError: + return {} + + +class UCSParameters(BaseParameters): + api_map = { + 'filename': 'file_name', + 'encrypted': 'encrypted', + 'file_size': 'file_size', + 'apiRawValues': 'variables' + } + + returnables = [ + 'file_name', + 'encrypted', + 'file_size', + 'file_created_date' + ] + + @property + def file_name(self): + name = self._values['variables']['filename'].split("/")[-1] + return name + + @property + def encrypted(self): + return self._values['variables']['encrypted'] + + @property + def file_size(self): + val = self._values['variables']['file_size'] + size = re.findall(r'\d+', val)[0] + return size + + @property + def file_created_date(self): + date = self._values['variables']['file_created_date'] + return date + + +class UCSFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(UCSFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(ucs_files=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['file_name']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + attrs = resource + params = UCSParameters(params=attrs) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/sys/ucs".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}".format(self.module.params['data_increment'], skip) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class UsersParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'partitionAccess': 'partition_access', + } + + returnables = [ + 'full_path', + 'name', + 'description', + 'partition_access', + 'shell', + ] + + @property + def partition_access(self): + result = [] + if self._values['partition_access'] is None: + return [] + for partition in self._values['partition_access']: + del partition['nameReference'] + result.append(partition) + return result + + @property + def shell(self): + if self._values['shell'] in [None, 'none']: + return None + return self._values['shell'] + + +class UsersFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(UsersFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(users=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + attrs = resource + params = UsersParameters(params=attrs) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/user".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class UdpProfilesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'allowNoPayload': 'allow_no_payload', + 'bufferMaxBytes': 'buffer_max_bytes', + 'bufferMaxPackets': 'buffer_max_packets', + 'datagramLoadBalancing': 'datagram_load_balancing', + 'defaultsFrom': 'parent', + 'idleTimeout': 'idle_timeout', + 'ipDfMode': 'ip_df_mode', + 'ipTosToClient': 'ip_tos_to_client', + 'ipTtlMode': 'ip_ttl_mode', + 'ipTtlV4': 'ip_ttl_v4', + 'ipTtlV6': 'ip_ttl_v6', + 'linkQosToClient': 'link_qos_to_client', + 'noChecksum': 'no_checksum', + 'proxyMss': 'proxy_mss', + } + + returnables = [ + 'full_path', + 'name', + 'parent', + 'description', + 'allow_no_payload', + 'buffer_max_bytes', + 'buffer_max_packets', + 'datagram_load_balancing', + 'idle_timeout', + 'ip_df_mode', + 'ip_tos_to_client', + 'ip_ttl_mode', + 'ip_ttl_v4', + 'ip_ttl_v6', + 'link_qos_to_client', + 'no_checksum', + 'proxy_mss', + ] + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def allow_no_payload(self): + return flatten_boolean(self._values['allow_no_payload']) + + @property + def datagram_load_balancing(self): + return flatten_boolean(self._values['datagram_load_balancing']) + + @property + def proxy_mss(self): + return flatten_boolean(self._values['proxy_mss']) + + @property + def no_checksum(self): + return flatten_boolean(self._values['no_checksum']) + + +class UdpProfilesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(UdpProfilesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(udp_profiles=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = UdpProfilesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/udp".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class VcmpGuestsParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'allowedSlots': 'allowed_slots', + 'assignedSlots': 'assigned_slots', + 'bootPriority': 'boot_priority', + 'coresPerSlot': 'cores_per_slot', + 'initialImage': 'initial_image', + 'initialHotfix': 'hotfix_image', + 'managementGw': 'mgmt_route', + 'managementIp': 'mgmt_address', + 'managementNetwork': 'mgmt_network', + 'minSlots': 'min_number_of_slots', + 'slots': 'number_of_slots', + 'sslMode': 'ssl_mode', + 'virtualDisk': 'virtual_disk' + } + + returnables = [ + 'name', + 'full_path', + 'allowed_slots', + 'assigned_slots', + 'boot_priority', + 'cores_per_slot', + 'hostname', + 'hotfix_image', + 'initial_image', + 'mgmt_route', + 'mgmt_address', + 'mgmt_network', + 'vlans', + 'min_number_of_slots', + 'number_of_slots', + 'ssl_mode', + 'state', + 'virtual_disk', + ] + + +class VcmpGuestsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(VcmpGuestsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(vcmp_guests=facts) + return result + + def _exec_module(self): + if 'vcmp' not in self.provisioned_modules: + return [] + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = VcmpGuestsParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class VirtualAddressesParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'arp': 'arp_enabled', + 'autoDelete': 'auto_delete_enabled', + 'connectionLimit': 'connection_limit', + 'icmpEcho': 'icmp_echo', + 'mask': 'netmask', + 'routeAdvertisement': 'route_advertisement', + 'trafficGroup': 'traffic_group', + 'inheritedTrafficGroup': 'inherited_traffic_group' + } + + returnables = [ + 'full_path', + 'name', + 'address', + 'arp_enabled', + 'auto_delete_enabled', + 'connection_limit', + 'description', + 'enabled', + 'icmp_echo', + 'floating', + 'netmask', + 'route_advertisement', + 'traffic_group', + 'spanning', + 'inherited_traffic_group' + ] + + @property + def spanning(self): + return flatten_boolean(self._values['spanning']) + + @property + def arp_enabled(self): + return flatten_boolean(self._values['arp_enabled']) + + @property + def route_advertisement(self): + return flatten_boolean(self._values['route_advertisement']) + + @property + def auto_delete_enabled(self): + return flatten_boolean(self._values['auto_delete_enabled']) + + @property + def inherited_traffic_group(self): + return flatten_boolean(self._values['inherited_traffic_group']) + + @property + def icmp_echo(self): + return flatten_boolean(self._values['icmp_echo']) + + @property + def floating(self): + return flatten_boolean(self._values['floating']) + + @property + def enabled(self): + return flatten_boolean(self._values['enabled']) + + +class VirtualAddressesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(VirtualAddressesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(virtual_addresses=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + params = VirtualAddressesParameters(params=resource) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual-address".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class VirtualServersParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + 'autoLasthop': 'auto_lasthop', + 'bwcPolicy': 'bw_controller_policy', + 'cmpEnabled': 'cmp_enabled', + 'connectionLimit': 'connection_limit', + 'fallbackPersistence': 'fallback_persistence_profile', + 'persist': 'persistence_profile', + 'translatePort': 'translate_port', + 'translateAddress': 'translate_address', + 'lastHopPool': 'last_hop_pool', + 'nat64': 'nat64_enabled', + 'sourcePort': 'source_port_behavior', + 'ipIntelligencePolicy': 'ip_intelligence_policy', + 'ipProtocol': 'protocol', + 'pool': 'default_pool', + 'rateLimitMode': 'rate_limit_mode', + 'rateLimitSrcMask': 'rate_limit_source_mask', + 'rateLimitDstMask': 'rate_limit_destination_mask', + 'rateLimit': 'rate_limit', + 'sourceAddressTranslation': 'snat_type', + 'gtmScore': 'gtm_score', + 'rateClass': 'rate_class', + 'source': 'source_address', + 'auth': 'authentication_profile', + 'mirror': 'connection_mirror_enabled', + 'rules': 'irules', + 'securityLogProfiles': 'security_log_profiles', + 'profilesReference': 'profiles', + 'policiesReference': 'policies', + } + + returnables = [ + 'full_path', + 'name', + 'auto_lasthop', + 'bw_controller_policy', + 'cmp_enabled', + 'connection_limit', + 'description', + 'enabled', + 'fallback_persistence_profile', + 'persistence_profile', + 'translate_port', + 'translate_address', + 'vlans', + 'destination', + 'last_hop_pool', + 'nat64_enabled', + 'source_port_behavior', + 'ip_intelligence_policy', + 'protocol', + 'default_pool', + 'rate_limit_mode', + 'rate_limit_source_mask', + 'rate_limit', + 'snat_type', + 'snat_pool', + 'gtm_score', + 'rate_class', + 'rate_limit_destination_mask', + 'source_address', + 'authentication_profile', + 'connection_mirror_enabled', + 'irules', + 'security_log_profiles', + 'type', + 'policies', + 'profiles', + 'destination_address', + 'destination_port', + 'availability_status', + 'status_reason', + 'total_requests', + 'client_side_bits_in', + 'client_side_bits_out', + 'client_side_current_connections', + 'client_side_evicted_connections', + 'client_side_max_connections', + 'client_side_pkts_in', + 'client_side_pkts_out', + 'client_side_slow_killed', + 'client_side_total_connections', + 'cmp_mode', + 'ephemeral_bits_in', + 'ephemeral_bits_out', + 'ephemeral_current_connections', + 'ephemeral_evicted_connections', + 'ephemeral_max_connections', + 'ephemeral_pkts_in', + 'ephemeral_pkts_out', + 'ephemeral_slow_killed', + 'ephemeral_total_connections', + 'total_software_accepted_syn_cookies', + 'total_hardware_accepted_syn_cookies', + 'total_hardware_syn_cookies', + 'hardware_syn_cookie_instances', + 'total_software_rejected_syn_cookies', + 'software_syn_cookie_instances', + 'current_syn_cache', + 'syn_cache_overflow', + 'total_software_syn_cookies', + 'syn_cookies_status', + 'max_conn_duration', + 'mean_conn_duration', + 'min_conn_duration', + 'cpu_usage_ratio_last_5_min', + 'cpu_usage_ratio_last_5_sec', + 'cpu_usage_ratio_last_1_min', + ] + + @property + def max_conn_duration(self): + return self._values['stats']['csMaxConnDur'] + + @property + def mean_conn_duration(self): + return self._values['stats']['csMeanConnDur'] + + @property + def min_conn_duration(self): + return self._values['stats']['csMinConnDur'] + + @property + def cpu_usage_ratio_last_5_min(self): + return self._values['stats']['fiveMinAvgUsageRatio'] + + @property + def cpu_usage_ratio_last_5_sec(self): + return self._values['stats']['fiveSecAvgUsageRatio'] + + @property + def cpu_usage_ratio_last_1_min(self): + return self._values['stats']['oneMinAvgUsageRatio'] + + @property + def cmp_mode(self): + return self._values['stats']['cmpEnableMode'] + + @property + def availability_status(self): + return self._values['stats']['status']['availabilityState'] + + @property + def status_reason(self): + return self._values['stats']['status']['statusReason'] + + @property + def total_requests(self): + return self._values['stats']['totRequests'] + + @property + def ephemeral_bits_in(self): + return self._values['stats']['ephemeral']['bitsIn'] + + @property + def ephemeral_bits_out(self): + return self._values['stats']['ephemeral']['bitsOut'] + + @property + def ephemeral_current_connections(self): + return self._values['stats']['ephemeral']['curConns'] + + @property + def ephemeral_evicted_connections(self): + return self._values['stats']['ephemeral']['evictedConns'] + + @property + def ephemeral_max_connections(self): + return self._values['stats']['ephemeral']['maxConns'] + + @property + def ephemeral_pkts_in(self): + return self._values['stats']['ephemeral']['pktsIn'] + + @property + def ephemeral_pkts_out(self): + return self._values['stats']['ephemeral']['pktsOut'] + + @property + def ephemeral_slow_killed(self): + return self._values['stats']['ephemeral']['slowKilled'] + + @property + def ephemeral_total_connections(self): + return self._values['stats']['ephemeral']['totConns'] + + @property + def client_side_bits_in(self): + return self._values['stats']['clientside']['bitsIn'] + + @property + def client_side_bits_out(self): + return self._values['stats']['clientside']['bitsOut'] + + @property + def client_side_current_connections(self): + return self._values['stats']['clientside']['curConns'] + + @property + def client_side_evicted_connections(self): + return self._values['stats']['clientside']['evictedConns'] + + @property + def client_side_max_connections(self): + return self._values['stats']['clientside']['maxConns'] + + @property + def client_side_pkts_in(self): + return self._values['stats']['clientside']['pktsIn'] + + @property + def client_side_pkts_out(self): + return self._values['stats']['clientside']['pktsOut'] + + @property + def client_side_slow_killed(self): + return self._values['stats']['clientside']['slowKilled'] + + @property + def client_side_total_connections(self): + return self._values['stats']['clientside']['totConns'] + + @property + def total_software_accepted_syn_cookies(self): + return self._values['stats']['syncookie']['accepts'] + + @property + def total_hardware_accepted_syn_cookies(self): + return self._values['stats']['syncookie']['hwAccepts'] + + @property + def total_hardware_syn_cookies(self): + return self._values['stats']['syncookie']['hwSyncookies'] + + @property + def hardware_syn_cookie_instances(self): + return self._values['stats']['syncookie']['hwsyncookieInstance'] + + @property + def total_software_rejected_syn_cookies(self): + return self._values['stats']['syncookie']['rejects'] + + @property + def software_syn_cookie_instances(self): + return self._values['stats']['syncookie']['swsyncookieInstance'] + + @property + def current_syn_cache(self): + return self._values['stats']['syncookie']['syncacheCurr'] + + @property + def syn_cache_overflow(self): + return self._values['stats']['syncookie']['syncacheOver'] + + @property + def total_software_syn_cookies(self): + return self._values['stats']['syncookie']['syncookies'] + + @property + def syn_cookies_status(self): + return self._values['stats']['syncookieStatus'] + + @property + def destination_address(self): + if self._values['destination'] is None: + return None + tup = self.destination_tuple + return tup.ip + + @property + def destination_port(self): + if self._values['destination'] is None: + return None + tup = self.destination_tuple + return tup.port + + @property + def type(self): + """Attempt to determine the current server type + + This check is very unscientific. It turns out that this information is not + exactly available anywhere on a BIG-IP. Instead, we rely on a semi-reliable + means for determining what the type of the virtual server is. Hopefully it + always works. + + There are a handful of attributes that can be used to determine a specific + type. There are some types though that can only be determined by looking at + the profiles that are assigned to them. We follow that method for those + complicated types; message-routing, fasthttp, and fastl4. + + Because type determination is an expensive operation, we cache the result + from the operation. + + Returns: + string: The server type. + """ + if self._values['l2Forward'] is True: + result = 'forwarding-l2' + elif self._values['ipForward'] is True: + result = 'forwarding-ip' + elif self._values['stateless'] is True: + result = 'stateless' + elif self._values['reject'] is True: + result = 'reject' + elif self._values['dhcpRelay'] is True: + result = 'dhcp' + elif self._values['internal'] is True: + result = 'internal' + elif self.has_fasthttp_profiles: + result = 'performance-http' + elif self.has_fastl4_profiles: + result = 'performance-l4' + elif self.has_message_routing_profiles: + result = 'message-routing' + else: + result = 'standard' + return result + + @property + def profiles(self): + """Returns a list of profiles from the API + + The profiles are formatted so that they are usable in this module and + are able to be compared by the Difference engine. + + Returns: + list (:obj:`list` of :obj:`dict`): List of profiles. + + Each dictionary in the list contains the following three (3) keys. + + * name + * context + * fullPath + + Raises: + F5ModuleError: If the specified context is a value other that + ``all``, ``server-side``, or ``client-side``. + """ + if 'items' not in self._values['profiles']: + return None + result = [] + for item in self._values['profiles']['items']: + context = item['context'] + if context == 'serverside': + context = 'server-side' + elif context == 'clientside': + context = 'client-side' + name = item['name'] + if context in ['all', 'server-side', 'client-side']: + result.append(dict(name=name, context=context, full_path=item['fullPath'])) + else: + raise F5ModuleError( + "Unknown profile context found: '{0}'".format(context) + ) + return result + + @property + def has_message_routing_profiles(self): + if self.profiles is None: + return None + current = self._read_current_message_routing_profiles_from_device() + result = [x['name'] for x in self.profiles if x['name'] in current] + if len(result) > 0: + return True + return False + + @property + def has_fastl4_profiles(self): + if self.profiles is None: + return None + current = self._read_current_fastl4_profiles_from_device() + result = [x['name'] for x in self.profiles if x['name'] in current] + if len(result) > 0: + return True + return False + + @property + def has_fasthttp_profiles(self): + """Check if ``fasthttp`` profile is in API profiles + + This method is used to determine the server type when doing comparisons + in the Difference class. + + Returns: + bool: True if server has ``fasthttp`` profiles. False otherwise. + """ + if self.profiles is None: + return None + current = self._read_current_fasthttp_profiles_from_device() + result = [x['name'] for x in self.profiles if x['name'] in current] + if len(result) > 0: + return True + return False + + def _read_current_message_routing_profiles_from_device(self): + result = [] + result += self._read_diameter_profiles_from_device() + result += self._read_sip_profiles_from_device() + return result + + def _read_diameter_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/diameter/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = [x['name'] for x in response['items']] + return result + + def _read_sip_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/sip/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = [x['name'] for x in response['items']] + return result + + def _read_current_fastl4_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + result = [x['name'] for x in response['items']] + return result + + def _read_current_fasthttp_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fasthttp/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = [x['name'] for x in response['items']] + return result + + @property + def security_log_profiles(self): + if self._values['security_log_profiles'] is None: + return None + result = list(set([x.strip('"') for x in self._values['security_log_profiles']])) + result.sort() + return result + + @property + def snat_type(self): + if self._values['snat_type'] is None: + return None + if 'type' in self._values['snat_type']: + if self._values['snat_type']['type'] == 'automap': + return 'automap' + elif self._values['snat_type']['type'] == 'none': + return 'none' + elif self._values['snat_type']['type'] == 'snat': + return 'snat' + + @property + def snat_pool(self): + if self._values['snat_type'] is None: + return None + if 'type' in self._values['snat_type']: + if self._values['snat_type']['type'] == 'automap': + return 'none' + elif self._values['snat_type']['type'] == 'none': + return 'none' + elif self._values['snat_type']['type'] == 'snat': + return self._values['snat_type']["pool"] + + @property + def connection_mirror_enabled(self): + if self._values['connection_mirror_enabled'] is None: + return None + elif self._values['connection_mirror_enabled'] == 'enabled': + return 'yes' + return 'no' + + @property + def rate_limit(self): + if self._values['rate_limit'] is None: + return None + elif self._values['rate_limit'] == 'disabled': + return -1 + return int(self._values['rate_limit']) + + @property + def nat64_enabled(self): + if self._values['nat64_enabled'] is None: + return None + elif self._values['nat64_enabled'] == 'enabled': + return 'yes' + return 'no' + + @property + def enabled(self): + if self._values['enabled'] is None: + return 'no' + elif self._values['enabled'] is True: + return 'yes' + return 'no' + + @property + def translate_port(self): + if self._values['translate_port'] is None: + return None + elif self._values['translate_port'] == 'enabled': + return 'yes' + return 'no' + + @property + def translate_address(self): + if self._values['translate_address'] is None: + return None + elif self._values['translate_address'] == 'enabled': + return 'yes' + return 'no' + + @property + def persistence_profile(self): + """Return persistence profile in a consumable form + + I don't know why the persistence profile is stored this way, but below is the + general format of it. + + "persist": [ + { + "name": "msrdp", + "partition": "Common", + "tmDefault": "yes", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/persistence/msrdp/~Common~msrdp?ver=13.1.0.4" + } + } + ], + + As you can see, this is quite different from something like the fallback + persistence profile which is just simply + + /Common/fallback1 + + This method makes the persistence profile look like the fallback profile. + + Returns: + string: The persistence profile configured on the virtual. + """ + if self._values['persistence_profile'] is None: + return None + profile = self._values['persistence_profile'][0] + result = fq_name(profile['partition'], profile['name']) + return result + + @property + def destination_tuple(self): + Destination = namedtuple('Destination', ['ip', 'port', 'route_domain', 'mask']) + + # Remove the partition + if self._values['destination'] is None: + result = Destination(ip=None, port=None, route_domain=None, mask=None) + return result + destination = re.sub(r'^/[a-zA-Z0-9_.-]+/([a-zA-Z0-9_.-]+\/)?', '', self._values['destination']) + # Covers the following examples + # + # /Common/2700:bc00:1f10:101::6%2.80 + # 2700:bc00:1f10:101::6%2.80 + # 1.1.1.1%2:80 + # /Common/1.1.1.1%2:80 + # /Common/2700:bc00:1f10:101::6%2.any + # /Common/Shared/1.1.1.1:80 + # + pattern = r'(?P[^%]+)%(?P[0-9]+)[:.](?P[0-9]+|any)' + matches = re.search(pattern, destination) + if matches: + try: + port = int(matches.group('port')) + except ValueError: + # Can be a port of "any". This only happens with IPv6 + port = matches.group('port') + if port == 'any': + port = 0 + result = Destination( + ip=matches.group('ip'), + port=port, + route_domain=int(matches.group('route_domain')), + mask=self.mask + ) + return result + + pattern = r'(?P[^%]+)%(?P[0-9]+)' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('ip'), + port=None, + route_domain=int(matches.group('route_domain')), + mask=self.mask + ) + return result + + # this will match any IPV4 Address and port, no RD + pattern = r'^(?P(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4]' \ + r'[0-9]|25[0-5])):(?P[0-9]+)' + + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('ip'), + port=int(matches.group('port')), + route_domain=None, + mask=self.mask + ) + return result + + # match standalone IPV6 address, no port + pattern = r'^([0-9a-f]{0,4}:){2,7}(:|[0-9a-f]{1,4})$' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=destination, + port=None, + route_domain=None, + mask=self.mask + ) + return result + + # match IPV6 address with port + pattern = r'(?P([0-9a-f]{0,4}:){2,7}(:|[0-9a-f]{1,4}).(?P[0-9]+|any))' + matches = re.search(pattern, destination) + if matches: + ip = matches.group('ip').split('.')[0] + try: + port = int(matches.group('port')) + except ValueError: + # Can be a port of "any". This only happens with IPv6 + port = matches.group('port') + if port == 'any': + port = 0 + result = Destination( + ip=ip, + port=port, + route_domain=None, + mask=self.mask + ) + return result + + # this will match any alphanumeric Virtual Address and port + pattern = r'(?P^[a-zA-Z0-9_.-]+):(?P[0-9]+)' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('name'), + port=int(matches.group('port')), + route_domain=None, + mask=self.mask + ) + return result + + # this will match any alphanumeric Virtual Address + pattern = r'(?P^[a-zA-Z0-9_.-]+)' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('name'), + port=None, + route_domain=None, + mask=self.mask + ) + return result + + # match IPv6 wildcard with port without RD + pattern = r'(?P[^.]+).(?P[0-9]+|any)' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('ip'), + port=matches.group('port'), + route_domain=None, + mask=self.mask + ) + return result + + result = Destination(ip=None, port=None, route_domain=None, mask=None) + return result + + @property + def policies(self): + if 'items' not in self._values['policies']: + return None + results = [] + for item in self._values['policies']['items']: + results.append(item['fullPath']) + return results + + +class VirtualServersFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(VirtualServersFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(virtual_servers=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + attrs = resource + attrs['stats'] = self.read_stats_from_device(attrs['fullPath']) + params = VirtualServersParameters(client=self.client, params=attrs) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + def read_stats_from_device(self, full_path): + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=full_path) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = parseStats(response) + try: + return result['stats'] + except KeyError: + return {} + + +class VlansParameters(BaseParameters): + api_map = { + 'autoLasthop': 'auto_lasthop', + 'cmpHash': 'cmp_hash_algorithm', + 'failsafeAction': 'failsafe_action', + 'failsafe': 'failsafe_enabled', + 'failsafeTimeout': 'failsafe_timeout', + 'ifIndex': 'if_index', + 'learning': 'learning_mode', + 'interfacesReference': 'interfaces', + 'sourceChecking': 'source_check_enabled', + 'fullPath': 'full_path' + } + + returnables = [ + 'full_path', + 'name', + 'auto_lasthop', + 'cmp_hash_algorithm', + 'description', + 'failsafe_action', + 'failsafe_enabled', + 'failsafe_timeout', + 'if_index', + 'learning_mode', + 'interfaces', + 'mtu', + 'sflow_poll_interval', + 'sflow_poll_interval_global', + 'sflow_sampling_rate', + 'sflow_sampling_rate_global', + 'source_check_enabled', + 'true_mac_address', + 'tag', + ] + + @property + def interfaces(self): + if self._values['interfaces'] is None: + return None + if 'items' not in self._values['interfaces']: + return None + result = [] + for item in self._values['interfaces']['items']: + tmp = dict( + name=item['name'], + full_path=item['fullPath'] + ) + if 'tagged' in item: + tmp['tagged'] = 'yes' + else: + tmp['tagged'] = 'no' + result.append(tmp) + return result + + @property + def sflow_poll_interval(self): + return int(self._values['sflow']['pollInterval']) + + @property + def sflow_poll_interval_global(self): + return flatten_boolean(self._values['sflow']['pollIntervalGlobal']) + + @property + def sflow_sampling_rate(self): + return int(self._values['sflow']['samplingRate']) + + @property + def sflow_sampling_rate_global(self): + return flatten_boolean(self._values['sflow']['samplingRateGlobal']) + + @property + def source_check_state(self): + return flatten_boolean(self._values['source_check_state']) + + @property + def true_mac_address(self): + # Who made this field a "description"!? + return self._values['stats']['macTrue'] + + @property + def tag(self): + # We can't agree on field names...SMH + return self._values['stats']['id'] + + @property + def failsafe_enabled(self): + return flatten_boolean(self._values['failsafe_enabled']) + + +class VlansFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(VlansFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(vlans=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + attrs = resource + attrs['stats'] = self.read_stats_from_device(attrs['fullPath']) + params = VlansParameters(params=attrs) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/net/vlan".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + def read_stats_from_device(self, full_path): + uri = "https://{0}:{1}/mgmt/tm/net/vlan/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=full_path) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + result = parseStats(response) + try: + return result['stats'] + except KeyError: + return {} + + +class ManagementRouteParameters(BaseParameters): + api_map = { + 'fullPath': 'full_path', + } + + returnables = [ + 'full_path', + 'name', + 'description', + 'gateway', + 'mtu', + 'network', + ] + + +class ManagementRouteFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(ManagementRouteFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(management_routes=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.increment_read() + for resource in collection: + attrs = resource + params = ManagementRouteParameters(params=attrs) + results.append(params) + return results + + def increment_read(self): + n = 0 + result = [] + while True: + items = self.read_collection_from_device(skip=n) + if not items: + break + result.extend(items) + n = n + self.module.params['data_increment'] + return result + + def read_collection_from_device(self, skip=0): + uri = "https://{0}:{1}/mgmt/tm/sys/management-route".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?expandSubcollections=true&$top={0}&$skip={1}&$filter=partition+eq+{2}".format( + self.module.params['data_increment'], skip, self.module.params['partition'] + ) + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'items' not in response: + return [] + result = response['items'] + return result + + +class RemoteSyslogParameters(BaseParameters): + api_map = { + 'remoteServers': 'servers', + } + + returnables = [ + 'servers', + ] + + def _morph_keys(self, key_map, item): + for k, v in iteritems(key_map): + item[v] = item.pop(k, None) + result = self._filter_params(item) + return result + + def _format_servers(self, items): + result = list() + key_map = { + 'name': 'name', + 'remotePort': 'remote_port', + 'localIp': 'local_ip', + 'host': 'remote_host' + } + for item in items: + output = self._morph_keys(key_map, item) + result.append(output) + return result + + @property + def servers(self): + if self._values['servers'] is None: + return None + return self._format_servers(self._values['servers']) + + +class RemoteSyslogFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(RemoteSyslogFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(remote_syslog=facts) + return result + + def _exec_module(self): + facts = self.read_collection_from_device() + params = RemoteSyslogParameters(params=facts) + results = params.to_return() + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/syslog/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + return response + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.kwargs = kwargs + self.want = Parameters(params=self.module.params) + self.managers = { + 'apm-access-profiles': ApmAccessProfileFactManager, + 'apm-access-policies': ApmAccessPolicyFactManager, + 'as3': As3FactManager, + 'asm-policy-stats': AsmPolicyStatsFactManager, + 'asm-policies': AsmPolicyFactManager, + 'asm-server-technologies': AsmServerTechnologyFactManager, + 'asm-signature-sets': AsmSignatureSetsFactManager, + 'client-ssl-profiles': ClientSslProfilesFactManager, + 'cfe': CFEFactManager, + 'devices': DevicesFactManager, + 'device-groups': DeviceGroupsFactManager, + 'do': DOFactManager, + 'external-monitors': ExternalMonitorsFactManager, + 'fasthttp-profiles': FastHttpProfilesFactManager, + 'fastl4-profiles': FastL4ProfilesFactManager, + 'gateway-icmp-monitors': GatewayIcmpMonitorsFactManager, + 'gtm-a-pools': GtmAPoolsFactManager, + 'gtm-servers': GtmServersFactManager, + 'gtm-a-wide-ips': GtmAWideIpsFactManager, + 'gtm-aaaa-pools': GtmAaaaPoolsFactManager, + 'gtm-aaaa-wide-ips': GtmAaaaWideIpsFactManager, + 'gtm-cname-pools': GtmCnamePoolsFactManager, + 'gtm-cname-wide-ips': GtmCnameWideIpsFactManager, + 'gtm-mx-pools': GtmMxPoolsFactManager, + 'gtm-mx-wide-ips': GtmMxWideIpsFactManager, + 'gtm-naptr-pools': GtmNaptrPoolsFactManager, + 'gtm-naptr-wide-ips': GtmNaptrWideIpsFactManager, + 'gtm-srv-pools': GtmSrvPoolsFactManager, + 'gtm-srv-wide-ips': GtmSrvWideIpsFactManager, + 'gtm-topology-regions': GtmTopologyRegionFactManager, + 'http-monitors': HttpMonitorsFactManager, + 'https-monitors': HttpsMonitorsFactManager, + 'http-profiles': HttpProfilesFactManager, + 'iapp-services': IappServicesFactManager, + 'iapplx-packages': IapplxPackagesFactManager, + 'icmp-monitors': IcmpMonitorsFactManager, + 'interfaces': InterfacesFactManager, + 'internal-data-groups': InternalDataGroupsFactManager, + 'irules': IrulesFactManager, + 'license': LicenseFactManager, + 'ltm-pools': LtmPoolsFactManager, + 'ltm-policies': LtmPolicyFactManager, + 'management-routes': ManagementRouteFactManager, + 'nodes': NodesFactManager, + 'oneconnect-profiles': OneConnectProfilesFactManager, + 'partitions': PartitionFactManager, + 'provision-info': ProvisionInfoFactManager, + 'route-domains': RouteDomainFactManager, + 'remote-syslog': RemoteSyslogFactManager, + 'self-ips': SelfIpsFactManager, + 'server-ssl-profiles': ServerSslProfilesFactManager, + 'software-volumes': SoftwareVolumesFactManager, + 'software-images': SoftwareImagesFactManager, + 'software-hotfixes': SoftwareHotfixesFactManager, + 'ssl-certs': SslCertificatesFactManager, + 'ssl-keys': SslKeysFactManager, + 'sync-status': SyncStatusFactManager, + 'system-db': SystemDbFactManager, + 'system-info': SystemInfoFactManager, + 'ts': TSFactManager, + 'tcp-monitors': TcpMonitorsFactManager, + 'tcp-half-open-monitors': TcpHalfOpenMonitorsFactManager, + 'tcp-profiles': TcpProfilesFactManager, + 'traffic-groups': TrafficGroupsFactManager, + 'trunks': TrunksFactManager, + 'ucs': UCSFactManager, + 'udp-profiles': UdpProfilesFactManager, + 'users': UsersFactManager, + 'vcmp-guests': VcmpGuestsFactManager, + 'virtual-addresses': VirtualAddressesFactManager, + 'virtual-servers': VirtualServersFactManager, + 'vlans': VlansFactManager, + } + + def exec_module(self): + start = datetime.datetime.now().isoformat() + client = F5RestClient(**self.module.params) + version = tmos_version(client) + self.handle_all_keyword() + self.handle_profiles_keyword() + self.handle_monitors_keyword() + self.handle_gtm_pools_keyword() + self.handle_gtm_wide_ips_keyword() + self.handle_packages_keyword() + self.filter_excluded_meta_facts() + res = self.check_valid_gather_subset(self.want.gather_subset) + if res: + invalid = ','.join(res) + raise F5ModuleError( + "The specified 'gather_subset' options are invalid: {0}".format(invalid) + ) + result = self.filter_excluded_facts() + + managers = [] + for name in result: + manager = self.get_manager(name) + if manager: + managers.append(manager) + + if not managers: + result = dict( + queried=False + ) + return result + + result = self.execute_managers(managers) + if result: + result['queried'] = True + else: + result['queried'] = False + send_teem(start, client, self.module, version) + return result + + def filter_excluded_facts(self): + # Remove the excluded entries from the list of possible facts + exclude = [x[1:] for x in self.want.gather_subset if x[0] == '!'] + include = [x for x in self.want.gather_subset if x[0] != '!'] + result = [x for x in include if x not in exclude] + return result + + def filter_excluded_meta_facts(self): + gather_subset = set(self.want.gather_subset) + gather_subset -= {'!all', '!profiles', '!monitors', '!gtm-pools', '!gtm-wide-ips', '!packages'} + keys = self.managers.keys() + + if '!all' in self.want.gather_subset: + gather_subset.clear() + if '!profiles' in self.want.gather_subset: + gather_subset -= {x for x in keys if '-profiles' in x} + if '!monitors' in self.want.gather_subset: + gather_subset -= {x for x in keys if '-monitors' in x} + if '!gtm-pools' in self.want.gather_subset: + gather_subset -= {x for x in keys if x.startswith('gtm-') and x.endswith('-pools')} + if '!gtm-wide-ips' in self.want.gather_subset: + gather_subset -= {x for x in keys if x.startswith('gtm-') and x.endswith('-wide-ips')} + if '!packages' in self.want.gather_subset: + gather_subset -= {'as3', 'do', 'cfe', 'ts'} + + self.want.update({'gather_subset': list(gather_subset)}) + + def handle_all_keyword(self): + if 'all' not in self.want.gather_subset: + return + managers = list(self.managers.keys()) + self.want.gather_subset + managers.remove('all') + self.want.update({'gather_subset': managers}) + + def handle_profiles_keyword(self): + if 'profiles' not in self.want.gather_subset: + return + managers = [x for x in self.managers.keys() if '-profiles' in x] + self.want.gather_subset + managers.remove('profiles') + self.want.update({'gather_subset': managers}) + + def handle_monitors_keyword(self): + if 'monitors' not in self.want.gather_subset: + return + managers = [x for x in self.managers.keys() if '-monitors' in x] + self.want.gather_subset + managers.remove('monitors') + self.want.update({'gather_subset': managers}) + + def handle_gtm_pools_keyword(self): + if 'gtm-pools' not in self.want.gather_subset: + return + keys = self.managers.keys() + managers = [x for x in keys if x.startswith('gtm-') and x.endswith('-pools')] + managers += self.want.gather_subset + managers.remove('gtm-pools') + self.want.update({'gather_subset': managers}) + + def handle_gtm_wide_ips_keyword(self): + if 'gtm-wide-ips' not in self.want.gather_subset: + return + keys = self.managers.keys() + managers = [x for x in keys if x.startswith('gtm-') and x.endswith('-wide-ips')] + managers += self.want.gather_subset + managers.remove('gtm-wide-ips') + self.want.update({'gather_subset': managers}) + + def handle_packages_keyword(self): + if 'packages' not in self.want.gather_subset: + return + managers = ['as3', 'do', 'cfe', 'ts'] + managers += self.want.gather_subset + managers.remove('packages') + self.want.update({'gather_subset': managers}) + + def check_valid_gather_subset(self, includes): + """Check that the specified subset is valid + + The ``gather_subset`` parameter is specified as a "raw" field which means that + any Python type could technically be provided + + :param includes: + :return: + """ + keys = self.managers.keys() + result = [] + for x in includes: + if x not in keys: + if x[0] == '!': + if x[1:] not in keys: + result.append(x) + else: + result.append(x) + return result + + def execute_managers(self, managers): + results = dict() + client = F5RestClient(**self.module.params) + prov = modules_provisioned(client) + for manager in managers: + manager.provisioned_modules = prov + result = manager.exec_module() + results.update(result) + return results + + def get_manager(self, which): + result = {} + manager = self.managers.get(which, None) + if not manager: + return result + kwargs = dict() + kwargs.update(self.kwargs) + + kwargs['client'] = F5RestClient(**self.module.params) + result = manager(**kwargs) + return result + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + data_increment=dict( + type='int', + default=10 + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + gather_subset=dict( + type='list', + elements='str', + required=True, + aliases=['include'], + choices=[ + # Meta choices + 'all', + 'monitors', + 'profiles', + 'gtm-pools', + 'gtm-wide-ips', + 'packages', + + # Non-meta choices + 'apm-access-profiles', + 'apm-access-policies', + 'as3', + 'asm-policies', + 'asm-policy-stats', + 'asm-server-technologies', + 'asm-signature-sets', + 'client-ssl-profiles', + 'cfe', + 'devices', + 'device-groups', + 'do', + 'external-monitors', + 'fasthttp-profiles', + 'fastl4-profiles', + 'gateway-icmp-monitors', + 'gtm-a-pools', + 'gtm-servers', + 'gtm-a-wide-ips', + 'gtm-aaaa-pools', + 'gtm-aaaa-wide-ips', + 'gtm-cname-pools', + 'gtm-cname-wide-ips', + 'gtm-mx-pools', + 'gtm-mx-wide-ips', + 'gtm-naptr-pools', + 'gtm-naptr-wide-ips', + 'gtm-srv-pools', + 'gtm-srv-wide-ips', + 'gtm-topology-regions', + 'http-profiles', + 'http-monitors', + 'https-monitors', + 'iapp-services', + 'iapplx-packages', + 'icmp-monitors', + 'interfaces', + 'internal-data-groups', + 'irules', + 'license', + 'ltm-pools', + 'ltm-policies', + 'management-routes', + 'nodes', + 'oneconnect-profiles', + 'partitions', + 'provision-info', + 'remote-syslog', + 'route-domains', + 'self-ips', + 'server-ssl-profiles', + 'software-volumes', + 'software-images', + 'software-hotfixes', + 'ssl-certs', + 'ssl-keys', + 'sync-status', + 'system-db', + 'system-info', + 'ts', + 'tcp-monitors', + 'tcp-half-open-monitors', + 'tcp-profiles', + 'traffic-groups', + 'trunks', + 'udp-profiles', + 'users', + 'ucs', + 'vcmp-guests', + 'virtual-addresses', + 'virtual-servers', + 'vlans', + + # Negations of meta choices + '!all', + "!monitors", + '!profiles', + '!gtm-pools', + '!gtm-wide-ips', + '!packages', + + # Negations of non-meta-choices + '!apm-access-profiles', + '!apm-access-policies', + '!as3', + '!do', + '!ts', + '!cfe', + '!asm-policy-stats', + '!asm-policies', + '!asm-server-technologies', + '!asm-signature-sets', + '!client-ssl-profiles', + '!devices', + '!device-groups', + '!external-monitors', + '!fasthttp-profiles', + '!fastl4-profiles', + '!gateway-icmp-monitors', + '!gtm-a-pools', + '!gtm-servers', + '!gtm-a-wide-ips', + '!gtm-aaaa-pools', + '!gtm-aaaa-wide-ips', + '!gtm-cname-pools', + '!gtm-cname-wide-ips', + '!gtm-mx-pools', + '!gtm-mx-wide-ips', + '!gtm-naptr-pools', + '!gtm-naptr-wide-ips', + '!gtm-srv-pools', + '!gtm-srv-wide-ips', + '!gtm-topology-regions', + '!http-profiles', + '!http-monitors', + '!https-monitors', + '!iapp-services', + '!iapplx-packages', + '!icmp-monitors', + '!interfaces', + '!internal-data-groups', + '!irules', + '!license', + '!ltm-pools', + '!ltm-policies', + '!management-routes', + '!nodes', + '!oneconnect-profiles', + '!partitions', + '!provision-info', + '!remote-syslog', + '!route-domains', + '!self-ips', + '!server-ssl-profiles', + '!software-volumes', + '!software-images', + '!software-hotfixes', + '!ssl-certs', + '!ssl-keys', + '!sync-status', + '!system-db', + '!system-info', + '!tcp-monitors', + '!tcp-half-open-monitors', + '!tcp-profiles', + '!traffic-groups', + '!trunks', + '!udp-profiles', + '!users', + '!ucs', + '!vcmp-guests', + '!virtual-addresses', + '!virtual-servers', + '!vlans', + ] + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + + # issue originally found and submitted in https://github.com/F5Networks/f5-ansible/pull/1477 by @traittinen + + ansible_facts = dict() + + for key, value in iteritems(results): + key = 'ansible_net_%s' % key + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts, **results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_license.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_license.py new file mode 100644 index 00000000..9b7eec22 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_license.py @@ -0,0 +1,950 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2016, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_license +short_description: Manage license installation and activation on BIG-IP devices +description: + - Manage license installation and activation on a BIG-IP. +version_added: "1.0.0" +options: + license_key: + description: + - The registration key to use to license the BIG-IP. + - This parameter is required if the C(state) is equal to C(present) or C(latest). + - This parameter is not required when C(state) is C(absent) and will be + ignored if it is provided. + type: str + addon_keys: + description: + - The list of addon keys to use to in conjunction with the base license. + - This parameter will be ignored if no C(license_key) is provided. + - This parameter is not required when C(state) is C(absent) and will be + ignored if it is provided. + type: list + elements: str + version_added: "1.2.0" + license_server: + description: + - The F5 license server to use when getting a license and validating a dossier. + - This parameter is required if the C(state) is equal to C(present) or C(latest). + - This parameter is not required when C(state) is C(absent) and will be + ignored if it is provided. + type: str + default: activate.f5.com + state: + description: + - The state of the license on the system. + - When C(present), only guarantees that a license exists. + - When C(absent), removes the license on the system. + - When C(latest), ensures that the license is always valid. This is not idempotent state since re-run can modify result. + - When C(revoked), removes the license on the system and revokes its future usage + on the F5 license servers. + type: str + choices: + - absent + - latest + - present + - revoked + default: present + accept_eula: + description: + - Declares whether you accept the BIG-IP EULA or not. By default, this + value is C(no). You must specifically declare you have viewed and + accepted the license. This module does not present you with the EULA, + so it is incumbent on you to read it. + - The EULA can be found here; https://support.f5.com/csp/article/K12902. + - This parameter is not required when C(state) is C(absent) and will be + ignored if it is provided. + type: bool + default: no + force: + description: + - Declares whether to force license renewal. By default, this + value is C(no). + - This parameter is not required and will be ignored if it is provided. + type: bool + default: no +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - This module can be used to license BIG-IPs that do not have access to internet. + - Only the Ansible Controller needs internet access as the license activation is done on the Ansible Controller from + which the module is running. +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) + - Andrey Kashcheev (@andreykashcheev) +''' + +EXAMPLES = ''' +- name: License BIG-IP using a key + bigip_device_license: + license_key: "XXXXX-XXXXX-XXXXX-XXXXX-XXXXXXX" + provider: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + delegate_to: localhost + +- name: License BIG-IP using a key + bigip_device_license: + license_key: "XXXXX-XXXXX-XXXXX-XXXXX-XXXXXXX" + provider: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + delegate_to: localhost + +- name: Remove the license from the system + bigip_device_license: + state: "absent" + provider: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' +import re +import time +import xml.etree.ElementTree +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, is_empty_list, f5_argument_spec +) +from ..module_utils.icontrol import iControlRestSession + +from ..module_utils.teem import send_teem + + +class LicenseXmlParser(object): + def __init__(self, content=None): + self.raw_content = content + try: + self.content = xml.etree.ElementTree.fromstring(content) + except xml.etree.ElementTree.ParseError as ex: + raise F5ModuleError("Provided XML payload is invalid. Received '{0}'.".format(str(ex))) + + @property + def namespaces(self): + result = { + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance' + } + return result + + @property + def eula(self): + try: + root = self.content.findall('.//eula', self.namespaces) + return root[0].text + except Exception: + return None + + @property + def license(self): + try: + root = self.content.findall('.//license', self.namespaces) + return root[0].text + except Exception: + return None + + def find_element(self, value): + root = self.content.findall('.//multiRef', self.namespaces) + if len(root) == 0: + return None + for elem in root: + for k, v in iteritems(elem.attrib): + if value in v: + return elem + + @property + def state(self): + elem = self.find_element('TransactionState') + if elem is not None: + return elem.text + + @property + def fault_number(self): + fault = self.get_fault() + return fault.get('faultNumber', None) + + @property + def fault_text(self): + fault = self.get_fault() + return fault.get('faultText', None) + + def get_fault(self): + result = dict() + + self.set_result_for_license_fault(result) + self.set_result_for_general_fault(result) + + if 'faultNumber' not in result: + result['faultNumber'] = None + return result + + def set_result_for_license_fault(self, result): + root = self.find_element('LicensingFault') + if root is None: + return result + for elem in root: + if elem.tag == 'faultNumber': + result['faultNumber'] = elem.text + elif elem.tag == 'faultText': + tmp = elem.attrib.get('{http://www.w3.org/2001/XMLSchema-instance}nil', None) + if tmp == 'true': + result['faultText'] = None + else: + result['faultText'] = elem.text + + def set_result_for_general_fault(self, result): + namespaces = { + 'ns2': 'http://schemas.xmlsoap.org/soap/envelope/' + } + root = self.content.findall('.//ns2:Fault', namespaces) + if len(root) == 0: + return None + for elem in root[0]: + if elem.tag == 'faultstring': + result['faultText'] = elem.text + + def json(self): + result = dict( + eula=self.eula or None, + license=self.license or None, + state=self.state or None, + fault_number=self.fault_number, + fault_text=self.fault_text or None + ) + return result + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'licenseEndDateTime': 'license_end_date_time', + } + + api_attributes = [ + + ] + + returnables = [ + + ] + + updatables = [ + + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def license_options(self): + result = dict( + eula=self.eula or '', + email=self.email or '', + first_name=self.first_name or '', + last_name=self.last_name or '', + company=self.company or '', + phone=self.phone or '', + job_title=self.job_title or '', + address=self.address or '', + city=self.city or '', + state=self.state or '', + postal_code=self.postal_code or '', + country=self.country or '' + ) + return result + + @property + def license_url(self): + result = 'https://{0}/license/services/urn:com.f5.license.v5b.ActivationService'.format(self.license_server) + return result + + @property + def license_envelope(self): + result = """ + + + + + {1} + {eula} + {email} + {first_name} + {last_name} + {company} + {phone} + {job_title} +
{address}
+ {city} + {state} + {postal_code} + {country} +
+
+
""" + result = result.format(self.license_server, self.dossier, **self.license_options) + return result + + @property + def addon_keys(self): + if self._values['license_key'] is None: + return None + if self._values['addon_keys'] is None or is_empty_list(self._values['addon_keys']): + return None + result = ','.join(self._values['addon_keys']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params, client=self.client) + self.have = ApiParameters(client=self.client) + self.changes = UsableChanges() + self.escape_patterns = r'([$"' + "'])" + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + changed = False + result = dict() + state = self.want.state + if state == "present": + changed = self.present() + elif state == "latest": + changed = self.valid() + elif state == "absent": + changed = self.absent() + elif state == "revoked": + changed = self.revoke() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, None) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists() and not self.is_revoked(): + return False + else: + return self.create() + + def valid(self): + if self.exists() and not self.is_revoked(): + if self.want.force or not self.license_valid(): + return self.create() + else: + return False + else: + return self.create() + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def revoke(self): + if self.is_revoked(): + return False + else: + # When revoking a license, it should be acceptable to auto-accept the + # license since you accepted it the first time when you activated the + # license you are now revoking. + self.want.update({'accept_eula': True}) + + # Revoking seems to just be another way of saying "get me a new license". + # There appear to be revoke-specific wording in the license and I assume + # some special revoke-like signing is happening, but the process is essentially + # just another form of "create". + return self.create() + + def revoke_from_device(self): + if self.module.check_mode: + return True + + dossier = self.read_dossier_from_device() + if dossier: + self.want.update({'dossier': dossier}) + else: + raise F5ModuleError("Dossier not generated.") + + if self.is_revoked(): + return False + + def is_revoked(self): + command = '-c "egrep Revoked /config/bigip.license"' + params = dict( + command='run', + utilCmdArgs=command + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + if 'commandResult' in response and 'Revoked' in response['commandResult']: + return True + return False + + def read_dossier_from_device(self): + params = dict( + command='run', + utilCmdArgs='-b "{0}"'.format(self.want.license_key) + ) + if self.want.addon_keys: + addons = self.want.addon_keys + params['utilCmdArgs'] = '-a {0} '.format(addons) + params['utilCmdArgs'] + + if self.want.state == 'revoked': + params['utilCmdArgs'] = '-r ' + params['utilCmdArgs'] + + uri = "https://{0}:{1}/mgmt/tm/util/get-dossier".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + try: + if self.want.state == 'revoked': + return response['commandResult'][8:] + else: + return response['commandResult'] + except Exception: + return None + + def generate_license_from_remote(self): + mgmt = iControlRestSession( + validate_certs=False, + headers={ + 'SOAPAction': '""', + 'Content-Type': 'text/xml; charset=utf-8', + } + ) + + for x in range(0, 10): + try: + resp = mgmt.post( + self.want.license_url, + data=self.want.license_envelope, + ) + except Exception: + # Failures to connect to licensing server must be passed upstream not supressed + raise + + try: + resp = LicenseXmlParser(content=resp.content) + result = resp.json() + except F5ModuleError: + # This error occurs when there is a problem with the license server and it + # starts returning invalid XML (like if they upgraded something and the server + # is redirecting improperly. + # + # There's no way to recover from this error except by notifying F5 that there + # is an issue with the license server. + raise + except Exception: + # Any other exceptions must be raised also + raise + + if result['state'] == 'EULA_REQUIRED': + self.want.update({'eula': result['eula']}) + continue + if result['state'] == 'LICENSE_RETURNED': + return result + elif result['state'] == 'EMAIL_REQUIRED': + raise F5ModuleError("Email must be provided") + elif result['state'] == 'CONTACT_INFO_REQUIRED': + raise F5ModuleError("Contact info must be provided") + else: + raise F5ModuleError(result['fault_text']) + + def create(self): + self._set_changed_options() + if not self.want.accept_eula: + raise F5ModuleError( + "You must read and accept the product EULA to license the box." + ) + if self.module.check_mode: + return True + + dossier = self.read_dossier_from_device() + if dossier: + self.want.update({'dossier': dossier}) + else: + raise F5ModuleError("Dossier not generated.") + + self.create_on_device() + self.wait_for_mcpd() + if not self.exists(): + raise F5ModuleError( + "Failed to license the device." + ) + return True + + def absent(self): + if self.any_license_exists(): + self.remove() + self.wait_for_mcpd() + if self.exists(): + raise F5ModuleError( + "Failed to remove the license from the device." + ) + return True + return False + + def license_valid(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/shared/licensing/registration".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + try: + return int(float(response['expiresInDays'])) > 0 + except Exception: + pass + return False + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/shared/licensing/registration".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + try: + if response['registrationKey'] == self.want.license_key: + return True + except Exception: + pass + return False + + def wait_for_mcpd(self): + nops = 0 + + # Sleep a little to let mcpd settle and begin properly + time.sleep(5) + + while nops < 4: + try: + if self._is_mcpd_ready_on_device(): + nops += 1 + else: + nops = 0 + except Exception as ex: + if '"message":"X-F5-Auth-Token has expired."' in str(ex): + raise F5ModuleError("X-F5-Auth-Token has expired.") + pass + time.sleep(5) + + def _is_mcpd_ready_on_device(self): + try: + command = "tmsh show sys mcp-state | grep running" + params = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(command) + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'commandResult' in response: + return True + except Exception as ex: + if '"message":"X-F5-Auth-Token has expired."' in str(ex): + raise + pass + return False + + def any_license_exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/shared/licensing/registration".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + try: + if response['registrationKey'] is not None: + return True + except Exception: + pass + return False + + def create_on_device(self): + license = self.generate_license_from_remote() + if license is None: + raise F5ModuleError( + "Failed to generate license from F5 activation servers." + ) + result = self.upload_license_to_device(license) + if not result: + raise F5ModuleError( + "Failed to install license on device." + ) + result = self.upload_eula_to_device(license) + if not result: + raise F5ModuleError( + "Failed to upload EULA file to device." + ) + result = self.reload_license() + if not result: + raise F5ModuleError( + "Failed to reload license configuration." + ) + + def upload_license_to_device(self, license): + license_payload = re.sub(self.escape_patterns, r'\\\1', license['license']) + command_arg = """cat > /config/bigip.license < /LICENSE.F5 < 1000: + raise F5ModuleError( + "Invalid ha_load_factor value, correct range is 1 - 1000, specified value: {0}.".format(value)) + return value + + @property + def auto_failback_time(self): + if self._values['auto_failback_time'] is None: + return None + value = self._values['auto_failback_time'] + if value < 0 or value > 300: + raise F5ModuleError( + "Invalid auto_failback_time value, correct range is 0 - 300, specified value: {0}.".format(value)) + return value + + @property + def auto_failback(self): + result = flatten_boolean(self._values['auto_failback']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + return None + + @property + def ha_order(self): + if self._values['ha_order'] is None: + return None + if len(self._values['ha_order']) == 1 and self._values['ha_order'][0] == '': + if self.auto_failback == 'true': + raise F5ModuleError( + 'Cannot enable auto failback when HA order list is empty, at least one device must be specified.' + ) + return 'none' + result = [fq_name(self.partition, value) for value in self._values['ha_order']] + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + + @property + def mac_address(self): + if self._values['mac_address'] is None: + return None + if self._values['mac_address'] == 'none': + return '' + return self._values['mac_address'] + + @property + def ha_group(self): + if self._values['ha_group'] is None: + return None + if self._values['ha_group'] == 'none': + return '' + return self._values['ha_group'] + + @property + def auto_failback(self): + result = self._values['auto_failback'] + if result == 'true': + return 'yes' + if result == 'false': + return 'no' + return None + + @property + def ha_order(self): + if self._values['ha_order'] is None: + return None + if self._values['ha_order'] == 'none': + return '' + return self._values['ha_order'] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def ha_group(self): + if self.want.ha_group is None: + return None + if self.have.ha_group is None and self.want.ha_group == 'none': + return None + if self.want.ha_group != self.have.ha_group: + if self.have.auto_failback == 'true' and self.want.auto_failback != 'false': + raise F5ModuleError( + "The auto_failback parameter on the device must disabled to use ha_group failover method." + ) + return self.want.ha_group + + @property + def ha_order(self): + # Device order is literally derived from the order in the array, + # hence lists with the same elements but in different order cannot be equal, so cmp_simple_list + # function will not work here. + if self.want.ha_order is None: + return None + if self.have.ha_order is None and self.want.ha_order == 'none': + return None + if self.want.ha_order != self.have.ha_order: + return self.want.ha_order + + @property + def partition(self): + raise F5ModuleError( + "Partition cannot be changed for a traffic group. Only /Common is allowed." + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.want.partition.lower().strip('/') != 'common': + raise F5ModuleError( + "Traffic groups can only be created in the /Common partition" + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/cm/traffic-group/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/cm/traffic-group/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/cm/traffic-group/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cm/traffic-group/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/cm/traffic-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + mac_address=dict(), + ha_order=dict( + type='list', + elements='str', + ), + ha_group=dict(), + ha_load_factor=dict( + type='int' + ), + auto_failback=dict( + type='bool', + ), + auto_failback_time=dict( + type='int' + ), + state=dict( + default='present', + choices=['absent', 'present'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_trust.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_trust.py new file mode 100644 index 00000000..88918278 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_device_trust.py @@ -0,0 +1,373 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_device_trust +short_description: Manage the trust relationships between BIG-IPs +description: + - Manage the trust relationships between BIG-IP systems. Devices, once peered, cannot + be updated. If updating is needed, the peer must first be removed before it + can be re-added to the trust. +version_added: "1.0.0" +options: + peer_server: + description: + - The peer address to connect to and trust for synchronizing the configuration. + This is typically the management address of the remote device, but may + also be a Self IP address. + type: str + required: True + peer_hostname: + description: + - The hostname you want to associate with the device. This value is + used to easily distinguish this device in BIG-IP configuration. + - When trusting a new device, if this parameter is not specified, the value + of C(peer_server) is used as a default. + type: str + peer_user: + description: + - The API username of the remote peer device you are trusting. Note + that the CLI user cannot be used unless it too has an API account. If this + value is not specified, then the value of C(user), or the environment + variable C(F5_USER) is used. + type: str + peer_password: + description: + - The password of the API username of the remote peer device you are + trusting. If this value is not specified, then the value of C(password), + or the environment variable C(F5_PASSWORD) is used. + type: str + type: + description: + - Specifies whether the device you are adding is a Peer or a Subordinate. + The default is C(peer). + - The difference between the two is a matter of mitigating risk of + compromise. + - A subordinate device cannot sign a certificate for another device. + - In the case where the security of an authority device in a trust domain + is compromised, the risk of compromise is minimized for any subordinate + device. + - Designating devices as subordinate devices is recommended for device + groups with a large number of member devices, where the risk of compromise + is high. + type: str + choices: + - peer + - subordinate + default: peer + state: + description: + - When C(present), ensures the specified devices are trusted. + - When C(absent), removes the device trusts. + type: str + choices: + - absent + - present + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Add trusts for all peer devices to Active device + bigip_device_trust: + peer_server: "{{ item.ansible_host }}" + peer_hostname: "{{ item.inventory_hostname }}" + peer_user: "{{ item.bigip_username }}" + peer_password: "{{ item.bigip_password }}" + provider: + server: lb.mydomain.com + user: admin + password: secret + loop: hostvars + when: inventory_hostname in groups['master'] + delegate_to: localhost +''' + +RETURN = r''' +peer_server: + description: The remote IP address of the trusted peer. + returned: changed + type: str + sample: 10.0.2.15 +peer_hostname: + description: The remote hostname used to identify the trusted peer. + returned: changed + type: str + sample: test-bigip-02.localhost.localdomain +''' + +import re +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'deviceName': 'peer_hostname', + 'caDevice': 'type', + 'device': 'peer_server', + 'username': 'peer_user', + 'password': 'peer_password' + } + + api_attributes = [ + 'name', + 'caDevice', + 'device', + 'deviceName', + 'username', + 'password' + ] + + returnables = [ + 'peer_server', 'peer_hostname' + ] + + updatables = [] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + @property + def peer_server(self): + if self._values['peer_server'] is None: + return None + if is_valid_ip(self._values['peer_server']): + return self._values['peer_server'] + raise F5ModuleError( + "The provided 'peer_server' parameter is not an IP address." + ) + + @property + def peer_hostname(self): + if self._values['peer_hostname'] is None: + return self.peer_server + regex = re.compile(r'[^a-zA-Z0-9.\-_]') + result = regex.sub('_', self._values['peer_hostname']) + return result + + @property + def type(self): + if self._values['type'] == 'peer': + return True + return False + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = Parameters(params=self.module.params) + self.changes = Parameters() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Parameters(params=changed) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def provided_password(self): + if self.want.password: + return self.password + if self.want.provider.get('password', None): + return self.want.provider.get('password') + if self.module.params.get('password', None): + return self.module.params.get('password') + + def provided_username(self): + if self.want.username: + return self.username + if self.want.provider.get('user', None): + return self.provider.get('user') + if self.module.params.get('user', None): + return self.module.params.get('user') + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def create(self): + self._set_changed_options() + if self.want.peer_user is None: + self.want.update({'peer_user': self.provided_username()}) + if self.want.peer_password is None: + self.want.update({'peer_password': self.provided_password()}) + if self.want.peer_hostname is None: + self.want.update({'peer_hostname': self.want.peer_server}) + if self.module.check_mode: + return True + + self.create_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + if self.want.peer_hostname is None: + self.want.update({'peer_hostname': self.want.peer_server}) + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to remove the trusted peer.") + return True + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/cm/device".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + for device in response['items']: + try: + if device['managementIp'] == self.want.peer_server: + return True + except KeyError: + pass + return False + + def create_on_device(self): + params = self.want.api_params() + params.update({ + "command": "run", + "name": 'Root', + }) + uri = "https://{0}:{1}/mgmt/tm/cm/add-to-trust/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + params = self.want.api_params() + params.update({ + "command": "run", + "deviceName": self.want.peer_hostname, + "name": self.want.peer_hostname, + }) + uri = "https://{0}:{1}/mgmt/tm/cm/remove-from-trust/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + peer_server=dict(required=True), + peer_hostname=dict(), + peer_user=dict(), + peer_password=dict(no_log=True), + type=dict( + choices=['peer', 'subordinate'], + default='peer' + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_cache_resolver.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_cache_resolver.py new file mode 100644 index 00000000..d442a8b6 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_cache_resolver.py @@ -0,0 +1,538 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: bigip_dns_cache_resolver +short_description: Manage DNS resolver cache configuration on a BIG-IP +description: + - Manage the DNS resolver cache configuration on BIG-IP devices. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the cache. + type: str + required: True + answer_default_zones: + description: + - Specifies whether the system answers DNS queries for the default + zones localhost, reverse 127.0.0.1 and ::1, and AS112. + - When creating a new cache resolver, if this parameter is not specified, the + default is C(no). + type: bool + forward_zones: + description: + - Forward zones associated with the cache. + - To remove all forward zones, specify a value of C(none). + suboptions: + name: + description: + - Specifies an FQDN for the forward zone. + type: str + nameservers: + description: + - Specifies the IP address and service port of a recursive + nameserver that answers DNS queries for the zone when the + response cannot be found in the DNS cache. + type: list + elements: dict + suboptions: + address: + description: + - Address of recursive nameserver. + type: str + port: + description: + - Port of recursive nameserver. + - When specifying new nameservers, if this value is not provided, the + default is C(53). + type: int + type: raw + route_domain: + description: + - Specifies the route domain the resolver uses for outbound traffic. + type: str + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a DNS resolver cache + bigip_dns_cache: + name: foo + answer_default_zones: yes + forward_zones: + - name: foo.bar.com + nameservers: + - address: 1.2.3.4 + port: 53 + - address: 5.6.7.8 + route_domain: 0 + 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. + returned: changed + type: str + sample: Foo is bar +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'routeDomain': 'route_domain', + 'answerDefaultZones': 'answer_default_zones', + 'forwardZones': 'forward_zones', + } + + api_attributes = [ + 'routeDomain', + 'answerDefaultZones', + 'forwardZones', + ] + + returnables = [ + 'route_domain', + 'answer_default_zones', + 'forward_zones', + ] + + updatables = [ + 'route_domain', + 'answer_default_zones', + 'forward_zones', + ] + + @property + def route_domain(self): + if self._values['route_domain'] is None: + return None + return fq_name(self.partition, self._values['route_domain']) + + @property + def answer_default_zones(self): + return flatten_boolean(self._values['answer_default_zones']) + + +class ApiParameters(Parameters): + @property + def forward_zones(self): + if self._values['forward_zones'] is None: + return None + result = [] + for x in self._values['forward_zones']: + tmp = dict( + name=x['name'], + nameservers=[] + ) + if 'nameservers' in x: + tmp['nameservers'] = [y['name'] for y in x['nameservers']] + tmp['nameservers'].sort() + result.append(tmp) + return result + + +class ModuleParameters(Parameters): + @property + def forward_zones(self): + if self._values['forward_zones'] is None: + return None + elif self._values['forward_zones'] in ['', 'none']: + return '' + result = [] + for x in self._values['forward_zones']: + if 'name' not in x: + raise F5ModuleError( + "A 'name' key must be provided when specifying a list of forward zones." + ) + tmp = dict( + name=x['name'], + nameservers=[] + ) + if 'nameservers' in x: + for ns in x['nameservers']: + if 'address' not in ns: + raise F5ModuleError( + "An 'address' key must be provided when specifying a list of forward zone nameservers." + ) + item = '{0}:{1}'.format(ns['address'], ns.get('port', 53)) + tmp['nameservers'].append(item) + tmp['nameservers'].sort() + result.append(tmp) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def forward_zones(self): + if self._values['forward_zones'] is None: + return None + result = [] + for x in self._values['forward_zones']: + tmp = {'name': x['name']} + if 'nameservers' in x: + tmp['nameservers'] = [] + for y in x['nameservers']: + tmp['nameservers'].append(dict(name=y)) + result.append(tmp) + return result + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def forward_zones(self): + if self.want.forward_zones is None: + return None + if self.have.forward_zones is None and self.want.forward_zones in ['', 'none']: + return None + if self.have.forward_zones is not None and self.want.forward_zones in ['', 'none']: + return [] + if self.have.forward_zones is None: + return dict( + forward_zones=self.want.forward_zones + ) + + want = sorted(self.want.forward_zones, key=lambda x: x['name']) + have = sorted(self.have.forward_zones, key=lambda x: x['name']) + + wnames = [x['name'] for x in want] + hnames = [x['name'] for x in have] + + if set(wnames) != set(hnames): + return dict( + forward_zones=self.want.forward_zones + ) + + for idx, x in enumerate(want): + wns = x.get('nameservers', []) + hns = have[idx].get('nameservers', []) + if set(wns) != set(hns): + return dict( + forward_zones=self.want.forward_zones + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/cache/resolver/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/dns/cache/resolver/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/cache/resolver/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/cache/resolver/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/cache/resolver/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + route_domain=dict(), + answer_default_zones=dict(type='bool'), + forward_zones=dict( + type='raw', + options=dict( + name=dict(), + nameservers=dict( + type='list', + elements='dict', + options=dict( + address=dict(), + port=dict(type='int') + ) + ) + ) + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_nameserver.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_nameserver.py new file mode 100644 index 00000000..9da3370c --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_nameserver.py @@ -0,0 +1,461 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_dns_nameserver +short_description: Manage LTM DNS nameservers on a BIG-IP +description: + - Manages LTM DNS nameservers on a BIG-IP. These nameservers form part of what is + known as DNS Express on a BIG-IP. This module does not configure GTM (DNS module) related + functionality, nor does it configure system-level name servers that affect the + ability of the base system to resolve DNS names. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the nameserver. + type: str + required: True + address: + description: + - Specifies the IP address on which the DNS nameserver (client) or back-end DNS + authoritative server (DNS Express server) listens for DNS messages. + - When creating a new nameserver, if this value is not specified, the default + is C(127.0.0.1). + type: str + service_port: + description: + - Specifies the service port on which the DNS nameserver (client) or back-end DNS + authoritative server (DNS Express server) listens for DNS messages. + - When creating a new nameserver, if this value is not specified, the default + is C(53). + type: str + route_domain: + description: + - Specifies the local route domain the DNS nameserver (client) or back-end + DNS authoritative server (DNS Express server) uses for outbound traffic. + - When creating a new nameserver, if this value is not specified, the default + is C(0). + type: str + tsig_key: + description: + - Specifies the TSIG key the system uses to communicate with this DNS nameserver + (client) or back-end DNS authoritative server (DNS Express server) for AXFR zone + transfers. + - If the nameserver is a client, then the system uses this TSIG key to verify the + request and sign the response. + - If this nameserver is a DNS Express server, then this TSIG key must match the + TSIG key for the zone on the back-end DNS authoritative server. + type: str + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a nameserver + bigip_dns_nameserver: + name: foo + address: 10.10.10.10 + service_port: 53 + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +address: + description: Address which the nameserver listens for DNS messages. + returned: changed + type: str + sample: 127.0.0.1 +service_port: + description: Service port on which the nameserver listens for DNS messages. + returned: changed + type: int + sample: 53 +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'routeDomain': 'route_domain', + 'port': 'service_port', + 'tsigKey': 'tsig_key' + } + + api_attributes = [ + 'address', + 'routeDomain', + 'port', + 'tsigKey' + ] + + returnables = [ + 'address', + 'service_port', + 'route_domain', + 'tsig_key', + ] + + updatables = [ + 'address', + 'service_port', + 'route_domain', + 'tsig_key', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def tsig_key(self): + if self._values['tsig_key'] in [None, '']: + return self._values['tsig_key'] + return fq_name(self.partition, self._values['tsig_key']) + + @property + def route_domain(self): + if self._values['route_domain'] is None: + return None + return fq_name(self.partition, self._values['route_domain']) + + @property + def service_port(self): + if self._values['service_port'] is None: + return None + try: + return int(self._values['service_port']) + except ValueError: + # Reserving the right to add well-known ports + raise F5ModuleError( + "The 'service_port' must be in numeric form." + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def tsig_key(self): + if self.want.tsig_key is None: + return None + if self.have.tsig_key is None and self.want.tsig_key == '': + return None + if self.want.tsig_key != self.have.tsig_key: + return self.want.tsig_key + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/nameserver/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.address is None: + self.want.update({'address': '127.0.0.1'}) + if self.want.service_port is None: + self.want.update({'service_port': '53'}) + if self.want.route_domain is None: + self.want.update({'route_domain': '/Common/0'}) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/dns/nameserver/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/nameserver/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/nameserver/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/nameserver/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + address=dict(), + service_port=dict(), + route_domain=dict(), + tsig_key=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_resolver.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_resolver.py new file mode 100644 index 00000000..2eac6f15 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_resolver.py @@ -0,0 +1,528 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_dns_resolver +short_description: Manage DNS resolvers on a BIG-IP +description: + - Manage DNS resolvers on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the DNS resolver. + type: str + required: True + route_domain: + description: + - Specifies the route domain the resolver uses for outbound traffic. + type: int + cache_size: + description: + - Specifies the size of the internal DNS resolver cache. + - When creating a new resolver, if this parameter is not specified, the default + is 5767168 bytes. + - After the cache reaches this size, when new or refreshed content arrives, the + system removes expired and older content and caches the new or updated content. + type: int + answer_default_zones: + description: + - Specifies whether the system answers DNS queries for the default zones localhost, + reverse 127.0.0.1 and ::1, and AS112. + - When creating a new resolver, if this parameter is not specified, the default + is C(no), meaning the system passes along the DNS queries for the default zones. + type: bool + randomize_query_case: + description: + - When C(yes), specifies the internal DNS resolver randomizes character case + in domain name queries issued to the root DNS servers. + - When creating a new resolver, if this parameter is not specified, the default + is C(yes). + type: bool + use_ipv4: + description: + - Specifies whether the system can use IPv4 to query backend nameservers. + - An IPv4 Self IP and default route must be available for these queries to work + successfully. + - When creating a new resolver, if this parameter is not specified, the default + is C(yes). + type: bool + use_ipv6: + description: + - Specifies whether the system can use IPv6 to query backend nameservers. + - An IPv6 Self IP and default route must be available for these queries to work + successfully. + - When creating a new resolver, if this parameter is not specified, the default + is C(yes). + type: bool + use_udp: + description: + - Specifies whether the system answers and issues UDP-formatted queries. + - When creating a new resolver, if this parameter is not specified, the default + is C(yes). + type: bool + use_tcp: + description: + - Specifies whether the system answers and issues TCP-formatted queries. + - When creating a new resolver, if this parameter is not specified, the default + is C(yes). + type: bool + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a simple DNS responder for OCSP stapling + bigip_dns_resolver: + name: resolver1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +route_domain: + description: The new route domain of the resource. + returned: changed + type: str + sample: /Common/0 +cache_size: + description: The new cache size of the resource. + returned: changed + type: int + sample: 50000 +answer_default_zones: + description: The new Answer Default Zones setting. + returned: changed + type: bool + sample: yes +randomize_query_case: + description: The new Randomize Query Character Case setting. + returned: changed + type: bool + sample: no +use_ipv4: + description: The new Use IPv4 setting. + returned: changed + type: bool + sample: yes +use_ipv6: + description: The new Use IPv6 setting. + returned: changed + type: bool + sample: no +use_udp: + description: The new Use UDP setting. + returned: changed + type: bool + sample: yes +use_tcp: + description: The new Use TCP setting. + returned: changed + type: bool + sample: no +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'answerDefaultZones': 'answer_default_zones', + 'cacheSize': 'cache_size', + 'randomizeQueryNameCase': 'randomize_query_case', + 'routeDomain': 'route_domain', + 'useIpv4': 'use_ipv4', + 'useIpv6': 'use_ipv6', + 'useTcp': 'use_tcp', + 'useUdp': 'use_udp', + } + + api_attributes = [ + 'answerDefaultZones', + 'cacheSize', + 'randomizeQueryNameCase', + 'routeDomain', + 'useIpv4', + 'useIpv6', + 'useTcp', + 'useUdp', + ] + + returnables = [ + 'answer_default_zones', + 'cache_size', + 'randomize_query_case', + 'route_domain', + 'use_ipv4', + 'use_ipv6', + 'use_tcp', + 'use_udp', + ] + + updatables = [ + 'answer_default_zones', + 'cache_size', + 'randomize_query_case', + 'route_domain', + 'use_ipv4', + 'use_ipv6', + 'use_tcp', + 'use_udp', + ] + + @property + def answer_default_zones(self): + result = flatten_boolean(self._values['answer_default_zones']) + return result + + @property + def randomize_query_case(self): + result = flatten_boolean(self._values['randomize_query_case']) + return result + + @property + def use_ipv4(self): + result = flatten_boolean(self._values['use_ipv4']) + return result + + @property + def use_ipv6(self): + result = flatten_boolean(self._values['use_ipv6']) + return result + + @property + def use_tcp(self): + result = flatten_boolean(self._values['use_tcp']) + return result + + @property + def use_udp(self): + result = flatten_boolean(self._values['use_udp']) + return result + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def route_domain(self): + if self._values['route_domain'] is None: + return None + result = fq_name(self.partition, self._values['route_domain']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/net/dns-resolver/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/net/dns-resolver/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/net/dns-resolver/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/dns-resolver/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/dns-resolver/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + route_domain=dict(type='int'), + cache_size=dict(type='int'), + answer_default_zones=dict(type='bool'), + randomize_query_case=dict(type='bool'), + use_ipv4=dict(type='bool'), + use_ipv6=dict(type='bool'), + use_udp=dict(type='bool'), + use_tcp=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_zone.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_zone.py new file mode 100644 index 00000000..491ad934 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_dns_zone.py @@ -0,0 +1,697 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_dns_zone +short_description: Manage DNS zones on BIG-IP +description: + - Manage DNS zones on BIG-IP. The zones managed here are primarily used + for configuring DNS Express on a BIG-IP. This module does not configure + zones that are found in BIG-IP ZoneRunner. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the DNS zone. + - The name must begin with a letter and contain only letters, numbers, + and the underscore character. + type: str + required: True + dns_express: + description: + - DNS express related settings. + type: dict + suboptions: + server: + description: + - Specifies the back-end authoritative DNS server from which the BIG-IP + system receives AXFR zone transfers for the DNS Express zone. + type: str + enabled: + description: + - Specifies the current status of the DNS Express zone. + type: bool + notify_action: + description: + - Specifies the action the system takes when a NOTIFY message is received + for this DNS Express zone. + - If a TSIG key is configured for the zone, the signature is only validated + for C(consume) and C(repeat) actions. + - When C(consume), the NOTIFY message is seen only by DNS Express. + - When C(bypass), the NOTIFY message does not go to DNS Express, but + instead goes to a back-end DNS server (subject to the value of the + Unhandled Query Action configured in the DNS profile applied to the + listener that handles the DNS request). + - When C(repeat), the NOTIFY message goes to both DNS Express and any + back-end DNS server. + type: str + choices: + - consume + - bypass + - repeat + allow_notify_from: + description: + - Specifies the IP addresses from which the system accepts NOTIFY messages + for this DNS Express zone. + type: list + elements: str + verify_tsig: + description: + - Specifies whether the system verifies the identity of the authoritative + nameserver that sends updated information for this DNS Express zone. + type: bool + response_policy: + description: + - Specifies whether this DNS Express zone is a DNS response policy zone (RPZ). + type: bool + nameservers: + description: + - Specifies the DNS nameservers to which the system sends NOTIFY messages. + type: list + elements: str + tsig_server_key: + description: + - Specifies the TSIG key the system uses to authenticate the back-end DNS + authoritative server that sends AXFR zone transfers to the BIG-IP system. + type: str + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Greg Crosby (@crosbygw) +''' + +EXAMPLES = r''' +- name: Create a DNS zone for DNS express + bigip_dns_zone: + name: zone.foo.com + dns_express: + enabled: yes + server: dns-lab + allow_notify_from: + - 192.168.39.10 + notify_action: consume + verify_tsig: no + response_policy: no + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Disable DNS express zone, change server, and modify notify_action to bypass + bigip_dns_zone: + name: zone.foo.com + dns_express: + enabled: no + server: foo1.server.com + allow_notify_from: + - 192.168.39.10 + notify_action: bypass + verify_tsig: no + response_policy: no + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Add nameservers + bigip_dns_zone: + name: zone.foo.com + nameservers: + - foo1.nameserver.com + - foo2.nameserver.com + - foo3.nameserver.com + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove nameserver + bigip_dns_zone: + name: zone.foo.com + nameservers: + - foo1.nameserver.com + - foo2.nameserver.com + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove all nameservers + bigip_dns_zone: + name: zone.foo.com + nameservers: none + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Add tsig_server_key + bigip_dns_zone: + name: zone.foo.com + tsig_server_key: key1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove tsig_server_key + bigip_dns_zone: + name: zone.foo.com + tsig_server_key: none + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove zone + bigip_dns_zone: + name: zone.foo.com + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +''' + +RETURN = r''' +enabled: + description: Whether the zone is enabled or not. + returned: changed + type: bool + sample: yes +allow_notify_from: + description: The new DNS Express Allow NOTIFY From value. + returned: changed + type: list + sample: ['1.1.1.1', '2.2.2.2'] +notify_action: + description: The new DNS Express Notify Action value. + returned: changed + type: str + sample: consume +verify_tsig: + description: The new DNS Express Verify Notify TSIG value. + returned: changed + type: bool + sample: yes +express_server: + description: The new DNS Express Server value. + returned: changed + type: str + sample: server1 +response_policy: + description: The new DNS Express Response Policy value. + returned: changed + type: bool + sample: no +nameservers: + description: The new Zone Transfer Clients Nameservers value. + returned: changed + type: list + sample: ['/Common/server1', '/Common/server2'] +tsig_server_key: + description: The new TSIG Server Key value. + returned: changed + type: str + sample: /Common/key1 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_simple_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'dnsExpressEnabled': 'enabled', + 'dnsExpressAllowNotify': 'allow_notify_from', + 'dnsExpressNotifyAction': 'notify_action', + 'dnsExpressNotifyTsigVerify': 'verify_tsig', + 'dnsExpressServer': 'express_server', + 'responsePolicy': 'response_policy', + 'transferClients': 'nameservers', + 'serverTsigKey': 'tsig_server_key', + } + + api_attributes = [ + 'dnsExpressEnabled', + 'dnsExpressAllowNotify', + 'dnsExpressNotifyAction', + 'dnsExpressNotifyTsigVerify', + 'dnsExpressServer', + 'responsePolicy', + 'transferClients', + 'serverTsigKey', + ] + + returnables = [ + 'enabled', + 'allow_notify_from', + 'notify_action', + 'verify_tsig', + 'express_server', + 'response_policy', + 'nameservers', + 'tsig_server_key', + ] + + updatables = [ + 'enabled', + 'allow_notify_from', + 'notify_action', + 'verify_tsig', + 'express_server', + 'response_policy', + 'nameservers', + 'tsig_server_key', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def express_server(self): + try: + if self._values['dns_express']['server'] is None: + return None + if self._values['dns_express']['server'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['dns_express']['server']) + except (TypeError, KeyError): + return None + + @property + def nameservers(self): + if self._values['nameservers'] is None: + return None + elif len(self._values['nameservers']) == 1 and self._values['nameservers'][0] in ['', 'none']: + return '' + return [fq_name(self.partition, x) for x in self._values['nameservers']] + + @property + def tsig_server_key(self): + if self._values['tsig_server_key'] is None: + return None + if self._values['tsig_server_key'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['tsig_server_key']) + + @property + def enabled(self): + try: + return flatten_boolean(self._values['dns_express']['enabled']) + except (TypeError, KeyError): + return None + + @property + def verify_tsig(self): + try: + return flatten_boolean(self._values['dns_express']['verify_tsig']) + except (TypeError, KeyError): + return None + + @property + def notify_action(self): + try: + return self._values['dns_express']['notify_action'] + except (TypeError, KeyError): + return None + + @property + def response_policy(self): + try: + return flatten_boolean(self._values['dns_express']['response_policy']) + except (TypeError, KeyError): + return None + + @property + def allow_notify_from(self): + try: + v = self._values['dns_express']['allow_notify_from'] + if v is None: + return None + elif len(v) == 1 and v[0] in ['', 'none']: + return '' + return v + except (TypeError, KeyError): + return None + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + @property + def allow_notify_from(self): + return cmp_simple_list(self.want.allow_notify_from, self.have.allow_notify_from) + + @property + def nameservers(self): + return cmp_simple_list(self.want.nameservers, self.have.nameservers) + + @property + def express_server(self): + if self.want.express_server is None: + return None + if self.want.express_server == '' and self.have.express_server is None: + return None + if self.want.express_server != self.have.express_server: + return self.want.express_server + + @property + def tsig_server_key(self): + if self.want.tsig_server_key is None: + return None + if self.want.tsig_server_key == '' and self.have.tsig_server_key is None: + return None + if self.want.tsig_server_key != self.have.tsig_server_key: + return self.want.tsig_server_key + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/zone/{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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/dns/zone/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/zone/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/zone/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/dns/zone/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + dns_express=dict( + type='dict', + options=dict( + server=dict(), + enabled=dict(type='bool'), + notify_action=dict( + choices=['consume', 'bypass', 'repeat'] + ), + allow_notify_from=dict( + type='list', + elements='str', + ), + verify_tsig=dict(type='bool'), + response_policy=dict(type='bool') + ) + ), + nameservers=dict( + type='list', + elements='str', + ), + tsig_server_key=dict(), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_file_copy.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_file_copy.py new file mode 100644 index 00000000..dd93e191 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_file_copy.py @@ -0,0 +1,695 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_file_copy +short_description: Manage files in datastores on a BIG-IP +description: + - Manages files on a variety of datastores on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - The name of the file as it should reside on the BIG-IP. + - If this is not specified, then the filename provided in the C(source) + parameter is used instead. + type: str + source: + description: + - Specifies the path of the file to upload. + - This parameter is required if C(state) is C(present). + type: path + aliases: + - src + datastore: + description: + - Specifies the datastore to put the file in. + - There are several different datastores and each of them allows files + to be exposed in different ways. + - When C(external-monitor), the specified file will be stored as + an external monitor file and be available for use in external monitors. + - When C(ifile), the specified file will be stored as an iFile. + - When C(lw4o6-table), the specified file will be stored as a Lightweight 4 + over 6 (lw4o6) tunnel binding table, which includes an IPv6 address for the + lwB4, public IPv4 address, and restricted port set. + type: str + choices: + - external-monitor + - ifile + - lw4o6-table + default: ifile + force: + description: + - Force overwriting a file. + - By default, files will only be overwritten if the SHA of the file is different + for the given filename. This parameter can be used to force overwriting the file + even if it already exists and its SHA matches. + - The C(lw4o6-table) datastore does not keep checksums of its file. Therefore, you + would need to provide this argument to update any of these files. + type: bool + default: no + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Upload a file as an iFile + bigip_file_copy: + name: foo + source: /path/to/file.txt + datastore: ifile + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +# Upload a directory of files +- name: Recursively upload web related files in /var/tmp/project + find: + paths: /var/tmp/project + patterns: "^.*?\\.(?:html|?:css|?:js)$" + use_regex: yes + register: f + +- name: Upload a directory of files as a set of iFiles + bigip_file_copy: + source: "{{ item.path }}" + datastore: ifile + provider: + password: secret + server: lb.mydomain.com + user: admin + loop: f + delegate_to: localhost +# End upload a directory of files + +- name: Upload a file to use in an external monitor + bigip_file_copy: + source: /path/to/files/external.sh + datastore: external-monitor + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import hashlib +import os +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import ( + upload_file, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + + ] + + returnables = [ + + ] + + updatables = [ + 'checksum', + ] + + +class ApiParameters(Parameters): + @property + def checksum(self): + """Returns a plain checksum value without the leading extra characters + + Values are stored in the REST as the following. + + ``"checksum": "SHA1:77002:b84015799949ac4acad87b81691455242a31e894"`` + + Returns: + string: The parsed SHA1 checksum. + """ + if self._values['checksum'] is None: + return None + return str(self._values['checksum'].split(':')[2]) + + +class ModuleParameters(Parameters): + @property + def checksum(self): + """Return SHA1 checksum of the file on disk + + Returns: + string: The SHA1 checksum of the file. + + References: + - https://stackoverflow.com/a/22058673/661215 + """ + if self._values['datastore'] == 'lw4o6-table': + return None + + sha1 = hashlib.sha1() + with open(self._values['source'], 'rb') as f: + while True: + data = f.read(4096) + if not data: + break + sha1.update(data) + return sha1.hexdigest() + + @property + def name(self): + if self._values['name'] is not None: + return self._values['name'] + if self._values['source'] is None: + return None + return os.path.basename(self._values['source']) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update() and not self.want.force: + return False + if self.module.check_mode: + return True + self.remove_from_device() + self.upload_to_device() + self.create_on_device() + self.remove_uploaded_file_from_device(self.want.name) + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.module.check_mode: + return True + self.upload_to_device() + self.create_on_device() + self.remove_uploaded_file_from_device(self.want.name) + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def upload_to_device(self): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, self.want.source, self.want.name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def remove_uploaded_file_from_device(self, name): + filepath = '/var/config/rest/downloads/{0}'.format(name) + params = { + "command": "run", + "utilCmdArgs": filepath + } + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class IFileManager(BaseManager): + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['source-path'] = 'file:/var/config/rest/downloads/{0}'.format(self.want.name) + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/file/ifile/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ifile/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ifile/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ifile/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ExternalMonitorManager(BaseManager): + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['source-path'] = 'file:/var/config/rest/downloads/{0}'.format(self.want.name) + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/file/external-monitor/".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 exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/external-monitor/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/external-monitor/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/external-monitor/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class Lw4o6Manager(BaseManager): + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['source-path'] = 'file:/var/config/rest/downloads/{0}'.format(self.want.name) + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/file/lwtunneltbl/".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 exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/lwtunneltbl/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/lwtunneltbl/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/lwtunneltbl/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.kwargs = kwargs + + def exec_module(self): + if self.module.params['datastore'] == 'ifile': + manager = self.get_manager('v1') + elif self.module.params['datastore'] == 'external-monitor': + manager = self.get_manager('v2') + elif self.module.params['datastore'] == 'lw4o6-table': + manager = self.get_manager('v3') + else: + raise F5ModuleError( + "Unknown datastore specified." + ) + return manager.exec_module() + + def get_manager(self, type): + if type == 'v1': + return IFileManager(**self.kwargs) + elif type == 'v2': + return ExternalMonitorManager(**self.kwargs) + elif type == 'v3': + return Lw4o6Manager(**self.kwargs) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(), + source=dict( + type='path', + aliases=['src'], + ), + datastore=dict( + choices=[ + 'external-monitor', + 'ifile', + 'lw4o6-table', + ], + default='ifile' + ), + force=dict(type='bool', default='no'), + 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.required_if = [ + ['state', 'present', ['source']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_address_list.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_address_list.py new file mode 100644 index 00000000..5bb375f6 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_address_list.py @@ -0,0 +1,1016 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_address_list +short_description: Manage address lists on BIG-IP AFM +description: + - Manages the AFM (Advanced Firewall Manager) address lists on a BIG-IP. This module + can be used to add and remove address list entries. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the address list. + type: str + required: True + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + description: + description: + - Description of the address list. + type: str + geo_locations: + description: + - List of geolocations specified by their C(country) and C(region). + type: list + elements: dict + suboptions: + country: + description: + - The country name or code of the geolocation to use. + - In addition to the country full names, you may also specify their abbreviated + form, such as C(US) instead of C(United States). + - Valid country codes can be found here https://countrycode.org/. + type: str + required: True + choices: + - Any valid 2 character ISO country code. + - Any valid country name. + region: + description: + - Region name of the country to use. + type: str + addresses: + description: + - Individual addresses you want to add to the list. These addresses differ + from ranges and lists of lists, such as what can be used in C(address_ranges) + and C(address_lists) respectively. + - This list can also include networks that have CIDR notation. + type: list + elements: str + address_ranges: + description: + - A list of address ranges where the range starts with a port number, is followed + by a dash (-), and then a second number. + - If the first address is greater than the second number, the numbers will be + reversed so they are properly formatted. For example, C(2.2.2.2-1.1.1). would become + C(1.1.1.1-2.2.2.2). + type: list + elements: str + address_lists: + description: + - Simple list of existing address lists to add to this list. Address lists can be + specified in either their fully qualified name (/Common/foo) or their short + name (foo). If a short name is used, the C(partition) argument will automatically + be prepended to the short name. + type: list + elements: str + fqdns: + description: + - A list of fully qualified domain names (FQDNs). + - An FQDN has at least one decimal point in it, separating the host from the domain. + - To add FQDNs to a list requires that a global FQDN resolver is configured. + This must be done using C(bigip_command) or from the GUI + of the BIG-IP. If using C(bigip_command), you can do this with C(tmsh modify security + firewall global-fqdn-policy FOO) where C(FOO) is a DNS resolver configured + at C(tmsh create net dns-resolver FOO). + type: list + elements: str + state: + description: + - When C(present), ensures the address list and entries exists. + - When C(absent), ensures the address list is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an address list + bigip_firewall_address_list: + name: foo + addresses: + - 3.3.3.3 + - 4.4.4.4 + - 5.5.5.5 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the address list. + returned: changed + type: str + sample: My address list +addresses: + description: The new list of addresses applied to the address list. + returned: changed + type: list + sample: [1.1.1.1, 2.2.2.2] +address_ranges: + description: The new list of address ranges applied to the address list. + returned: changed + type: list + sample: [1.1.1.1-2.2.2.2, 3.3.3.3-4.4.4.4] +address_lists: + description: The new list of address list names applied to the address list. + returned: changed + type: list + sample: [/Common/list1, /Common/list2] +fqdns: + description: The new list of FQDN names applied to the address list. + returned: changed + type: list + sample: [google.com, mit.edu] +geo_locations: + description: The new list of geolocations applied to the address list. + returned: changed + type: complex + contains: + country: + description: Country of the geolocation. + returned: changed + type: str + sample: US + region: + description: Region of the geolocation. + returned: changed + type: str + sample: California +''' + +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ipaddress import ( + ip_address, ip_interface +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.ipaddress import ( + is_valid_ip, is_valid_ip_interface +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'addressLists': 'address_lists', + 'geo': 'geo_locations', + } + + api_attributes = [ + 'addressLists', + 'addresses', + 'description', + 'fqdns', + 'geo', + ] + + returnables = [ + 'addresses', + 'address_ranges', + 'address_lists', + 'description', + 'fqdns', + 'geo_locations', + ] + + updatables = [ + 'addresses', + 'address_ranges', + 'address_lists', + 'description', + 'fqdns', + 'geo_locations', + ] + + +class ApiParameters(Parameters): + @property + def address_ranges(self): + if self._values['addresses'] is None: + return None + result = [] + for address_range in self._values['addresses']: + if '-' not in address_range['name']: + continue + result.append(address_range['name'].strip()) + result = sorted(result) + return result + + @property + def address_lists(self): + if self._values['address_lists'] is None: + return None + result = [] + for x in self._values['address_lists']: + item = '/{0}/{1}'.format(x['partition'], x['name']) + result.append(item) + result = sorted(result) + return result + + @property + def addresses(self): + if self._values['addresses'] is None: + return None + result = [x['name'] for x in self._values['addresses'] if '-' not in x['name']] + result = sorted(result) + return result + + @property + def fqdns(self): + if self._values['fqdns'] is None: + return None + result = [str(x['name']) for x in self._values['fqdns']] + result = sorted(result) + return result + + @property + def geo_locations(self): + if self._values['geo_locations'] is None: + return None + result = [str(x['name']) for x in self._values['geo_locations']] + result = sorted(result) + return result + + +class ModuleParameters(Parameters): + def __init__(self, params=None): + super(ModuleParameters, self).__init__(params=params) + self.country_iso_map = { + 'Afghanistan': 'AF', + 'Albania': 'AL', + 'Algeria': 'DZ', + 'American Samoa': 'AS', + 'Andorra': 'AD', + 'Angola': 'AO', + 'Anguilla': 'AI', + 'Antarctica': 'AQ', + 'Antigua and Barbuda': 'AG', + 'Argentina': 'AR', + 'Armenia': 'AM', + 'Aruba': 'AW', + 'Australia': 'AU', + 'Austria': 'AT', + 'Azerbaijan': 'AZ', + 'Bahamas': 'BS', + 'Bahrain': 'BH', + 'Bangladesh': 'BD', + 'Barbados': 'BB', + 'Belarus': 'BY', + 'Belgium': 'BE', + 'Belize': 'BZ', + 'Benin': 'BJ', + 'Bermuda': 'BM', + 'Bhutan': 'BT', + 'Bolivia': 'BO', + 'Bosnia and Herzegovina': 'BA', + 'Botswana': 'BW', + 'Brazil': 'BR', + 'Brunei': 'BN', + 'Bulgaria': 'BG', + 'Burkina Faso': 'BF', + 'Burundi': 'BI', + 'Cameroon': 'CM', + 'Canada': 'CA', + 'Cape Verde': 'CV', + 'Central African Republic': 'CF', + 'Chile': 'CL', + 'China': 'CN', + 'Christmas Island': 'CX', + 'Cocos Islands': 'CC', + 'Colombia': 'CO', + 'Cook Islands': 'CK', + 'Costa Rica': 'CR', + 'Cuba': 'CU', + 'Curacao': 'CW', + 'Cyprus': 'CY', + 'Czech Republic': 'CZ', + 'Democratic Republic of the Congo': 'CD', + 'Denmark': 'DK', + 'Djibouti': 'DJ', + 'Dominica': 'DM', + 'Dominican Republic': 'DO', + 'Ecuador': 'EC', + 'Egypt': 'EG', + 'Eritrea': 'ER', + 'Estonia': 'EE', + 'Ethiopia': 'ET', + 'Falkland Islands': 'FK', + 'Faroe Islands': 'FO', + 'Fiji': 'FJ', + 'Finland': 'FI', + 'France': 'FR', + 'French Polynesia': 'PF', + 'Gabon': 'GA', + 'Gambia': 'GM', + 'Georgia': 'GE', + 'Germany': 'DE', + 'Ghana': 'GH', + 'Gilbraltar': 'GI', + 'Greece': 'GR', + 'Greenland': 'GL', + 'Grenada': 'GD', + 'Guam': 'GU', + 'Guatemala': 'GT', + 'Guernsey': 'GG', + 'Guinea': 'GN', + 'Guinea-Bissau': 'GW', + 'Guyana': 'GY', + 'Haiti': 'HT', + 'Honduras': 'HN', + 'Hong Kong': 'HK', + 'Hungary': 'HU', + 'Iceland': 'IS', + 'India': 'IN', + 'Indonesia': 'ID', + 'Iran': 'IR', + 'Iraq': 'IQ', + 'Ireland': 'IE', + 'Isle of Man': 'IM', + 'Israel': 'IL', + 'Italy': 'IT', + 'Ivory Coast': 'CI', + 'Jamaica': 'JM', + 'Japan': 'JP', + 'Jersey': 'JE', + 'Jordan': 'JO', + 'Kazakhstan': 'KZ', + 'Laos': 'LA', + 'Latvia': 'LV', + 'Lebanon': 'LB', + 'Lesotho': 'LS', + 'Liberia': 'LR', + 'Libya': 'LY', + 'Liechtenstein': 'LI', + 'Lithuania': 'LT', + 'Luxembourg': 'LU', + 'Macau': 'MO', + 'Macedonia': 'MK', + 'Madagascar': 'MG', + 'Malawi': 'MW', + 'Malaysia': 'MY', + 'Maldives': 'MV', + 'Mali': 'ML', + 'Malta': 'MT', + 'Marshall Islands': 'MH', + 'Mauritania': 'MR', + 'Mauritius': 'MU', + 'Mayotte': 'YT', + 'Mexico': 'MX', + 'Micronesia': 'FM', + 'Moldova': 'MD', + 'Monaco': 'MC', + 'Mongolia': 'MN', + 'Montenegro': 'ME', + 'Montserrat': 'MS', + 'Morocco': 'MA', + 'Mozambique': 'MZ', + 'Myanmar': 'MM', + 'Namibia': 'NA', + 'Nauru': 'NR', + 'Nepal': 'NP', + 'Netherlands': 'NL', + 'Netherlands Antilles': 'AN', + 'New Caledonia': 'NC', + 'New Zealand': 'NZ', + 'Nicaragua': 'NI', + 'Niger': 'NE', + 'Nigeria': 'NG', + 'Niue': 'NU', + 'North Korea': 'KP', + 'Northern Mariana Islands': 'MP', + 'Norway': 'NO', + 'Oman': 'OM', + 'Pakistan': 'PK', + 'Palau': 'PW', + 'Palestine': 'PS', + 'Panama': 'PA', + 'Papua New Guinea': 'PG', + 'Paraguay': 'PY', + 'Peru': 'PE', + 'Philippines': 'PH', + 'Pitcairn': 'PN', + 'Poland': 'PL', + 'Portugal': 'PT', + 'Puerto Rico': 'PR', + 'Qatar': 'QA', + 'Republic of the Congo': 'CG', + 'Reunion': 'RE', + 'Romania': 'RO', + 'Russia': 'RU', + 'Rwanda': 'RW', + 'Saint Barthelemy': 'BL', + 'Saint Helena': 'SH', + 'Saint Kitts and Nevis': 'KN', + 'Saint Lucia': 'LC', + 'Saint Martin': 'MF', + 'Saint Pierre and Miquelon': 'PM', + 'Saint Vincent and the Grenadines': 'VC', + 'Samoa': 'WS', + 'San Marino': 'SM', + 'Sao Tome and Principe': 'ST', + 'Saudi Arabia': 'SA', + 'Senegal': 'SN', + 'Serbia': 'RS', + 'Seychelles': 'SC', + 'Sierra Leone': 'SL', + 'Singapore': 'SG', + 'Sint Maarten': 'SX', + 'Slovakia': 'SK', + 'Slovenia': 'SI', + 'Solomon Islands': 'SB', + 'Somalia': 'SO', + 'South Africa': 'ZA', + 'South Korea': 'KR', + 'South Sudan': 'SS', + 'Spain': 'ES', + 'Sri Lanka': 'LK', + 'Sudan': 'SD', + 'Suriname': 'SR', + 'Svalbard and Jan Mayen': 'SJ', + 'Swaziland': 'SZ', + 'Sweden': 'SE', + 'Switzerland': 'CH', + 'Syria': 'SY', + 'Taiwan': 'TW', + 'Tajikstan': 'TJ', + 'Tanzania': 'TZ', + 'Thailand': 'TH', + 'Togo': 'TG', + 'Tokelau': 'TK', + 'Tonga': 'TO', + 'Trinidad and Tobago': 'TT', + 'Tunisia': 'TN', + 'Turkey': 'TR', + 'Turkmenistan': 'TM', + 'Turks and Caicos Islands': 'TC', + 'Tuvalu': 'TV', + 'U.S. Virgin Islands': 'VI', + 'Uganda': 'UG', + 'Ukraine': 'UA', + 'United Arab Emirates': 'AE', + 'United Kingdom': 'GB', + 'United States': 'US', + 'Uruguay': 'UY', + 'Uzbekistan': 'UZ', + 'Vanuatu': 'VU', + 'Vatican': 'VA', + 'Venezuela': 'VE', + 'Vietnam': 'VN', + 'Wallis and Futuna': 'WF', + 'Western Sahara': 'EH', + 'Yemen': 'YE', + 'Zambia': 'ZM', + 'Zimbabwe': 'ZW' + } + self.choices_iso_codes = self.country_iso_map.values() + + def is_valid_hostname(self, host): + """Reasonable attempt at validating a hostname + + Compiled from various paragraphs outlined here + https://tools.ietf.org/html/rfc3696#section-2 + https://tools.ietf.org/html/rfc1123 + + Notably, + * Host software MUST handle host names of up to 63 characters and + SHOULD handle host names of up to 255 characters. + * The "LDH rule", after the characters that it permits. (letters, digits, hyphen) + * If the hyphen is used, it is not permitted to appear at + either the beginning or end of a label + + :param host: + :return: + """ + if len(host) > 255: + return False + host = host.rstrip(".") + allowed = re.compile(r'(?!-)[A-Z0-9-]{1,63}(?[^%]+)%(?P[0-9]+)' + matches = re.search(pattern, address) + if matches: + addr = matches.group('ip') + rd = matches.group('route_domain') + return addr, rd + return None, None + + @property + def addresses(self): + if self._values['addresses'] is None: + return None + result = [] + for x in self._values['addresses']: + addr, rd = self._get_rd(x) + if addr and rd: + if is_valid_ip(addr): + result.append(str(ip_address(u'{0}'.format(addr))) + '%' + rd) + elif is_valid_ip_interface(addr): + result.append(str(ip_interface(u'{0}'.format(x))) + '%' + rd) + else: + raise F5ModuleError( + "Address {0} must be either an IPv4 or IPv6 address or network appended" + "by a '%' and a route domain number e.g. 1.2.3.4%1 .".format(x) + ) + else: + if is_valid_ip(x): + result.append(str(ip_address(u'{0}'.format(x)))) + elif is_valid_ip_interface(x): + result.append(str(ip_interface(u'{0}'.format(x)))) + else: + raise F5ModuleError( + "Address {0} must be either an IPv4 or IPv6 address or network.".format(x) + ) + result = sorted(result) + return result + + @property + def address_ranges(self): + if self._values['address_ranges'] is None: + return None + result = [] + for address_range in self._values['address_ranges']: + start, stop = address_range.split('-') + start = start.strip() + stop = stop.strip() + + start = ip_address(u'{0}'.format(start)) + stop = ip_address(u'{0}'.format(stop)) + if start.version != stop.version: + raise F5ModuleError( + "When specifying a range, IP addresses must be of the same type; IPv4 or IPv6." + ) + if int(start) > int(stop): + stop, start = start, stop + item = '{0}-{1}'.format(str(start), str(stop)) + result.append(item) + result = sorted(result) + return result + + @property + def address_lists(self): + if self._values['address_lists'] is None: + return None + result = [] + for x in self._values['address_lists']: + item = fq_name(self.partition, x) + result.append(item) + result = sorted(result) + return result + + @property + def fqdns(self): + if self._values['fqdns'] is None: + return None + result = [] + for x in self._values['fqdns']: + if self.is_valid_hostname(x): + result.append(x) + else: + raise F5ModuleError( + "The hostname '{0}' looks invalid.".format(x) + ) + result = sorted(result) + return result + + @property + def geo_locations(self): + if self._values['geo_locations'] is None: + return None + result = [] + for x in self._values['geo_locations']: + if x['region'] is not None and x['region'].strip() != '': + tmp = '{0}:{1}'.format(x['country'], x['region']) + else: + tmp = x['country'] + result.append(tmp) + result = sorted(result) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ReportableChanges(Changes): + @property + def addresses(self): + if self._values['addresses'] is None: + return None + result = [] + for item in self._values['addresses']: + if '-' in item['name']: + continue + result.append(item['name']) + return result + + @property + def address_ranges(self): + if self._values['address_ranges'] is None: + return None + result = [] + for item in self._values['addresses']: + if '-' not in item['name']: + continue + start, stop = item['name'].split('-') + start = start.strip() + stop = stop.strip() + + start = ip_address(u'{0}'.format(start)) + stop = ip_address(u'{0}'.format(stop)) + if start.version != stop.version: + raise F5ModuleError( + "When specifying a range, IP addresses must be of the same type; IPv4 or IPv6." + ) + if int(start) > int(stop): + stop, start = start, stop + item = '{0}-{1}'.format(str(start), str(stop)) + result.append(item) + result = sorted(result) + return result + + @property + def address_lists(self): + if self._values['address_lists'] is None: + return None + result = [] + for x in self._values['address_lists']: + item = '/{0}/{1}'.format(x['partition'], x['name']) + result.append(item) + result = sorted(result) + return result + + +class UsableChanges(Changes): + @property + def addresses(self): + if self._values['addresses'] is None and self._values['address_ranges'] is None: + return None + result = [] + if self._values['addresses']: + result += [dict(name=str(x)) for x in self._values['addresses']] + if self._values['address_ranges']: + result += [dict(name=str(x)) for x in self._values['address_ranges']] + return result + + @property + def address_lists(self): + if self._values['address_lists'] is None: + return None + result = [] + for x in self._values['address_lists']: + partition, name = x.split('/')[1:] + result.append(dict( + name=name, + partition=partition + )) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def addresses(self): + if self.want.addresses is None: + return None + elif self.have.addresses is None: + return self.want.addresses + if sorted(self.want.addresses) != sorted(self.have.addresses): + return self.want.addresses + + @property + def address_lists(self): + if self.want.address_lists is None: + return None + elif self.have.address_lists is None: + return self.want.address_lists + if sorted(self.want.address_lists) != sorted(self.have.address_lists): + return self.want.address_lists + + @property + def address_ranges(self): + if self.want.address_ranges is None: + return None + elif self.have.address_ranges is None: + return self.want.address_ranges + if sorted(self.want.address_ranges) != sorted(self.have.address_ranges): + return self.want.address_ranges + + @property + def fqdns(self): + if self.want.fqdns is None: + return None + elif self.have.fqdns is None: + return self.want.fqdns + if sorted(self.want.fqdns) != sorted(self.have.fqdns): + return self.want.fqdns + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self.have = ApiParameters() + self._update_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/address-list/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/security/firewall/address-list/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/firewall/address-list/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/address-list/{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 + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/address-list/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + description=dict(), + name=dict(required=True), + addresses=dict( + type='list', + elements='str', + ), + address_ranges=dict( + type='list', + elements='str', + ), + address_lists=dict( + type='list', + elements='str', + ), + geo_locations=dict( + type='list', + elements='dict', + options=dict( + country=dict( + required=True, + ), + region=dict() + ) + ), + fqdns=dict( + type='list', + elements='str', + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_dos_profile.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_dos_profile.py new file mode 100644 index 00000000..295dd878 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_dos_profile.py @@ -0,0 +1,421 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_dos_profile +short_description: Manage AFM DoS profiles on a BIG-IP +description: + - Manages AFM (Advanced Firewall Manager) Denial of Service (DoS) profiles on a BIG-IP. To manage the vectors + associated with a DoS profile, refer to the C(bigip_firewall_dos_vector) module. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + description: + description: + - The description of the DoS profile. + type: str + default_whitelist: + description: + - The default whitelist address list for the system to use to determine which + IP addresses are legitimate. + - The system does not examine traffic from the IP addresses in the list when + performing DoS prevention. + - To define a new whitelist, use the C(bigip_firewall_address_list) module. + type: str + threshold_sensitivity: + description: + - Specifies the threshold sensitivity for the DoS profile. + - Thresholds for detecting attacks are higher when sensitivity is C(low), and + lower when sensitivity is C(high). + - When creating a new profile, if this parameter is not specified, the default + is C(medium). + type: str + choices: + - low + - medium + - high + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a new DoS profile + bigip_firewall_dos_profile: + name: profile1 + description: DoS profile 1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +threshold_sensitivity: + description: The new threshold sensitivity of the profile. + returned: changed + type: str + sample: low +default_whitelist: + description: The new whitelist attached to the profile. + returned: changed + type: str + sample: /Common/whitelist1 +description: + description: The description of the profile. + returned: changed + type: str + sample: New description +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'thresholdSensitivity': 'threshold_sensitivity', + 'whitelist': 'default_whitelist', + } + + api_attributes = [ + 'thresholdSensitivity', + 'whitelist', + 'description', + ] + + returnables = [ + 'threshold_sensitivity', + 'default_whitelist', + 'description', + ] + + updatables = [ + 'threshold_sensitivity', + 'default_whitelist', + 'description', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def default_whitelist(self): + if self._values['default_whitelist'] is None: + return None + return fq_name(self.partition, self._values['default_whitelist']) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + 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.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/security/dos/profile/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + 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.name) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + 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.name) + ) + resp = self.client.api.delete(uri) + if resp.status == 200: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + 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.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + threshold_sensitivity=dict( + choices=['low', 'medium', 'high'] + ), + default_whitelist=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_dos_vector.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_dos_vector.py new file mode 100644 index 00000000..0eeca627 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_dos_vector.py @@ -0,0 +1,1627 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_dos_vector +short_description: Manage attack vector configuration in an AFM DoS profile +description: + - Manage the attack vector configuration in an AFM (Advanced Firewall Manager) DoS profile. In addition to the normal + AFM DoS profile vectors, this module can manage the device-configuration vectors. + See the module documentation for details about this method. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the vector to modify. + - Vectors that ship with the device are "hard-coded" in that the list + of vectors is known to the system and users cannot add new vectors. Users only + manipulate the existing vectors; all of which are disabled by default. + - When C(bad-icmp-chksum), configures the "Bad ICMP Checksum" + Network Security vector. + - When C(bad-icmp-frame), configures the "Bad ICMP Frame" + Network Security vector. + - When C(bad-igmp-frame), configures the "Bad IGMP Frame" + Network Security vector. + - When C(bad-ip-opt), configures the "IP Option Illegal Length" + Network Security vector. + - When C(bad-ipv6-hop-cnt), configures the "Bad IPv6 Hop Count" + Network Security vector. + - When C(bad-ipv6-ver), configures the "Bad IPv6 Version" + Network Security vector. + - When C(bad-sctp-chksum), configures the "Bad SCTP Checksum" + Network Security vector. + - When C(bad-tcp-chksum), configures the "Bad TCP Checksum" + Network Security vector. + - When C(bad-tcp-flags-all-clr), configures the "Bad TCP Flags (All Cleared)" + Network Security vector. + - When C(bad-tcp-flags-all-set), configures the "Bad TCP Flags (All Flags Set)" + Network Security vector. + - When C(bad-ttl-val), configures the "Bad IP TTL Value" Network Security vector. + - When C(bad-udp-chksum), configures the "Bad UDP Checksum" Network Security vector. + - When C(bad-udp-hdr), configures the "Bad UDP Header (UDP Length > IP + Length or L2 Length)" Network Security vector. + - When C(bad-ver), configures the "Bad IP Version" Network Security vector. + - When C(arp-flood), configures the "ARP Flood" Network Security vector. + - When C(flood), configures the "Single Endpoint Flood" Network Security vector. + - When C(igmp-flood), configures the "IGMP Flood" Network Security vector. + - When C(igmp-frag-flood), configures the "IGMP Fragment Flood" + Network Security vector. + - When C(ip-bad-src), configures the "Bad Source" Network Security vector. + - When C(ip-err-chksum), configures the "IP Error Checksum" + Network Security vector. + - When C(ip-len-gt-l2-len), configures the "IP Length > L2 Length" + Network Security vector. + - When C(ip-other-frag), configures the "IP Fragment Error" + Network Security vector. + - When C(ip-overlap-frag), configures the "IP Fragment Overlap" + Network Security vector. + - When C(ip-short-frag), configures the "IP Fragment Too Small" + Network Security vector. + - When C(ip-uncommon-proto), configures the "IP Uncommon Proto" + Network Security vector. + - When C(ip-unk-prot), configures the "IP Unknown Protocol" + Network Security vector. + - When C(ipv4-mapped-ipv6), configures the "IPv4 Mapped IPv6" + Network Security vector. + - When C(ipv6-atomic-frag), configures the "IPv6 Atomic Fragment" + Network Security vector. + - When C(ipv6-bad-src), configures the "Bad IPv6 Addr" + Network Security vector. + - When C(ipv6-len-gt-l2-len), configures the "IPv6 Length > L2 Length" + Network Security vector. + - When C(ipv6-other-frag), configures the "IPv6 Fragment Error" + Network Security vector. + - When C(ipv6-overlap-frag), configures the "IPv6 Fragment Overlap" + Network Security vector. + - When C(ipv6-short-frag), configures the "IPv6 Fragment Too Small" + Network Security vector. + - When C(l2-len-ggt-ip-len), configures the "L2 Length >> IP Length" + Network Security vector. + - When C(l4-ext-hdrs-go-end), configures the "No L4 (Extension + Headers Go To Or Past The End of Frame)" Network Security vector. + - When C(land-attack), configures the "LAND Attack" + Network Security vector. + - When C(no-l4), configures the "No L4" Network Security vector. + - When C(no-listener-match), configures the "No Listener Match" + Network Security vector. + - When C(non-tcp-connection), configures the "Non TCP Connection" + Network Security vector. + - When C(payload-len-ls-l2-len), configures the "Payload Length < + L2 Length" Network Security vector. + - When C(routing-header-type-0), configures the "Routing Header Type + 0" Network Security vector. + - When C(syn-and-fin-set), configures the "SYN && FIN Set" + Network Security vector. + - When C(tcp-ack-flood), configures the "TCP BADACK Flood" + Network Security vector. + - When C(tcp-hdr-len-gt-l2-len), configures the "TCP Header Length > + L2 Length" Network Security vector. + - When C(tcp-hdr-len-too-short), configures the "TCP Header Length + Too Short (Length < 5)" Network Security vector. + - When C(hdr-len-gt-l2-len), configures the "Header Length > L2 Length" + Network Security vector. + - When C(hdr-len-too-short), configures the "Header Length Too Short" + Network Security vector. + - When C(bad-ext-hdr-order), configures the "IPv6 Extended Headers Wrong order" + Network Security vector. + - When C(ext-hdr-too-large), configures the "IPv6 extension header too large" + Network Security vector. + - When C(hop-cnt-low), configures the "IPv6 hop count <= " Network + Security vector. + - When C(host-unreachable), configures the "Host Unreachable" Network Security + vector. + - When C(icmp-frag), configures the "ICMP Fragment" Network Security vector. + - When C(icmp-frame-too-large), configures the "ICMP Frame Too Large" + Network Security vector. + - When C(icmpv4-flood), configures the "ICMPv4 flood" Network Security vector. + - When C(icmpv6-flood), configures the "ICMPv6 flood" Network Security vector. + - When C(ip-frag-flood), configures the "IP Fragment Flood" Network Security vector. + - When C(ip-low-ttl), configures the "TTL <= " Network Security vector. + - When C(ip-opt-frames), configures the "IP Option Frames" Network Security vector. + - When C(ipv6-ext-hdr-frames), configures the "IPv6 Extended Header Frames" + Network Security vector. + - When C(ipv6-frag-flood), configures the "IPv6 Fragment Flood" Network Security + vector. + - When C(opt-present-with-illegal-len), configures the "Option Present With Illegal + Length" Network Security vector. + - When C(sweep), configures the "Sweep" Network Security vector. + - When C(tcp-bad-urg), configures the "TCP Flags-Bad URG" Network Security vector. + - When C(tcp-half-open), configures the "TCP Half Open" Network Security vector. + - When C(tcp-opt-overruns-tcp-hdr), configures the "TCP Option Overruns TCP Header" + Network Security vector. + - When C(tcp-psh-flood), configures the "TCP PUSH Flood" Network Security vector. + - When C(tcp-rst-flood), configures the "TCP RST Flood" Network Security vector. + - When C(tcp-syn-flood), configures the "TCP SYN Flood" Network Security vector. + - When C(tcp-syn-oversize), configures the "TCP SYN Oversize" Network Security + vector. + - When C(tcp-synack-flood), configures the "TCP SYN ACK Flood" Network Security + vector. + - When C(tcp-window-size), configures the "TCP Window Size" Network Security + vector. + - When C(tidcmp), configures the "TIDCMP" Network Security vector. + - When C(too-many-ext-hdrs), configures the "Too Many Extension Headers" Network + Security vector. + - When C(dup-ext-hdr), configures the "IPv6 Duplicate Extension Headers" Network + Security vector. + - When C(fin-only-set), configures the "FIN Only Set" Network Security vector. + - When C(ether-brdcst-pkt), configures the "Ethernet Broadcast Packet" Network + Security vector. + - When C(ether-multicst-pkt), configures the "Ethernet Multicast Packet" Network + Security vector. + - When C(ether-mac-sa-eq-da), configures the "Ethernet MAC Source Address == + Destination Address" Network + Security vector. + - When C(udp-flood), configures the "UDP Flood" Network Security vector. + - When C(unk-ipopt-type), configures the "Unknown Option Type" Network + Security vector. + - When C(unk-tcp-opt-type), configures the "Unknown TCP Option Type" Network + Security vector. + - When C(a), configures the "DNS A Query" DNS Protocol Security vector. + - When C(aaaa), configures the "DNS AAAA Query" DNS Protocol Security vector. + - When C(any), configures the "DNS ANY Query" DNS Protocol Security vector. + - When C(axfr), configures the "DNS AXFR Query" DNS Protocol Security vector. + - When C(cname), configures the "DNS CNAME Query" DNS Protocol Security vector. + - When C(dns-malformed), configures the "dns-malformed" DNS Protocol Security vector. + - When C(dns-nxdomain-query), configures the "dns-nxdomain-query" + DNS Protocol Security vector. + - When C(dns-response-flood), configures the "dns-response-flood" + DNS Protocol Security vector. + - When C(dns-oversize), configures the "dns-oversize" + DNS Protocol Security vector. + - When C(ixfr), configures the "DNS IXFR Query" DNS Protocol Security vector. + - When C(mx), configures the "DNS MX Query" DNS Protocol Security vector. + - When C(ns), configures the "DNS NS Query" DNS Protocol Security vector. + - When C(other), configures the "DNS OTHER Query" DNS Protocol Security vector. + - When C(ptr), configures the "DNS PTR Query" DNS Protocol Security vector. + - When C(qdcount), configures the "DNS QDCOUNT Query" DNS Protocol Security vector. + - When C(soa), configures the "DNS SOA Query" DNS Protocol Security vector. + - When C(srv), configures the "DNS SRV Query" DNS Protocol Security vector. + - When C(txt), configures the "DNS TXT Query" DNS Protocol Security vector. + - When C(ack), configures the "SIP ACK Method" SIP Protocol Security vector. + - When C(bye), configures the "SIP BYE Method" SIP Protocol Security vector. + - When C(cancel), configures the "SIP CANCEL Method" SIP Protocol Security vector. + - When C(invite), configures the "SIP INVITE Method" SIP Protocol Security vector. + - When C(message), configures the "SIP MESSAGE Method" SIP Protocol Security vector. + - When C(notify), configures the "SIP NOTIFY Method" SIP Protocol Security vector. + - When C(options), configures the "SIP OPTIONS Method" SIP Protocol Security vector. + - When C(other), configures the "SIP OTHER Method" SIP Protocol Security vector. + - When C(prack), configures the "SIP PRACK Method" SIP Protocol Security vector. + - When C(publish), configures the "SIP PUBLISH Method" SIP Protocol Security vector. + - When C(register), configures the "SIP REGISTER Method" SIP Protocol Security vector. + - When C(sip-malformed), configures the "sip-malformed" SIP Protocol Security vector. + - When C(subscribe), configures the "SIP SUBSCRIBE Method" SIP Protocol Security vector. + - When C(uri-limit), configures the "uri-limit" SIP Protocol Security vector. + type: str + required: True + choices: + - bad-icmp-chksum + - bad-icmp-frame + - bad-igmp-frame + - bad-ip-opt + - bad-ipv6-hop-cnt + - bad-ipv6-ver + - bad-sctp-chksum + - bad-tcp-chksum + - bad-tcp-flags-all-clr + - bad-tcp-flags-all-set + - bad-ttl-val + - bad-udp-chksum + - bad-udp-hdr + - bad-ver + - arp-flood + - flood + - igmp-flood + - igmp-frag-flood + - ip-bad-src + - ip-err-chksum + - ip-len-gt-l2-len + - ip-other-frag + - ip-overlap-frag + - ip-short-frag + - ip-uncommon-proto + - ip-unk-prot + - ipv4-mapped-ipv6 + - ipv6-atomic-frag + - ipv6-bad-src + - ipv6-len-gt-l2-len + - ipv6-other-frag + - ipv6-overlap-frag + - ipv6-short-frag + - l2-len-ggt-ip-len + - l4-ext-hdrs-go-end + - land-attack + - no-l4 + - no-listener-match + - non-tcp-connection + - payload-len-ls-l2-len + - routing-header-type-0 + - syn-and-fin-set + - tcp-ack-flood + - tcp-hdr-len-gt-l2-len + - tcp-hdr-len-too-short + - hdr-len-gt-l2-len + - hdr-len-too-short + - bad-ext-hdr-order + - ext-hdr-too-large + - hop-cnt-low + - host-unreachable + - icmp-frag + - icmp-frame-too-large + - icmpv4-flood + - icmpv6-flood + - ip-frag-flood + - ip-low-ttl + - ip-opt-frames + - ipv6-ext-hdr-frames + - ipv6-frag-flood + - opt-present-with-illegal-len + - sweep + - tcp-bad-urg + - tcp-half-open + - tcp-opt-overruns-tcp-hdr + - tcp-psh-flood + - tcp-rst-flood + - tcp-syn-flood + - tcp-syn-oversize + - tcp-synack-flood + - tcp-window-size + - tidcmp + - too-many-ext-hdrs + - dup-ext-hdr + - fin-only-set + - ether-brdcst-pkt + - ether-multicst-pkt + - ether-mac-sa-eq-da + - udp-flood + - unk-ipopt-type + - unk-tcp-opt-type + - a + - aaaa + - any + - axfr + - cname + - dns-malformed + - dns-nxdomain-query + - dns-response-flood + - dns-oversize + - ixfr + - mx + - ns + - other + - ptr + - qdcount + - soa + - srv + - txt + - ack + - bye + - cancel + - invite + - message + - notify + - options + - other + - prack + - publish + - register + - sip-malformed + - subscribe + - uri-limit + profile: + description: + - Specifies the name of the profile to manage vectors in. + - The name C(device-config) is reserved for use by this module. + - Vectors can be managed in either DoS Profiles or Device Configuration. By + specifying a profile of 'device-config', this module will specifically tailor + configuration of the provided vectors to the Device Configuration. + type: str + required: True + auto_blacklist: + description: + - Automatically blacklists detected bad actors. + - To enable this parameter, the C(bad_actor_detection) must also be enabled. + - This parameter is not supported by the C(dns-malformed) vector. + - This parameter is not supported by the C(qdcount) vector. + type: bool + bad_actor_detection: + description: + - Whether Bad Actor detection is enabled or disabled for a vector, if available. + - This parameter must be enabled to enable the C(auto_blacklist) parameter. + - This parameter is not supported by the C(dns-malformed) vector. + - This parameter is not supported by the C(qdcount) vector. + type: bool + attack_ceiling: + description: + - Specifies the absolute maximum allowable for packets of this type. + - This setting rate limits packets to the packets per second setting, when + specified. + - To set no hard limit and allow automatic thresholds to manage all rate limiting, + set this to C(infinite). + type: str + attack_floor: + description: + - Specifies packets per second to identify an attack. + - These settings provide an absolute minimum of packets to allow before the attack + is identified. + - As the automatic detection thresholds adjust to traffic and CPU usage on the + system over time, this attack floor becomes less relevant. + - This value may not exceed the value in C(attack_floor). + type: str + allow_advertisement: + description: + - Specifies addresses that are identified for blacklisting are advertised to + BGP routers. + type: bool + simulate_auto_threshold: + description: + - Specifies results of the current automatic thresholds are logged, though + manual thresholds are enforced, and no action is taken on automatic thresholds. + - The C(sweep) vector does not support this parameter. + type: bool + blacklist_detection_seconds: + description: + - Detection before blacklisting occurs, in seconds. + type: int + blacklist_duration: + description: + - Duration the blacklist will last, in seconds. + type: int + per_source_ip_detection_threshold: + description: + - Specifies the number of packets per second to identify an IP address as a bad + actor. + type: str + per_source_ip_mitigation_threshold: + description: + - Specifies the rate limit applied to a source IP that is identified as a bad + actor. + type: str + detection_threshold_percent: + description: + - Lists the threshold percent increase over time that the system must detect in + traffic in order to detect this attack. + - The C(tcp-half-open) vector does not support this parameter. + type: str + aliases: + - rate_increase + detection_threshold_eps: + description: + - Lists how many packets per second the system must discover in traffic in order + to detect this attack. + type: str + aliases: + - rate_threshold + mitigation_threshold_eps: + description: + - Specifies the maximum number of this type of packet per second the system allows + for a vector. + - The system drops packets once the traffic level exceeds the rate limit. + type: str + aliases: + - rate_limit + threshold_mode: + description: + - The C(dns-malformed) vector does not support C(fully-automatic) or C(stress-based-mitigation) + for this parameter. + - The C(qdcount) vector does not support C(fully-automatic) or C(stress-based-mitigation) + for this parameter. + - The C(sip-malformed) vector does not support C(fully-automatic) or C(stress-based-mitigation) + for this parameter. + type: str + choices: + - manual + - stress-based-mitigation + - fully-automatic + state: + description: + - When C(state) is C(mitigate), ensures the vector enforces limits and + thresholds. + - When C(state) is C(detect-only), ensures the vector does not enforce limits + and thresholds (rate limiting, dropping, etc), but is still tracked in logs and statistics. + - When C(state) is C(disabled), ensures the vector does not enforce limits + and thresholds, but is still tracked in logs and statistics. + - When C(state) is C(learn-only), ensures the vector does not "detect" any attacks. + Only learning and stat collecting is performed. + type: str + choices: + - mitigate + - detect-only + - learn-only + - disabled + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +requirements: + - BIG-IP >= v13.0.0 +author: + - Tim Rupp (@caphrim007) + - Nitin Khanna (@nitinthewiz) +''' + +EXAMPLES = r''' +- name: Enable DNS AAAA vector mitigation + bigip_firewall_dos_vector: + name: aaaa + state: mitigate + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +allow_advertisement: + description: The new Allow External Advertisement setting. + returned: changed + type: bool + sample: yes +auto_blacklist: + description: The new Auto Blacklist setting. + returned: changed + type: bool + sample: no +bad_actor_detection: + description: The new Bad Actor Detection setting. + returned: changed + type: bool + sample: no +blacklist_detection_seconds: + description: The new Sustained Attack Detection Time setting. + returned: changed + type: int + sample: 60 +blacklist_duration: + description: The new Category Duration Time setting. + returned: changed + type: int + sample: 14400 +attack_ceiling: + description: The new Attack Ceiling EPS setting. + returned: changed + type: str + sample: infinite +attack_floor: + description: The new Attack Floor EPS setting. + returned: changed + type: str + sample: infinite +blacklist_category: + description: The new Category Name setting. + returned: changed + type: str + sample: /Common/cloud_provider_networks +per_source_ip_detection_threshold: + description: The new Per Source IP Detection Threshold EPS setting. + returned: changed + type: str + sample: 23 +per_source_ip_mitigation_threshold: + description: The new Per Source IP Mitigation Threshold EPS setting. + returned: changed + type: str + sample: infinite +detection_threshold_percent: + description: The new Detection Threshold Percent setting. + returned: changed + type: str + sample: infinite +detection_threshold_eps: + description: The new Detection Threshold EPS setting. + returned: changed + type: str + sample: infinite +mitigation_threshold_eps: + description: The new Mitigation Threshold EPS setting. + returned: changed + type: str + sample: infinite +threshold_mode: + description: The new Mitigation Threshold EPS setting. + returned: changed + type: str + sample: infinite +simulate_auto_threshold: + description: The new Simulate Auto Threshold setting. + returned: changed + type: bool + sample: no +state: + description: The new state of the vector. + returned: changed + type: str + sample: mitigate +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + +NETWORK_SECURITY_VECTORS = [ + 'bad-icmp-chksum', # Bad ICMP Checksum + 'bad-icmp-frame', # Bad ICMP Frame + 'bad-igmp-frame', # Bad IGMP Frame + 'bad-ip-opt', # IP Option Illegal Length + 'bad-ipv6-hop-cnt', # Bad IPv6 Hop Count + 'bad-ipv6-ver', # Bad IPv6 Version + 'bad-sctp-chksum', # Bad SCTP Checksum + 'bad-tcp-chksum', # Bad TCP Checksum + 'bad-tcp-flags-all-clr', # Bad TCP Flags (All Cleared) + 'bad-tcp-flags-all-set', # Bad TCP Flags (All Flags Set) + 'bad-ttl-val', # Bad IP TTL Value + 'bad-udp-chksum', # Bad UDP Checksum + 'bad-udp-hdr', # Bad UDP Header (UDP Length > IP Length or L2 Length) + 'bad-ver', # Bad IP Version + 'arp-flood', # ARP Flood + 'flood', # Single Endpoint Flood + 'igmp-flood', # IGMP Flood + 'igmp-frag-flood', # IGMP Fragment Flood + 'ip-bad-src', # Bad Source + 'ip-err-chksum', # IP Error Checksum + 'ip-len-gt-l2-len', # IP Length > L2 Length + 'ip-other-frag', # IP Fragment Error + 'ip-overlap-frag', # IP Fragment Overlap + 'ip-short-frag', # IP Fragment Too Small + 'ip-uncommon-proto', # IP Uncommon Proto + 'ip-unk-prot', # IP Unknown Protocol + 'ipv4-mapped-ipv6', # IPv4 Mapped IPv6 + 'ipv6-atomic-frag', # IPv6 Atomic Fragment + 'ipv6-bad-src', # Bad IPv6 Addr + 'ipv6-len-gt-l2-len', # IPv6 Length > L2 Length + 'ipv6-other-frag', # IPv6 Fragment Error + 'ipv6-overlap-frag', # IPv6 Fragment Overlap + 'ipv6-short-frag', # IPv6 Fragment Too Small + 'l2-len-ggt-ip-len', # L2 Length >> IP Length + 'l4-ext-hdrs-go-end', # No L4 (Extension Headers Go To Or Past The End of Frame) + 'land-attack', # LAND Attack + 'no-l4', # No L4 + 'no-listener-match', # No Listener Match + 'non-tcp-connection', # Non TCP Connection + 'payload-len-ls-l2-len', # Payload Length < L2 Length + 'routing-header-type-0', # Routing Header Type 0 + 'syn-and-fin-set', # SYN && FIN Set + 'tcp-ack-flood', # TCP BADACK Flood + 'tcp-hdr-len-gt-l2-len', # TCP Header Length > L2 Length + 'tcp-hdr-len-too-short', # TCP Header Length Too Short (Length < 5) + 'hdr-len-gt-l2-len', # Header Length > L2 Length + 'hdr-len-too-short', # Header Length Too Short + 'bad-ext-hdr-order', # IPv6 Extended Headers Wrong order + 'ext-hdr-too-large', # IPv6 extension header too large + 'hop-cnt-low', # IPv6 hop count <= + 'host-unreachable', # Host Unreachable + 'icmp-frag', # ICMP Fragment + 'icmp-frame-too-large', # ICMP Frame Too Large + 'icmpv4-flood', # ICMPv4 flood + 'icmpv6-flood', # ICMPv6 flood + 'ip-frag-flood', # IP Fragment Flood + 'ip-low-ttl', # TTL <= + 'ip-opt-frames', # IP Option Frames + 'ipv6-ext-hdr-frames', # IPv6 Extended Header Frames + 'ipv6-frag-flood', # IPv6 Fragment Flood + 'opt-present-with-illegal-len', # Option Present With Illegal Length + 'sweep', # Sweep + 'tcp-bad-urg', # TCP Flags-Bad URG + 'tcp-half-open', # TCP Half Open + 'tcp-opt-overruns-tcp-hdr', # TCP Option Overruns TCP Header + 'tcp-psh-flood', # TCP PUSH Flood + 'tcp-rst-flood', # TCP RST Flood + 'tcp-syn-flood', # TCP SYN Flood + 'tcp-syn-oversize', # TCP SYN Oversize + 'tcp-synack-flood', # TCP SYN ACK Flood + 'tcp-window-size', # TCP Window Size + 'tidcmp', # TIDCMP + 'too-many-ext-hdrs', # Too Many Extension Headers + 'dup-ext-hdr', # IPv6 Duplicate Extension Headers + 'fin-only-set', # FIN Only Set + 'ether-brdcst-pkt', # Ethernet Broadcast Packet + 'ether-multicst-pkt', # Ethernet Multicast Packet + 'ether-mac-sa-eq-da', # Ethernet MAC Source Address == Destination Address + 'udp-flood', # UDP Flood + 'unk-ipopt-type', # Unknown Option Type + 'unk-tcp-opt-type', # Unknown TCP Option Type +] + +PROTOCOL_SIP_VECTORS = [ + 'ack', # SIP ACK Method + 'bye', # SIP BYE Method + 'cancel', # SIP CANCEL Method + 'invite', # SIP INVITE Method + 'message', # SIP MESSAGE Method + 'notify', # SIP NOTIFY Method + 'options', # SIP OPTIONS Method + 'other', # SIP OTHER Method + 'prack', # SIP PRACK Method + 'publish', # SIP PUBLISH Method + 'register', # SIP REGISTER Method + 'sip-malformed', # sip-malformed + 'subscribe', # SIP SUBSCRIBE Method + 'uri-limit', # uri-limit +] + +PROTOCOL_DNS_VECTORS = [ + 'a', # DNS A Query + 'aaaa', # DNS AAAA Query + 'any', # DNS ANY Query + 'axfr', # DNS AXFR Query + 'cname', # DNS CNAME Query + 'dns-malformed', # DNS Malformed + 'dns-nxdomain-query', # DNS NXDOMAIN Query + 'dns-response-flood', # DNS Response Flood + 'dns-oversize', # DNS Oversize + 'ixfr', # DNS IXFR Query + 'mx', # DNS MX Query + 'ns', # DNS NS Query + 'other', # DNS OTHER Query + 'ptr', # DNS PTR Query + 'qdcount', # DNS QDCOUNT LIMIT + 'soa', # DNS SOA Query + 'srv', # DNS SRV Query + 'txt', # DNS TXT Query +] + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'allowAdvertisement': 'allow_advertisement', + 'autoBlacklisting': 'auto_blacklist', + + # "autoThreshold": "disabled", + # This is a deprecated parameter in 13.1.0. Use threshold_mode instead + + 'badActor': 'bad_actor_detection', + 'blacklistCategory': 'blacklist_category', + 'blacklistDetectionSeconds': 'blacklist_detection_seconds', + 'blacklistDuration': 'blacklist_duration', + 'ceiling': 'attack_ceiling', + # "enforce": "enabled", + 'floor': 'attack_floor', + 'perSourceIpDetectionPps': 'per_source_ip_detection_threshold', + 'perSourceIpLimitPps': 'per_source_ip_mitigation_threshold', + 'rateIncrease': 'detection_threshold_percent', + 'rateLimit': 'mitigation_threshold_eps', + 'rateThreshold': 'detection_threshold_eps', + 'simulateAutoThreshold': 'simulate_auto_threshold', + 'thresholdMode': 'threshold_mode', + + # device-config specific settings + 'scrubbingDetectionSeconds': 'sustained_attack_detection_time', + 'scrubbingDuration': 'category_detection_time', + 'perDstIpDetectionPps': 'per_dest_ip_detection_threshold', + 'perDstIpLimitPps': 'per_dest_ip_mitigation_threshold', + + # The following are not enabled for device-config because I + # do not know what parameters in TMUI they map to. Additionally, + # they do not appear to have any "help" documentation available + # in ``tmsh help security dos device-config``. + # + # "allowUpstreamScrubbing": "disabled", + # "attackedDst": "disabled", + # "autoScrubbing": "disabled", + 'defaultInternalRateLimit': 'mitigation_threshold_eps', + 'detectionThresholdPercent': 'detection_threshold_percent', + 'detectionThresholdPps': 'detection_threshold_eps', + } + + api_attributes = [ + 'allowAdvertisement', + 'autoBlacklisting', + 'autoThreshold', + 'badActor', + 'blacklistCategory', + 'blacklistDetectionSeconds', + 'blacklistDuration', + 'ceiling', + 'enforce', + 'floor', + 'perSourceIpDetectionPps', + 'perSourceIpLimitPps', + 'rateIncrease', + 'rateLimit', + 'rateThreshold', + 'simulateAutoThreshold', + 'state', + 'thresholdMode', + + # device-config specific + 'scrubbingDetectionSeconds', + 'scrubbingDuration', + 'perDstIpDetectionPps', + 'perDstIpLimitPps', + 'defaultInternalRateLimit', + 'detectionThresholdPercent', + 'detectionThresholdPps', + + # Attributes on the DoS profiles that hold the different vectors + # + # Each of these attributes is a list of dictionaries. Each dictionary + # contains the settings that affect the way the vector works. + # + # The vectors appear to all have the same attributes even if those + # attributes are not used. There may be cases where this is not true, + # however, and for those vectors we should either include specific + # error detection, or pass the unfiltered values through to mcpd and + # handle any unintuitive error messages that mcpd returns. + 'dosDeviceVector', + 'dnsQueryVector', + 'networkAttackVector', + 'sipAttackVector', + ] + + returnables = [ + 'allow_advertisement', + 'auto_blacklist', + 'bad_actor_detection', + 'blacklist_detection_seconds', + 'blacklist_duration', + 'attack_ceiling', + 'attack_floor', + 'blacklist_category', + 'per_source_ip_detection_threshold', + 'per_source_ip_mitigation_threshold', + 'detection_threshold_percent', + 'detection_threshold_eps', + 'mitigation_threshold_eps', + 'threshold_mode', + 'simulate_auto_threshold', + 'state', + ] + + updatables = [ + 'allow_advertisement', + 'auto_blacklist', + 'bad_actor_detection', + 'blacklist_detection_seconds', + 'blacklist_duration', + 'attack_ceiling', + 'attack_floor', + 'blacklist_category', + 'per_source_ip_detection_threshold', + 'per_source_ip_mitigation_threshold', + 'detection_threshold_percent', + 'detection_threshold_eps', + 'mitigation_threshold_eps', + 'threshold_mode', + 'simulate_auto_threshold', + 'state', + ] + + @property + def allow_advertisement(self): + return flatten_boolean(self._values['allow_advertisement']) + + @property + def auto_blacklist(self): + return flatten_boolean(self._values['auto_blacklist']) + + @property + def simulate_auto_threshold(self): + return flatten_boolean(self._values['simulate_auto_threshold']) + + @property + def bad_actor_detection(self): + return flatten_boolean(self._values['bad_actor_detection']) + + @property + def detection_threshold_percent(self): + if self._values['detection_threshold_percent'] in [None, "infinite"]: + return self._values['detection_threshold_percent'] + return int(self._values['detection_threshold_percent']) + + @property + def per_source_ip_mitigation_threshold(self): + if self._values['per_source_ip_mitigation_threshold'] in [None, "infinite"]: + return self._values['per_source_ip_mitigation_threshold'] + return int(self._values['per_source_ip_mitigation_threshold']) + + @property + def per_dest_ip_mitigation_threshold(self): + if self._values['per_dest_ip_mitigation_threshold'] in [None, "infinite"]: + return self._values['per_dest_ip_mitigation_threshold'] + return int(self._values['per_dest_ip_mitigation_threshold']) + + @property + def mitigation_threshold_eps(self): + if self._values['mitigation_threshold_eps'] in [None, "infinite"]: + return self._values['mitigation_threshold_eps'] + return int(self._values['mitigation_threshold_eps']) + + @property + def detection_threshold_eps(self): + if self._values['detection_threshold_eps'] in [None, "infinite"]: + return self._values['detection_threshold_eps'] + return int(self._values['detection_threshold_eps']) + + @property + def attack_ceiling(self): + if self._values['attack_ceiling'] in [None, "infinite"]: + return self._values['attack_ceiling'] + return int(self._values['attack_ceiling']) + + @property + def blacklist_category(self): + if self._values['blacklist_category'] is None: + return None + return fq_name(self.partition, self._values['blacklist_category']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def allow_advertisement(self): + if self._values['allow_advertisement'] is None: + return None + if self._values['allow_advertisement'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def auto_blacklist(self): + if self._values['auto_blacklist'] is None: + return None + if self._values['auto_blacklist'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def simulate_auto_threshold(self): + if self._values['simulate_auto_threshold'] is None: + return None + if self._values['simulate_auto_threshold'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def bad_actor_detection(self): + if self._values['bad_actor_detection'] is None: + return None + if self._values['bad_actor_detection'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def allow_advertisement(self): + return flatten_boolean(self._values['allow_advertisement']) + + @property + def auto_blacklist(self): + return flatten_boolean(self._values['auto_blacklist']) + + @property + def simulate_auto_threshold(self): + return flatten_boolean(self._values['simulate_auto_threshold']) + + @property + def bad_actor_detection(self): + return flatten_boolean(self._values['bad_actor_detection']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + want = getattr(self.want, param) + try: + have = getattr(self.have, param) + if want != have: + return want + except AttributeError: + return want + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + + # A list of all the vectors queried from the API when reading current info + # from the device. This is used when updating the API as the value that needs + # to be updated is a list of vectors and PATCHing a list would override any + # default settings. + self.vectors = dict() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + return self.update() + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def format_vectors(self, vectors): + result = None + for x in vectors: + vector = ApiParameters(params=x) + self.vectors[vector.name] = x + if vector.name == self.want.name: + result = vector + if not result: + return ApiParameters() + return result + + def _update(self, vtype): + self.have = self.format_vectors(self.read_current_from_device()) + if not self.should_update(): + return False + if self.module.check_mode: + return True + + # A disabled vector does not appear in the list of existing vectors + if self.want.state == 'disabled': + if self.want.profile == 'device-config' and self.have.state == 'disabled': + return False + # For non-device-config + if self.want.name not in self.vectors: + return False + + # At this point we know the existing vector is not disabled, so we need + # to change it in some way. + # + # First, if we see that the vector is in the current list of vectors, + # we are going to update it + changes = dict(self.changes.api_params()) + if self.want.name in self.vectors: + self.vectors[self.want.name].update(changes) + else: + # else, we are going to add it to the list of vectors + self.vectors[self.want.name] = changes + + # Since the name attribute is not a parameter tracked in the Parameter + # classes, we will add the name to the list of attributes so that when + # we update the API, it creates the correct vector + self.vectors[self.want.name].update({'name': self.want.name}) + + # Finally, the disabled state forces us to remove the vector from the + # list. However, items are only removed from the list if the profile + # being configured is not a device-config + if self.want.state == 'disabled': + if self.want.profile != 'device-config': + del self.vectors[self.want.name] + + # All of the vectors must be re-assembled into a list of dictionaries + # so that when we PATCH the API endpoint, the vectors list is filled + # correctly. + # + # There are **not** individual API endpoints for the individual vectors. + # Instead, the endpoint includes a list of vectors that is part of the + # DoS profile + result = [v for k, v in iteritems(self.vectors)] + + self.changes = Changes(params={vtype: result}) + self.update_on_device() + return True + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.kwargs = kwargs + + def exec_module(self): + if self.module.params['profile'] == 'device-config': + manager = self.get_manager('v1') + elif self.module.params['name'] in NETWORK_SECURITY_VECTORS: + manager = self.get_manager('v2') + elif self.module.params['name'] in PROTOCOL_DNS_VECTORS: + manager = self.get_manager('v3') + elif self.module.params['name'] in PROTOCOL_SIP_VECTORS: + manager = self.get_manager('v4') + else: + raise F5ModuleError( + "Unknown vector type specified." + ) + return manager.exec_module() + + def get_manager(self, type): + if type == 'v1': + return DeviceConfigManager(**self.kwargs) + elif type == 'v2': + return NetworkSecurityManager(**self.kwargs) + elif type == 'v3': + return ProtocolDnsManager(**self.kwargs) + elif type == 'v4': + return ProtocolSipManager(**self.kwargs) + + +class DeviceConfigManager(BaseManager): + """Manages AFM DoS Device Configuration settings. + + DeviceConfiguration is a special type of profile that is specific to the + BIG-IP device's management interface; not the data plane interfaces. + + There are many similar vectors that can be managed here. This configuration + is a super-set of the base DoS profile vector configuration and includes + several attributes per-vector that are not found in the DoS profile configuration. + These include, + + * allowUpstreamScrubbing + * attackedDst + * autoScrubbing + * defaultInternalRateLimit + * detectionThresholdPercent + * detectionThresholdPps + * perDstIpDetectionPps + * perDstIpLimitPps + * scrubbingDetectionSeconds + * scrubbingDuration + """ + def __init__(self, *args, **kwargs): + super(DeviceConfigManager, self).__init__(**kwargs) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def update(self): + name = self.normalize_names_in_device_config(self.want.name) + + self.want.update({'name': name}) + + return self._update('dosDeviceVector') + + def normalize_names_in_device_config(self, name): + # Overwrite specific names because they do not align with DoS Profile names + # + # The following names (on the right) differ from the functionally equivalent + # names (on the left) found in DoS Profiles. This seems like a bug to me, + # but I do not expect it to be fixed, so this works around it in the meantime. + name_map = { + 'hop-cnt-low': 'hop-cnt-leq-one', + 'ip-low-ttl': 'ttl-leq-one', + } + + # Attempt to normalize, else just return the name. This handles the default + # case where the name is actually correct and would not be found in the + # ``name_map`` above. + result = name_map.get(name, name) + return result + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/dos/device-config/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'dos-device-config') + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/device-config/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'dos-device-config') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + result = response.get('dosDeviceVector', []) + return result + raise F5ModuleError(resp.content) + + +class NetworkSecurityManager(BaseManager): + """Manages AFM DoS Profile Network Security settings. + + Network Security settings are a sub-collection attached to each profile. + + There are many similar vectors that can be managed here. This configuration + is a sub-set of the device-config DoS vector configuration and excludes + several attributes per-vector that are found in the device-config configuration. + These include, + + * rateIncrease + * rateLimit + * rateThreshold + """ + def __init__(self, *args, **kwargs): + super(NetworkSecurityManager, self).__init__(**kwargs) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def update(self): + return self._update('networkAttackVector') + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/dos-network/{3}".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.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/dos-network/{3}".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: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + # in v15 and v16 new dos profiles do not have the vector family set so we need to "create" vector container + # on 404 response + if resp.status == 404 or 'code' in response and response['code'] == 404: + self.create_vector_container_on_device() + return self.read_current_from_device() + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response.get('networkAttackVector', []) + raise F5ModuleError(resp.content) + + def create_vector_container_on_device(self): + params = {"name": self.want.profile} + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/dos-network/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile) + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ProtocolDnsManager(BaseManager): + """Manages AFM DoS Profile Protocol DNS settings. + + Protocol DNS settings are a sub-collection attached to each profile. + + There are many similar vectors that can be managed here. This configuration + is a sub-set of the device-config DoS vector configuration and excludes + several attributes per-vector that are found in the device-config configuration. + These include, + + * rateIncrease + * rateLimit + * rateThreshold + """ + def __init__(self, *args, **kwargs): + super(ProtocolDnsManager, self).__init__(**kwargs) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def update(self): + return self._update('dnsQueryVector') + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-dns/{3}".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.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-dns/{3}".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: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + # in v15 and v16 new dos profiles do not have the vector family set so we need to "create" vector container + # on 404 response + if resp.status == 404 or 'code' in response and response['code'] == 404: + self.create_vector_container_on_device() + return self.read_current_from_device() + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response.get('dnsQueryVector', []) + raise F5ModuleError(resp.content) + + def create_vector_container_on_device(self): + params = {"name": self.want.profile} + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-dns/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile) + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ProtocolSipManager(BaseManager): + """Manages AFM DoS Profile Protocol SIP settings. + + Protocol SIP settings are a sub-collection attached to each profile. + + There are many similar vectors that can be managed here. This configuration + is a sub-set of the device-config DoS vector configuration and excludes + several attributes per-vector that are found in the device-config configuration. + These include, + + * rateIncrease + * rateLimit + * rateThreshold + """ + def __init__(self, *args, **kwargs): + super(ProtocolSipManager, self).__init__(**kwargs) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def update(self): + return self._update('sipAttackVector') + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-sip/{3}".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.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-sip/{3}".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: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + # in v15 and v16 new dos profiles do not have the vector family set so we need to "create" vector container + # on 404 response + if resp.status == 404 or 'code' in response and response['code'] == 404: + self.create_vector_container_on_device() + return self.read_current_from_device() + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response.get('sipAttackVector', []) + raise F5ModuleError(resp.content) + + def create_vector_container_on_device(self): + params = {"name": self.want.profile} + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-sip/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile) + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True, + choices=[ + 'bad-icmp-chksum', + 'bad-icmp-frame', + 'bad-igmp-frame', + 'bad-ip-opt', + 'bad-ipv6-hop-cnt', + 'bad-ipv6-ver', + 'bad-sctp-chksum', + 'bad-tcp-chksum', + 'bad-tcp-flags-all-clr', + 'bad-tcp-flags-all-set', + 'bad-ttl-val', + 'bad-udp-chksum', + 'bad-udp-hdr', + 'bad-ver', + 'arp-flood', + 'flood', + 'igmp-flood', + 'igmp-frag-flood', + 'ip-bad-src', + 'ip-err-chksum', + 'ip-len-gt-l2-len', + 'ip-other-frag', + 'ip-overlap-frag', + 'ip-short-frag', + 'ip-uncommon-proto', + 'ip-unk-prot', + 'ipv4-mapped-ipv6', + 'ipv6-atomic-frag', + 'ipv6-bad-src', + 'ipv6-len-gt-l2-len', + 'ipv6-other-frag', + 'ipv6-overlap-frag', + 'ipv6-short-frag', + 'l2-len-ggt-ip-len', + 'l4-ext-hdrs-go-end', + 'land-attack', + 'no-l4', + 'no-listener-match', + 'non-tcp-connection', + 'payload-len-ls-l2-len', + 'routing-header-type-0', + 'syn-and-fin-set', + 'tcp-ack-flood', + 'tcp-hdr-len-gt-l2-len', + 'tcp-hdr-len-too-short', + 'hdr-len-gt-l2-len', + 'hdr-len-too-short', + 'bad-ext-hdr-order', + 'ext-hdr-too-large', + 'hop-cnt-low', + 'host-unreachable', + 'icmp-frag', + 'icmp-frame-too-large', + 'icmpv4-flood', + 'icmpv6-flood', + 'ip-frag-flood', + 'ip-low-ttl', + 'ip-opt-frames', + 'ipv6-ext-hdr-frames', + 'ipv6-frag-flood', + 'opt-present-with-illegal-len', + 'sweep', + 'tcp-bad-urg', + 'tcp-half-open', + 'tcp-opt-overruns-tcp-hdr', + 'tcp-psh-flood', + 'tcp-rst-flood', + 'tcp-syn-flood', + 'tcp-syn-oversize', + 'tcp-synack-flood', + 'tcp-window-size', + 'tidcmp', + 'too-many-ext-hdrs', + 'dup-ext-hdr', + 'fin-only-set', + 'ether-brdcst-pkt', + 'ether-multicst-pkt', + 'ether-mac-sa-eq-da', + 'udp-flood', + 'unk-ipopt-type', + 'unk-tcp-opt-type', + 'a', + 'aaaa', + 'any', + 'axfr', + 'cname', + 'dns-malformed', + 'dns-nxdomain-query', + 'dns-response-flood', + 'dns-oversize', + 'ixfr', + 'mx', + 'ns', + 'other', + 'ptr', + 'qdcount', + 'soa', + 'srv', + 'txt', + 'ack', + 'bye', + 'cancel', + 'invite', + 'message', + 'notify', + 'options', + 'other', + 'prack', + 'publish', + 'register', + 'sip-malformed', + 'subscribe', + 'uri-limit', + ] + ), + profile=dict(required=True), + allow_advertisement=dict(type='bool'), + auto_blacklist=dict(type='bool'), + simulate_auto_threshold=dict(type='bool'), + bad_actor_detection=dict(type='bool'), + blacklist_detection_seconds=dict(type='int'), + blacklist_duration=dict(type='int'), + attack_ceiling=dict(), + attack_floor=dict(), + per_source_ip_detection_threshold=dict(), + per_source_ip_mitigation_threshold=dict(), + # sustained_attack_detection_time=dict(), + # category_detection_time=dict(), + # per_dest_ip_detection_threshold=dict(), + # per_dest_ip_mitigation_threshold=dict(), + + detection_threshold_percent=dict( + aliases=['rate_increase'] + ), + detection_threshold_eps=dict( + aliases=['rate_threshold'] + ), + mitigation_threshold_eps=dict( + aliases=['rate_limit'] + ), + threshold_mode=dict( + choices=['manual', 'stress-based-mitigation', 'fully-automatic'] + ), + state=dict( + choices=['mitigate', 'detect-only', 'learn-only', 'disabled'], + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_global_rules.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_global_rules.py new file mode 100644 index 00000000..7b7f0de0 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_global_rules.py @@ -0,0 +1,368 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_global_rules +short_description: Manage AFM global rule settings on BIG-IP +description: + - Configures the global network firewall rules on AFM (Advanced Firewall Manager). + These firewall rules are applied to all packets except those going through + the management interface. They are applied first, before any firewall rules + for the packet's virtual server, route domain, and/or self IP address. +version_added: "1.0.0" +options: + enforced_policy: + description: + - Specifies an enforced firewall policy. + - C(enforced_policy) rules are enforced globally. + type: str + service_policy: + description: + - Specifies a service policy that would apply to traffic globally. + - The service policy is applied to all flows, provided there are + no other context specific service policy configurations that + override the global service policy. For example, when a service + policy is configured both at a global level and on a + firewall rule, and a flow matches the rule, the more specific + service policy configuration in the rule will override the service + policy setting at the global level. + - The service policy associated here can be created using the + C(bigip_service_policy) module. + type: str + staged_policy: + description: + - Specifies a staged firewall policy. + - C(staged_policy) rules are not enforced while all the visibility + aspects (statistics, reporting, and logging) function as if + the staged-policy rules were enforced globally. + type: str + description: + description: + - Description for the global list of firewall rules. + type: str +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Change enforced policy in AFM global rules + bigip_firewall_global_rules: + enforced_policy: enforcing1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +enforced_policy: + description: The new global Enforced Policy. + returned: changed + type: str + sample: /Common/enforced1 +service_policy: + description: The new global Service Policy. + returned: changed + type: str + sample: /Common/service1 +staged_policy: + description: The new global Staged Policy. + returned: changed + type: str + sample: /Common/staged1 +description: + description: The new description. + returned: changed + type: str + sample: My description +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'enforcedPolicy': 'enforced_policy', + 'servicePolicy': 'service_policy', + 'stagedPolicy': 'staged_policy', + } + + api_attributes = [ + 'enforcedPolicy', + 'servicePolicy', + 'stagedPolicy', + 'description', + ] + + returnables = [ + 'enforced_policy', + 'service_policy', + 'staged_policy', + 'description', + ] + + updatables = [ + 'enforced_policy', + 'service_policy', + 'staged_policy', + 'description', + ] + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def enforced_policy(self): + if self._values['enforced_policy'] is None: + return None + if self._values['enforced_policy'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['enforced_policy']) + + @property + def service_policy(self): + if self._values['service_policy'] is None: + return None + if self._values['service_policy'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['service_policy']) + + @property + def staged_policy(self): + if self._values['staged_policy'] is None: + return None + if self._values['staged_policy'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['staged_policy']) + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def enforced_policy(self): + return cmp_str_with_none(self.want.enforced_policy, self.have.enforced_policy) + + @property + def staged_policy(self): + return cmp_str_with_none(self.want.staged_policy, self.have.staged_policy) + + @property + def service_policy(self): + return cmp_str_with_none(self.want.service_policy, self.have.service_policy) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + return self.update() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/firewall/global-rules".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/global-rules".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + enforced_policy=dict(), + service_policy=dict(), + staged_policy=dict(), + description=dict(), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_log_profile.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_log_profile.py new file mode 100644 index 00000000..70088bb1 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_log_profile.py @@ -0,0 +1,870 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_log_profile +short_description: Manages AFM logging profiles configured in the system +description: + - Manages AFM (Advanced Firewall Manager) logging profiles configured in the system along with basic information about each profile. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the log profile. + type: str + required: True + description: + description: + - Description of the log profile. + type: str + dos_protection: + description: + - Configures DoS related settings of the log profile. + suboptions: + dns_publisher: + description: + - Specifies the name of the log publisher used for DNS DoS events. + - To specify the log_publisher on a different partition from the AFM log profile, specify the name in fullpath + format, e.g. C(/Foobar/log-publisher), otherwise the partition for the log publisher + is inferred from the C(partition) module parameter. + type: str + sip_publisher: + description: + - Specifies the name of the log publisher used for SIP DoS events. + - To specify the log_publisher on a different partition from the AFM log profile, specify the name in fullpath + format, e.g. C(/Foobar/log-publisher), otherwise the partition for the log publisher + is inferred from the C(partition) module parameter. + type: str + network_publisher: + description: + - Specifies the name of the log publisher used for DoS Network events. + - To specify the log_publisher on a different partition from the AFM log profile, specify the name in fullpath + format, e.g. C(/Foobar/log-publisher), otherwise the partition for the log publisher + is inferred from the C(partition) module parameter. + type: str + type: dict + ip_intelligence: + description: + - Configures IP Intelligence related settings of the log profile. + suboptions: + log_publisher: + description: + - Specifies the name of the log publisher used for IP Intelligence events. + - To specify the log_publisher on a different partition from the AFM log profile, specify the name in fullpath + format, e.g. C(/Foobar/log-publisher), otherwise the partition for the log publisher + is inferred the from C(partition) module parameter. + type: str + rate_limit: + description: + - Defines a rate limit for all combined IP intelligence log messages per second. Beyond this rate limit, + log messages are not logged until the threshold drops below the specified rate. + - To specify an indefinite rate, use the value C(indefinite). + - If specifying a numeric rate, the value must be between C(1) and C(4294967295). + type: str + log_rtbh: + description: + - When C(yes), specifies remotely triggered blackholing events are logged. + type: bool + log_shun: + description: + - When C(yes), specifies IP Intelligence shun list events are logged. + - This option can only be set on the C(global-network) built-in profile. + type: bool + log_translation_fields: + description: + - This option is used to enable or disable the logging of translated (i.e server side) fields in IP + Intelligence log messages. + - Translated fields include (but are not limited to) source address/port, destination address/port, + IP protocol, route domain, and VLAN. + type: bool + type: dict + port_misuse: + description: + - Port Misuse log configuration. + suboptions: + log_publisher: + description: + - Specifies the name of the log publisher used for Port Misuse events. + - To specify the log_publisher on a different partition from the AFM log profile, specify the name in fullpath + format, e.g. C(/Foobar/log-publisher), otherwise the partition for the log publisher + is inferred from the C(partition) module parameter. + type: str + rate_limit: + description: + - Defines a rate limit for all combined port misuse log messages per second. Beyond this rate limit, + log messages are not logged until the threshold drops below the specified rate. + - To specify an indefinite rate, use the value C(indefinite). + - If specifying a numeric rate, the value must be between C(1) and C(4294967295). + type: str + type: dict + partition: + description: + - Device partition to create log profile on. + - Parameter also used when specifying names for log publishers, unless log publisher names are in fullpath format. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures the resource exists. + - When C(state) is C(absent), ensures the resource is removed. Attempts to remove built-in system profiles are + ignored and no change is returned. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a basic log profile with port misuse + bigip_firewall_log_profile: + name: barbaz + port_misuse: + rate_limit: 30000 + log_publisher: local-db-pub + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Change ip_intelligence settings, publisher on different partition, remove port misuse + bigip_firewall_log_profile: + name: barbaz + ip_intelligence: + rate_limit: 400000 + log_translation_fields: yes + log_rtbh: yes + log_publisher: "/foobar/non-local-db" + port_misuse: + log_publisher: "" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a log profile with dos protection, different partition + bigip_firewall_log_profile: + name: foobar + partition: foobar + dos_protection: + dns_publisher: "/Common/local-db-pub" + sip_publisher: "non-local-db" + network_publisher: "/Common/local-db-pub" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove log profile + bigip_firewall_log_profile: + name: barbaz + partition: Common + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: New description of the AFM log profile. + returned: changed + type: str + sample: This is my description +dos_protection: + description: Log publishers used in DoS related settings of the log profile. + type: complex + returned: changed + contains: + dns_publisher: + description: The name of the log publisher used for DNS DoS events. + returned: changed + type: str + sample: "/Common/local-db-publisher" + sip_publisher: + description: The name of the log publisher used for SIP DoS events. + returned: changed + type: str + sample: "/Common/local-db-publisher" + network_publisher: + description: The name of the log publisher used for DoS Network events. + returned: changed + type: str + sample: "/Common/local-db-publisher" + sample: hash/dictionary of values +ip_intelligence: + description: IP Intelligence related settings of the log profile. + type: complex + returned: changed + contains: + log_publisher: + description: The name of the log publisher used for IP Intelligence events. + returned: changed + type: str + sample: "/Common/local-db-publisher" + rate_limit: + description: The rate limit for all combined IP intelligence log messages per second. + returned: changed + type: str + sample: "indefinite" + log_rtbh: + description: Logging of remotely triggered blackholing events. + returned: changed + type: bool + sample: yes + log_shun: + description: Logging of IP Intelligence shun list events. + returned: changed + type: bool + sample: no + log_translation_fields: + description: Logging of translated fields in IP Intelligence log messages. + returned: changed + type: bool + sample: no + sample: hash/dictionary of values +port_misuse: + description: Port Misuse related settings of the log profile. + type: complex + returned: changed + contains: + log_publisher: + description: The name of the log publisher used for Port Misuse events. + returned: changed + type: str + sample: "/Common/local-db-publisher" + rate_limit: + description: The rate limit for all combined Port Misuse log messages per second. + returned: changed + type: str + sample: "indefinite" + sample: hash/dictionary of values +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import compare_dictionary +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'ipIntelligence': 'ip_intelligence', + 'portMisuse': 'port_misuse', + 'protocolDnsDosPublisher': 'dns_publisher', + 'protocolSipDosPublisher': 'sip_publisher', + 'dosNetworkPublisher': 'network_publisher', + } + + api_attributes = [ + 'description', + 'ipIntelligence', + 'portMisuse', + 'dosNetworkPublisher', + 'protocolDnsDosPublisher', + 'protocolSipDosPublisher', + ] + + returnables = [ + 'ip_intelligence', + 'dns_publisher', + 'sip_publisher', + 'network_publisher', + 'port_misuse', + 'description', + 'ip_log_publisher', + 'ip_rate_limit', + 'ip_log_rthb', + 'ip_log_shun', + 'ip_log_translation_fields', + 'port_rate_limit', + 'port_log_publisher', + ] + + updatables = [ + 'dns_publisher', + 'sip_publisher', + 'network_publisher', + 'description', + 'ip_log_publisher', + 'ip_rate_limit', + 'ip_log_rthb', + 'ip_log_shun', + 'ip_log_translation_fields', + 'port_rate_limit', + 'port_log_publisher', + ] + + +class ApiParameters(Parameters): + @property + def ip_log_publisher(self): + result = self._values['ip_intelligence'].get('logPublisher', None) + return result + + @property + def ip_rate_limit(self): + return self._values['ip_intelligence']['aggregateRate'] + + @property + def port_rate_limit(self): + return self._values['port_misuse']['aggregateRate'] + + @property + def port_log_publisher(self): + result = self._values['port_misuse'].get('logPublisher', None) + return result + + @property + def ip_log_rtbh(self): + return self._values['ip_intelligence']['logRtbh'] + + @property + def ip_log_shun(self): + if self._values['name'] != 'global-network': + return None + return self._values['ip_intelligence']['logShun'] + + @property + def ip_log_translation_fields(self): + return self._values['ip_intelligence']['logTranslationFields'] + + +class ModuleParameters(Parameters): + def _transform_log_publisher(self, log_publisher): + if log_publisher is None: + return None + if log_publisher in ['', 'none']: + return {} + return fq_name(self.partition, log_publisher) + + def _validate_rate_limit(self, rate_limit): + if rate_limit is None: + return None + if rate_limit == 'indefinite': + return 4294967295 + if 0 <= int(rate_limit) <= 4294967295: + return int(rate_limit) + raise F5ModuleError( + "Valid 'maximum_age' must be in range 0 - 4294967295, or 'indefinite'." + ) + + @property + def ip_log_rtbh(self): + if self._values['ip_intelligence'] is None: + return None + result = flatten_boolean(self._values['ip_intelligence']['log_rtbh']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def ip_log_shun(self): + if self._values['ip_intelligence'] is None: + return None + if 'global-network' not in self._values['name']: + return None + result = flatten_boolean(self._values['ip_intelligence']['log_shun']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def ip_log_translation_fields(self): + if self._values['ip_intelligence'] is None: + return None + result = flatten_boolean(self._values['ip_intelligence']['log_translation_fields']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def ip_log_publisher(self): + if self._values['ip_intelligence'] is None: + return None + result = self._transform_log_publisher(self._values['ip_intelligence']['log_publisher']) + return result + + @property + def ip_rate_limit(self): + if self._values['ip_intelligence'] is None: + return None + return self._validate_rate_limit(self._values['ip_intelligence']['rate_limit']) + + @property + def port_rate_limit(self): + if self._values['port_misuse'] is None: + return None + return self._validate_rate_limit(self._values['port_misuse']['rate_limit']) + + @property + def port_log_publisher(self): + if self._values['port_misuse'] is None: + return None + result = self._transform_log_publisher(self._values['port_misuse']['log_publisher']) + return result + + @property + def dns_publisher(self): + if self._values['dos_protection'] is None: + return None + result = self._transform_log_publisher(self._values['dos_protection']['dns_publisher']) + return result + + @property + def sip_publisher(self): + if self._values['dos_protection'] is None: + return None + result = self._transform_log_publisher(self._values['dos_protection']['sip_publisher']) + return result + + @property + def network_publisher(self): + if self._values['dos_protection'] is None: + return None + result = self._transform_log_publisher(self._values['dos_protection']['network_publisher']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def ip_intelligence(self): + to_filter = dict( + logPublisher=self._values['ip_log_publisher'], + aggregateRate=self._values['ip_rate_limit'], + logRtbh=self._values['ip_log_rtbh'], + logShun=self._values['ip_log_shun'], + logTranslationFields=self._values['ip_log_translation_fields'] + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def port_misuse(self): + to_filter = dict( + logPublisher=self._values['port_log_publisher'], + aggregateRate=self._values['port_rate_limit'] + ) + result = self._filter_params(to_filter) + if result: + return result + + +class ReportableChanges(Changes): + returnables = [ + 'ip_intelligence', + 'port_misuse', + 'description', + 'dos_protection', + ] + + def _change_rate_limit_value(self, value): + if value == 4294967295: + return 'indefinite' + else: + return value + + @property + def ip_log_rthb(self): + result = flatten_boolean(self._values['ip_log_rtbh']) + return result + + @property + def ip_log_shun(self): + result = flatten_boolean(self._values['ip_log_shun']) + return result + + @property + def ip_log_translation_fields(self): + result = flatten_boolean(self._values['ip_log_translation_fields']) + return result + + @property + def ip_intelligence(self): + if self._values['ip_intelligence'] is None: + return None + to_filter = dict( + log_publisher=self._values['ip_log_publisher'], + rate_limit=self._change_rate_limit_value(self._values['ip_rate_limit']), + log_rtbh=self.ip_log_rtbh, + log_shun=self.ip_log_shun, + log_translation_fields=self.ip_log_translation_fields + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def port_misuse(self): + if self._values['port_misuse'] is None: + return None + to_filter = dict( + log_publisher=self._values['port_log_publisher'], + rate_limit=self._change_rate_limit_value(self._values['port_rate_limit']), + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def dos_protection(self): + to_filter = dict( + dns_publisher=self._values['dns_publisher'], + sip_publisher=self._values['sip_publisher'], + network_publisher=self._values['network_publisher'], + ) + result = self._filter_params(to_filter) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def ip_log_publisher(self): + result = compare_dictionary(self.want.ip_log_publisher, self.have.ip_log_publisher) + return result + + @property + def port_log_publisher(self): + result = compare_dictionary(self.want.port_log_publisher, self.have.port_log_publisher) + return result + + @property + def dns_publisher(self): + result = compare_dictionary(self.want.dns_publisher, self.have.dns_publisher) + return result + + @property + def sip_publisher(self): + result = compare_dictionary(self.want.sip_publisher, self.have.sip_publisher) + return result + + @property + def network_publisher(self): + result = compare_dictionary(self.want.network_publisher, self.have.network_publisher) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + # Built-in profiles cannot be removed + built_ins = [ + 'Log all requests', 'Log illegal requests', + 'global-network', 'local-dos' + ] + if self.want.name in built_ins: + return False + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/security/log/profile/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True + ), + description=dict(), + dos_protection=dict( + type='dict', + options=dict( + dns_publisher=dict(), + sip_publisher=dict(), + network_publisher=dict() + ) + ), + ip_intelligence=dict( + type='dict', + options=dict( + log_publisher=dict(), + log_translation_fields=dict(type='bool'), + rate_limit=dict(), + log_rtbh=dict(type='bool'), + log_shun=dict(type='bool') + ) + ), + port_misuse=dict( + type='dict', + options=dict( + log_publisher=dict(), + rate_limit=dict() + ) + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_log_profile_network.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_log_profile_network.py new file mode 100644 index 00000000..8c8217d1 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_log_profile_network.py @@ -0,0 +1,1271 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_log_profile_network +short_description: Configures Network Firewall related settings of the log profile +description: + - Configures Network Firewall related settings of the log profile. +version_added: "1.0.0" +options: + profile_name: + description: + - Specifies the name of the AFM (Advanced Firewall Manager) log profile to be updated. + type: str + required: True + log_publisher: + description: + - Specifies the name of the log publisher used for Network events. + - To specify the log_publisher on a different partition from the AFM log profile, specify the name in fullpath + format, e.g. C(/Foobar/log-publisher), otherwise the partition for the log publisher is inferred from the + C(partition) module parameter. + type: str + rate_limit: + description: + - Defines a rate limit for all combined network firewall log messages per second. Beyond this rate limit, + log messages are not logged. + - To specify an indefinite rate, use the value C(indefinite). + - If specifying a numeric rate, the value must be between C(1) and C(4294967295). + type: str + log_matches_accept_rule: + description: + - Modifies log settings for ACL rules configured with an "accept" or "accept decisively" action. + type: dict + suboptions: + enabled: + description: + - This option enables or disables the logging of packets that match ACL rules configured with + an "accept" or "accept decisively" action. + type: bool + rate_limit: + description: + - This option sets rate limits for the logging of packets that match ACL rules + configured with an "accept" or "accept decisively" action. + - This option is effective only if logging of this message type is enabled. + type: int + log_matches_drop_rule: + description: + - Modifies log settings for ACL rules configured with a drop action. + type: dict + suboptions: + enabled: + description: + - This option enables or disables the logging of packets that match ACL rules + configured with a drop action. + type: bool + rate_limit: + description: + - This option sets rate limits for the logging of packets that match ACL rules + configured with a drop action. + - This option is effective only if logging of this message type is enabled. + type: int + log_matches_reject_rule: + description: + - Modifies log settings for ACL rules configured with a reject action. + type: dict + suboptions: + enabled: + description: + - This option enables or disables the logging of packets that match ACL rules + configured with a reject action. + type: bool + rate_limit: + description: + - This option sets rate limits for the logging of packets that match ACL rules + configured with a reject action. + - This option is effective only if logging of this message type is enabled. + type: int + log_ip_errors: + description: + - Modifies log settings for logging of IP error packets. + type: dict + suboptions: + enabled: + description: + - This option enables or disables the logging of IP error packets. + type: bool + rate_limit: + description: + - This option sets rate limits for the logging of IP error packets. + - This option is effective only if logging of this message type is enabled. + type: int + log_tcp_errors: + description: + - Modifies log settings for the logging of TCP error packets. + type: dict + suboptions: + enabled: + description: + - This option enables or disables the logging of TCP error packets. + type: bool + rate_limit: + description: + - This option sets rate limits for the logging of TCP error packets. + - This option is effective only if logging of this message type is enabled. + type: int + log_tcp_events: + description: + - Modifies the log settings for logging of TCP events on the client side. + type: dict + suboptions: + enabled: + description: + - This option enables or disables the logging of TCP events on the client side. + - Only B(Established) and B(Closed) states of a TCP session are logged if this option is enabled. + type: bool + rate_limit: + description: + - This option sets rate limits for the logging of TCP events on the client side. + - This option is effective only if logging of this message type is enabled. + type: int + log_translation_fields: + description: + - This option enables or disables the logging of translated (i.e server side) fields in ACL + match and TCP events. + - Translated fields include (but are not limited to) source address/port, destination address/port, + IP protocol, route domain, and VLAN. + type: bool + log_storage_format: + description: + - Specifies the type of the storage format. + - When creating a new log profile, if this parameter is not specified, the default is C(none). + - When C(field-list), specifies the log displays only the items you specify in the C(log_message_fields) list + with C(log_format_delimiter) as the delimiter between the items. + - When C(none), the messages will be logged in the default format, which is C("management_ip_address", + "bigip_hostname","context_type", "context_name","src_geo","src_ip", "dest_geo","dest_ip","src_port", + "dest_port","vlan","protocol","route_domain", "translated_src_ip", "translated_dest_ip", + "translated_src_port","translated_dest_port", "translated_vlan","translated_ip_protocol", + "translated_route_domain", "acl_policy_type", "acl_policy_name","acl_rule_name","action", + "drop_reason","sa_translation_type", "sa_translation_pool","flow_id", "source_user", + "source_fqdn","dest_fqdn"). + choices: + - field-list + - none + type: str + log_format_delimiter: + description: + - Specifies the delimiter string when using a C(log_storage_format) of C(field-list). + - When creating a new profile, if this parameter is not specified, the default value of C(,) + (the comma character) is used. + - This option is valid when the C(log_storage_format) is set to C(field-list). It is ignored otherwise. + - Depending on the delimiter used, it may be necessary to wrap the delimiter + in quotes to prevent YAML errors from occurring. + - The special character C($) is reserved for internal use, + and will raise an error if used. + - The maximum length allowed for this parameter is C(31) characters. + type: str + log_message_fields: + description: + - Specifies a set of fields to be logged. + - This option is valid when the C(log_storage_format) is set to C(field-list). It is ignored otherwise. + - The order of the list is important, as the server displays the selected traffic items in the log + sequentially according to it. + type: list + elements: str + choices: + - acl_policy_name + - acl_policy_type + - acl_rule_name + - action + - bigip_hostname + - context_name + - context_type + - date_time + - dest_fqdn + - dest_geo + - dest_ip + - dest_port + - drop_reason + - management_ip_address + - protocol + - route_domain + - sa_translation_pool + - sa_translation_type + - source_fqdn + - source_user + - src_geo + - src_ip + - src_port + - translated_dest_ip + - translated_dest_port + - translated_ip_protocol + - translated_route_domain + - translated_src_ip + - translated_src_port + - translated_vlan + - vlan + partition: + description: + - Device partition to create log profile on. + - This parameter is also used when specifying names for log publishers, unless log publisher names are in fullpath format. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures the resource exists. + - The only built-in profile that allows updating network log settings is global-network, attempts to do so on other + built-in profiles will be ignored. + - When C(state) is C(absent), ensures that the resource is removed. + - The C(absent) state is ignored for global-network log profile. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add network settings to log profile + bigip_firewall_log_profile_network: + profile_name: barbaz + rate_limit: 150000 + log_publisher: local-db-pub + log_tcp_errors: + enabled: yes + rate_limit: 10000 + log_tcp_events: + enabled: yes + rate_limit: 40000 + log_storage_format: "field-list" + log_message_fields: + - vlan + - translated_vlan + - src_ip + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Change delimiter and log fields + bigip_firewall_log_profile_network: + profile_name: barbaz + log_format_delimiter: '.' + log_message_fields: + - translated_dest_ip + - translated_dest_port + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify built-in profile + bigip_firewall_log_profile_network: + profile_name: "global-network" + log_publisher: "/foobar/log1" + log_ip_errors: + enabled: yes + rate_limit: 60000 + log_matches_reject_rule: + enabled: yes + rate_limit: 2000 + log_translation_fields: yes + log_storage_format: "field-list" + log_format_delimiter: '.' + log_message_fields: + - protocol + - dest_ip + - dest_port + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove custom log profile network log settings + bigip_firewall_log_profile_network: + profile_name: "{{ log_profile }}" + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +log_publisher: + description: The name of the log publisher used for Network events. + returned: changed + type: str + sample: /Common/log-publisher +rate_limit: + description: The rate limit for all combined network firewall log messages per second. + returned: changed + type: str + sample: "indefinite" +log_matches_accept_rule: + description: Log settings for ACL rules configured with an "accept" or "accept decisively" action. + type: complex + returned: changed + contains: + enabled: + description: Enable or disable the logging of packets that match ACL rules. + returned: changed + type: bool + sample: yes + rate_limit: + description: The rate limit for the logging of packets that match ACL rules. + returned: changed + type: str + sample: "indefinite" + sample: hash/dictionary of values +log_matches_drop_rule: + description: Log settings for ACL rules configured with a drop action. + type: complex + returned: changed + contains: + enabled: + description: Enable or disable the logging of packets that match ACL rules. + returned: changed + type: bool + sample: yes + rate_limit: + description: The rate limit for the logging of packets that match ACL rules. + returned: changed + type: str + sample: "indefinite" + sample: hash/dictionary of values +log_matches_reject_rule: + description: Log settings for ACL rules configured with a reject action. + type: complex + returned: changed + contains: + enabled: + description: Enable or disable the logging of packets that match ACL rules. + returned: changed + type: bool + sample: yes + rate_limit: + description: The rate limit for the logging of packets that match ACL rules. + returned: changed + type: str + sample: "indefinite" + sample: hash/dictionary of values +log_ip_errors: + description: Log settings for logging of IP error packets. + type: complex + returned: changed + contains: + enabled: + description: Enable or disable the logging of IP error packets. + returned: changed + type: bool + sample: yes + rate_limit: + description: The rate limit for the logging of IP error packets. + returned: changed + type: str + sample: "indefinite" + sample: hash/dictionary of values +log_tcp_errors: + description: Log settings for logging of TCP error packets. + type: complex + returned: changed + contains: + enabled: + description: Enable or disable the logging of TCP error packets. + returned: changed + type: bool + sample: yes + rate_limit: + description: The rate limit for the logging of TCP error packets. + returned: changed + type: str + sample: "indefinite" + sample: hash/dictionary of values +log_tcp_events: + description: Log settings for logging of TCP events on the client side. + type: complex + returned: changed + contains: + enabled: + description: Enable or disable the logging of TCP events on the client side. + returned: changed + type: bool + sample: yes + rate_limit: + description: The rate limit for the logging of TCP events on the client side. + returned: changed + type: str + sample: "indefinite" + sample: hash/dictionary of values +log_translation_fields: + description: Enable or disable the logging of translated (i.e server side) fields in ACL match and TCP events. + returned: changed + type: bool + sample: yes +log_storage_format: + description: The type of the storage format. + returned: changed + type: str + sample: "field-list" +log_format_delimiter: + description: The delimiter string when using a log_storage_format of field-list. + returned: changed + type: str + sample: "." +log_message_fields: + description: The delimiter string when using a log_storage_format of field-list. + returned: changed + type: list + sample: ["acl_policy_name", "acl_policy_type"] +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'publisher': 'log_publisher', + 'rateLimit': 'rate_limits', + } + + api_attributes = [ + 'publisher', + 'format', + 'rateLimit', + 'filter', + ] + + returnables = [ + 'rate_acl_match_accept', + 'rate_acl_match_drop', + 'rate_acl_match_reject', + 'rate_tcp_errors', + 'rate_tcp_events', + 'rate_ip_errors', + 'rate_limit', + 'log_acl_match_accept', + 'log_acl_match_drop', + 'log_acl_match_reject', + 'log_tcp_errors', + 'log_tcp_events', + 'log_ip_errors', + 'log_translation_fields', + 'log_publisher', + 'log_format_delimiter', + 'log_storage_format', + 'log_message_fields', + 'log_matches_accept_rule', + 'log_matches_drop_rule', + 'log_matches_reject_rule', + ] + + updatables = [ + 'rate_acl_match_accept', + 'rate_acl_match_drop', + 'rate_acl_match_reject', + 'rate_tcp_errors', + 'rate_tcp_events', + 'rate_ip_errors', + 'rate_limit', + 'log_acl_match_accept', + 'log_acl_match_drop', + 'log_acl_match_reject', + 'log_tcp_errors', + 'log_tcp_events', + 'log_ip_errors', + 'log_translation_fields', + 'log_publisher', + 'log_format_delimiter', + 'log_storage_format', + 'log_message_fields', + ] + + +class ApiParameters(Parameters): + @property + def rate_acl_match_accept(self): + if self._values['rate_limits'] is None: + return None + return self._values['rate_limits']['aclMatchAccept'] + + @property + def log_acl_match_accept(self): + if self._values['filter'] is None: + return None + return self._values['filter']['logAclMatchAccept'] + + @property + def rate_acl_match_drop(self): + if self._values['rate_limits'] is None: + return None + return self._values['rate_limits']['aclMatchDrop'] + + @property + def log_acl_match_drop(self): + if self._values['filter'] is None: + return None + return self._values['filter']['logAclMatchDrop'] + + @property + def rate_acl_match_reject(self): + if self._values['rate_limits'] is None: + return None + return self._values['rate_limits']['aclMatchReject'] + + @property + def log_acl_match_reject(self): + if self._values['filter'] is None: + return None + return self._values['filter']['logAclMatchReject'] + + @property + def rate_tcp_errors(self): + if self._values['rate_limits'] is None: + return None + return self._values['rate_limits']['tcpErrors'] + + @property + def log_tcp_errors(self): + if self._values['filter'] is None: + return None + return self._values['filter']['logTcpErrors'] + + @property + def rate_tcp_events(self): + if self._values['rate_limits'] is None: + return None + return self._values['rate_limits']['tcpEvents'] + + @property + def log_tcp_events(self): + if self._values['filter'] is None: + return None + return self._values['filter']['logTcpEvents'] + + @property + def rate_ip_errors(self): + if self._values['rate_limits'] is None: + return None + return self._values['rate_limits']['ipErrors'] + + @property + def log_ip_errors(self): + if self._values['filter'] is None: + return None + return self._values['filter']['logIpErrors'] + + @property + def log_translation_fields(self): + if self._values['filter'] is None: + return None + return self._values['filter']['logTranslationFields'] + + @property + def rate_limit(self): + if self._values['rate_limits'] is None: + return None + return self._values['rate_limits']['aggregateRate'] + + @property + def log_format_delimiter(self): + if self._values['format'] is None: + return None + return self._values['format']['fieldListDelimiter'] + + @property + def log_storage_format(self): + if self._values['format'] is None: + return None + return self._values['format']['type'] + + @property + def log_message_fields(self): + if self._values['format'] is None: + return None + if 'fieldList' in self._values['format']: + return self._values['format']['fieldList'] + + +class ModuleParameters(Parameters): + def _validate_aggregate_rate(self, aggregate_rate): + if aggregate_rate is None: + return None + if aggregate_rate == 'indefinite': + return 4294967295 + if 0 <= int(aggregate_rate) <= 4294967295: + return int(aggregate_rate) + raise F5ModuleError( + "Valid 'maximum_age' must be in range 0 - 4294967295, or 'indefinite'." + ) + + @property + def rate_acl_match_accept(self): + if self._values['log_matches_accept_rule'] is None: + return None + return self._validate_aggregate_rate(self._values['log_matches_accept_rule']['rate_limit']) + + @property + def log_acl_match_accept(self): + if self._values['log_matches_accept_rule'] is None: + return None + result = flatten_boolean(self._values['log_matches_accept_rule']['enabled']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def rate_acl_match_drop(self): + if self._values['log_matches_drop_rule'] is None: + return None + return self._validate_aggregate_rate(self._values['log_matches_drop_rule']['rate_limit']) + + @property + def log_acl_match_drop(self): + if self._values['log_matches_drop_rule'] is None: + return None + result = flatten_boolean(self._values['log_matches_drop_rule']['enabled']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def rate_acl_match_reject(self): + if self._values['log_matches_reject_rule'] is None: + return None + return self._validate_aggregate_rate(self._values['log_matches_reject_rule']['rate_limit']) + + @property + def log_acl_match_reject(self): + if self._values['log_matches_reject_rule'] is None: + return None + result = flatten_boolean(self._values['log_matches_reject_rule']['enabled']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def rate_tcp_errors(self): + if self._values['log_tcp_errors'] is None: + return None + return self._validate_aggregate_rate(self._values['log_tcp_errors']['rate_limit']) + + @property + def log_tcp_errors(self): + if self._values['log_tcp_errors'] is None: + return None + result = flatten_boolean(self._values['log_tcp_errors']['enabled']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def rate_tcp_events(self): + if self._values['log_tcp_events'] is None: + return None + return self._validate_aggregate_rate(self._values['log_tcp_events']['rate_limit']) + + @property + def log_tcp_events(self): + if self._values['log_tcp_events'] is None: + return None + result = flatten_boolean(self._values['log_tcp_events']['enabled']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def rate_ip_errors(self): + if self._values['log_ip_errors'] is None: + return None + return self._validate_aggregate_rate(self._values['log_ip_errors']['rate_limit']) + + @property + def log_ip_errors(self): + if self._values['log_ip_errors'] is None: + return None + result = flatten_boolean(self._values['log_ip_errors']['enabled']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def log_translation_fields(self): + result = flatten_boolean(self._values['log_translation_fields']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + return result + + @property + def log_publisher(self): + log_publisher = self._values['log_publisher'] + if log_publisher is None: + return None + if log_publisher in ['', 'none']: + return log_publisher + return fq_name(self.partition, log_publisher) + + @property + def rate_limit(self): + return self._validate_aggregate_rate(self._values['rate_limit']) + + @property + def log_format_delimiter(self): + if self._values['log_format_delimiter'] is None: + return None + if len(self._values['log_format_delimiter']) > 31: + raise F5ModuleError('The maximum length of delimiter is 31 characters.') + if "$" in self._values['log_format_delimiter']: + raise F5ModuleError("Cannot use '$' character as a part of delimiter.") + return self._values['log_format_delimiter'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def rate_limits(self): + to_filter = dict( + aclMatchAccept=self._values['rate_acl_match_accept'], + aclMatchDrop=self._values['rate_acl_match_drop'], + aclMatchReject=self._values['rate_acl_match_reject'], + ipErrors=self._values['rate_ip_errors'], + tcpErrors=self._values['rate_tcp_errors'], + tcpEvents=self._values['rate_tcp_events'], + aggregateRate=self._values['rate_limit'], + ) + result = self._filter_params(to_filter) + return result + + @property + def format(self): + to_filter = dict( + fieldListDelimiter=self._values['log_format_delimiter'], + type=self._values['log_storage_format'], + fieldList=self._values['log_message_fields'] + ) + result = self._filter_params(to_filter) + return result + + @property + def filter(self): + to_filter = dict( + logAclMatchAccept=self._values['log_acl_match_accept'], + logAclMatchDrop=self._values['log_acl_match_drop'], + logAclMatchReject=self._values['log_acl_match_reject'], + logIpErrors=self._values['log_ip_errors'], + logTcpErrors=self._values['log_tcp_errors'], + logTcpEvents=self._values['log_tcp_events'], + logTranslationFields=self._values['log_translation_fields'], + ) + result = self._filter_params(to_filter) + return result + + +class ReportableChanges(Changes): + def _change_aggregate_rate_value(self, value): + if value == 4294967295: + return 'indefinite' + else: + return value + + def _rebuild_params(self, enabled, rate_limit): + to_filter = dict( + enabled=flatten_boolean(self._values[enabled]), + rate_limit=self._change_aggregate_rate_value(self._values[rate_limit]) + ) + result = self._filter_params(to_filter) + return result + + @property + def log_matches_accept_rule(self): + result = self._rebuild_params('log_acl_match_accept', 'rate_acl_match_accept') + if result: + return result + + @property + def log_acl_match_accept(self): + return None + + @property + def rate_acl_match_accept(self): + return None + + @property + def log_matches_drop_rule(self): + result = self._rebuild_params('log_acl_match_drop', 'rate_acl_match_drop') + if result: + return result + + @property + def log_acl_match_drop(self): + return None + + @property + def rate_acl_match_drop(self): + return None + + @property + def log_matches_reject_rule(self): + result = self._rebuild_params('log_acl_match_reject', 'rate_acl_match_reject') + if result: + return result + + @property + def log_acl_match_reject(self): + return None + + @property + def rate_acl_match_reject(self): + return None + + @property + def log_ip_errors(self): + result = self._rebuild_params('log_ip_errors', 'rate_ip_errors') + if result: + return result + + @property + def rate_ip_errors(self): + return None + + @property + def log_tcp_errors(self): + result = self._rebuild_params('log_tcp_errors', 'rate_tcp_errors') + if result: + return result + + @property + def rate_tcp_errors(self): + return None + + @property + def log_tcp_events(self): + result = self._rebuild_params('log_tcp_events', 'rate_tcp_events') + if result: + return result + + @property + def rate_tcp_events(self): + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def log_message_fields(self): + if self.want.log_message_fields is None: + return None + if len(self.want.log_message_fields) == 1: + if self.have.log_message_fields is None and self.want.log_message_fields[0] in ['', 'none']: + return None + if self.have.log_message_fields is not None and self.want.log_message_fields[0] in ['', 'none']: + return [] + if self.have.log_message_fields is None: + return self.want.log_message_fields + if set(self.want.log_message_fields) != set(self.have.log_message_fields): + return self.want.log_message_fields + return None + + @property + def log_publisher(self): + return cmp_str_with_none(self.want.log_publisher, self.have.log_publisher) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + built_ins = ['Log all requests', 'Log illegal requests', 'local-dos'] + if self.want.profile_name in built_ins: + return False + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + # Built-in profile global-network cannot disable network log profile + if 'global-network' in self.want.profile_name: + return False + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _internal_name(self): + name = self.want.profile_name + partition = self.want.partition + if 'global-network' in name: + return 'global-network' + return transform_name(partition, name) + + def _profile_exists(self): + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{2}/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile_name), + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def exists(self): + if not self._profile_exists(): + raise F5ModuleError( + "Specified AFM log profile: {0} does not exist".format(self.want.profile_name) + ) + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{2}/network/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile_name), + self._internal_name() + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.profile_name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{2}/network/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile_name) + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{2}/network/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile_name), + self._internal_name() + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{2}/network/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile_name), + self._internal_name() + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/log/profile/{2}/network/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile_name), + self._internal_name() + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.choices = [ + 'acl_policy_name', 'acl_policy_type', 'acl_rule_name', 'action', + 'bigip_hostname', 'context_name', 'context_type', 'date_time', + 'dest_fqdn', 'dest_geo', 'dest_ip', 'dest_port', 'drop_reason', + 'management_ip_address', 'protocol', 'route_domain', 'sa_translation_pool', + 'sa_translation_type', 'source_fqdn', 'source_user', 'src_geo', 'src_ip', + 'src_port', 'translated_dest_ip', 'translated_dest_port', 'translated_ip_protocol', + 'translated_route_domain', 'translated_src_ip', 'translated_src_port', 'translated_vlan', + 'vlan' + ] + argument_spec = dict( + profile_name=dict( + required=True + ), + rate_limit=dict(), + log_publisher=dict(), + log_matches_accept_rule=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + rate_limit=dict() + ) + ), + log_matches_drop_rule=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + rate_limit=dict() + ) + ), + log_matches_reject_rule=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + rate_limit=dict() + ) + ), + log_tcp_errors=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + rate_limit=dict() + ) + ), + log_tcp_events=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + rate_limit=dict() + ) + ), + log_ip_errors=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + rate_limit=dict() + ) + ), + log_translation_fields=dict(type='bool'), + log_storage_format=dict( + choices=['none', 'field-list'] + ), + log_format_delimiter=dict(), + log_message_fields=dict( + type='list', + elements='str', + choices=self.choices + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_policy.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_policy.py new file mode 100644 index 00000000..8d063a04 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_policy.py @@ -0,0 +1,535 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_policy +short_description: Manage AFM security firewall policies on a BIG-IP +description: + - Manages AFM (Advanced Firewall Manager) security firewall policies on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - The name of the policy to create. + type: str + required: True + description: + description: + - The description to attach to the policy. + - This parameter is only supported on versions of BIG-IP >= 12.1.0. On earlier + versions it is simply ignored. + type: str + state: + description: + - When C(state) is C(present), ensures the policy exists. + - When C(state) is C(absent), ensures the policy is removed. + type: str + choices: + - present + - absent + default: present + rules: + description: + - Specifies a list of rules you want associated with this policy. + The order of this list is the order they will be evaluated by BIG-IP. + If the specified rules do not exist (for example when creating a new + policy), they will be created. + - Rules specified here, if they do not exist, will be created with "default deny" + behavior. It is expected that you follow-up after this module with the actual + configuration for these rules. + - The C(bigip_firewall_rule) module can also be used to create and + edit existing and new rules. + type: list + elements: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a basic policy with some rule stubs + bigip_firewall_policy: + name: foo + rules: + - rule1 + - rule2 + - rule3 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the policy. + returned: changed + type: str + sample: My firewall policy +rules: + description: The list of rules, in the order that they are evaluated, on the device. + returned: changed + type: list + sample: ['rule1', 'rule2', 'rule3'] +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'rulesReference': 'rules' + } + + api_attributes = [ + 'description' + ] + + returnables = [ + 'description', + 'rules', + ] + + updatables = [ + 'description', + 'rules' + ] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ModuleParameters(Parameters): + @property + def rules(self): + if self._values['rules'] is None: + return None + # In case rule values are unicode (as they may be coming from the API + result = [str(x) for x in self._values['rules']] + return result + + +class ApiParameters(Parameters): + @property + def rules(self): + result = [] + if self._values['rules'] is None or 'items' not in self._values['rules']: + return [] + for idx, item in enumerate(self._values['rules']['items']): + result.append(dict(item=item['fullPath'], order=idx)) + result = [x['item'] for x in sorted(result, key=lambda k: k['order'])] + return result + + +class Changes(Parameters): + pass + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def rules(self): + if self.want.rules is None: + return None + if self.have.rules is None: + return self.want.rules + if set(self.want.rules) != set(self.have.rules): + return self.want.rules + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Changes(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = Changes(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/security/firewall/policy/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if self.want.rules: + self._upsert_policy_rules_on_device() + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + if params: + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + if self.changes.rules is not None: + self._upsert_policy_rules_on_device() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/?expandSubcollections=true".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def rule_exists(self, rule): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + rule.replace('/', '_') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_default_rule_on_device(self, rule): + params = dict( + name=rule.replace('/', '_'), + action='reject', + # Adding items to the end of the list causes the list of rules to match + # what the user specified in the original list. + placeAfter='last', + ) + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_rule_from_device(self, rule): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + rule.replace('/', '_'), + ) + # this response returns no payload + resp = self.client.api.delete(uri) + if resp.status not in [200, 201]: + raise F5ModuleError(resp.content) + + def move_rule_to_front(self, rule): + params = dict( + placeAfter='last' + ) + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + rule.replace('/', '_') + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def _upsert_policy_rules_on_device(self): + rules = self.changes.rules + if rules is None: + rules = [] + self._remove_rule_difference(rules) + + for idx, rule in enumerate(rules): + if not self.rule_exists(rule): + self.create_default_rule_on_device(rule) + for idx, rule in enumerate(rules): + self.move_rule_to_front(rule) + + def _remove_rule_difference(self, rules): + if rules is None or self.have.rules is None: + return + have_rules = set(self.have.rules) + want_rules = set(rules) + removable = have_rules.difference(want_rules) + for remove in removable: + self.remove_rule_from_device(remove) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + rules=dict( + type='list', + elements='str', + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_port_list.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_port_list.py new file mode 100644 index 00000000..57b1a788 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_port_list.py @@ -0,0 +1,655 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_port_list +short_description: Manage port lists on BIG-IP AFM +description: + - Manages the AFM (Advanced Firewall Manager) port lists on a BIG-IP. This module can be used to add + and remove port list entries. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the port list. + type: str + required: True + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + description: + description: + - Description of the port list. + type: str + ports: + description: + - Simple list of port values to add to the list. + type: list + elements: str + port_ranges: + description: + - A list of port ranges where the range starts with a port number, is followed + by a dash (-), and then a second number. + - If the first number is greater than the second number, the numbers will be + reversed to be properly formatted, for example 90-78 would become 78-90. + type: list + elements: str + port_lists: + description: + - Simple list of existing port lists to add to this list. Port lists can be + specified in either their fully qualified name (/Common/foo) or their short + name (foo). If a short name is used, the C(partition) argument will automatically + be prepended to the short name. + type: list + elements: str + state: + description: + - When C(present), ensures the address list and entries exists. + - When C(absent), ensures the address list is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a simple port list + bigip_firewall_port_list: + name: foo + ports: + - 80 + - 443 + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Override the above list of ports with a new list + bigip_firewall_port_list: + name: foo + ports: + - 3389 + - 8080 + - 25 + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create port list with series of ranges + bigip_firewall_port_list: + name: foo + port_ranges: + - 25-30 + - 80-500 + - 50-78 + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Use multiple types of port arguments + bigip_firewall_port_list: + name: foo + port_ranges: + - 25-30 + - 80-500 + - 50-78 + ports: + - 8080 + - 443 + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove port list + bigip_firewall_port_list: + name: foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create port list from a file with one port per line + bigip_firewall_port_list: + name: lot-of-ports + ports: "{{ lookup('file', 'my-large-port-list.txt').split('\n') }}" + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the port list. + returned: changed + type: str + sample: My port list +ports: + description: The new list of ports applied to the port list. + returned: changed + type: list + sample: [80, 443] +port_ranges: + description: The new list of port ranges applied to the port list. + returned: changed + type: list + sample: [80-100, 200-8080] +port_lists: + description: The new list of port list names applied to the port list. + returned: changed + type: list + sample: [/Common/list1, /Common/list2] +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'portLists': 'port_lists', + } + + api_attributes = [ + 'portLists', 'ports', 'description', + ] + + returnables = [ + 'ports', 'port_ranges', 'port_lists', 'description', + ] + + updatables = [ + 'description', 'ports', 'port_ranges', 'port_lists', + ] + + +class ApiParameters(Parameters): + @property + def port_ranges(self): + if self._values['ports'] is None: + return None + result = [] + for port_range in self._values['ports']: + if '-' not in port_range['name']: + continue + start, stop = port_range['name'].split('-') + start = int(start.strip()) + stop = int(stop.strip()) + if start > stop: + stop, start = start, stop + item = '{0}-{1}'.format(start, stop) + result.append(item) + return result + + @property + def port_lists(self): + if self._values['port_lists'] is None: + return None + result = [] + for x in self._values['port_lists']: + item = '/{0}/{1}'.format(x['partition'], x['name']) + result.append(item) + return result + + @property + def ports(self): + if self._values['ports'] is None: + return None + result = [int(x['name']) for x in self._values['ports'] if '-' not in x['name']] + return result + + +class ModuleParameters(Parameters): + @property + def ports(self): + if self._values['ports'] is None: + return None + if any(x for x in self._values['ports'] if '-' in str(x)): + raise F5ModuleError( + "Ports must be whole numbers between 0 and 65,535" + ) + if any(x for x in self._values['ports'] if 0 < int(x) > 65535): + raise F5ModuleError( + "Ports must be whole numbers between 0 and 65,535" + ) + result = [int(x) for x in self._values['ports']] + return result + + @property + def port_ranges(self): + if self._values['port_ranges'] is None: + return None + result = [] + for port_range in self._values['port_ranges']: + if '-' not in port_range: + continue + start, stop = port_range.split('-') + start = int(start.strip()) + stop = int(stop.strip()) + if start > stop: + stop, start = start, stop + if 0 < start > 65535 or 0 < stop > 65535: + raise F5ModuleError( + "Ports must be whole numbers between 0 and 65,535" + ) + item = '{0}-{1}'.format(start, stop) + result.append(item) + return result + + @property + def port_lists(self): + if self._values['port_lists'] is None: + return None + result = [] + for x in self._values['port_lists']: + item = fq_name(self.partition, x) + result.append(item) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ReportableChanges(Changes): + @property + def ports(self): + if not self._values['ports']: + return None + result = [] + for item in self._values['ports']: + if '-' in item['name']: + continue + result.append(item['name']) + return result + + @property + def port_ranges(self): + if not self._values['ports']: + return None + result = [] + for item in self._values['ports']: + if '-' not in item['name']: + continue + result.append(item['name']) + return result + + +class UsableChanges(Changes): + @property + def ports(self): + if self._values['ports'] is None and self._values['port_ranges'] is None: + return None + result = [] + if self._values['ports']: + # The values of the 'key' index literally need to be string values. + # If they are not, on BIG-IP 12.1.0 they will raise this REST exception. + # + # { + # "code": 400, + # "message": "one or more configuration identifiers must be provided", + # "errorStack": [], + # "apiError": 26214401 + # } + result += [dict(name=str(x)) for x in self._values['ports']] + if self._values['port_ranges']: + result += [dict(name=str(x)) for x in self._values['port_ranges']] + return result + + @property + def port_lists(self): + if self._values['port_lists'] is None: + return None + result = [] + for x in self._values['port_lists']: + partition, name = x.split('/')[1:] + result.append(dict( + name=name, + partition=partition + )) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def ports(self): + if self.want.ports is None: + return None + elif self.have.ports is None: + return self.want.ports + if sorted(self.want.ports) != sorted(self.have.ports): + return self.want.ports + + @property + def port_lists(self): + if self.want.port_lists is None: + return None + elif self.have.port_lists is None: + return self.want.port_lists + if sorted(self.want.port_lists) != sorted(self.have.port_lists): + return self.want.port_lists + + @property + def port_ranges(self): + if self.want.port_ranges is None: + return None + elif self.have.port_ranges is None: + return self.want.port_ranges + if sorted(self.want.port_ranges) != sorted(self.have.port_ranges): + return self.want.port_ranges + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + if not module_provisioned(self.client, 'afm'): + raise F5ModuleError( + "AFM must be provisioned to use this module." + ) + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/port-list/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + 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/security/firewall/port-list/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/port-list/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + 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/security/firewall/port-list/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/port-list/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + ports=dict( + type='list', + elements='str', + ), + port_ranges=dict( + type='list', + elements='str', + ), + port_lists=dict( + type='list', + elements='str', + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_rule.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_rule.py new file mode 100644 index 00000000..26973c81 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_rule.py @@ -0,0 +1,1321 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_rule +short_description: Manage AFM Firewall rules +description: + - Manages firewall rules in an AFM (Advanced Firewall Manager) firewall policy. New rules will always be added to the + end of the policy. Rules can be re-ordered using the C(bigip_security_policy) module. + Rules can also be pre-ordered using the C(bigip_security_policy) module and then later + updated using the C(bigip_firewall_rule) module. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the rule. + type: str + required: True + parent_policy: + description: + - The policy which contains the rule to be managed. + - One of either C(parent_policy) or C(parent_rule_list) is required. + type: str + parent_rule_list: + description: + - The rule list which contains the rule to be managed. + - One of either C(parent_policy) or C(parent_rule_list) is required. + type: str + action: + description: + - Specifies the action for the firewall rule. + - When C(accept), allows packets with the specified source, destination, + and protocol to pass through the firewall. Packets that match the rule + and are accepted, traverse the system as if the firewall is not present. + - When C(drop), drops packets with the specified source, destination, and + protocol. Dropping a packet is a silent action with no notification to + the source or destination systems. Dropping the packet causes the connection + to be retried until the retry threshold is reached. + - When C(reject), rejects packets with the specified source, destination, + and protocol. When a packet is rejected, the firewall sends a destination + unreachable message to the sender. + - When C(accept-decisively), allows packets with the specified source, + destination, and protocol to pass through the firewall, and does not require + any further processing by any of the further firewalls. Packets that match + the rule and are accepted, traverse the system as if the firewall is not + present. If the Rule List is applied to a virtual server, management IP, + or self IP firewall rule, then Accept Decisively is equivalent to Accept. + - When creating a new rule, if this parameter is not provided, the default is + C(reject). + type: str + choices: + - accept + - drop + - reject + - accept-decisively + status: + description: + - Indicates the activity state of the rule or rule list. + - When C(disabled), specifies the rule or rule list does not apply at all. + - When C(enabled), specifies the system applies the firewall rule or rule + list to the given context and addresses. + - When C(scheduled), specifies the system applies the rule or rule list + according to the specified schedule. + - When creating a new rule, if this parameter is not provided, the default + is C(enabled). + type: str + choices: + - enabled + - disabled + - scheduled + schedule: + description: + - Specifies a schedule for the firewall rule. + - You configure schedules to define days and times when the firewall rule is + made active. + type: str + description: + description: + - The rule description. + type: str + irule: + description: + - Specifies an iRule that is applied to the firewall rule. + - An iRule can be started when the firewall rule matches traffic. + type: str + protocol: + description: + - Specifies the protocol to which the rule applies. + - Protocols may be specified by either their name or numeric value. + - A special protocol value C(any) can be specified to match any protocol. The + numeric equivalent of this protocol is C(255). + type: str + source: + description: + - Specifies packet sources to which the rule applies. + - Leaving this field blank applies the rule to all addresses and all ports. + - You can specify the following source items. An IPv4 or IPv6 address, an IPv4 + or IPv6 address range, geographic location, VLAN, address list, port, + port range, port list or address list. + - You can specify a mix of different types of items for the source address. + type: list + elements: dict + suboptions: + address: + description: + - Specifies a specific IP address. + type: str + address_list: + description: + - Specifies an existing address list. + type: str + address_range: + description: + - Specifies an address range. + type: str + country: + description: + - Specifies a country code. + type: str + port: + description: + - Specifies a single numeric port. + - This option is only valid when C(protocol) is C(tcp)(6) or C(udp)(17). + type: int + port_list: + description: + - Specifes an existing port list. + - This option is only valid when C(protocol) is C(tcp)(6) or C(udp)(17). + type: str + port_range: + description: + - Specifies a range of ports, which is two port values separated by + a hyphen. The port to the left of the hyphen should be less than the + port to the right. + - This option is only valid when C(protocol) is C(tcp)(6) or C(udp)(17). + type: str + vlan: + description: + - Specifies VLANs to which the rule applies. + - The VLAN source refers to the packet's source. + type: str + destination: + description: + - Specifies packet destinations to which the rule applies. + - Leaving this field blank applies the rule to all addresses and all ports. + - You can specify the following destination items. An IPv4 or IPv6 address, + an IPv4 or IPv6 address range, geographic location, VLAN, address list, port, + port range, port list or address list. + - You can specify a mix of different types of items for the source address. + type: list + elements: dict + suboptions: + address: + description: + - Specifies a specific IP address. + type: str + address_list: + description: + - Specifies an existing address list. + type: str + address_range: + description: + - Specifies an address range. + type: str + country: + description: + - Specifies a country code. + type: str + port: + description: + - Specifies a single numeric port. + - This option is only valid when C(protocol) is C(tcp)(6) or C(udp)(17). + type: int + port_list: + description: + - Specifes an existing port list. + - This option is only valid when C(protocol) is C(tcp)(6) or C(udp)(17). + type: str + port_range: + description: + - Specifies a range of ports, which is two port values separated by + a hyphen. The port to the left of the hyphen should be less than the + port to the right. + - This option is only valid when C(protocol) is C(tcp)(6) or C(udp)(17). + type: str + logging: + description: + - Specifies whether logging is enabled or disabled for the firewall rule. + - When creating a new rule, if this parameter is not specified, the default + if C(no). + type: bool + rule_list: + description: + - Specifies an existing rule list to use in the rule. + - This parameter is mutually exclusive with many of the other individual-rule + specific settings. This includes C(logging), C(action), C(source), + C(destination), C(irule'), C(protocol) and C(logging). + - This parameter is only used when C(parent_policy) is specified, otherwise it is ignored. + type: str + icmp_message: + description: + - Specifies the Internet Control Message Protocol (ICMP) or ICMPv6 message + C(type) and C(code) the rule uses. + - This parameter is only relevant when C(protocol) is either C(icmp)(1) or + C(icmpv6)(58). + type: list + elements: dict + suboptions: + type: + description: + - Specifies the type of ICMP message. + - You can specify control messages, such as Echo Reply (0) and Destination + Unreachable (3), or you can specify C(any) to indicate the system + applies the rule for all ICMP messages. + - You can also specify an arbitrary ICMP message. + - The ICMP protocol contains definitions for the existing message type and + number pairs. + type: str + code: + description: + - Specifies the code returned in response to the specified ICMP message type. + - You can specify codes, each set appropriate to the associated type, such + as No Code (0) (associated with Echo Reply (0)) and Host Unreachable (1) + (associated with Destination Unreachable (3)), or you can specify C(any) + to indicate the system applies the rule for all codes in response to + that specific ICMP message. + - You can also specify an arbitrary code. + - The ICMP protocol contains definitions for the existing message code and + number pairs. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures the rule exists. + - When C(state) is C(absent), ensures the rule is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a new rule in the foo firewall policy + bigip_firewall_rule: + name: foo + parent_policy: policy1 + protocol: tcp + source: + - address: 1.2.3.4 + - address: "::1" + - address_list: foo-list1 + - address_range: 1.1.1.1-2.2.2.2 + - vlan: vlan1 + - country: US + - port: 22 + - port_list: port-list1 + - port_range: 80-443 + destination: + - address: 1.2.3.4 + - address: "::1" + - address_list: foo-list1 + - address_range: 1.1.1.1-2.2.2.2 + - country: US + - port: 22 + - port_list: port-list1 + - port_range: 80-443 + irule: irule1 + action: accept + logging: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create an ICMP specific rule + bigip_firewall_rule: + name: foo + protocol: icmp + icmp_message: + type: 0 + source: + - country: US + action: drop + logging: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Add a new policy rule that uses an existing rule list + bigip_firewall_rule: + name: foo + parent_policy: foo_policy + rule_list: rule-list1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +name: + description: Name of the rule. + returned: changed + type: str + sample: FooRule +parent_policy: + description: The policy which contains the rule to be managed. + returned: changed + type: str + sample: FooPolicy +parent_rule_list: + description: The rule list which contains the rule to be managed. + returned: changed + type: str + sample: FooRuleList +action: + description: The action for the firewall rule. + returned: changed + type: str + sample: drop +status: + description: The activity state of the rule or rule list. + returned: changed + type: str + sample: scheduled +schedule: + description: The schedule for the firewall rule. + returned: changed + type: str + sample: Foo_schedule +description: + description: The rule description. + returned: changed + type: str + sample: MyRule +irule: + description: The iRule that is applied to the firewall rule. + returned: changed + type: str + sample: _sys_auth_radius +protocol: + description: The protocol to which the rule applies. + returned: changed + type: str + sample: any +source: + description: The packet sources to which the rule applies. + returned: changed + type: complex + contains: + address: + description: A specific IP address. + returned: changed + type: str + sample: 192.168.1.1 + address_list: + description: An existing address list. + returned: changed + type: str + sample: foo-list1 + address_range: + description: The address range. + returned: changed + type: str + sample: 1.1.1.1-2.2.2.2 + country: + description: A country code. + returned: changed + type: str + sample: US + port: + description: Single numeric port. + returned: changed + type: int + sample: 8080 + port_list: + description: An existing port list. + returned: changed + type: str + sample: port-list1 + port_range: + description: The port range. + returned: changed + type: str + sample: 80-443 + vlan: + description: Source VLANs for the packets. + returned: changed + type: str + sample: vlan1 + sample: hash/dictionary of values +destination: + description: The packet destinations to which the rule applies. + returned: changed + type: complex + contains: + address: + description: A specific IP address. + returned: changed + type: str + sample: 192.168.1.1 + address_list: + description: An existing address list. + returned: changed + type: str + sample: foo-list1 + address_range: + description: The address range. + returned: changed + type: str + sample: 1.1.1.1-2.2.2.2 + country: + description: A country code. + returned: changed + type: str + sample: US + port: + description: Single numeric port. + returned: changed + type: int + sample: 8080 + port_list: + description: An existing port list. + returned: changed + type: str + sample: port-list1 + port_range: + description: The port range. + returned: changed + type: str + sample: 80-443 + sample: hash/dictionary of values +logging: + description: Enable or Disable logging for the firewall rule. + returned: changed + type: bool + sample: yes +rule_list: + description: An existing rule list to use in the parent policy. + returned: changed + type: str + sample: rule-list-1 +icmp_message: + description: The (ICMP) or ICMPv6 message C(type) and C(code) that the rule uses. + returned: changed + type: complex + contains: + type: + description: The type of ICMP message. + returned: changed + type: str + sample: 0 + code: + description: The code returned in response to the specified ICMP message type. + returned: changed + type: str + sample: 1 + sample: hash/dictionary of values +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'ipProtocol': 'protocol', + 'log': 'logging', + 'icmp': 'icmp_message', + 'ruleList': 'rule_list' + } + + api_attributes = [ + 'irule', + 'ipProtocol', + 'log', + 'schedule', + 'status', + 'destination', + 'source', + 'icmp', + 'action', + 'description', + 'ruleList', + ] + + returnables = [ + 'logging', + 'protocol', + 'irule', + 'source', + 'destination', + 'action', + 'status', + 'schedule', + 'description', + 'icmp_message', + 'rule_list', + ] + + updatables = [ + 'logging', + 'protocol', + 'irule', + 'source', + 'destination', + 'action', + 'status', + 'schedule', + 'description', + 'icmp_message', + 'rule_list', + ] + + protocol_map = { + '1': 'icmp', + '6': 'tcp', + '17': 'udp', + '58': 'icmpv6', + '255': 'any', + } + + +class ApiParameters(Parameters): + @property + def logging(self): + if self._values['logging'] is None: + return None + if self._values['logging'] == 'yes': + return True + return False + + @property + def protocol(self): + if self._values['protocol'] is None: + return None + if self._values['protocol'] in self.protocol_map: + return self.protocol_map[self._values['protocol']] + return self._values['protocol'] + + @property + def source(self): + result = [] + if self._values['source'] is None: + return None + v = self._values['source'] + if 'addressLists' in v: + result += [('address_list', x) for x in v['addressLists']] + if 'vlans' in v: + result += [('vlan', x) for x in v['vlans']] + if 'geo' in v: + result += [('geo', x['name']) for x in v['geo']] + if 'addresses' in v: + result += [('address', x['name']) for x in v['addresses']] + if 'ports' in v: + result += [('port', str(x['name'])) for x in v['ports']] + if 'portLists' in v: + result += [('port_list', x) for x in v['portLists']] + if result: + return result + return None + + @property + def destination(self): + result = [] + if self._values['destination'] is None: + return None + v = self._values['destination'] + if 'addressLists' in v: + result += [('address_list', x) for x in v['addressLists']] + if 'geo' in v: + result += [('geo', x['name']) for x in v['geo']] + if 'addresses' in v: + result += [('address', x['name']) for x in v['addresses']] + if 'ports' in v: + result += [('port', x['name']) for x in v['ports']] + if 'portLists' in v: + result += [('port_list', x) for x in v['portLists']] + if result: + return result + return None + + @property + def icmp_message(self): + if self._values['icmp_message'] is None: + return None + result = [x['name'] for x in self._values['icmp_message']] + return result + + +class ModuleParameters(Parameters): + @property + def irule(self): + if self._values['irule'] is None: + return None + if self._values['irule'] == '': + return '' + return fq_name(self.partition, self._values['irule']) + + @property + def description(self): + if self._values['description'] is None: + return None + if self._values['description'] == '': + return '' + return self._values['description'] + + @property + def schedule(self): + if self._values['schedule'] is None: + return None + if self._values['schedule'] == '': + return '' + return fq_name(self.partition, self._values['schedule']) + + @property + def source(self): + result = [] + if self._values['source'] is None: + return None + for x in self._values['source']: + if 'address' in x and x['address'] is not None: + result += [('address', x['address'])] + elif 'address_range' in x and x['address_range'] is not None: + result += [('address', x['address_range'])] + elif 'address_list' in x and x['address_list'] is not None: + result += [('address_list', fq_name(self.partition, x['address_list']))] + # result += [('address_list', x['address_list'])] + elif 'country' in x and x['country'] is not None: + result += [('geo', x['country'])] + elif 'vlan' in x and x['vlan'] is not None: + result += [('vlan', fq_name(self.partition, x['vlan']))] + elif 'port' in x and x['port'] is not None: + result += [('port', str(x['port']))] + elif 'port_range' in x and x['port_range'] is not None: + result += [('port', x['port_range'])] + elif 'port_list' in x and x['port_list'] is not None: + result += [('port_list', fq_name(self.partition, x['port_list']))] + if result: + return result + return None + + @property + def destination(self): + result = [] + if self._values['destination'] is None: + return None + for x in self._values['destination']: + if 'address' in x and x['address'] is not None: + result += [('address', x['address'])] + elif 'address_range' in x and x['address_range'] is not None: + result += [('address', x['address_range'])] + elif 'address_list' in x and x['address_list'] is not None: + result += [('address_list', fq_name(self.partition, x['address_list']))] + # result += [('address_list', x['address_list'])] + elif 'country' in x and x['country'] is not None: + result += [('geo', x['country'])] + elif 'port' in x and x['port'] is not None: + result += [('port', str(x['port']))] + elif 'port_range' in x and x['port_range'] is not None: + result += [('port', x['port_range'])] + elif 'port_list' in x and x['port_list'] is not None: + result += [('port_list', fq_name(self.partition, x['port_list']))] + if result: + return result + return None + + @property + def icmp_message(self): + if self._values['icmp_message'] is None: + return None + result = [] + for x in self._values['icmp_message']: + type = x.get('type', '255') + code = x.get('code', '255') + + if type is None or type == 'any': + type = '255' + if code is None or code == 'any': + code = '255' + + if type == '255' and code == '255': + result.append("255") + elif type == '255' and code != '255': + raise F5ModuleError( + "A type of 'any' (255) requires a code of 'any'." + ) + elif code == '255': + result.append(type) + else: + result.append('{0}:{1}'.format(type, code)) + result = list(set(result)) + return result + + @property + def rule_list(self): + if self._values['rule_list'] is None: + return None + if self._values['parent_policy'] is not None: + return fq_name(self.partition, self._values['rule_list']) + return None + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def logging(self): + if self._values['logging'] is None: + return None + if self._values['logging'] is True: + return "yes" + return "no" + + @property + def source(self): + if self._values['source'] is None: + return None + result = dict( + addresses=[], + addressLists=[], + vlans=[], + geo=[], + ports=[], + portLists=[] + ) + for x in self._values['source']: + if x[0] == 'address': + result['addresses'].append({'name': x[1]}) + elif x[0] == 'address_list': + result['addressLists'].append(x[1]) + elif x[0] == 'vlan': + result['vlans'].append(x[1]) + elif x[0] == 'geo': + result['geo'].append({'name': x[1]}) + elif x[0] == 'port': + result['ports'].append({'name': str(x[1])}) + elif x[0] == 'port_list': + result['portLists'].append(x[1]) + return result + + @property + def destination(self): + if self._values['destination'] is None: + return None + result = dict( + addresses=[], + addressLists=[], + vlans=[], + geo=[], + ports=[], + portLists=[] + ) + for x in self._values['destination']: + if x[0] == 'address': + result['addresses'].append({'name': x[1]}) + elif x[0] == 'address_list': + result['addressLists'].append(x[1]) + elif x[0] == 'geo': + result['geo'].append({'name': x[1]}) + elif x[0] == 'port': + result['ports'].append({'name': str(x[1])}) + elif x[0] == 'port_list': + result['portLists'].append(x[1]) + return result + + @property + def icmp_message(self): + if self._values['icmp_message'] is None: + return None + result = [] + for x in self._values['icmp_message']: + result.append({'name': x}) + return result + + +class ReportableChanges(Changes): + @property + def source(self): + if self._values['source'] is None: + return None + result = [] + v = self._values['source'] + if v['addressLists']: + result += [('address_list', x) for x in v['addressLists']] + if v['vlans']: + result += [('vlan', x) for x in v['vlans']] + if v['geo']: + result += [('geo', x['name']) for x in v['geo']] + if v['addresses']: + result += [('address', x['name']) for x in v['addresses']] + if v['ports']: + result += [('port', str(x)) for x in v['ports']] + if v['portLists']: + result += [('port_list', x) for x in v['portLists']] + if result: + return dict(result) + return None + + @property + def destination(self): + if self._values['destination'] is None: + return None + result = [] + v = self._values['destination'] + if v['addressLists']: + result += [('address_list', x) for x in v['addressLists']] + if v['geo']: + result += [('geo', x['name']) for x in v['geo']] + if v['addresses']: + result += [('address', x['name']) for x in v['addresses']] + if v['ports']: + result += [('port', str(x)) for x in v['ports']] + if v['portLists']: + result += [('port_list', x) for x in v['portLists']] + if result: + return dict(result) + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def irule(self): + if self.want.irule is None: + return None + if self.have.irule is None and self.want.irule == '': + return None + if self.have.irule is None: + return self.want.irule + if self.want.irule != self.have.irule: + return self.want.irule + + @property + def description(self): + if self.want.description is None: + return None + if self.have.description is None and self.want.description == '': + return None + if self.have.description is None: + return self.want.description + if self.want.description != self.have.description: + return self.want.description + + @property + def source(self): + if self.want.source is None: + return None + if self.want.source is None and self.have.source is None: + return None + if self.have.source is None: + return self.want.source + if set(self.want.source) != set(self.have.source): + return self.want.source + + @property + def destination(self): + if self.want.destination is None: + return None + if self.want.destination is None and self.have.destination is None: + return None + if self.have.destination is None: + return self.want.destination + if set(self.want.destination) != set(self.have.destination): + return self.want.destination + + @property + def icmp_message(self): + if self.want.icmp_message is None: + return None + if self.want.icmp_message is None and self.have.icmp_message is None: + return None + if self.have.icmp_message is None: + return self.want.icmp_message + if set(self.want.icmp_message) != set(self.have.icmp_message): + return self.want.icmp_message + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + name = self.want.name + if self.want.parent_policy: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_policy), + name.replace('/', '_') + ) + else: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_rule_list), + name.replace('/', '_') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self.set_reasonable_creation_defaults() + if self.want.status == 'scheduled' and self.want.schedule is None: + raise F5ModuleError( + "A 'schedule' must be specified when 'status' is 'scheduled'." + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def set_reasonable_creation_defaults(self): + if self.want.action is None: + self.changes.update({'action': 'reject'}) + if self.want.logging is None: + self.changes.update({'logging': False}) + if self.want.status is None: + self.changes.update({'status': 'enabled'}) + + def create_on_device(self): + params = self.changes.api_params() + name = self.want.name + params['name'] = name.replace('/', '_') + params['partition'] = self.want.partition + params['placeAfter'] = 'last' + + if self.want.parent_policy: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_policy), + ) + else: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/rules/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_rule_list), + ) + if self.changes.protocol not in ['icmp', 'icmpv6']: + if self.changes.icmp_message is not None: + raise F5ModuleError( + "The 'icmp_message' can only be specified when 'protocol' is 'icmp' or 'icmpv6'." + ) + + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + name = self.want.name + if self.want.parent_policy and self.want.rule_list: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_policy), + name.replace('/', '_') + ) + + elif self.want.parent_policy: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_policy), + name.replace('/', '_') + ) + else: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_rule_list), + name.replace('/', '_') + ) + + if self.have.protocol not in ['icmp', 'icmpv6'] and self.changes.protocol not in ['icmp', 'icmpv6']: + if self.changes.icmp_message is not None: + raise F5ModuleError( + "The 'icmp_message' can only be specified when 'protocol' is 'icmp' or 'icmpv6'." + ) + if self.changes.protocol in ['icmp', 'icmpv6']: + self.changes.update({'source': {}}) + self.changes.update({'destination': {}}) + + params = self.changes.api_params() + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + name = self.want.name + if self.want.parent_policy: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_policy), + name.replace('/', '_') + ) + else: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_rule_list), + name.replace('/', '_') + ) + + resp = self.client.api.delete(uri) + if resp.status == 200: + return True + + def read_current_from_device(self): + if self.want.parent_policy: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_policy), + self.want.name + ) + else: + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.parent_rule_list), + self.want.name + ) + + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent_policy=dict(), + parent_rule_list=dict(), + logging=dict(type='bool'), + protocol=dict(), + irule=dict(), + description=dict(), + source=dict( + type='list', + elements='dict', + options=dict( + address=dict(), + address_list=dict(), + address_range=dict(), + country=dict(), + port=dict(type='int'), + port_list=dict(), + port_range=dict(), + vlan=dict(), + ), + mutually_exclusive=[[ + 'address', 'address_list', 'address_range', 'country', 'vlan', + 'port', 'port_range', 'port_list' + ]] + ), + destination=dict( + type='list', + elements='dict', + options=dict( + address=dict(), + address_list=dict(), + address_range=dict(), + country=dict(), + port=dict(type='int'), + port_list=dict(), + port_range=dict(), + ), + mutually_exclusive=[[ + 'address', 'address_list', 'address_range', 'country', + 'port', 'port_range', 'port_list' + ]] + ), + action=dict( + choices=['accept', 'drop', 'reject', 'accept-decisively'] + ), + status=dict( + choices=['enabled', 'disabled', 'scheduled'] + ), + schedule=dict(), + rule_list=dict(), + icmp_message=dict( + type='list', + elements='dict', + options=dict( + type=dict(), + code=dict(), + ) + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.mutually_exclusive = [ + ['rule_list', 'action'], + ['rule_list', 'source'], + ['rule_list', 'destination'], + ['rule_list', 'irule'], + ['rule_list', 'protocol'], + ['rule_list', 'logging'], + ['parent_policy', 'parent_rule_list'] + ] + self.required_one_of = [ + ['parent_policy', 'parent_rule_list'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + required_one_of=spec.required_one_of + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_rule_list.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_rule_list.py new file mode 100644 index 00000000..65408261 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_rule_list.py @@ -0,0 +1,536 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_rule_list +short_description: Manage AFM security firewall policies on a BIG-IP +description: + - Manages AFM (Advanced Firewall Manager) security firewall policies on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - The name of the policy to create. + type: str + required: True + description: + description: + - The description to attach to the policy. + - This parameter is only supported on versions of BIG-IP >= 12.1.0. On earlier + versions it is ignored. + type: str + state: + description: + - When C(state) is C(present), ensures the rule list exists. + - When C(state) is C(absent), ensures the rule list is removed. + type: str + choices: + - present + - absent + default: present + rules: + description: + - Specifies a list of rules you want associated with this policy. + The order of this list is the order they will be evaluated by BIG-IP. + If the specified rules do not exist (for example when creating a new + policy) then they will be created. + - Rules specified here, if they do not exist, will be created with "default deny" + behavior. It is expected that you follow-up this module with the actual + configuration for these rules. + - The C(bigip_firewall_rule) module can also be used to create, as well as + edit, existing and new rules. + type: list + elements: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a basic policy with some rule stubs + bigip_firewall_rule_list: + name: foo + rules: + - rule1 + - rule2 + - rule3 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the policy. + returned: changed + type: str + sample: My firewall policy +rules: + description: The list of rules on the device, in the order that they are evaluated. + returned: changed + type: list + sample: ['rule1', 'rule2', 'rule3'] +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'rulesReference': 'rules' + } + + api_attributes = [ + 'description' + ] + + returnables = [ + 'description', + 'rules', + ] + + updatables = [ + 'description', + 'rules' + ] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ModuleParameters(Parameters): + @property + def rules(self): + if self._values['rules'] is None: + return None + # In case rule values are unicode (as they may be coming from the API + result = [str(x) for x in self._values['rules']] + return result + + +class ApiParameters(Parameters): + @property + def rules(self): + result = [] + if self._values['rules'] is None or 'items' not in self._values['rules']: + return [] + for idx, item in enumerate(self._values['rules']['items']): + result.append(dict(item=item['fullPath'], order=idx)) + result = [x['item'] for x in sorted(result, key=lambda k: k['order'])] + return result + + +class Changes(Parameters): + pass + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def rules(self): + if self.want.rules is None: + return None + if self.have.rules is None: + return self.want.rules + if set(self.want.rules) != set(self.have.rules): + return self.want.rules + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Changes(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = Changes(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/security/firewall/rule-list/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if self.want.rules: + self._upsert_policy_rules_on_device() + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + if params: + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + if self.changes.rules is not None: + self._upsert_policy_rules_on_device() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{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 + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/?expandSubcollections=true".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def rule_exists(self, rule): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + rule.replace('/', '_') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_default_rule_on_device(self, rule): + params = dict( + name=rule.replace('/', '_'), + action='reject', + # Adding items to the end of the list causes the list of rules to match + # what the user specified in the original list. + placeAfter='last', + ) + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/rules/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_rule_from_device(self, rule): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + rule.replace('/', '_'), + ) + # this response returns no payload + resp = self.client.api.delete(uri) + if resp.status in [400, 403]: + raise F5ModuleError(resp.content) + + def move_rule_to_front(self, rule): + params = dict( + placeAfter='last' + ) + uri = "https://{0}:{1}/mgmt/tm/security/firewall/rule-list/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + rule.replace('/', '_') + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def _upsert_policy_rules_on_device(self): + rules = self.changes.rules + if rules is None: + rules = [] + self._remove_rule_difference(rules) + + for idx, rule in enumerate(rules): + if not self.rule_exists(rule): + self.create_default_rule_on_device(rule) + for idx, rule in enumerate(rules): + self.move_rule_to_front(rule) + + def _remove_rule_difference(self, rules): + if rules is None or self.have.rules is None: + return + have_rules = set(self.have.rules) + want_rules = set(rules) + removable = have_rules.difference(want_rules) + for remove in removable: + self.remove_rule_from_device(remove) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + rules=dict( + type='list', + elements='str', + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_schedule.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_schedule.py new file mode 100644 index 00000000..3b43de19 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_firewall_schedule.py @@ -0,0 +1,664 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_firewall_schedule +short_description: Manage BIG-IP AFM schedule configurations +description: + - Manage BIG-IP AFM (Avanced Firewall Manager) schedule configurations. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the AFM schedule configuration. + type: str + required: True + description: + description: + - Specifies the user defined description text. + type: str + daily_hour_end: + description: + - Specifies the time of day the rule will stop being used. + - When not defined, the default of C(24:00) is used when creating a new schedule. + - The time zone is always assumed to be UTC and values must be provided as C(HH:MM) using 24hour clock format. + type: str + daily_hour_start: + description: + - Specifies the time of day the rule will start to be in use. + - The value must be a time before C(daily_hour_end). + - When not defined, the default of C(0:00) is used when creating a new schedule. + - When the value is set to C(all-day) both C(daily_hour_end) and C(daily_hour_start) are reset to their respective + defaults. + - The time zone is always assumed to be UTC and values must be provided as C(HH:MM) using 24hour clock format. + type: str + date_valid_end: + description: + - Specifies the end date/time this schedule will apply to the rule. + - The date must be after C(date_valid_start) + - When not defined, the default of C(indefinite) is used when creating a new schedule. + - The time zone is always assumed to be UTC. + - The datetime format should always be in C(YYYY-MM-DD:HH:MM:SS) format. + type: str + date_valid_start: + description: + - Specifies the start date/time this schedule will apply to the rule. + - When not defined the default of C(epoch) is used when creating a new schedule. + - The time zone is always assumed to be UTC. + - The datetime format should always be in C(YYYY-MM-DD:HH:MM:SS) format. + type: str + days_of_week: + description: + - Specifies which days of the week the rule will be applied. + - When not defined, the default value of C(all) is used when creating a new schedule. + - The C(all) value is mutually exclusive with other choices. + type: list + elements: str + choices: + - sunday + - monday + - tuesday + - wednesday + - thursday + - friday + - saturday + - all + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a 6 hour two day schedule, no start/end date + bigip_firewall_schedule: + name: barfoo + daily_hour_start: 13:00 + daily_hour_end: 19:00 + days_of_week: + - monday + - tuesday + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a seven day schedule with start/end date + bigip_firewall_schedule: + name: foobar + date_valid_start: "{{ lookup('pipe','date +%Y-%m-%d:%H:%M:%S') }}" + date_valid_end: "{{ lookup('pipe','date -d \"now + 7 days\" +%Y-%m-%d:%H:%M:%S') }}" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify created schedule to all-day + bigip_firewall_schedule: + name: barfoo + daily_hour_start: all-day + days_of_week: + - monday + - tuesday + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify a schedule to have no end date + bigip_firewall_schedule: + name: foobar + date_valid_start: "{{ lookup('pipe','date +%Y-%m-%d:%H:%M:%S') }}" + date_valid_end: "indefinite" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove created schedule + bigip_firewall_schedule: + name: foobar + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +daily_hour_start: + description: The time of day the rule will start to be in use. + returned: changed + type: str + sample: '13:00' +daily_hour_end: + description: The time of day the rule will stop being used. + returned: changed + type: str + sample: '18:00' +date_valid_start: + description: The start date/time schedule will apply to the rule. + returned: changed + type: str + sample: 2019-03-01:15:30:00 +date_valid_end: + description: The end date/time schedule will apply to the rule. + returned: changed + type: str + sample: 2019-03-11:15:30:00 +days_of_week: + description: The days of the week the rule will be applied. + returned: changed + type: list + sample: ["monday","tuesday"] +description: + description: The user defined description text. + returned: changed + type: str + sample: Foo is bar +''' + +import re +import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.compare import ( + cmp_str_with_none, cmp_simple_list +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'dailyHourEnd': 'daily_hour_end', + 'dailyHourStart': 'daily_hour_start', + 'dateValidEnd': 'date_valid_end', + 'dateValidStart': 'date_valid_start', + 'daysOfWeek': 'days_of_week', + } + + api_attributes = [ + 'dailyHourEnd', + 'dailyHourStart', + 'dateValidEnd', + 'dateValidStart', + 'daysOfWeek', + 'description', + ] + + returnables = [ + 'daily_hour_end', + 'daily_hour_start', + 'date_valid_end', + 'date_valid_start', + 'days_of_week', + 'description' + ] + + updatables = [ + 'daily_hour_end', + 'daily_hour_start', + 'date_valid_end', + 'date_valid_start', + 'days_of_week', + 'description' + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + def _convert_datetime(self, value): + p = r'(\d{4})-(\d{1,2})-(\d{1,2})[:, T](\d{2}):(\d{2}):(\d{2})' + match = re.match(p, value) + if match: + date = '{0}-{1}-{2}T{3}:{4}:{5}Z'.format(*match.group(1, 2, 3, 4, 5, 6)) + return date + raise F5ModuleError( + 'Invalid datetime provided.' + ) + + def _validate_time(self, value): + p = r'(\d{2}):(\d{2})' + match = re.match(p, value) + if match: + time = int(match.group(1)), int(match.group(2)) + try: + datetime.time(*time) + except ValueError as ex: + raise F5ModuleError(str(ex)) + + def _compare_date_time(self, value1, value2, time=False): + if time: + p1 = r'(\d{2}):(\d{2})' + m1 = re.match(p1, value1) + m2 = re.match(p1, value2) + if m1 and m2: + start = tuple(int(i) for i in m1.group(1, 2)) + end = tuple(int(i) for i in m2.group(1, 2)) + if datetime.time(*start) > datetime.time(*end): + raise F5ModuleError( + 'End time must be later than start time.' + ) + else: + p1 = r'(\d{4})-(\d{1,2})-(\d{1,2})[:, T](\d{2}):(\d{2}):(\d{2})' + m1 = re.match(p1, value1) + m2 = re.match(p1, value2) + if m1 and m2: + start = tuple(int(i) for i in m1.group(1, 2, 3, 4, 5, 6)) + end = tuple(int(i) for i in m2.group(1, 2, 3, 4, 5, 6)) + if datetime.datetime(*start) > datetime.datetime(*end): + raise F5ModuleError( + 'End date must be later than start date.' + ) + + @property + def daily_hour_start(self): + if self._values['daily_hour_start'] is None: + return None + if self._values['daily_hour_start'] == 'all-day': + return '0:00' + self._validate_time(self._values['daily_hour_start']) + if self._values['daily_hour_end'] is not None and self.daily_hour_end != '24:00': + self._compare_date_time(self._values['daily_hour_start'], self.daily_hour_end, time=True) + return self._values['daily_hour_start'] + + @property + def daily_hour_end(self): + if self._values['daily_hour_end'] is None: + return None + if self._values['daily_hour_start'] == 'all-day': + return '24:00' + if not self._values['daily_hour_end'] == '24:00': + self._validate_time(self._values['daily_hour_end']) + return self._values['daily_hour_end'] + + @property + def date_valid_end(self): + if self._values['date_valid_end'] is None: + return None + if self._values['date_valid_end'] in ['2038-1-18:19:14:07', 'indefinite']: + return 'indefinite' + result = self._convert_datetime(self._values['date_valid_end']) + return result + + @property + def date_valid_start(self): + if self._values['date_valid_start'] is None: + return None + if self._values['date_valid_start'] in ['1970-1-1:00:00:00', 'epoch']: + return 'epoch' + result = self._convert_datetime(self._values['date_valid_start']) + if self._values['date_valid_end']: + if self._values['date_valid_end'] not in ['2038-1-18:19:14:07', 'indefinite']: + self._compare_date_time(result, self.date_valid_end) + return result + + @property + def days_of_week(self): + if self._values['days_of_week'] is None: + return None + if 'all' in self._values['days_of_week']: + if len(self._values['days_of_week']) > 1 and self._values['days_of_week'] is list: + raise F5ModuleError( + "The 'all' value must not be specified with other choices." + ) + week = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] + return week + return self._values['days_of_week'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + def _convert_datetime(self, value): + if value is None: + return None + p = r'(\d{4})-(\d{1,2})-(\d{1,2})[:, T](\d{2}):(\d{2}):(\d{2})' + match = re.match(p, value) + if match: + date = '{0}-{1}-{2}:{3}:{4}:{5}'.format(*match.group(1, 2, 3, 4, 5, 6)) + return date + + @property + def date_valid_end(self): + result = self._convert_datetime(self._values['date_valid_end']) + return result + + @property + def date_valid_start(self): + result = self._convert_datetime(self._values['date_valid_start']) + return result + + @property + def days_of_week(self): + if self._values['days_of_week'] is None: + return None + if len(self._values['days_of_week']) == 7: + return 'all' + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def days_of_week(self): + return cmp_simple_list(self.want.days_of_week, self.have.days_of_week) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/schedule/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/security/firewall/schedule/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/firewall/schedule/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/schedule/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/firewall/schedule/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True + ), + description=dict(), + daily_hour_end=dict(), + daily_hour_start=dict(), + date_valid_end=dict(), + date_valid_start=dict(), + days_of_week=dict( + type='list', + elements='str', + choices=[ + 'sunday', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'all', + ] + ), + state=dict(default='present', choices=['absent', 'present']), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_datacenter.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_datacenter.py new file mode 100644 index 00000000..e8ef7af7 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_datacenter.py @@ -0,0 +1,495 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_datacenter +short_description: Manage Datacenter configuration in BIG-IP +description: + - Manage BIG-IP data center configuration. A data center defines the location + where the physical network components reside, such as the server and link + objects that share the same subnet on the network. This module is able to + manipulate the data center definitions in a BIG-IP. +version_added: "1.0.0" +options: + contact: + description: + - The name of the contact for the data center. + type: str + description: + description: + - The description of the data center. + type: str + location: + description: + - The location of the data center. + type: str + name: + description: + - The name of the data center. + type: str + required: True + state: + description: + - The virtual address state. If C(absent), an attempt to delete the + virtual address will be made. This will only succeed if this + virtual address is not in use by a virtual server. C(present) creates + the virtual address and enables it. If C(enabled), enables the virtual + address if it exists. If C(disabled), creates the virtual address if + needed, and sets state to C(disabled). + type: str + choices: + - present + - absent + - enabled + - disabled + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create data center "New York" + bigip_gtm_datacenter: + name: New York + location: 222 West 23rd + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +contact: + description: The contact that was set on the datacenter. + returned: changed + type: str + sample: admin@root.local +description: + description: The description for the datacenter. + returned: changed + type: str + sample: Datacenter in NYC +enabled: + description: Whether the datacenter is enabled or not. + returned: changed + type: bool + sample: true +disabled: + description: Whether the datacenter is disabled or not. + returned: changed + type: bool + sample: true +state: + description: State of the datacenter. + returned: changed + type: str + sample: disabled +location: + description: The location for the datacenter. + returned: changed + type: str + sample: 222 West 23rd +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = {} + + updatables = [ + 'location', + 'description', + 'contact', + 'state', + ] + + returnables = [ + 'location', + 'description', + 'contact', + 'state', + 'enabled', + 'disabled', + ] + + api_attributes = [ + 'enabled', + 'location', + 'description', + 'contact', + 'disabled', + ] + + +class ApiParameters(Parameters): + @property + def disabled(self): + if self._values['disabled'] is True: + return True + return None + + @property + def enabled(self): + if self._values['enabled'] is True: + return True + return None + + +class ModuleParameters(Parameters): + @property + def disabled(self): + if self._values['state'] == 'disabled': + return True + return None + + @property + def enabled(self): + if self._values['state'] in ['enabled', 'present']: + return True + return None + + @property + def state(self): + if self.enabled and self._values['state'] != 'present': + return 'enabled' + elif self.disabled and self._values['state'] != 'present': + return 'disabled' + else: + return self._values['state'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def disabled(self): + if self._values['state'] == 'disabled': + return True + + @property + def enabled(self): + if self._values['state'] in ['enabled', 'present']: + return True + + +class ReportableChanges(Changes): + @property + def disabled(self): + if self._values['state'] == 'disabled': + return True + elif self._values['state'] in ['enabled', 'present']: + return False + return None + + @property + def enabled(self): + if self._values['state'] in ['enabled', 'present']: + return True + elif self._values['state'] == 'disabled': + return False + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def state(self): + if self.want.enabled != self.have.enabled: + return dict( + state=self.want.state, + enabled=self.want.enabled + ) + if self.want.disabled != self.have.disabled: + return dict( + state=self.want.state, + disabled=self.want.disabled + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.pop('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state in ['present', 'enabled', 'disabled']: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + changed = False + if self.exists(): + changed = self.remove() + return changed + + def create(self): + self.have = ApiParameters() + self.should_update() + if self.module.check_mode: + return True + self.create_on_device() + if self.exists(): + return True + else: + raise F5ModuleError("Failed to create the datacenter") + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the datacenter") + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/datacenter/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/gtm/datacenter/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/datacenter/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/datacenter/{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/gtm/datacenter/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + contact=dict(), + description=dict(), + location=dict(), + name=dict(required=True), + state=dict( + default='present', + choices=['present', 'absent', 'disabled', 'enabled'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_dns_listener.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_dns_listener.py new file mode 100644 index 00000000..4d6201b0 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_dns_listener.py @@ -0,0 +1,875 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2020, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_dns_listener +short_description: Configures the BIG-IP DNS system to answer TCP or UDP DNS requests +description: + - Defines one or more Listener objects to control which protocols are available for the BIG-IP DNS system to process DNS requests. + - BIG-IP DNS Listeners allow TCP and UDP protocols. +version_added: "1.4.0" +options: + name: + description: + - Specifies the name of the DNS Listener. + type: str + required: True + description: + description: + - Provides a brief description for DNS Listener. + type: str + address: + description: + - Specifies the IP address on which the system listens. + type: str + required: True + mask: + description: + - Specifies the netmask for a network Listener only. + - Netmask clarifies whether the host bit is an actual zero or a wildcard representation. + type: str + enabled_vlans: + description: + - List of VLANs to be enabled. When a VLAN named C(all) is used, all + VLANs will be allowed. VLANs can be specified with or without the + leading partition. If the partition is not specified in the VLAN, + then the C(partition) option of this module will be used. + - This parameter is mutually exclusive with the C(disabled_vlans) parameter. + type: list + elements: str + disabled_vlans: + description: + - List of VLANs to be disabled. If the partition is not specified in the VLAN, + then the C(partition) option of this module will be used. + - This parameter is mutually exclusive with the C(enabled_vlans) parameters. + type: list + elements: str + pool: + description: + - Specifies a default pool to which the Listener automatically directs traffic. + type: str + port: + description: + - Specifies the port on which the Listener listens for connections. + - Valid range of values is between C(0) and C(65535) inclusive. + type: int + source_port: + description: + - Specifies whether the system preserves the source port of the connection. + type: str + translate_address: + description: + - Enables or disables address translation for the Listener. + type: bool + translate_port: + description: + - Enables or disables port translation. + type: bool + irules: + description: + - Specifies list of iRules to run on the Listener. + - iRules help automate the intercepting, processing, and routing of application traffic. + - If you want to remove existing iRules, provide an empty list value; C([]). + See the documentation for an example. + type: list + elements: str + advertise: + description: + - Specifies whether this Listener's address is advertised to surrounding routers. + type: bool + auto_lasthop: + description: + - Specifies whether to automatically map the last hop for pools or not. + type: str + last_hop_pool: + description: + - Specifies the name of the last hop pool that you want the Listener to use to direct reply traffic to the last hop router. + type: str + fallback_persistence: + description: + - Specifies a fallback persistence profile for the Listener to use when the default persistence profile is not available. + type: str + ip_protocol: + description: + - Specifies the protocol on which this Listener receives network traffic. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - DNS Listener state. + - When C(present), ensures the pool is created and enabled. + - When C(absent), ensures the pool is removed from the system. + - When C(enabled) or C(disabled), ensures the pool is enabled or disabled respectively) on the remote device. + type: str + choices: + - present + - absent + - enabled + - disabled + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Andrey Kashcheev (@andreykashcheev) +''' + +EXAMPLES = r''' + +- name: 'Create DNS Listener' + bigip_gtm_dns_listener: + address: '192.0.1.0' + advertise: false + auto_lasthop: default + description: 'this is a test DNS listener' + enabled_vlans: + - /Common/external + ip_protocol: tcp + irules: + - /Common/irule1 + mask: '255.255.255.0' + pool: /Common/webpool + name: test-dns-listener + port: 30025 + provider: + password: secret + server: lb.mydomain.com + user: admin + source_port: preserve + state: present + translate_address: yes + translate_port: yes + delegate_to: localhost + +- name: 'Disable a DNS Listener' + bigip_gtm_dns_listener: + address: '192.0.1.0' + state: disabled + name: test-dns-listener + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +name: + description: DNS Listener name. + returned: changed + type: str + sample: test-dns-listener +mask: + description: Subnet mask used by the Listener to identify address range. + returned: changed + type: str + sample: 255.255.0.0 +address: + description: IP address on which the system listens. + returned: changed + type: str + sample: 10.0.0.2 +port: + description: Port on which the system listens. + returned: changed + type: int + sample: 53 +source_port: + description: Specifies if system preserves the source port of the connection. + returned: changed + type: str + sample: preserve +advertise: + description: Specifies if the Listener advertises to surrounding routers. + returned: changed + type: bool + sample: yes +auto_lasthop: + description: Shows whether the system automatically maps the last hop for pools. + returned: changed + type: str + sample: default +translate_address: + description: Specifies if address translation is enabled. + returned: changed + type: str + sample: enabled +translate_port: + description: Specifies if port translation is enabled. + returned: changed + type: str + sample: enabled +fallback_persistence: + description: Fallback persistence profile for the Listener to use when the default persistence profile is not available. + returned: changed + type: str + sample: /Common/fallback-profile +enabled: + description: Provides DNS Listener state. + returned: changed + type: bool + sample: yes +ip_protocol: + description: IP protocol used by the DNS Listener. + returned: changed + type: str + sample: tcp +disabled_vlans: + description: List of VLANs the virtual is disabled for. + returned: changed + type: list + sample: ['/Common/vlan1', '/Common/vlan2'] +enabled_vlans: + description: List of VLANs the virtual is enabled for. + returned: changed + type: list + sample: ['/Common/vlan5', '/Common/vlan6'] +irules: + description: List of rules run by the DNS Listener. + returned: changed + type: list + sample: ['/Common/rule1', '/Common/rule2'] +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, is_empty_list, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'sourcePort': 'source_port', + 'translateAddress': 'translate_address', + 'translatePort': 'translate_port', + 'vlansDisabled': 'vlans_disabled', + 'vlansEnabled': 'vlans_enabled', + 'rules': 'irules', + 'autoLasthop': 'auto_lasthop', + 'lastHopPool': 'last_hop_pool', + 'fallbackPersistence': 'fallback_persistence', + 'ipProtocol': 'ip_protocol' + } + + api_attributes = [ + 'address', + 'port', + 'advertise', + 'description', + 'sourcePort', + 'translateAddress', + 'translatePort', + 'vlansDisabled', + 'vlansEnabled', + 'vlans', + 'rules', + 'autoLasthop', + 'pool', + 'lastHopPool', + 'fallbackPersistence', + 'ipProtocol', + 'mask', + 'disabled', + 'enabled' + ] + + returnables = [ + 'description', + 'disabled', + 'enabled', + 'ip_protocol', + 'mask', + 'address', + 'disabled_vlans', + 'enabled_vlans', + 'port', + 'advertise', + 'auto_lasthop', + 'translate_address', + 'translate_port', + 'fallback_persistence', + 'source_port', + 'vlans', + 'pool', + 'last_hop_pool', + 'irules', + 'vlans_enabled', + 'vlans_disabled' + ] + + updatables = [ + 'description', + 'disabled', + 'enabled', + 'mask', + 'address', + 'port', + 'advertise', + 'auto_lasthop', + 'disabled_vlans', + 'enabled_vlans', + 'state', + 'ip_protocol', + 'fallback_persistence', + 'translate_address', + 'translate_port', + 'source_port', + 'vlans', + 'pool', + 'last_hop_pool', + 'irules', + 'vlans_enabled', + 'vlans_disabled' + ] + + @property + def state(self): + if self._values['state'] == 'enabled': + return 'present' + return self._values['state'] + + @property + def enabled(self): + if self._values['enabled'] is None: + return None + return True + + @property + def disabled(self): + if self._values['disabled'] is None: + return None + return True + + +class ApiParameters(Parameters): + @property + def irules(self): + if self._values['irules'] is None: + return [] + return self._values['irules'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def translate_address(self): + result = flatten_boolean(self._values['translate_address']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def translate_port(self): + result = flatten_boolean(self._values['translate_port']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def advertise(self): + if self._values['advertise'] is None: + return None + return flatten_boolean(self._values['advertise']) + + @property + def vlans_enabled(self): + if self._values['enabled_vlans'] is None: + return None + # elif self._values['vlans_enabled'] is False: + # # This is a special case for 'all' enabled VLANs + # return False + if self._values['disabled_vlans'] is None: + return True + return False + + @property + def vlans_disabled(self): + if self._values['disabled_vlans'] is None: + return None + # elif self._values['vlans_disabled'] is True: + # # This is a special case for 'all' enabled VLANs + # return True + elif self._values['enabled_vlans'] is None: + return True + return False + + @property + def enabled_vlans(self): + if self._values['enabled_vlans'] is None: + return None + elif any(x.lower() for x in self._values['enabled_vlans'] if x.lower() in ['all', '*']): + result = [fq_name(self.partition, 'all')] + self._values['vlans_disabled'] = True + self._values['vlans_enabled'] = False + return result + results = list(set([fq_name(self.partition, x) for x in self._values['enabled_vlans']])) + results.sort() + return results + + @property + def disabled_vlans(self): + if self._values['disabled_vlans'] is None: + return None + elif any(x.lower() for x in self._values['disabled_vlans'] if x.lower() in ['all', '*']): + raise F5ModuleError( + 'You cannot disable all VLANs. You must name them individually.' + ) + results = list(set([fq_name(self.partition, x) for x in self._values['disabled_vlans']])) + results.sort() + return results + + @property + def vlans(self): + disabled = self.disabled_vlans + if disabled: + return self.disabled_vlans + return self.enabled_vlans + + @property + def irules(self): + results = [] + if self._values['irules'] is None: + return None + if is_empty_list(self._values['irules']): + return [] + for irule in self._values['irules']: + result = fq_name(self.partition, irule) + results.append(result) + return results + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def vlans(self): + if self._values['vlans'] is None: + return None + elif len(self._values['vlans']) == 0: + return [] + elif any(x for x in self._values['vlans'] if x.lower() in ['/common/all', 'all']): + return [] + return self._values['vlans'] + + +class ReportableChanges(Changes): + @property + def enabled_vlans(self): + if self._values['vlans'] is None: + return None + if len(self._values['vlans']) == 0 and self._values['vlans_disabled'] is True: + return 'all' + elif len(self._values['vlans']) > 0 and self._values['vlans_enabled'] is True: + return self._values['vlans'] + + @property + def disabled_vlans(self): + if self._values['vlans'] is None: + return None + if len(self._values['vlans']) > 0 and self._values['vlans_disabled'] is True: + return self._values['vlans'] + + @property + def irules(self): + if self._values['irules'] is None: + return None + if not self._values['irules']: + return [] + return self._values['irules'] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def _update_vlan_status(self, result): + if self.want.vlans_disabled is not None: + if self.want.vlans_disabled != self.have.vlans_disabled: + result['vlans_disabled'] = self.want.vlans_disabled + result['vlans_enabled'] = not self.want.vlans_disabled + elif self.want.vlans_enabled is not None: + if any(x.lower().endswith('/all') for x in self.want.vlans): + if self.have.vlans_enabled is True: + return None + elif self.want.vlans_enabled != self.have.vlans_enabled: + result['vlans_disabled'] = not self.want.vlans_enabled + result['vlans_enabled'] = self.want.vlans_enabled + + @property + def state(self): + if self.want.state == 'disabled' and self.have.enabled: + return dict( + disabled=True + ) + elif self.want.state in ['present', 'enabled'] and self.have.disabled: + return dict( + enabled=True + ) + + @property + def vlans(self): + if self.want.vlans is None: + return None + elif self.want.vlans == [] and self.have.vlans is None: + return None + elif self.want.vlans == self.have.vlans: + return None + + # Specifically looking for /all because the vlans return value will be + # an FQDN list. This means that 'all' will be returned as '/partition/all', + # ex, /Common/all. + # + # We do not want to accidentally match values that would end with the word + # 'all', like 'vlansall'. Therefore we look for the forward slash because this + # is a path delimiter. + elif any(x.lower().endswith('/all') for x in self.want.vlans): + if self.have.vlans is None: + return None + else: + return [] + else: + return self.want.vlans + + @property + def enabled_vlans(self): + return self.vlan_status + + @property + def disabled_vlans(self): + return self.vlan_status + + @property + def vlan_status(self): + result = dict() + vlans = self.vlans + if vlans is not None: + result['vlans'] = vlans + self._update_vlan_status(result) + return result + + @property + def irules(self): + if self.want.irules is None: + return None + if self.want.irules == '' and len(self.have.irules) > 0: + return [] + if not self.want.irules: + return None + if self.want.irules != self.have.irules: + return self.want.irules + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ['present', 'disabled']: + changed = self.present() + elif state == 'absent': + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError('Failed to delete the resource.') + return True + + def create(self): + if self.want.state == 'disabled': + self.want.update({'disabled': True}) + elif self.want.state in ['present', 'enabled']: + self.want.update({'enabled': True}) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = 'https://{0}:{1}/mgmt/tm/gtm/listener/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/gtm/listener/'.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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = 'https://{0}:{1}/mgmt/tm/gtm/listener/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = 'https://{0}:{1}/mgmt/tm/gtm/listener/{2}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = 'https://{0}:{1}/mgmt/tm/gtm/listener/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + address=dict(required=True), + port=dict(type='int'), + advertise=dict(type='bool'), + enabled_vlans=dict( + type='list', + elements='str', + ), + disabled_vlans=dict( + type='list', + elements='str', + ), + irules=dict( + type='list', + elements='str' + ), + translate_address=dict(type='bool'), + translate_port=dict(type='bool'), + fallback_persistence=dict(), + last_hop_pool=dict(), + pool=dict(), + auto_lasthop=dict(), + source_port=dict(), + ip_protocol=dict(), + mask=dict(), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['absent', 'present', 'enabled', 'disabled'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.mutually_exclusive = [ + ['enabled_vlans', 'disabled_vlans'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_global.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_global.py new file mode 100644 index 00000000..d001b88a --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_global.py @@ -0,0 +1,342 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_global +short_description: Manages global GTM settings +description: + - Manages global BIG-IP GTM (now BIG-IP DNS) settings. These settings include general, load balancing, and metrics + related settings. +version_added: "1.0.0" +options: + synchronization: + description: + - Specifies whether this system is a member of a synchronization group. + - When you enable synchronization, the system periodically queries other systems in + the synchronization group to obtain and distribute configuration and metrics collection + updates. + - The synchronization group may contain systems configured as Global Traffic Manager (DNS) and + Link Controller systems. + type: bool + synchronization_group_name: + description: + - Specifies the name of the synchronization group to which the system belongs. + type: str + synchronize_zone_files: + description: + - Specifies the system synchronizes Domain Name System (DNS) zone files among the + synchronization group members. + type: bool +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Configure synchronization settings + bigip_gtm_global: + synchronization: yes + synchronization_group_name: my-group + synchronize_zone_files: yes + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +synchronization: + description: The synchronization setting on the system. + returned: changed + type: bool + sample: true +synchronization_group_name: + description: The synchronization group name. + returned: changed + type: str + sample: my-group +synchronize_zone_files: + description: Whether or not the system will synchronize zone files. + returned: changed + type: str + sample: my-group +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'synchronizationGroupName': 'synchronization_group_name', + 'synchronizeZoneFiles': 'synchronize_zone_files', + } + + api_attributes = [ + 'synchronizeZoneFiles', + 'synchronizationGroupName', + 'synchronization', + ] + + returnables = [ + 'synchronization', + 'synchronization_group_name', + 'synchronize_zone_files', + ] + + updatables = [ + 'synchronization', + 'synchronization_group_name', + 'synchronize_zone_files', + ] + + +class ApiParameters(Parameters): + @property + def synchronization(self): + if self._values['synchronization'] is None: + return None + elif self._values['synchronization'] == 'no': + return False + else: + return True + + @property + def synchronize_zone_files(self): + if self._values['synchronize_zone_files'] is None: + return None + elif self._values['synchronize_zone_files'] == 'no': + return False + else: + return True + + @property + def synchronization_group_name(self): + if self._values['synchronization_group_name'] is None: + return None + return str(self._values['synchronization_group_name']) + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def synchronization(self): + if self._values['synchronization'] is None: + return None + elif self._values['synchronization'] is False: + return 'no' + else: + return 'yes' + + @property + def synchronize_zone_files(self): + if self._values['synchronize_zone_files'] is None: + return None + elif self._values['synchronize_zone_files'] is False: + return 'no' + else: + return 'yes' + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def synchronization_group_name(self): + if self.want.synchronization_group_name is None: + return None + if self.want.synchronization_group_name == '' and self.have.synchronization_group_name is None: + return None + if self.want.synchronization_group_name != self.have.synchronization_group_name: + return self.want.synchronization_group_name + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + return self.update() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/global-settings/general/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/global-settings/general/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + synchronization=dict(type='bool'), + synchronization_group_name=dict(), + synchronize_zone_files=dict(type='bool') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_bigip.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_bigip.py new file mode 100644 index 00000000..df044902 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_bigip.py @@ -0,0 +1,680 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_monitor_bigip +short_description: Manages F5 BIG-IP GTM BIG-IP monitors +description: + - Manages F5 BIG-IP GTM (now BIG-IP DNS) BIG-IP monitors. This monitor is used by GTM to monitor + BIG-IPs themselves. +version_added: "1.0.0" +options: + name: + description: + - Name of the monitor. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(bigip) + parent on the C(Common) partition. + type: str + default: "/Common/bigip" + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value will be + '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value will be + '*'. Note that if specifying an IP address, you must use a value between 1 and 65535. + type: str + interval: + description: + - Specifies, in seconds, the frequency at which the system issues the monitor + check when either the resource is down or the status of the resource is unknown. + - When creating a new monitor, if this parameter is not provided, the + default value will be C(30). This value B(must) be less than the C(timeout) value. + type: int + timeout: + description: + - Specifies the number of seconds the target has in which to respond to the + monitor request. + - If the target responds within the set time period, it is considered up. + - If the target does not respond within the set time period, it is considered down. + - When this value is set to 0 (zero), the system uses the interval from the parent monitor. + - When creating a new monitor, if this parameter is not provided, + the default value will be C(90). + type: int + ignore_down_response: + description: + - Specifies the monitor allows more than one probe attempt per interval. + - When C(yes), specifies the monitor ignores down responses for the duration of + the monitor timeout. Once the monitor timeout is reached without the system receiving + an up response, the system marks the object down. + - When C(no), specifies the monitor immediately marks an object down when it + receives a down response. + - When creating a new monitor, if this parameter is not provided, the default + value will be C(no). + type: bool + aggregate_dynamic_ratios: + description: + - Specifies how the system combines the module values to create the proportion + (score) for the load balancing operation. + - The score represents the module's estimated capacity for handing traffic. + - Averaged values are appropriate for downstream Web Accelerator or Application + Security Manager (ASM) virtual servers. + - When creating a new monitor, if this parameter is not specified, the default + of C(none) is used, meaning the system does not use the scores in the load + balancing operation. + - When C(none), specifies the monitor ignores the nodes and pool member scores. + - When C(average-nodes), specifies the system averages the dynamic ratios + on the nodes associated with the monitor's target virtual servers and returns + that average as the virtual servers' score. + - When C(sum-nodes), specifies the system adds together the scores of the + nodes associated with the monitor's target virtual servers and uses that value + in the load balancing operation. + - When C(average-members), specifies the system averages the dynamic ratios + on the pool members associated with the monitor's target virtual servers and + returns that average as the virtual servers' score. + - When C(sum-members), specifies the system adds together the scores of the + pool members associated with the monitor's target virtual servers and uses + that value in the load balancing operation. + type: str + choices: + - none + - average-nodes + - sum-nodes + - average-members + - sum-members + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create BIG-IP Monitor + bigip_gtm_monitor_bigip: + state: present + ip: 10.10.10.10 + name: my_monitor + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove BIG-IP Monitor + bigip_gtm_monitor_bigip: + state: absent + name: my_monitor + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add BIG-IP monitor for all addresses, port 514 + bigip_gtm_monitor_bigip: + port: 514 + name: my_monitor + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: bigip +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +aggregate_dynamic_ratios: + description: The new aggregate of to the monitor. + returned: changed + type: str + sample: sum-members +ignore_down_response: + description: Whether to ignore the down response or not. + returned: changed + type: bool + sample: True +''' + +import os +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'ignoreDownResponse': 'ignore_down_response', + 'aggregateDynamicRatios': 'aggregate_dynamic_ratios', + } + + api_attributes = [ + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'ignoreDownResponse', + 'aggregateDynamicRatios', + ] + + returnables = [ + 'parent', + 'ip', + 'port', + 'interval', + 'timeout', + 'ignore_down_response', + 'aggregate_dynamic_ratios', + ] + + updatables = [ + 'destination', + 'interval', + 'timeout', + 'ignore_down_response', + 'aggregate_dynamic_ratios', + ] + + @property + def interval(self): + if self._values['interval'] is None: + return None + + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def type(self): + return 'bigip' + + +class ApiParameters(Parameters): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + if self._values['ignore_down_response'] == 'disabled': + return False + return True + + +class ModuleParameters(Parameters): + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @property + def parent(self): + if self._values['parent'] is None: + return None + if self._values['parent'].startswith('/'): + parent = os.path.basename(self._values['parent']) + result = '/{0}/{1}'.format(self.partition, parent) + else: + result = '/{0}/{1}'.format(self.partition, self._values['parent']) + return result + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def ignore_down_response(self): + if self._values['ignore_down_response']: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 120}) + if self.want.interval is None: + self.want.update({'interval': 30}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + if self.want.ignore_down_response is None: + self.want.update({'ignore_down_response': False}) + if self.want.aggregate_dynamic_ratios is None: + self.want.update({'aggregate_dynamic_ratios': 'none'}) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state in ["present", "disabled"]: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + self._set_changed_options() + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the monitor.") + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/bigip/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/gtm/monitor/bigip/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/bigip/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/bigip/{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/gtm/monitor/bigip/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/bigip'), + ip=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + ignore_down_response=dict(type='bool'), + aggregate_dynamic_ratios=dict( + choices=[ + 'none', 'average-nodes', 'sum-nodes', 'average-members', 'sum-members' + ] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_external.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_external.py new file mode 100644 index 00000000..32a4c823 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_external.py @@ -0,0 +1,697 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_monitor_external +short_description: Manages external GTM monitors on a BIG-IP +description: + - Manages external GTM (now BIG-IP DNS) monitors on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the monitor. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(http) + parent on the C(Common) partition. + type: str + default: "/Common/external" + arguments: + description: + - Specifies any command-line arguments the script requires. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, then the default value will be + '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value will be + '*'. Note that if specifying an IP address, you must use a value between 1 and 65535. + type: str + external_program: + description: + - Specifies the name of the file for the monitor to use. In order to reference + a file, you must first import it using options on the System > File Management > External + Monitor Program File List > Import screen. The BIG-IP system automatically + places the file in the proper location on the file system. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. If this parameter is not provided when creating + a new monitor, the default value will be 30. This value B(must) + be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + to any number, however, it should be 3 times the + interval number of seconds plus 1 second. If this parameter is not + provided when creating a new monitor, the default value will be 120. + type: int + variables: + description: + - Specifies any variables the script requires. + - Double quotes in values will be suppressed. + type: dict + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an external monitor + bigip_gtm_monitor_external: + name: foo + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Create an external monitor with variables + bigip_gtm_monitor_external: + name: foo + timeout: 10 + variables: + var1: foo + var2: bar + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add a variable to an existing set + bigip_gtm_monitor_external: + name: foo + timeout: 10 + variables: + var1: foo + var2: bar + cat: dog + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: external +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +''' + +import os +import re +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.six import iteritems + + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import compare_dictionary +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'apiRawValues': 'variables', + 'run': 'external_program', + 'args': 'arguments', + } + + api_attributes = [ + 'defaultsFrom', 'interval', 'timeout', 'destination', 'run', 'args', + ] + + returnables = [ + 'parent', 'ip', 'port', 'interval', 'timeout', 'variables', 'external_program', + 'arguments', + ] + + updatables = [ + 'destination', 'interval', 'timeout', 'variables', 'external_program', + 'arguments', + ] + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, d, port = value.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = value.rpartition(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + + # Per BZ617284, the BIG-IP UI does not raise a warning about this. + # So I do + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + if self._values['parent'].startswith('/'): + parent = os.path.basename(self._values['parent']) + result = '/{0}/{1}'.format(self.partition, parent) + else: + result = '/{0}/{1}'.format(self.partition, self._values['parent']) + return result + + @property + def type(self): + return 'external' + + +class ApiParameters(Parameters): + @property + def variables(self): + if self._values['variables'] is None: + return None + pattern = r'^userDefined\s(?P.*)' + result = {} + for k, v in iteritems(self._values['variables']): + matches = re.match(pattern, k) + if not matches: + raise F5ModuleError( + "Unable to find the variable 'key' in the API payload." + ) + key = matches.group('key') + result[key] = v + return result + + +class ModuleParameters(Parameters): + @property + def variables(self): + if self._values['variables'] is None: + return None + result = {} + for k, v in iteritems(self._values['variables']): + result[k] = str(v).replace('"', '') + return result + + @property + def external_program(self): + if self._values['external_program'] is None: + return None + return fq_name(self.partition, self._values['external_program']) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + @property + def variables(self): + if self.want.variables is None: + return None + if self.have.variables is None: + return dict( + variables=self.want.variables + ) + result = dict() + + different = compare_dictionary(self.want.variables, self.have.variables) + if not different: + return None + + for k, v in iteritems(self.want.variables): + if k in self.have.variables and v != self.have.variables[k]: + result[k] = v + elif k not in self.have.variables: + result[k] = v + for k, v in iteritems(self.have.variables): + if k not in self.want.variables: + result[k] = "none" + if result: + result = dict( + variables=result + ) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 120}) + if self.want.interval is None: + self.want.update({'interval': 30}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def create(self): + self._set_changed_options() + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the monitor.") + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/external/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/gtm/monitor/external/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if self.want.variables: + self.set_variable_on_device(self.want.variables) + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + if params: + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/external/{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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + if self.changes.variables: + self.set_variable_on_device(self.changes.variables) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/external/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/external/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def set_variable_on_device(self, commands): + command = ' '.join(['user-defined {0} \\\"{1}\\\"'.format(k, v) for k, v in iteritems(commands)]) + command = 'tmsh modify gtm monitor external {0} {1}'.format(self.want.name, command) + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(command) + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/external'), + arguments=dict(), + ip=dict(), + port=dict(), + external_program=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + state=dict( + default='present', + choices=['present', 'absent'] + ), + variables=dict(type='dict'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_firepass.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_firepass.py new file mode 100644 index 00000000..dfb638ed --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_firepass.py @@ -0,0 +1,808 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_monitor_firepass +short_description: Manages F5 BIG-IP GTM FirePass monitors +description: + - Manages F5 BIG-IP GTM (now BIG-IP DNS) FirePass monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(tcp) + parent on the C(Common) partition. + type: str + default: /Common/firepass_gtm + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is + '*'. + - If this value is an IP address, a C(port) number must be specified. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is + '*'. Note that if specifying an IP address, a value between 1 and 65535 + must be specified. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template runs. + - If this parameter is not provided when creating a new monitor, then + the default value is 30. + - This value B(must) be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + to any number, however, it should be 3 times the + interval number of seconds plus 1 second. + - If this parameter is not provided when creating a new monitor, + the default value is 90. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present + probe_timeout: + description: + - Specifies the number of seconds after which the system times out the probe request + to the system. + - When creating a new monitor, if this parameter is not provided, the default + value is C(5). + type: int + ignore_down_response: + description: + - Specifies the monitor allows more than one probe attempt per interval. + - When C(yes), specifies the monitor ignores down responses for the duration of + the monitor timeout. Once the monitor timeout is reached without the system receiving + an up response, the system marks the object down. + - When C(no), specifies the monitor immediately marks an object down when it + receives a down response. + - When creating a new monitor, if this parameter is not provided, the default + value is C(no). + type: bool + target_username: + description: + - Specifies the user name, if the monitored target requires authentication. + type: str + target_password: + description: + - Specifies the password, if the monitored target requires authentication. + type: str + update_password: + description: + - C(always) updates passwords if the C(target_password) is specified. + - C(on_create) only sets the password for newly created monitors. + type: str + choices: + - always + - on_create + default: always + cipher_list: + description: + - Specifies the list of ciphers for this monitor. + - The items in the cipher list are separated with the colon C(:) symbol. + - When creating a new monitor, if this parameter is not specified, the default + list is C(HIGH:!ADH). + type: str + max_load_average: + description: + - Specifies the number the monitor uses to mark the Secure Access Manager + system up or down. + - The system compares the Max Load Average setting against a one-minute average + of the Secure Access Manager system load. + - When the Secure Access Manager system-load average falls within the specified + Max Load Average, the monitor marks the Secure Access Manager system up. + - When the average exceeds the setting, the monitor marks the system down. + - When creating a new monitor, if this parameter is not specified, the default + is C(12). + type: int + concurrency_limit: + description: + - Specifies the maximum percentage of licensed connections currently in use under + which the monitor marks the Secure Access Manager system up. + - As an example, a setting of 95 percent means that the monitor marks the Secure + Access Manager system up until 95 percent of licensed connections are in use. + - When the number of in-use licensed connections exceeds 95 percent, the monitor + marks the Secure Access Manager system down. + - When creating a new monitor, if this parameter is not specified, the default is C(95). + type: int +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a GTM FirePass monitor + bigip_gtm_monitor_firepass: + name: my_monitor + ip: 1.1.1.1 + port: 80 + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove FirePass Monitor + bigip_gtm_monitor_firepass: + name: my_monitor + state: absent + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add FirePass monitor for all addresses, port 514 + bigip_gtm_monitor_firepass: + name: my_monitor + port: 514 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: firepass_gtm +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +port: + description: The new port the monitor checks the resource on. + returned: changed + type: str + sample: 8080 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +ignore_down_response: + description: Whether to ignore the down response or not. + returned: changed + type: bool + sample: True +probe_timeout: + description: The new timeout in which the system will timeout the monitor probe. + returned: changed + type: int + sample: 10 +cipher_list: + description: The new value for the cipher list. + returned: changed + type: str + sample: +3DES:+kEDH +max_load_average: + description: The new value for the max load average. + returned: changed + type: int + sample: 12 +concurrency_limit: + description: The new value for the concurrency limit. + returned: changed + type: int + sample: 95 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'ignoreDownResponse': 'ignore_down_response', + 'probeTimeout': 'probe_timeout', + 'username': 'target_username', + 'password': 'target_password', + 'cipherlist': 'cipher_list', + 'concurrencyLimit': 'concurrency_limit', + 'maxLoadAverage': 'max_load_average', + } + + api_attributes = [ + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'probeTimeout', + 'ignoreDownResponse', + 'username', + 'password', + 'cipherlist', + 'concurrencyLimit', + 'maxLoadAverage', + ] + + returnables = [ + 'parent', + 'ip', + 'port', + 'interval', + 'timeout', + 'probe_timeout', + 'ignore_down_response', + 'cipher_list', + 'max_load_average', + 'concurrency_limit', + ] + + updatables = [ + 'destination', + 'interval', + 'timeout', + 'probe_timeout', + 'ignore_down_response', + 'ip', + 'port', + 'target_username', + 'target_password', + 'cipher_list', + 'max_load_average', + 'concurrency_limit', + ] + + +class ApiParameters(Parameters): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + if self._values['ignore_down_response'] == 'disabled': + return False + return True + + +class ModuleParameters(Parameters): + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): # lgtm [py/similar-function] + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, port = value.split(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def probe_timeout(self): + if self._values['probe_timeout'] is None: + return None + return int(self._values['probe_timeout']) + + @property + def max_load_average(self): + if self._values['max_load_average'] is None: + return None + return int(self._values['max_load_average']) + + @property + def concurrency_limit(self): + if self._values['concurrency_limit'] is None: + return None + return int(self._values['concurrency_limit']) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + elif self._values['ignore_down_response'] is True: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] == 'enabled': + return True + return False + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + @property + def target_password(self): + if self.want.target_password != self.have.target_password: + if self.want.update_password == 'always': + result = self.want.target_password + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 90}) + if self.want.interval is None: + self.want.update({'interval': 30}) + if self.want.probe_timeout is None: + self.want.update({'probe_timeout': 5}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + if self.want.ignore_down_response is None: + self.want.update({'ignore_down_response': False}) + if self.want.cipher_list is None: + self.want.update({'cipher_list': 'HIGH:!ADH'}) + if self.want.max_load_average is None: + self.want.update({'max_load_average': 12}) + if self.want.concurrency_limit is None: + self.want.update({'concurrency_limit': 95}) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_default_creation_values() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/gtm/monitor/firepass/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/firepass_gtm'), + ip=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + ignore_down_response=dict(type='bool'), + probe_timeout=dict(type='int'), + target_username=dict(), + target_password=dict(no_log=True), + cipher_list=dict(), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + max_load_average=dict(type='int'), + concurrency_limit=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_http.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_http.py new file mode 100644 index 00000000..3838482c --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_http.py @@ -0,0 +1,851 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_monitor_http +short_description: Manages F5 BIG-IP GTM HTTP monitors +description: + - Manages F5 BIG-IP GTM (now BIG-IP DNS) HTTP monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(http) + parent on the C(Common) partition. + type: str + default: /Common/http + send: + description: + - The send string for the monitor call. + - When creating a new monitor, if this parameter is not provided, the + default of C(GET /\r\n) is used. + type: str + receive: + description: + - The receive string for the monitor call. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is + '*'. + - If this value is an IP address, then a C(port) number must be specified. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is + '*'. If specifying an IP address, you must use a value between 1 and 65535. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. + - If this parameter is not provided when creating a new monitor, the + default value is 30. + - This value B(must) be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + to any number, however, it should be 3 times the + interval number of seconds plus 1 second. + - If this parameter is not provided when creating a new monitor, then + default value is 120. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present + probe_timeout: + description: + - Specifies the number of seconds after which the system times out the probe request + to the system. + - When creating a new monitor, if this parameter is not provided, the default + value is C(5). + type: int + ignore_down_response: + description: + - Specifies the monitor allows more than one probe attempt per interval. + - When C(yes), specifies the monitor ignores down responses for the duration of + the monitor timeout. Once the monitor timeout is reached without the system receiving + an up response, the system marks the object down. + - When C(no), specifies the monitor immediately marks an object down when it + receives a down response. + - When creating a new monitor, if this parameter is not provided, the default + value is C(no). + type: bool + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + - A monitor in transparent mode directs traffic through the associated pool members + or nodes (usually a router or firewall) to the aliased destination (that is, it + probes the C(ip)-C(port) combination specified in the monitor). + - If the monitor cannot successfully reach the aliased destination, the pool member + or node through which the monitor traffic was sent is marked down. + - When creating a new monitor, if this parameter is not provided, the default + value is no C(no). + type: bool + reverse: + description: + - Instructs the system to mark the target resource down when the test is successful. + This setting is useful, for example, if the content on your web site home page is + dynamic and changes frequently, you may want to set up a reverse ECV service check + that looks for the string Error. + - A match for this string means the web server was down. + - To use this option, you must specify values for C(send) and C(receive). + type: bool + target_username: + description: + - Specifies the user name, if the monitored target requires authentication. + type: str + target_password: + description: + - Specifies the password, if the monitored target requires authentication. + type: str + update_password: + description: + - C(always) updates passwords if the C(target_password) is specified. + - C(on_create) only sets the password for newly created monitors. + type: str + choices: + - always + - on_create + default: always +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a GTM HTTP monitor + bigip_gtm_monitor_http: + name: my_monitor + ip: 1.1.1.1 + port: 80 + send: my send string + receive: my receive string + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove HTTP Monitor + bigip_gtm_monitor_http: + name: my_monitor + state: absent + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add HTTP monitor for all addresses, port 514 + bigip_gtm_monitor_http: + name: my_monitor + port: 514 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: http +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +port: + description: The new port the monitor checks the resource on. + returned: changed + type: str + sample: 8080 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +ignore_down_response: + description: Whether to ignore the down response or not. + returned: changed + type: bool + sample: True +send: + description: The new send string for this monitor. + returned: changed + type: str + sample: tcp string to send +receive: + description: The new receive string for this monitor. + returned: changed + type: str + sample: tcp string to receive +probe_timeout: + description: The new timeout in which the system will timeout the monitor probe. + returned: changed + type: int + sample: 10 +reverse: + description: The new value for whether the monitor operates in reverse mode. + returned: changed + type: bool + sample: False +transparent: + description: The new value for whether the monitor operates in transparent mode. + returned: changed + type: bool + sample: False +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'ignoreDownResponse': 'ignore_down_response', + 'probeTimeout': 'probe_timeout', + 'recv': 'receive', + 'username': 'target_username', + 'password': 'target_password', + } + + api_attributes = [ + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'transparent', + 'probeTimeout', + 'ignoreDownResponse', + 'reverse', + 'send', + 'recv', + 'username', + 'password', + ] + + returnables = [ + 'parent', + 'ip', + 'port', + 'interval', + 'timeout', + 'transparent', + 'probe_timeout', + 'ignore_down_response', + 'send', + 'receive', + 'reverse', + ] + + updatables = [ + 'destination', + 'interval', + 'timeout', + 'transparent', + 'probe_timeout', + 'ignore_down_response', + 'send', + 'receive', + 'reverse', + 'ip', + 'port', + 'target_username', + 'target_password', + ] + + +class ApiParameters(Parameters): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + if self._values['ignore_down_response'] == 'disabled': + return False + return True + + @property + def transparent(self): + if self._values['transparent'] is None: + return None + if self._values['transparent'] == 'disabled': + return False + return True + + @property + def reverse(self): + if self._values['reverse'] is None: + return None + if self._values['reverse'] == 'disabled': + return False + return True + + +class ModuleParameters(Parameters): + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): # lgtm [py/similar-function] + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, port = value.split(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def probe_timeout(self): + if self._values['probe_timeout'] is None: + return None + return int(self._values['probe_timeout']) + + @property + def type(self): + return 'http' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def transparent(self): + if self._values['transparent'] is None: + return None + elif self._values['transparent'] is True: + return 'enabled' + return 'disabled' + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + elif self._values['ignore_down_response'] is True: + return 'enabled' + return 'disabled' + + @property + def reverse(self): + if self._values['reverse'] is None: + return None + elif self._values['reverse'] is True: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def transparent(self): + if self._values['transparent'] == 'enabled': + return True + return False + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] == 'enabled': + return True + return False + + @property + def reverse(self): + if self._values['reverse'] == 'enabled': + return True + return False + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + @property + def target_password(self): + if self.want.target_password != self.have.target_password: + if self.want.update_password == 'always': + result = self.want.target_password + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 120}) + if self.want.interval is None: + self.want.update({'interval': 30}) + if self.want.probe_timeout is None: + self.want.update({'probe_timeout': 5}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + if self.want.ignore_down_response is None: + self.want.update({'ignore_down_response': False}) + if self.want.transparent is None: + self.want.update({'transparent': False}) + if self.want.send is None: + self.want.update({'send': 'GET /\r\n'}) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def create(self): + self._set_default_creation_values() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the monitor.") + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/http/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/http/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/http/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + 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/gtm/monitor/http/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/http/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/http'), + send=dict(), + receive=dict(), + ip=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + ignore_down_response=dict(type='bool'), + transparent=dict(type='bool'), + probe_timeout=dict(type='int'), + reverse=dict(type='bool'), + target_username=dict(), + target_password=dict(no_log=True), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_https.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_https.py new file mode 100644 index 00000000..ed7eb7c2 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_https.py @@ -0,0 +1,974 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_monitor_https +short_description: Manages F5 BIG-IP GTM HTTPS monitors +description: + - Manages F5 BIG-IP GTM (now BIG-IP DNS) HTTPS monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(https) + parent on the C(Common) partition. + type: str + default: /Common/https + send: + description: + - The send string for the monitor call. + - When creating a new monitor, if this parameter is not provided, the + default of C(GET /\r\n) is used. + type: str + receive: + description: + - The receive string for the monitor call. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is + '*'. + - If this value is an IP address, a C(port) number must be specified. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is + '*'. If specifying an IP address, you must specify a value between 1 and 65535. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. + - If this parameter is not provided when creating a new monitor, + the default value is 30. + - This value B(must) be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + to any number, however, it should be 3 times the + interval number of seconds plus 1 second. + - If this parameter is not provided when creating a new monitor, the + default value is 120. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present + probe_timeout: + description: + - Specifies the number of seconds after which the system times out the probe request + to the system. + - When creating a new monitor, if this parameter is not provided, then the default + value is C(5). + type: int + ignore_down_response: + description: + - Specifies the monitor allows more than one probe attempt per interval. + - When C(yes), specifies the monitor ignores down responses for the duration of + the monitor timeout. Once the monitor timeout is reached without the system receiving + an up response, the system marks the object down. + - When C(no), specifies the monitor immediately marks an object down when it + receives a down response. + - When creating a new monitor, if this parameter is not provided, the default + value is C(no). + type: bool + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + - A monitor in transparent mode directs traffic through the associated pool members + or nodes (usually a router or firewall) to the aliased destination (that is, it + probes the C(ip)-C(port) combination specified in the monitor). + - If the monitor cannot successfully reach the aliased destination, the pool member + or node through which the monitor traffic was sent is marked down. + - When creating a new monitor, if this parameter is not provided, the default + value is C(no). + type: bool + reverse: + description: + - Instructs the system to mark the target resource down when the test is successful. + This setting is useful, for example, if the content on your web site home page is + dynamic and changes frequently, you may want to set up a reverse ECV service check + that looks for the string Error. + - A match for this string means the web server was down. + - To use this option, you must specify values for C(send) and C(receive). + type: bool + target_username: + description: + - Specifies the user name, if the monitored target requires authentication. + type: str + target_password: + description: + - Specifies the password, if the monitored target requires authentication. + type: str + update_password: + description: + - C(always) updates passwords if the C(target_password) is specified. + - C(on_create) only sets the password for newly created monitors. + type: str + choices: + - always + - on_create + default: always + cipher_list: + description: + - Specifies the list of ciphers for this monitor. + - The items in the cipher list are separated with the colon C(:) symbol. + - When creating a new monitor, if this parameter is not specified, the default + list is C(DEFAULT:+SHA:+3DES:+kEDH). + type: str + compatibility: + description: + - Specifies, when enabled, the SSL options setting (in OpenSSL) is set to B(all). + - When creating a new monitor, if this value is not specified, the default is + C(yes) + type: bool + client_cert: + description: + - Specifies a fully-qualified path for a client certificate the monitor sends to + the target SSL server. + type: str + client_key: + description: + - Specifies a key for a client certificate the monitor sends to the target SSL server. + type: str +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a GTM HTTPS monitor + bigip_gtm_monitor_https: + name: my_monitor + ip: 1.1.1.1 + port: 80 + send: my send string + receive: my receive string + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove HTTPS Monitor + bigip_gtm_monitor_https: + name: my_monitor + state: absent + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add HTTPS monitor for all addresses, port 514 + bigip_gtm_monitor_https: + name: my_monitor + provider: + user: admin + password: secret + server: lb.mydomain.com + port: 514 + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: https +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +port: + description: The new port the monitor checks the resource on. + returned: changed + type: str + sample: 8080 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +ignore_down_response: + description: Whether to ignore the down response or not. + returned: changed + type: bool + sample: True +send: + description: The new send string for this monitor. + returned: changed + type: str + sample: tcp string to send +receive: + description: The new receive string for this monitor. + returned: changed + type: str + sample: tcp string to receive +probe_timeout: + description: The new timeout in which the system will timeout the monitor probe. + returned: changed + type: int + sample: 10 +reverse: + description: The new value for whether the monitor operates in reverse mode. + returned: changed + type: bool + sample: False +transparent: + description: The new value for whether the monitor operates in transparent mode. + returned: changed + type: bool + sample: False +cipher_list: + description: The new value for the cipher list. + returned: changed + type: str + sample: +3DES:+kEDH +compatibility: + description: The new SSL compatibility setting. + returned: changed + type: bool + sample: True +client_cert: + description: The new client cert setting. + returned: changed + type: str + sample: /Common/default +client_key: + description: The new client key setting. + returned: changed + type: str + sample: /Common/default +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'ignoreDownResponse': 'ignore_down_response', + 'probeTimeout': 'probe_timeout', + 'recv': 'receive', + 'username': 'target_username', + 'password': 'target_password', + 'cipherlist': 'cipher_list', + 'cert': 'client_cert', + 'key': 'client_key', + } + + api_attributes = [ + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'transparent', + 'probeTimeout', + 'ignoreDownResponse', + 'reverse', + 'send', + 'recv', + 'username', + 'password', + 'cipherlist', + 'compatibility', + 'cert', + 'key', + ] + + returnables = [ + 'parent', + 'ip', + 'port', + 'interval', + 'timeout', + 'transparent', + 'probe_timeout', + 'ignore_down_response', + 'send', + 'receive', + 'reverse', + 'cipher_list', + 'compatibility', + 'client_cert', + 'client_key', + ] + + updatables = [ + 'destination', + 'interval', + 'timeout', + 'transparent', + 'probe_timeout', + 'ignore_down_response', + 'send', + 'receive', + 'reverse', + 'ip', + 'port', + 'target_username', + 'target_password', + 'cipher_list', + 'compatibility', + 'client_cert', + 'client_key', + ] + + +class ApiParameters(Parameters): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + if self._values['ignore_down_response'] == 'disabled': + return False + return True + + @property + def transparent(self): + if self._values['transparent'] is None: + return None + if self._values['transparent'] == 'disabled': + return False + return True + + @property + def reverse(self): + if self._values['reverse'] is None: + return None + if self._values['reverse'] == 'disabled': + return False + return True + + @property + def compatibility(self): + if self._values['compatibility'] is None: + return None + if self._values['compatibility'] == 'disabled': + return False + return True + + +class ModuleParameters(Parameters): + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, port = value.split(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def probe_timeout(self): + if self._values['probe_timeout'] is None: + return None + return int(self._values['probe_timeout']) + + @property + def type(self): + return 'https' + + @property + def client_cert(self): + if self._values['client_cert'] is None: + return None + if self._values['client_cert'] == '': + return '' + result = fq_name(self.partition, self._values['client_cert']) + if not result.endswith('.crt'): + result += '.crt' + return result + + @property + def client_key(self): + if self._values['client_key'] is None: + return None + if self._values['client_key'] == '': + return '' + result = fq_name(self.partition, self._values['client_key']) + if not result.endswith('.key'): + result += '.key' + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def transparent(self): + if self._values['transparent'] is None: + return None + elif self._values['transparent'] is True: + return 'enabled' + return 'disabled' + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + elif self._values['ignore_down_response'] is True: + return 'enabled' + return 'disabled' + + @property + def reverse(self): + if self._values['reverse'] is None: + return None + elif self._values['reverse'] is True: + return 'enabled' + return 'disabled' + + @property + def compatibility(self): + if self._values['compatibility'] is None: + return None + elif self._values['compatibility'] is True: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def transparent(self): + if self._values['transparent'] == 'enabled': + return True + return False + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] == 'enabled': + return True + return False + + @property + def reverse(self): + if self._values['reverse'] == 'enabled': + return True + return False + + @property + def compatibility(self): + if self._values['compatibility'] == 'enabled': + return True + return False + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + @property + def target_password(self): + if self.want.target_password != self.have.target_password: + if self.want.update_password == 'always': + result = self.want.target_password + return result + + @property + def client_cert(self): + if self.have.client_cert is None and self.want.client_cert == '': + return None + if self.have.client_cert != self.want.client_cert: + return self.want.client_cert + + @property + def client_key(self): + if self.have.client_key is None and self.want.client_key == '': + return None + if self.have.client_key != self.want.client_key: + return self.want.client_key + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 120}) + if self.want.interval is None: + self.want.update({'interval': 30}) + if self.want.probe_timeout is None: + self.want.update({'probe_timeout': 5}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + if self.want.ignore_down_response is None: + self.want.update({'ignore_down_response': False}) + if self.want.transparent is None: + self.want.update({'transparent': False}) + if self.want.send is None: + self.want.update({'send': 'GET /\r\n'}) + if self.want.cipher_list is None: + self.want.update({'cipher_list': 'DEFAULT:+SHA:+3DES:+kEDH'}) + if self.want.compatibility is None: + self.want.update({'compatibility': True}) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_default_creation_values() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/https/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/gtm/monitor/https/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/https/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/https/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/https/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/https'), + send=dict(), + receive=dict(), + ip=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + ignore_down_response=dict(type='bool'), + transparent=dict(type='bool'), + probe_timeout=dict(type='int'), + reverse=dict(type='bool'), + target_username=dict(), + target_password=dict(no_log=True), + cipher_list=dict(), + compatibility=dict(type='bool'), + client_cert=dict(), + client_key=dict(), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_tcp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_tcp.py new file mode 100644 index 00000000..0519e164 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_tcp.py @@ -0,0 +1,809 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_monitor_tcp +short_description: Manages F5 BIG-IP GTM TCP monitors +description: + - Manages F5 BIG-IP GTM (now BIG-IP DNS) TCP monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(tcp) + parent on the C(Common) partition. + type: str + default: /Common/tcp + send: + description: + - The send string for the monitor call. + type: str + receive: + description: + - The receive string for the monitor call. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value will be + '*'. + - If this value is an IP address, a C(port) number must be specified. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value will be + '*'. Note that if using an IP address, you must specify a value between + 1 and 65535. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. + - If this parameter is not provided when creating a new monitor, the + default value will be 30. + - This value B(must) be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + number to any number you want, however, it should be 3 times the + interval number of seconds plus 1 second. + - If this parameter is not provided when creating a new monitor, the + default value will be 120. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present + probe_timeout: + description: + - Specifies the number of seconds after which the system times out the probe request + to the system. + - When creating a new monitor, if this parameter is not provided, then the default + value will be C(5). + type: int + ignore_down_response: + description: + - Specifies the monitor allows more than one probe attempt per interval. + - When C(yes), specifies the monitor ignores down responses for the duration of + the monitor timeout. Once the monitor timeout is reached without the system receiving + an up response, the system marks the object down. + - When C(no), specifies the monitor immediately marks an object down when it + receives a down response. + - When creating a new monitor, if this parameter is not provided, the default + value will be C(no). + type: bool + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + - A monitor in transparent mode directs traffic through the associated pool members + or nodes (usually a router or firewall) to the aliased destination (that is, it + probes the C(ip)-C(port) combination specified in the monitor). + - If the monitor cannot successfully reach the aliased destination, the pool member + or node through which the monitor traffic was sent is marked down. + - When creating a new monitor, if this parameter is not provided, then the default + value will be C(no). + type: bool + reverse: + description: + - Instructs the system to mark the target resource down when the test is successful. + This setting is useful, for example, if the content on your web site home page is + dynamic and changes frequently, you may want to set up a reverse ECV service check + that looks for the string Error. + - A match for this string means the web server was down. + - To use this option, you must specify values for C(send) and C(receive). + type: bool +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a GTM TCP monitor + bigip_gtm_monitor_tcp: + name: my_monitor + ip: 1.1.1.1 + port: 80 + send: my send string + receive: my receive string + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove TCP Monitor + bigip_gtm_monitor_tcp: + name: my_monitor + state: absent + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add TCP monitor for all addresses, port 514 + bigip_gtm_monitor_tcp: + name: my_monitor + port: 514 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: tcp +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +port: + description: The new port on which the monitor checks the resource. + returned: changed + type: str + sample: 8080 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +ignore_down_response: + description: Whether to ignore the down response or not. + returned: changed + type: bool + sample: True +send: + description: The new send string for this monitor. + returned: changed + type: str + sample: tcp string to send +receive: + description: The new receive string for this monitor. + returned: changed + type: str + sample: tcp string to receive +probe_timeout: + description: The new timeout in which the system will timeout the monitor probe. + returned: changed + type: int + sample: 10 +reverse: + description: The new value for whether the monitor operates in reverse mode. + returned: changed + type: bool + sample: False +transparent: + description: The new value for whether the monitor operates in transparent mode. + returned: changed + type: bool + sample: False +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'ignoreDownResponse': 'ignore_down_response', + 'probeTimeout': 'probe_timeout', + 'recv': 'receive', + } + + api_attributes = [ + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'transparent', + 'probeTimeout', + 'ignoreDownResponse', + 'reverse', + 'send', + 'recv', + ] + + returnables = [ + 'parent', + 'ip', + 'port', + 'interval', + 'timeout', + 'transparent', + 'probe_timeout', + 'ignore_down_response', + 'send', + 'receive', + 'reverse', + ] + + updatables = [ + 'destination', + 'interval', + 'timeout', + 'transparent', + 'probe_timeout', + 'ignore_down_response', + 'send', + 'receive', + 'reverse', + 'ip', + 'port', + ] + + +class ApiParameters(Parameters): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + if self._values['ignore_down_response'] == 'disabled': + return False + return True + + @property + def transparent(self): + if self._values['transparent'] is None: + return None + if self._values['transparent'] == 'disabled': + return False + return True + + @property + def reverse(self): + if self._values['reverse'] is None: + return None + if self._values['reverse'] == 'disabled': + return False + return True + + +class ModuleParameters(Parameters): + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + elif self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']): + return self._values['ip'] + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, port = value.split(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def probe_timeout(self): + if self._values['probe_timeout'] is None: + return None + return int(self._values['probe_timeout']) + + @property + def type(self): + return 'tcp' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def transparent(self): + if self._values['transparent'] is None: + return None + elif self._values['transparent'] is True: + return 'enabled' + return 'disabled' + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + elif self._values['ignore_down_response'] is True: + return 'enabled' + return 'disabled' + + @property + def reverse(self): + if self._values['reverse'] is None: + return None + elif self._values['reverse'] is True: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def transparent(self): + if self._values['transparent'] == 'enabled': + return True + return False + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] == 'enabled': + return True + return False + + @property + def reverse(self): + if self._values['reverse'] == 'enabled': + return True + return False + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 120}) + if self.want.interval is None: + self.want.update({'interval': 30}) + if self.want.probe_timeout is None: + self.want.update({'probe_timeout': 5}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + if self.want.ignore_down_response is None: + self.want.update({'ignore_down_response': False}) + if self.want.transparent is None: + self.want.update({'transparent': False}) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_default_creation_values() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/tcp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/gtm/monitor/tcp/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/tcp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/tcp/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/tcp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/tcp'), + send=dict(), + receive=dict(), + ip=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + ignore_down_response=dict(type='bool'), + transparent=dict(type='bool'), + probe_timeout=dict(type='int'), + reverse=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_tcp_half_open.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_tcp_half_open.py new file mode 100644 index 00000000..ee896a37 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_monitor_tcp_half_open.py @@ -0,0 +1,707 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_monitor_tcp_half_open +short_description: Manages F5 BIG-IP GTM TCP half-open monitors +description: + - Manages F5 BIG-IP GTM (now BIG-IP DNS) TCP half-open monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(tcp_half_open) + parent on the C(Common) partition. + type: str + default: "/Common/tcp_half_open" + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value will be + '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value will be + '*'. Note that if using an IP address, you must specify a value between 1 and 65535. + type: str + interval: + description: + - Specifies, in seconds, the frequency at which the system issues the monitor + check when either the resource is down or the status of the resource is unknown. + - When creating a new monitor, if this parameter is not provided, then the + default value will be C(30). This value B(must) be less than the C(timeout) value. + type: int + timeout: + description: + - Specifies the number of seconds the target has in which to respond to the + monitor request. + - If the target responds within the set time period, it is considered up. + - If the target does not respond within the set time period, it is considered down. + - When this value is set to 0 (zero), the system uses the interval from the parent monitor. + - When creating a new monitor, if this parameter is not provided, + the default value will be C(120). + type: int + probe_interval: + description: + - Specifies the number of seconds the big3d process waits before sending out a + subsequent probe attempt when a probe fails and multiple probe attempts have + been requested. + - When creating a new monitor, if this parameter is not provided, then the default + value will be C(1). + type: int + probe_timeout: + description: + - Specifies the number of seconds after which the system times out the probe request + to the system. + - When creating a new monitor, if this parameter is not provided, the default + value will be C(5). + type: int + probe_attempts: + description: + - Specifies the number of times the system attempts to probe the host server, after + which the system considers the host server down or unavailable. + - When creating a new monitor, if this parameter is not provided, the default + value will be C(3). + type: int + ignore_down_response: + description: + - Specifies that the monitor allows more than one probe attempt per interval. + - When C(yes), specifies the monitor ignores down responses for the duration of + the monitor timeout. Once the monitor timeout is reached without the system receiving + an up response, the system marks the object down. + - When C(no), specifies the monitor immediately marks an object down when it + receives a down response. + - When creating a new monitor, if this parameter is not provided, then the default + value will be C(no). + type: bool + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + - A monitor in transparent mode directs traffic through the associated pool members + or nodes (usually a router or firewall) to the aliased destination (that is, it + probes the C(ip)-C(port) combination specified in the monitor). + - If the monitor cannot successfully reach the aliased destination, the pool member + or node through which the monitor traffic was sent is marked down. + - When creating a new monitor, if this parameter is not provided, the default + value will be C(no). + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create TCP half-open Monitor + bigip_gtm_monitor_tcp_half_open: + state: present + ip: 10.10.10.10 + name: my_monitor + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove TCP half-open Monitor + bigip_gtm_monitor_tcp_half_open: + state: absent + name: my_monitor + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add half-open monitor for all addresses, port 514 + bigip_gtm_monitor_tcp_half_open: + port: 514 + name: my_monitor + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: tcp_half_open +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +probe_timeout: + description: The new timeout in which the system will timeout the monitor probe. + returned: changed + type: int + sample: 10 +probe_interval: + description: The new interval in which the system will check the monitor probe. + returned: changed + type: int + sample: 10 +probe_attempts: + description: The new number of attempts the system will make in checking the monitor probe. + returned: changed + type: int + sample: 10 +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'ignoreDownResponse': 'ignore_down_response', + 'probeAttempts': 'probe_attempts', + 'probeInterval': 'probe_interval', + 'probeTimeout': 'probe_timeout', + } + + api_attributes = [ + 'defaultsFrom', 'interval', 'timeout', 'destination', 'transparent', 'probeAttempts', + 'probeInterval', 'probeTimeout', 'ignoreDownResponse', + ] + + returnables = [ + 'parent', 'ip', 'port', 'interval', 'timeout', 'transparent', 'probe_attempts', + 'probe_interval', 'probe_timeout', 'ignore_down_response', + ] + + updatables = [ + 'destination', 'interval', 'timeout', 'transparent', 'probe_attempts', + 'probe_interval', 'probe_timeout', 'ignore_down_response', + ] + + @property + def interval(self): + if self._values['interval'] is None: + return None + + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def probe_attempts(self): + if self._values['probe_attempts'] is None: + return None + return int(self._values['probe_attempts']) + + @property + def probe_interval(self): + if self._values['probe_interval'] is None: + return None + return int(self._values['probe_interval']) + + @property + def probe_timeout(self): + if self._values['probe_timeout'] is None: + return None + return int(self._values['probe_timeout']) + + @property + def type(self): + return 'tcp_half_open' + + +class ApiParameters(Parameters): + @property + def ip(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return ip + + @property + def port(self): + try: + ip, d, port = self._values['destination'].rpartition(':') + except ValueError: + try: + ip, d, port = self._values['destination'].rpartition('.') + except ValueError as ex: + raise F5ModuleError(str(ex)) + return int(port) if port.isnumeric() else port + + @property + def ignore_down_response(self): + if self._values['ignore_down_response'] is None: + return None + if self._values['ignore_down_response'] == 'disabled': + return False + return True + + @property + def transparent(self): + if self._values['transparent'] is None: + return None + if self._values['transparent'] == 'disabled': + return False + return True + + +class ModuleParameters(Parameters): + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @property + def parent(self): + if self._values['parent'] is None: + return None + return fq_name(self.partition, self._values['parent']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def transparent(self): + if self._values['transparent']: + return 'enabled' + return 'disabled' + + @property + def ignore_down_response(self): + if self._values['ignore_down_response']: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 120}) + if self.want.interval is None: + self.want.update({'interval': 30}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + if self.want.probe_interval is None: + self.want.update({'probe_interval': 1}) + if self.want.probe_timeout is None: + self.want.update({'probe_timeout': 5}) + if self.want.probe_attempts is None: + self.want.update({'probe_attempts': 3}) + if self.want.ignore_down_response is None: + self.want.update({'ignore_down_response': False}) + if self.want.transparent is None: + self.want.update({'transparent': False}) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + self._set_default_creation_values() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the monitor.") + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/tcp-half-open/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + 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/gtm/monitor/tcp-half-open/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + 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/gtm/monitor/tcp-half-open/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/tcp-half-open/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/tcp-half-open/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/tcp_half_open'), + ip=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + probe_interval=dict(type='int'), + probe_timeout=dict(type='int'), + probe_attempts=dict(type='int'), + ignore_down_response=dict(type='bool'), + transparent=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_pool.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_pool.py new file mode 100644 index 00000000..2da7dd3d --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_pool.py @@ -0,0 +1,1125 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_pool +short_description: Manages F5 BIG-IP GTM pools +description: + - Manages F5 BIG-IP GTM (now BIG-IP DNS) pools. +version_added: "1.0.0" +options: + state: + description: + - Pool state. When C(present), ensures the pool is created and enabled. + When C(absent), ensures the pool is removed from the system. When + C(enabled) or C(disabled), ensures the pool is enabled or disabled + (respectively) on the remote device. + type: str + choices: + - present + - absent + - enabled + - disabled + default: present + preferred_lb_method: + description: + - The load balancing mode the system tries first. + type: str + choices: + - round-robin + - return-to-dns + - ratio + - topology + - static-persistence + - global-availability + - virtual-server-capacity + - least-connections + - lowest-round-trip-time + - fewest-hops + - packet-rate + - cpu + - completion-rate + - quality-of-service + - kilobytes-per-second + - drop-packet + - fallback-ip + - virtual-server-score + alternate_lb_method: + description: + - The load balancing mode the system tries if the + C(preferred_lb_method) is unsuccessful in picking a pool. + type: str + choices: + - round-robin + - return-to-dns + - none + - ratio + - topology + - static-persistence + - global-availability + - virtual-server-capacity + - packet-rate + - drop-packet + - fallback-ip + - virtual-server-score + fallback_lb_method: + description: + - The load balancing mode the system tries if both the + C(preferred_lb_method) and C(alternate_lb_method)s are unsuccessful + in picking a pool. + type: str + choices: + - round-robin + - return-to-dns + - ratio + - topology + - static-persistence + - global-availability + - virtual-server-capacity + - least-connections + - lowest-round-trip-time + - fewest-hops + - packet-rate + - cpu + - completion-rate + - quality-of-service + - kilobytes-per-second + - drop-packet + - fallback-ip + - virtual-server-score + - none + fallback_ip: + description: + - Specifies the IPv4 or IPv6 address of the server to which the system + directs requests when it cannot use one of its pools to do so. + Note that the system uses the fallback IP only if you select the + C(fallback_ip) load balancing method. + type: str + type: + description: + - The type of GTM pool you want to create. + type: str + required: True + choices: + - a + - aaaa + - cname + - mx + - naptr + - srv + name: + description: + - Name of the GTM pool. + type: str + required: True + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + members: + description: + - Members to assign to the pool. + - The order of the members in this list is the order they will be listed in the pool. + suboptions: + server: + description: + - Name of the server which the pool member is a part of. + type: str + required: True + virtual_server: + description: + - Name of the virtual server, associated with the server, the pool member is a part of. + type: str + required: True + type: list + elements: dict + monitors: + description: + - Specifies the health monitors the system currently uses to monitor this resource. + - When C(availability_requirements.type) is C(require), you may only have a single monitor in the + C(monitors) list. + type: list + elements: str + availability_requirements: + description: + - If you activate more than one health monitor, specifies the number of health + monitors that must receive successful responses in order for the link to be + considered available. + suboptions: + type: + description: + - Monitor rule type when C(monitors) is specified. + - When creating a new pool, if this value is not specified, the default of 'all' will be used. + type: str + required: True + choices: + - all + - at_least + - require + at_least: + description: + - Specifies the minimum number of active health monitors that must be successful + before the link is considered up. + - This parameter is only relevant when a C(type) of C(at_least) is used. + - This parameter will be ignored if a type of either C(all) or C(require) is used. + type: int + number_of_probes: + description: + - Specifies the minimum number of probes that must succeed for this server to be declared up. + - When creating a new virtual server, if this parameter is specified, the C(number_of_probers) + parameter must also be specified. + - The value of this parameter should always be B(lower) than, or B(equal to), + the value of C(number_of_probers). + - This parameter is only relevant when a C(type) of C(require) is used. + - This parameter will be ignored if a type of either C(all) or C(at_least) is used. + type: int + number_of_probers: + description: + - Specifies the number of probers that should be used when running probes. + - When creating a new virtual server, if this parameter is specified, the C(number_of_probes) + parameter must also be specified. + - The value of this parameter should always be B(higher) than, or B(equal to), + the value of C(number_of_probers). + - This parameter is only relevant when a C(type) of C(require) is used. + - This parameter will be ignored if a type of either C(all) or C(at_least) is used. + type: int + type: dict + max_answers_returned: + description: + - Specifies the maximum number of available virtual servers the system lists in a response. + - The maximum is 500. + type: int + ttl: + description: + - Specifies the number of seconds the IP address, once found, is valid. + type: int +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a GTM pool + bigip_gtm_pool: + name: my_pool + type: a + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Disable pool + bigip_gtm_pool: + state: disabled + name: my_pool + type: a + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +preferred_lb_method: + description: New preferred load balancing method for the pool. + returned: changed + type: str + sample: topology +alternate_lb_method: + description: New alternate load balancing method for the pool. + returned: changed + type: str + sample: drop-packet +fallback_lb_method: + description: New fallback load balancing method for the pool. + returned: changed + type: str + sample: fewest-hops +fallback_ip: + description: New fallback IP used when load balacing using the C(fallback_ip) method. + returned: changed + type: str + sample: 10.10.10.10 +monitors: + description: The new list of monitors for the resource. + returned: changed + type: list + sample: ['/Common/monitor1', '/Common/monitor2'] +members: + description: List of members in the pool. + returned: changed + type: complex + contains: + server: + description: The name of the server portion of the member. + returned: changed + type: str + virtual_server: + description: The name of the virtual server portion of the member. + returned: changed + type: str +max_answers_returned: + description: The new Maximum Answers Returned value. + returned: changed + type: int + sample: 25 +''' + +import copy +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'loadBalancingMode': 'preferred_lb_method', + 'alternateMode': 'alternate_lb_method', + 'fallbackMode': 'fallback_lb_method', + 'verifyMemberAvailability': 'verify_member_availability', + 'fallbackIpv4': 'fallback_ip', + 'fallbackIpv6': 'fallback_ip', + 'fallbackIp': 'fallback_ip', + 'membersReference': 'members', + 'monitor': 'monitors', + 'maxAnswersReturned': 'max_answers_returned', + } + + updatables = [ + 'alternate_lb_method', + 'fallback_ip', + 'fallback_lb_method', + 'members', + 'monitors', + 'preferred_lb_method', + 'state', + 'max_answers_returned', + 'ttl', + ] + + returnables = [ + 'alternate_lb_method', + 'fallback_ip', + 'fallback_lb_method', + 'members', + 'monitors', + 'preferred_lb_method', + 'enabled', + 'disabled', + 'availability_requirements', + 'max_answers_returned', + 'ttl', + ] + + api_attributes = [ + 'alternateMode', + 'disabled', + 'enabled', + 'fallbackIp', + 'fallbackIpv4', + 'fallbackIpv6', + 'fallbackMode', + 'loadBalancingMode', + 'members', + 'verifyMemberAvailability', + 'monitor', + 'maxAnswersReturned', + 'ttl', + ] + + @property + def type(self): + if self._values['type'] is None: + return None + return str(self._values['type']) + + @property + def verify_member_availability(self): + if self._values['verify_member_availability'] is None: + return None + elif self._values['verify_member_availability']: + return 'enabled' + else: + return 'disabled' + + @property + def fallback_ip(self): + if self._values['fallback_ip'] is None: + return None + if self._values['fallback_ip'] == 'any': + return 'any' + if self._values['fallback_ip'] == 'any6': + return 'any6' + if is_valid_ip(self._values['fallback_ip']): + return self._values['fallback_ip'] + else: + raise F5ModuleError( + 'The provided fallback address is not a valid IPv4 address' + ) + + @property + def state(self): + if self._values['state'] == 'enabled': + return 'present' + return self._values['state'] + + @property + def enabled(self): + if self._values['enabled'] is None: + return None + return True + + @property + def disabled(self): + if self._values['disabled'] is None: + return None + return True + + +class ApiParameters(Parameters): + @property + def members(self): + result = [] + if self._values['members'] is None or 'items' not in self._values['members']: + return [] + for item in self._values['members']['items']: + result.append(dict(item=item['fullPath'], order=item['memberOrder'])) + result = [x['item'] for x in sorted(result, key=lambda k: k['order'])] + return result + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + elif 'require ' in self._values['monitors']: + return 'require' + else: + return 'all' + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + if self._values['monitors'] == 'default': + return 'default' + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.availability_requirement_type == 'at_least': + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + elif self.availability_requirement_type == 'require': + monitors = ' '.join(monitors) + result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors) + else: + result = ' and '.join(monitors).strip() + return result + + @property + def number_of_probes(self): + """Returns the probes value from the monitor string. + + The monitor string for a Require monitor looks like this. + + require 1 from 2 { /Common/tcp } + + This method parses out the first of the numeric values. This values represents + the "probes" value that can be updated in the module. + + Returns: + int: The probes value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+(?P\d+)\s+from' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('probes') + + @property + def number_of_probers(self): + """Returns the probers value from the monitor string. + + The monitor string for a Require monitor looks like this. + + require 1 from 2 { /Common/tcp } + + This method parses out the first of the numeric values. This values represents + the "probers" value that can be updated in the module. + + Returns: + int: The probers value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+\d+\s+from\s+(?P\d+)\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('probers') + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + + The monitor string for a Require monitor looks like this. + + min 1 of { /Common/gateway_icmp } + + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('least') + + +class ModuleParameters(Parameters): + def _get_availability_value(self, type): + if self._values['availability_requirements'] is None: + return None + if self._values['availability_requirements'][type] is None: + return None + return int(self._values['availability_requirements'][type]) + + @property + def members(self): + if self._values['members'] is None: + return None + if not self._values['members']: + return [] + result = [] + for member in self._values['members']: + if 'server' not in member: + raise F5ModuleError( + "One of the provided members is missing a 'server' sub-option." + ) + if 'virtual_server' not in member: + raise F5ModuleError( + "One of the provided members is missing a 'virtual_server' sub-option." + ) + name = '{0}:{1}'.format(member['server'], member['virtual_server']) + name = fq_name(self.partition, name) + if name in result: + continue + result.append(name) + result = list(result) + return result + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + if len(self._values['monitors']) == 1 and self._values['monitors'][0] == '': + return 'default' + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.availability_requirement_type == 'at_least': + if self.at_least > len(self.monitors_list): + raise F5ModuleError( + "The 'at_least' value must not exceed the number of 'monitors'." + ) + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + elif self.availability_requirement_type == 'require': + monitors = ' '.join(monitors) + if self.number_of_probes > self.number_of_probers: + raise F5ModuleError( + "The 'number_of_probes' must not exceed the 'number_of_probers'." + ) + result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors) + else: + result = ' and '.join(monitors).strip() + + return result + + @property + def availability_requirement_type(self): + if self._values['availability_requirements'] is None: + return None + return self._values['availability_requirements']['type'] + + @property + def number_of_probes(self): + return self._get_availability_value('number_of_probes') + + @property + def number_of_probers(self): + return self._get_availability_value('number_of_probers') + + @property + def at_least(self): + return self._get_availability_value('at_least') + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def monitors(self): + monitor_string = self._values['monitors'] + if monitor_string is None: + return None + if '{' in monitor_string and '}' in monitor_string: + tmp = monitor_string.strip('}').split('{') + monitor = ''.join(tmp).rstrip() + return monitor + return monitor_string + + @property + def members(self): + results = [] + if self._values['members'] is None: + return None + for idx, member in enumerate(self._values['members']): + result = dict( + name=member, + memberOrder=idx + ) + results.append(result) + return results + + +class ReportableChanges(Changes): + @property + def members(self): + results = [] + if self._values['members'] is None: + return None + if not self._values['members']: + return [] + for member in self._values['members']: + parts = member.split(':') + results.append(dict( + server=fq_name(self.partition, parts[0]), + virtual_server=fq_name(self.partition, parts[1]) + )) + return results + + @property + def monitors(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + elif 'require ' in self._values['monitors']: + return 'require' + else: + return 'all' + + @property + def number_of_probes(self): + """Returns the probes value from the monitor string. + The monitor string for a Require monitor looks like this. + require 1 from 2 { /Common/tcp } + This method parses out the first of the numeric values. This values represents + the "probes" value that can be updated in the module. + Returns: + int: The probes value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+(?P\d+)\s+from' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('probes')) + + @property + def number_of_probers(self): + """Returns the probers value from the monitor string. + The monitor string for a Require monitor looks like this. + require 1 from 2 { /Common/tcp } + This method parses out the first of the numeric values. This values represents + the "probers" value that can be updated in the module. + Returns: + int: The probers value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+\d+\s+from\s+(?P\d+)\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('probers')) + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + The monitor string for a Require monitor looks like this. + min 1 of { /Common/gateway_icmp } + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('least')) + + @property + def availability_requirements(self): + if self._values['monitors'] is None: + return None + result = dict() + result['type'] = self.availability_requirement_type + result['at_least'] = self.at_least + result['number_of_probers'] = self.number_of_probers + result['number_of_probes'] = self.number_of_probes + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def state(self): + if self.want.state == 'disabled' and self.have.enabled: + return dict( + disabled=True + ) + elif self.want.state in ['present', 'enabled'] and self.have.disabled: + return dict( + enabled=True + ) + + @property + def monitors(self): + if self.want.monitors is None: + return None + if self.want.monitors == 'default' and self.have.monitors == 'default': + return None + if self.want.monitors == 'default' and self.have.monitors is None: + return None + if self.want.monitors == 'default' and len(self.have.monitors) > 0: + return 'default' + if self.have.monitors is None: + return self.want.monitors + if self.have.monitors != self.want.monitors: + return self.want.monitors + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = ApiParameters() + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ["present", "disabled"]: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def create(self): + if self.want.state == 'disabled': + self.want.update({'disabled': True}) + elif self.want.state in ['present', 'enabled']: + self.want.update({'enabled': True}) + + self._set_changed_options() + + if self.want.availability_requirement_type == 'require' and len(self.want.monitors_list) > 1: + raise F5ModuleError( + "Only one monitor may be specified when using an availability_requirement type of 'require'" + ) + + if self.module.check_mode: + return True + self.create_on_device() + if self.exists(): + return True + else: + raise F5ModuleError("Failed to create the GTM pool") + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the GTM pool") + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + 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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + 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/gtm/pool/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + transform_name(self.want.partition, self.want.name) + ) + + query = '?expandSubcollections=true' + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + 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/gtm/pool/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.states = ['absent', 'present', 'enabled', 'disabled'] + self.preferred_lb_methods = [ + 'round-robin', 'return-to-dns', 'ratio', 'topology', + 'static-persistence', 'global-availability', + 'virtual-server-capacity', 'least-connections', + 'lowest-round-trip-time', 'fewest-hops', 'packet-rate', 'cpu', + 'completion-rate', 'quality-of-service', 'kilobytes-per-second', + 'drop-packet', 'fallback-ip', 'virtual-server-score' + ] + self.alternate_lb_methods = [ + 'round-robin', 'return-to-dns', 'none', 'ratio', 'topology', + 'static-persistence', 'global-availability', + 'virtual-server-capacity', 'packet-rate', 'drop-packet', + 'fallback-ip', 'virtual-server-score' + ] + self.fallback_lb_methods = copy.copy(self.preferred_lb_methods) + self.fallback_lb_methods.append('none') + self.types = [ + 'a', 'aaaa', 'cname', 'mx', 'naptr', 'srv' + ] + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + state=dict( + default='present', + choices=self.states, + ), + preferred_lb_method=dict( + choices=self.preferred_lb_methods, + ), + fallback_lb_method=dict( + choices=self.fallback_lb_methods, + ), + alternate_lb_method=dict( + choices=self.alternate_lb_methods, + ), + fallback_ip=dict(), + type=dict( + required=True, + choices=self.types + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + members=dict( + type='list', + elements='dict', + options=dict( + server=dict(required=True), + virtual_server=dict(required=True) + ) + ), + availability_requirements=dict( + type='dict', + options=dict( + type=dict( + choices=['all', 'at_least', 'require'], + required=True + ), + at_least=dict(type='int'), + number_of_probes=dict(type='int'), + number_of_probers=dict(type='int') + ), + mutually_exclusive=[ + ['at_least', 'number_of_probes'], + ['at_least', 'number_of_probers'], + ], + required_if=[ + ['type', 'at_least', ['at_least']], + ['type', 'require', ['number_of_probes', 'number_of_probers']] + ] + ), + monitors=dict( + type='list', + elements='str', + ), + max_answers_returned=dict(type='int'), + ttl=dict(type='int') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_if = [ + ['preferred_lb_method', 'fallback-ip', ['fallback_ip']], + ['fallback_lb_method', 'fallback-ip', ['fallback_ip']], + ['alternate_lb_method', 'fallback-ip', ['fallback_ip']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_pool_member.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_pool_member.py new file mode 100644 index 00000000..0e3caf4b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_pool_member.py @@ -0,0 +1,1080 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_pool_member +short_description: Manage GTM pool member settings +description: + - Manages a variety of settings on GTM (now BIG-IP DNS) pool members. The settings that can be + adjusted with this module are much more broad that what can be done in the + C(bigip_gtm_pool) module. The pool module is intended to allow you to adjust + the member order in the pool, not the various settings of the members. The + C(bigip_gtm_pool_member) module should be used to adjust all of the other + settings. +version_added: "1.0.0" +options: + virtual_server: + description: + - Specifies the name of the GTM virtual server which is assigned to the specified + C(server). + type: str + server_name: + description: + - Specifies the GTM server which contains the C(virtual_server). + type: str + type: + description: + - The type of GTM pool that the member is in. + type: str + required: True + choices: + - a + - aaaa + - cname + - mx + - naptr + - srv + pool: + description: + - Name of the GTM pool. + - For pools created on different partitions, you must specify partition of the pool in the full path format, + for example, C(/FooBar/pool_name). + type: str + required: True + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + member_order: + description: + - Specifies the order in which the member will appear in the pool. + - The system uses this number with load balancing methods that involve prioritizing + pool members, such as the Ratio load balancing method. + - When creating a new member using this module, if the C(member_order) parameter + is not specified, it will default to C(0) (first member in the pool). + type: int + monitor: + description: + - Specifies the monitor assigned to this pool member. + - Pool members only support a single monitor. + - If the C(port) of the C(gtm_virtual_server) is C(*), the accepted values of this + parameter will be affected. + - If this parameter is not specified when creating a new pool member, the default + of C(default) will be used. + - To remove the monitor from the pool member, use the value C(none). + - For pool members created on different partitions, you can also specify the full + path to the Common monitor. For example, C(/Common/tcp). + type: str + ratio: + description: + - Specifies the weight of the pool member for load balancing purposes. + type: int + description: + description: + - The description of the pool member. + type: str + aggregate: + description: + - List of GTM pool member definitions to be created, modified, or removed. + - When using C(aggregates), if one of the aggregate definitions is invalid, the aggregate run will fail, + indicating the error it last encountered. + - The module will C(NOT) rollback any changes it has made prior to encountering the error. + - The module also will not indicate what changes were made prior to failure, therefore we strongly advise + you run the module in check mode to make basic validation, prior to module execution. + type: list + elements: dict + aliases: + - members + replace_all_with: + description: + - Removes members not defined in the C(aggregate) parameter. + - This operation is all or none, meaning it will stop if there are some pool members + that cannot be removed. + default: no + type: bool + aliases: + - purge + limits: + description: + - Specifies resource thresholds or limit requirements at the pool member level. + - When you enable one or more limit settings, the system then uses that data to take + members in and out of service. + - You can define limits for any or all of the limit settings. However, when a + member does not meet the resource threshold limit requirement, the system marks + the member as unavailable and directs load balancing traffic to another resource. + suboptions: + bits_enabled: + description: + - Whether the bits limit is enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + packets_enabled: + description: + - Whether the packets limit is enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + connections_enabled: + description: + - Whether the current connections limit is enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + bits_limit: + description: + - Specifies the maximum allowable data throughput rate + for the member, in bits per second. + - If the network traffic volume exceeds this limit, the system marks the + member as unavailable. + type: int + packets_limit: + description: + - Specifies the maximum allowable data transfer rate for the member, + in packets per second. + - If the network traffic volume exceeds this limit, the system marks the + member as unavailable. + type: int + connections_limit: + description: + - Specifies the maximum number of concurrent connections, combined, for all of + the members. + - If the connections exceed this limit, the system marks the server as + unavailable. + type: int + type: dict + state: + description: + - Pool member state. When C(present), ensures the pool member is + created and enabled. When C(absent), ensures the pool member is + removed from the system. When C(enabled) or C(disabled), ensures + the pool member is enabled or disabled (respectively) on the remote + device. + - We recommend you use the C(members) parameter of the C(bigip_gtm_pool) + module when adding and removing members, as it provides an easier way of + specifying order. If this is not possible, the C(state) parameter here + should be used. + - Remember that the order of the members will be affected if you add or remove them + using this method. To some extent, this can be controlled using the C(member_order) + parameter. + type: str + choices: + - present + - absent + - enabled + - disabled + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a GTM pool member + bigip_gtm_pool_member: + pool: pool1 + server_name: server1 + virtual_server: vs1 + type: a + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a GTM pool member different partition + bigip_gtm_pool_member: + server_name: /Common/foo_name + virtual_server: GTMVSName + type: a + pool: /FooBar/foo-pool + partition: Common + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Add GTM pool members aggregate + bigip_gtm_pool_member: + pool: pool1 + type: a + aggregate: + - server_name: server1 + virtual_server: vs1 + partition: Common + description: web server1 + member_order: 0 + - server_name: server2 + virtual_server: vs2 + partition: Common + description: web server2 + member_order: 1 + - server_name: server3 + virtual_server: vs3 + partition: Common + description: web server3 + member_order: 2 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add GTM pool members aggregate, remove non aggregates + bigip_gtm_pool_member: + pool: pool1 + type: a + aggregate: + - server_name: server1 + virtual_server: vs1 + partition: Common + description: web server1 + member_order: 0 + - server_name: server2 + virtual_server: vs2 + partition: Common + description: web server2 + member_order: 1 + - server_name: server3 + virtual_server: vs3 + partition: Common + description: web server3 + member_order: 2 + replace_all_with: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +bits_enabled: + description: Whether the bits limit is enabled. + returned: changed + type: bool + sample: yes +bits_limit: + description: The new bits_enabled limit. + returned: changed + type: int + sample: 100 +connections_enabled: + description: Whether the connections limit is enabled. + returned: changed + type: bool + sample: yes +connections_limit: + description: The new connections_limit limit. + returned: changed + type: int + sample: 100 +disabled: + description: Whether the pool member is disabled or not. + returned: changed + type: bool + sample: yes +enabled: + description: Whether the pool member is enabled or not. + returned: changed + type: bool + sample: yes +member_order: + description: The new order in which the member appears in the pool. + returned: changed + type: int + sample: 2 +monitor: + description: The new monitor assigned to the pool member. + returned: changed + type: str + sample: /Common/monitor1 +packets_enabled: + description: Whether the packets limit is enabled. + returned: changed + type: bool + sample: yes +packets_limit: + description: The new packets_limit limit. + returned: changed + type: int + sample: 100 +ratio: + description: The new weight of the member for load balancing. + returned: changed + type: int + sample: 10 +description: + description: The new description of the member. + returned: changed + type: str + sample: My description +replace_all_with: + description: Purges all non-aggregate pool members from device + returned: changed + type: bool + sample: yes +''' + +from copy import deepcopy +from datetime import datetime + +from ansible.module_utils.urls import urlparse +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, TransactionContextManager, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'limitMaxBps': 'bits_limit', + 'limitMaxBpsStatus': 'bits_enabled', + 'limitMaxConnections': 'connections_limit', + 'limitMaxConnectionsStatus': 'connections_enabled', + 'limitMaxPps': 'packets_limit', + 'limitMaxPpsStatus': 'packets_enabled', + 'memberOrder': 'member_order', + } + + api_attributes = [ + 'disabled', + 'enabled', + 'limitMaxBps', + 'limitMaxBpsStatus', + 'limitMaxConnections', + 'limitMaxConnectionsStatus', + 'limitMaxPps', + 'limitMaxPpsStatus', + 'memberOrder', + 'monitor', + 'ratio', + 'description', + ] + + returnables = [ + 'bits_enabled', + 'bits_limit', + 'connections_enabled', + 'connections_limit', + 'disabled', + 'enabled', + 'member_order', + 'monitor', + 'packets_enabled', + 'packets_limit', + 'ratio', + 'description', + ] + + updatables = [ + 'bits_enabled', + 'bits_limit', + 'connections_enabled', + 'connections_limit', + 'enabled', + 'member_order', + 'monitor', + 'packets_limit', + 'packets_enabled', + 'ratio', + 'description', + ] + + @property + def ratio(self): + if self._values['ratio'] is None: + return None + return int(self._values['ratio']) + + +class ApiParameters(Parameters): + def name(self): + # We need to do this because BIGIP allows / in names of GTM VS, allowing and users create such names incorrectly + # Despite the fact that GTM server and GTM Virtual Server cannot be created outside the Common partition + if self._values['subPath'] is None: + return self._values['name'] + result = self._values['subPath'] + self._values['name'] + return result + + @property + def enabled(self): + if 'enabled' in self._values: + return True + else: + return False + + @property + def disabled(self): + if 'disabled' in self._values: + return True + return False + + @property + def monitor(self): + if self._values['monitor'] is None: + return None + # The value of this parameter in the API includes an extra space + return self._values['monitor'].strip() + + +class ModuleParameters(Parameters): + def _get_limit_value(self, type): + if self._values['limits'] is None: + return None + if self._values['limits'][type] is None: + return None + return int(self._values['limits'][type]) + + def _get_limit_status(self, type): + if self._values['limits'] is None: + return None + if self._values['limits'][type] is None: + return None + if self._values['limits'][type]: + return 'enabled' + return 'disabled' + + @property + def name(self): + result = '{0}:{1}'.format(self.server_name, self.virtual_server) + return result + + @property + def type(self): + if self._values['type'] is None: + return None + return str(self._values['type']) + + @property + def enabled(self): + if self._values['state'] == 'enabled': + return True + elif self._values['state'] == 'disabled': + return False + else: + return None + + @property + def disabled(self): + if self._values['state'] == 'enabled': + return False + elif self._values['state'] == 'disabled': + return True + else: + return None + + @property + def bits_limit(self): + return self._get_limit_value('bits_limit') + + @property + def packets_limit(self): + return self._get_limit_value('packets_limit') + + @property + def connections_limit(self): + return self._get_limit_value('connections_limit') + + @property + def bits_enabled(self): + return self._get_limit_status('bits_enabled') + + @property + def packets_enabled(self): + return self._get_limit_status('packets_enabled') + + @property + def connections_enabled(self): + return self._get_limit_status('connections_enabled') + + @property + def monitor(self): + if self._values['monitor'] is None: + return None + elif self._values['monitor'] in ['default', '']: + return 'default' + return fq_name(self.partition, self._values['monitor']) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def disabled(self): + return flatten_boolean(self._values['disabled']) + + @property + def enabled(self): + return flatten_boolean(self._values['enabled']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + if self.want.description == '' and self.have.description is None: + return None + if self.want.description != self.have.description: + return self.want.description + + @property + def enabled(self): + if self.want.state == 'enabled' and self.have.disabled: + result = dict( + enabled=True, + disabled=False + ) + return result + elif self.want.state == 'disabled' and self.have.enabled: + result = dict( + enabled=False, + disabled=True + ) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = None + self.have = None + self.changes = None + self.replace_all_with = None + self.purge_links = list() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + wants = None + if self.module.params['replace_all_with']: + self.replace_all_with = True + + if self.module.params['aggregate']: + wants = self.merge_defaults_for_aggregate(self.module.params) + + result = dict() + changed = False + + if self.replace_all_with and self.purge_links: + self.purge() + changed = True + + if self.module.params['aggregate']: + result['aggregate'] = list() + for want in wants: + output = self.execute(want) + if output['changed']: + changed = output['changed'] + result['aggregate'].append(output) + else: + output = self.execute(self.module.params) + if output['changed']: + changed = output['changed'] + result.update(output) + if changed: + result['changed'] = True + send_teem(start, self.client, self.module, version) + return result + + def merge_defaults_for_aggregate(self, params): + defaults = deepcopy(params) + aggregate = defaults.pop('aggregate') + + for i, j in enumerate(aggregate): + for k, v in iteritems(defaults): + if k != 'replace_all_with': + if j.get(k, None) is None and v is not None: + aggregate[i][k] = v + + if self.replace_all_with: + self.compare_aggregate_names(aggregate) + + return aggregate + + def _combine_names(self, item): + server_name = transform_name(item['partition'], item['server_name']) + virtual_server = transform_name(name=item['virtual_server']) + result = '{0}:{1}'.format(server_name, virtual_server) + return result + + def _transform_api_names(self, item): + if 'subPath' in item and item['subPath'] is None: + return item['name'] + result = transform_name(item['fullPath']) + return result + + def compare_aggregate_names(self, items): + on_device = self._read_purge_collection() + + if not on_device: + return False + + aggregates = [self._combine_names(item) for item in items] + collection = [self._transform_api_names(item) for item in on_device] + + diff = set(collection) - set(aggregates) + + if diff: + to_purge = [item['selfLink'] for item in on_device if self._transform_api_names(item) in diff] + self.purge_links.extend(to_purge) + + def execute(self, params=None): + self.want = ModuleParameters(params=params) + self.have = ApiParameters() + self.changes = UsableChanges() + + changed = False + result = dict() + state = params['state'] + + if state in ['present', 'enabled', 'disabled']: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def purge(self): + if self.module.check_mode: + return True + self.purge_from_device() + return True + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.state == 'disabled': + self.want.update({'disabled': True}) + elif self.want.state in ['present', 'enabled']: + self.want.update({'enabled': True}) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + if not self.pool_exist(): + raise F5ModuleError('The specified GTM pool does not exist') + + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}/members/{4}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + transform_name(name=fq_name(self.want.partition, self.want.pool)), + 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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def pool_exist(self): + if self.replace_all_with: + type = self.module.params['type'] + pool_name = transform_name(name=fq_name(self.module.params['partition'], self.module.params['pool'])) + else: + pool_name = transform_name(name=fq_name(self.want.partition, self.want.pool)) + type = self.want.type + + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + type, + pool_name + + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def _read_purge_collection(self): + type = self.module.params['type'] + pool_name = transform_name(name=fq_name(self.module.params['partition'], self.module.params['pool'])) + + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}/members".format( + self.client.provider['server'], + self.client.provider['server_port'], + type, + pool_name + ) + + query = '?$select=name,selfLink,fullPath,subPath' + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'items' in response: + return response['items'] + return [] + raise F5ModuleError(resp.content) + + 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/gtm/pool/{2}/{3}/members/".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + transform_name(name=fq_name(self.want.partition, self.want.pool)), + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}/members/{4}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + transform_name(name=fq_name(self.want.partition, self.want.pool)), + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}/members/{4}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + transform_name(name=fq_name(self.want.partition, self.want.pool)), + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}/members/{4}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + transform_name(name=fq_name(self.want.partition, self.want.pool)), + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def _prepare_links(self, collection): + # this is to ensure no duplicates are in the provided collection + no_dupes = list(set(collection)) + links = list() + purge_paths = [urlparse(link).path for link in no_dupes] + + for path in purge_paths: + link = "https://{0}:{1}{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + path + ) + links.append(link) + return links + + def purge_from_device(self): + links = self._prepare_links(self.purge_links) + + with TransactionContextManager(self.client) as transact: + for link in links: + resp = transact.api.delete(link) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + return True + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.types = [ + 'a', 'aaaa', 'cname', 'mx', 'naptr', 'srv' + ] + element_spec = dict( + server_name=dict(), + virtual_server=dict(), + member_order=dict(type='int'), + monitor=dict(), + ratio=dict(type='int'), + description=dict(), + limits=dict( + type='dict', + options=dict( + bits_enabled=dict(type='bool'), + packets_enabled=dict(type='bool'), + connections_enabled=dict(type='bool'), + bits_limit=dict(type='int'), + packets_limit=dict(type='int'), + connections_limit=dict(type='int') + ) + ), + state=dict( + default='present', + choices=['present', 'absent', 'disabled', 'enabled'] + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + self.argument_spec = dict( + aggregate=dict( + type='list', + elements='dict', + options=aggregate_spec, + aliases=['members'], + required_one_of=[ + ['server_name', 'virtual_server'] + ], + required_together=[ + ['server_name', 'virtual_server'] + ] + + ), + pool=dict(required=True), + type=dict( + choices=self.types, + required=True + ), + replace_all_with=dict( + type='bool', + aliases=['purge'], + default='no' + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ) + + ) + self.argument_spec.update(element_spec) + self.argument_spec.update(f5_argument_spec) + self.required_together = [ + ['server_name', 'virtual_server'] + ] + self.mutually_exclusive = [ + ['server_name', 'aggregate'], + ['virtual_server', 'aggregate'] + ] + self.required_one_of = [ + ['server_name', 'virtual_server', 'aggregate'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + required_one_of=spec.required_one_of, + required_together=spec.required_together, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_server.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_server.py new file mode 100644 index 00000000..0dcb68f4 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_server.py @@ -0,0 +1,1803 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_server +short_description: Manages F5 BIG-IP GTM servers +description: + - Manage BIG-IP GTM (now BIG-IP DNS) server configuration. This module is able to manipulate the server + definitions in a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - The name of the server. + - If the virtual server is auto-discovered from the LTM,then the partition name needs to be included as + part of the virtual server name when referencing from the module e.g. "/Common/vsname". + type: str + required: True + state: + description: + - The server state. If C(absent), the module attempts to delete the server. + This will only succeed if this server is not in use by a virtual server. + C(present) creates the server and enables it. If C(enabled), enables the server + if it exists. If C(disabled), creates the server if needed, and sets state to + C(disabled). + type: str + choices: + - present + - absent + - enabled + - disabled + default: present + datacenter: + description: + - Data center to which the server belongs. When creating a new GTM server, this value + is required. + type: str + devices: + description: + - Lists the self IP addresses and translations for each device. When creating a + new GTM server, this value is required. This list is a complex list that + specifies a number of keys. + - The C(name) key specifies a name for the device. The device name must + be unique per server. This key is required. + - The C(address) key contains an IP address, or list of IP addresses, for the + destination server. This key is required. + - The C(translation) key contains an IP address to translate the C(address) + value above to. This key is optional. + - Specifying duplicate C(name) fields is a supported means of providing device + addresses. In this scenario, the addresses will be assigned to the C(name)'s list + of addresses. + type: raw + server_type: + description: + - Specifies the server type. The server type determines the metrics the + system can collect from the server. When creating a new GTM server, the default + value C(bigip) is used. + type: str + choices: + - alteon-ace-director + - cisco-css + - cisco-server-load-balancer + - generic-host + - radware-wsd + - windows-nt-4.0 + - bigip + - cisco-local-director-v2 + - extreme + - generic-load-balancer + - sun-solaris + - cacheflow + - cisco-local-director-v3 + - foundry-server-iron + - netapp + - windows-2000-server + aliases: + - product + link_discovery: + description: + - Specifies whether the system auto-discovers the links for this server. When + creating a new GTM server, if this parameter is not specified, the default + value C(disabled) is used. + - If you set this parameter to C(enabled) or C(enabled-no-delete), you must + also ensure the C(virtual_server_discovery) parameter is also set to + C(enabled) or C(enabled-no-delete). + type: str + choices: + - enabled + - disabled + - enabled-no-delete + virtual_server_discovery: + description: + - Specifies whether the system auto-discovers the virtual servers for this server. + When creating a new GTM server, if this parameter is not specified, the default + value C(disabled) is used. + type: str + choices: + - enabled + - disabled + - enabled-no-delete + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + iquery_options: + description: + - Specifies whether the Global Traffic Manager uses this BIG-IP + system to conduct a variety of probes before delegating traffic to it. + suboptions: + allow_path: + description: + - Specifies the system verifies the logical network route between a data + center server and a local DNS server. + type: bool + allow_service_check: + description: + - Specifies the system verifies that an application on a server is running, + by remotely running the application using an external service checker program. + type: bool + allow_snmp: + description: + - Specifies the system checks the performance of a server running an SNMP + agent. + type: bool + type: dict + monitors: + description: + - Specifies the health monitors the system currently uses to monitor this resource. + - When C(availability_requirements.type) is C(require), you may only have a single monitor in the + C(monitors) list. + type: list + elements: str + availability_requirements: + description: + - If you activate more than one health monitor, specifies the number of health + monitors that must receive successful responses in order for the link to be + considered available. + type: dict + suboptions: + type: + description: + - Monitor rule type when C(monitors) is specified. + - When creating a new pool, if this value is not specified, the default of B(all) will be used. + type: str + required: True + choices: + - all + - at_least + - require + at_least: + description: + - Specifies the minimum number of active health monitors that must be successful + before the link is considered up. + - This parameter is only relevant when a C(type) of C(at_least) is used. + - This parameter will be ignored if a type of either C(all) or C(require) is used. + type: int + number_of_probes: + description: + - Specifies the minimum number of probes that must succeed for this server to be declared up. + - When creating a new virtual server, if this parameter is specified, then the C(number_of_probers) + parameter must also be specified. + - The value of this parameter should always be B(lower) than, or B(equal to), + the value of C(number_of_probers). + - This parameter is only relevant when a C(type) of C(require) is used. + - This parameter will be ignored if a type of either C(all) or C(at_least) is used. + type: int + number_of_probers: + description: + - Specifies the number of probers that should be used when running probes. + - When creating a new virtual server, if this parameter is specified, the C(number_of_probes) + parameter must also be specified. + - The value of this parameter should always be B(higher) than, or B(equal to), + the value of C(number_of_probers). + - This parameter is only relevant when a C(type) of C(require) is used. + - This parameter will be ignored if a type of either C(all) or C(at_least) is used. + type: int + prober_preference: + description: + - Specifies the type of prober to use to monitor this server's resources. + - This option is ignored in C(TMOS) version C(12.x). + - From C(TMOS) version C(13.x) and up, when prober_preference is set to C(pool) + a C(prober_pool) parameter must be specified. + type: str + choices: + - inside-datacenter + - outside-datacenter + - inherit + - pool + prober_fallback: + description: + - Specifies the type of prober to use to monitor this server's resources + when the preferred prober is not available. + - This option is ignored in C(TMOS) version C(12.x). + - From C(TMOS) version C(13.x) and up, when prober_preference is set to C(pool) + a C(prober_pool) parameter must be specified. + - The choices are mutually exclusive with prober_preference parameter, + with the exception of the C(any-available) or C(none) options. + type: str + choices: + - any + - inside-datacenter + - outside-datacenter + - inherit + - pool + - none + prober_pool: + description: + - Specifies the name of the prober pool to use to monitor this server's resources. + - In C(TMOS) version C(13.x) and later, this parameter is mandatory when C(prober_preference) is set to C(pool). + - The format of the name can be either be prepended by partition (C(/Common/foo)), or specified + just as an object name (C(foo)). + - In C(TMOS) version C(12.x), prober_pool can be set to an empty string to revert to default setting of C(inherit). + type: str + limits: + description: + - Specifies resource thresholds or limit requirements at the pool member level. + - When you enable one or more limit settings, the system then uses that data to take + members in and out of service. + - You can define limits for any or all of the limit settings. However, when a + member does not meet the resource threshold limit requirement, the system marks + the member as unavailable and directs load balancing traffic to another resource. + suboptions: + bits_enabled: + description: + - Whether the bits limit it enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + packets_enabled: + description: + - Whether the packets limit it enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + connections_enabled: + description: + - Whether the current connections limit it enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + cpu_enabled: + description: + - Whether the CPU limit it enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + memory_enabled: + description: + - Whether the memory limit it enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + bits_limit: + description: + - Specifies the maximum allowable data throughput rate for the member, + in bits per second. + - If the network traffic volume exceeds this limit, the system marks the + member as unavailable. + type: int + packets_limit: + description: + - Specifies the maximum allowable data transfer rate for the member, + in packets per second. + - If the network traffic volume exceeds this limit, the system marks the + member as unavailable. + type: int + connections_limit: + description: + - Specifies the maximum number of concurrent connections, combined, for all of + the members. + - If the connections exceed this limit, the system marks the server as + unavailable. + type: int + cpu_limit: + description: + - Specifies the percent of CPU usage. + - If percent of CPU usage goes above the limit, the system marks the server as unavailable. + type: int + memory_limit: + description: + - Specifies the available memory required by the virtual servers on the server. + - If available memory falls below this limit, the system marks the server as unavailable. + type: int + type: dict +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Robert Teller (@r-teller) + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create server "GTM_Server" + bigip_gtm_server: + name: GTM_Server + datacenter: /Common/New York + server_type: bigip + link_discovery: disabled + virtual_server_discovery: disabled + devices: + - name: server_1 + address: 1.1.1.1 + - name: server_2 + address: 2.2.2.1 + translation: 192.168.2.1 + - name: server_2 + address: 2.2.2.2 + - name: server_3 + addresses: + - address: 3.3.3.1 + - address: 3.3.3.2 + - name: server_4 + addresses: + - address: 4.4.4.1 + translation: 192.168.14.1 + - address: 4.4.4.2 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Create server "GTM_Server" with expanded keys + bigip_gtm_server: + server: lb.mydomain.com + user: admin + password: secret + name: GTM_Server + datacenter: /Common/New York + server_type: bigip + link_discovery: disabled + virtual_server_discovery: disabled + devices: + - name: server_1 + address: 1.1.1.1 + - name: server_2 + address: 2.2.2.1 + translation: 192.168.2.1 + - name: server_2 + address: 2.2.2.2 + - name: server_3 + addresses: + - address: 3.3.3.1 + - address: 3.3.3.2 + - name: server_4 + addresses: + - address: 4.4.4.1 + translation: 192.168.14.1 + - address: 4.4.4.2 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +bits_enabled: + description: Whether the bits limit is enabled. + returned: changed + type: bool + sample: yes +bits_limit: + description: The new bits_enabled limit. + returned: changed + type: int + sample: 100 +connections_enabled: + description: Whether the connections limit is enabled. + returned: changed + type: bool + sample: yes +connections_limit: + description: The new connections_limit limit. + returned: changed + type: int + sample: 100 +monitors: + description: The new list of monitors for the resource. + returned: changed + type: list + sample: ['/Common/monitor1', '/Common/monitor2'] +link_discovery: + description: The new C(link_discovery) configured on the remote device. + returned: changed + type: str + sample: enabled +virtual_server_discovery: + description: The new C(virtual_server_discovery) name for the trap destination. + returned: changed + type: str + sample: disabled +server_type: + description: The new type of the server. + returned: changed + type: str + sample: bigip +datacenter: + description: The new C(datacenter) which the server is a part of. + returned: changed + type: str + sample: datacenter01 +packets_enabled: + description: Whether the packets limit is enabled. + returned: changed + type: bool + sample: yes +packets_limit: + description: The new packets_limit limit. + returned: changed + type: int + sample: 100 +''' + +import re +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, is_empty_list, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + +try: + from collections import OrderedDict +except ImportError: + try: + from ordereddict import OrderedDict + except ImportError: + pass + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'product': 'server_type', + 'virtualServerDiscovery': 'virtual_server_discovery', + 'linkDiscovery': 'link_discovery', + 'addresses': 'devices', + 'iqAllowPath': 'iquery_allow_path', + 'iqAllowServiceCheck': 'iquery_allow_service_check', + 'iqAllowSnmp': 'iquery_allow_snmp', + 'monitor': 'monitors', + 'proberPreference': 'prober_preference', + 'proberPool': 'prober_pool', + 'proberFallback': 'prober_fallback', + 'limitMaxBps': 'bits_limit', + 'limitMaxBpsStatus': 'bits_enabled', + 'limitMaxConnections': 'connections_limit', + 'limitMaxConnectionsStatus': 'connections_enabled', + 'limitMaxPps': 'packets_limit', + 'limitMaxPpsStatus': 'packets_enabled', + 'limitCpuUsage': 'cpu_limit', + 'limitCpuUsageStatus': 'cpu_enabled', + 'limitMemAvail': 'memory_limit', + 'limitMemAvailStatus': 'memory_enabled', + } + + api_attributes = [ + 'linkDiscovery', + 'virtualServerDiscovery', + 'product', + 'addresses', + 'datacenter', + 'enabled', + 'disabled', + 'iqAllowPath', + 'iqAllowServiceCheck', + 'iqAllowSnmp', + 'monitor', + 'proberPreference', + 'proberPool', + 'proberFallback', + 'limitMaxBps', + 'limitMaxBpsStatus', + 'limitMaxConnections', + 'limitMaxConnectionsStatus', + 'limitMaxPps', + 'limitMaxPpsStatus', + 'limitCpuUsage', + 'limitCpuUsageStatus', + 'limitMemAvail', + 'limitMemAvailStatus', + ] + + updatables = [ + 'link_discovery', + 'virtual_server_discovery', + 'server_type_and_devices', + 'datacenter', + 'state', + 'iquery_allow_path', + 'iquery_allow_service_check', + 'iquery_allow_snmp', + 'monitors', + 'prober_preference', + 'prober_pool', + 'prober_fallback', + 'bits_enabled', + 'bits_limit', + 'connections_enabled', + 'connections_limit', + 'packets_enabled', + 'packets_limit', + 'cpu_enabled', + 'cpu_limit', + 'memory_enabled', + 'memory_limit', + ] + + returnables = [ + 'link_discovery', + 'virtual_server_discovery', + 'server_type', + 'datacenter', + 'enabled', + 'iquery_allow_path', + 'iquery_allow_service_check', + 'iquery_allow_snmp', + 'devices', + 'monitors', + 'availability_requirements', + 'prober_preference', + 'prober_pool', + 'prober_fallback', + 'bits_enabled', + 'bits_limit', + 'connections_enabled', + 'connections_limit', + 'packets_enabled', + 'packets_limit', + 'cpu_enabled', + 'cpu_limit', + 'memory_enabled', + 'memory_limit', + ] + + +class ApiParameters(Parameters): + @property + def devices(self): + if self._values['devices'] is None: + return None + return self._values['devices'] + + @property + def server_type(self): + if self._values['server_type'] is None: + return None + elif self._values['server_type'] in ['single-bigip', 'redundant-bigip']: + return 'bigip' + else: + return self._values['server_type'] + + @property + def raw_server_type(self): + if self._values['server_type'] is None: + return None + return self._values['server_type'] + + @property + def enabled(self): + if self._values['enabled'] is None: + return None + return True + + @property + def disabled(self): + if self._values['disabled'] is None: + return None + return True + + @property + def iquery_allow_path(self): + if self._values['iquery_allow_path'] is None: + return None + elif self._values['iquery_allow_path'] == 'yes': + return True + return False + + @property + def iquery_allow_service_check(self): + if self._values['iquery_allow_service_check'] is None: + return None + elif self._values['iquery_allow_service_check'] == 'yes': + return True + return False + + @property + def iquery_allow_snmp(self): + if self._values['iquery_allow_snmp'] is None: + return None + elif self._values['iquery_allow_snmp'] == 'yes': + return True + return False + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + elif 'require ' in self._values['monitors']: + return 'require' + else: + return 'all' + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + if self._values['monitors'] == '/Common/bigip': + return '/Common/bigip' + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.availability_requirement_type == 'at_least': + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + elif self.availability_requirement_type == 'require': + monitors = ' '.join(monitors) + result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors) + else: + result = ' and '.join(monitors).strip() + return result + + @property + def number_of_probes(self): + """Returns the probes value from the monitor string. + + The monitor string for a Require monitor looks like this. + + require 1 from 2 { /Common/tcp } + + This method parses out the first of the numeric values. This values represents + the "probes" value that can be updated in the module. + + Returns: + int: The probes value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+(?P\d+)\s+from' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('probes') + + @property + def number_of_probers(self): + """Returns the probers value from the monitor string. + + The monitor string for a Require monitor looks like this. + + require 1 from 2 { /Common/tcp } + + This method parses out the first of the numeric values. This values represents + the "probers" value that can be updated in the module. + + Returns: + int: The probers value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+\d+\s+from\s+(?P\d+)\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('probers') + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + + The monitor string for a Require monitor looks like this. + + min 1 of { /Common/gateway_icmp } + + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('least') + + +class ModuleParameters(Parameters): + def _get_limit_value(self, type): + if self._values['limits'] is None: + return None + if self._values['limits'][type] is None: + return None + return int(self._values['limits'][type]) + + def _get_limit_status(self, type): + if self._values['limits'] is None: + return None + if self._values['limits'][type] is None: + return None + if self._values['limits'][type]: + return 'enabled' + return 'disabled' + + @property + def devices(self): + if self._values['devices'] is None: + return None + result = [] + + for device in self._values['devices']: + if not any(x for x in ['address', 'addresses'] if x in device): + raise F5ModuleError( + "The specified device list must contain an 'address' or 'addresses' key" + ) + + if 'address' in device: + translation = self._determine_translation(device) + name = device['address'] + device_name = device['name'] + result.append({ + 'name': name, + 'deviceName': device_name, + 'translation': translation + }) + elif 'addresses' in device: + for address in device['addresses']: + translation = self._determine_translation(address) + name = address['address'] + device_name = device['name'] + result.append({ + 'name': name, + 'deviceName': device_name, + 'translation': translation + }) + return result + + @property + def enabled(self): + if self._values['state'] in ['present', 'enabled']: + return True + return False + + @property + def datacenter(self): + if self._values['datacenter'] is None: + return None + return fq_name(self.partition, self._values['datacenter']) + + def _determine_translation(self, device): + if 'translation' not in device: + return 'none' + return device['translation'] + + @property + def state(self): + if self._values['state'] == 'enabled': + return 'present' + return self._values['state'] + + @property + def iquery_allow_path(self): + if self._values['iquery_options'] is None: + return None + elif self._values['iquery_options']['allow_path'] is None: + return None + return self._values['iquery_options']['allow_path'] + + @property + def iquery_allow_service_check(self): + if self._values['iquery_options'] is None: + return None + elif self._values['iquery_options']['allow_service_check'] is None: + return None + return self._values['iquery_options']['allow_service_check'] + + @property + def iquery_allow_snmp(self): + if self._values['iquery_options'] is None: + return None + elif self._values['iquery_options']['allow_snmp'] is None: + return None + return self._values['iquery_options']['allow_snmp'] + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + if is_empty_list(self._values['monitors']): + return '/Common/bigip' + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.availability_requirement_type == 'at_least': + if self.at_least > len(self.monitors_list): + raise F5ModuleError( + "The 'at_least' value must not exceed the number of 'monitors'." + ) + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + elif self.availability_requirement_type == 'require': + monitors = ' '.join(monitors) + if self.number_of_probes > self.number_of_probers: + raise F5ModuleError( + "The 'number_of_probes' must not exceed the 'number_of_probers'." + ) + result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors) + else: + result = ' and '.join(monitors).strip() + + return result + + def _get_availability_value(self, type): + if self._values['availability_requirements'] is None: + return None + if self._values['availability_requirements'][type] is None: + return None + return int(self._values['availability_requirements'][type]) + + @property + def availability_requirement_type(self): + if self._values['availability_requirements'] is None: + return None + return self._values['availability_requirements']['type'] + + @property + def number_of_probes(self): + return self._get_availability_value('number_of_probes') + + @property + def number_of_probers(self): + return self._get_availability_value('number_of_probers') + + @property + def at_least(self): + return self._get_availability_value('at_least') + + @property + def prober_pool(self): + if self._values['prober_pool'] is None: + return None + if self._values['prober_pool'] == '': + return self._values['prober_pool'] + result = fq_name(self.partition, self._values['prober_pool']) + return result + + @property + def prober_fallback(self): + if self._values['prober_fallback'] == 'any': + return 'any-available' + return self._values['prober_fallback'] + + @property + def bits_limit(self): + return self._get_limit_value('bits_limit') + + @property + def packets_limit(self): + return self._get_limit_value('packets_limit') + + @property + def connections_limit(self): + return self._get_limit_value('connections_limit') + + @property + def cpu_limit(self): + return self._get_limit_value('cpu_limit') + + @property + def memory_limit(self): + return self._get_limit_value('memory_limit') + + @property + def bits_enabled(self): + return self._get_limit_status('bits_enabled') + + @property + def packets_enabled(self): + return self._get_limit_status('packets_enabled') + + @property + def connections_enabled(self): + return self._get_limit_status('connections_enabled') + + @property + def cpu_enabled(self): + return self._get_limit_status('cpu_enabled') + + @property + def memory_enabled(self): + return self._get_limit_status('memory_enabled') + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def monitors(self): + monitor_string = self._values['monitors'] + if monitor_string is None: + return None + + if '{' in monitor_string and '}' in monitor_string: + tmp = monitor_string.strip('}').split('{') + monitor = ''.join(tmp).rstrip() + return monitor + + return monitor_string + + @property + def iquery_allow_path(self): + if self._values['iquery_allow_path'] is None: + return None + elif self._values['iquery_allow_path']: + return 'yes' + return 'no' + + @property + def iquery_allow_service_check(self): + if self._values['iquery_allow_service_check'] is None: + return None + elif self._values['iquery_allow_service_check']: + return 'yes' + return 'no' + + @property + def iquery_allow_snmp(self): + if self._values['iquery_allow_snmp'] is None: + return None + elif self._values['iquery_allow_snmp']: + return 'yes' + return 'no' + + +class ReportableChanges(Changes): + @property + def server_type(self): + if self._values['server_type'] in ['single-bigip', 'redundant-bigip']: + return 'bigip' + return self._values['server_type'] + + @property + def monitors(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + elif 'require ' in self._values['monitors']: + return 'require' + else: + return 'all' + + @property + def number_of_probes(self): + """Returns the probes value from the monitor string. + + The monitor string for a Require monitor looks like this. + + require 1 from 2 { /Common/tcp } + + This method parses out the first of the numeric values. This values represents + the "probes" value that can be updated in the module. + + Returns: + int: The probes value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+(?P\d+)\s+from' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('probes')) + + @property + def number_of_probers(self): + """Returns the probers value from the monitor string. + + The monitor string for a Require monitor looks like this. + + require 1 from 2 { /Common/tcp } + + This method parses out the first of the numeric values. This values represents + the "probers" value that can be updated in the module. + + Returns: + int: The probers value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+\d+\s+from\s+(?P\d+)\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('probers')) + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + + The monitor string for a Require monitor looks like this. + + min 1 of { /Common/gateway_icmp } + + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('least')) + + @property + def availability_requirements(self): + if self._values['monitors'] is None: + return None + result = dict() + result['type'] = self.availability_requirement_type + result['at_least'] = self.at_least + result['number_of_probers'] = self.number_of_probers + result['number_of_probes'] = self.number_of_probes + return result + + @property + def prober_fallback(self): + if self._values['prober_fallback'] == 'any-available': + return 'any' + return self._values['prober_fallback'] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + want = getattr(self.want, param) + try: + have = getattr(self.have, param) + if want != have: + return want + except AttributeError: + return want + + def _discovery_constraints(self): + if self.want.virtual_server_discovery is None: + virtual_server_discovery = self.have.virtual_server_discovery + else: + virtual_server_discovery = self.want.virtual_server_discovery + + if self.want.link_discovery is None: + link_discovery = self.have.link_discovery + else: + link_discovery = self.want.link_discovery + + if link_discovery in ['enabled', 'enabled-no-delete'] and virtual_server_discovery == 'disabled': + raise F5ModuleError( + "Virtual server discovery must be enabled if link discovery is enabled" + ) + + def _devices_changed(self): + if self.want.devices is None and self.want.server_type is None: + return None + if self.want.devices is None: + devices = self.have.devices + else: + devices = self.want.devices + if self.have.devices is None: + have_devices = [] + else: + have_devices = self.have.devices + if len(devices) == 0: + raise F5ModuleError( + "A GTM server must have at least one device associated with it." + ) + want = [OrderedDict(sorted(d.items())) for d in devices] + have = [OrderedDict(sorted(d.items())) for d in have_devices] + if len(have_devices) > 0: + if self._false_positive(devices, have_devices): + return False + if want != have: + return True + return False + + def _false_positive(self, devices, have_devices): + match = 0 + for w in devices: + for h in have_devices: + if w.items() == h.items(): + match = match + 1 + if match == len(devices): + return True + + def _server_type_changed(self): + if self.want.server_type is None: + self.want.update({'server_type': self.have.server_type}) + if self.want.server_type != self.have.server_type: + return True + return False + + @property + def link_discovery(self): + self._discovery_constraints() + if self.want.link_discovery != self.have.link_discovery: + return self.want.link_discovery + + @property + def virtual_server_discovery(self): + self._discovery_constraints() + if self.want.virtual_server_discovery != self.have.virtual_server_discovery: + return self.want.virtual_server_discovery + + def _handle_current_server_type_and_devices(self, devices_change, server_change): + result = {} + if devices_change: + result['devices'] = self.want.devices + if server_change: + result['server_type'] = self.want.server_type + return result + + def _handle_legacy_server_type_and_devices(self, devices_change, server_change): + result = {} + if server_change and devices_change: + result['devices'] = self.want.devices + if len(self.want.devices) > 1 and self.want.server_type == 'bigip': + if self.have.raw_server_type != 'redundant-bigip': + result['server_type'] = 'redundant-bigip' + elif self.want.server_type == 'bigip': + if self.have.raw_server_type != 'single-bigip': + result['server_type'] = 'single-bigip' + else: + result['server_type'] = self.want.server_type + + elif devices_change: + result['devices'] = self.want.devices + if len(self.want.devices) > 1 and self.have.server_type == 'bigip': + if self.have.raw_server_type != 'redundant-bigip': + result['server_type'] = 'redundant-bigip' + elif self.have.server_type == 'bigip': + if self.have.raw_server_type != 'single-bigip': + result['server_type'] = 'single-bigip' + else: + result['server_type'] = self.want.server_type + + elif server_change: + if len(self.have.devices) > 1 and self.want.server_type == 'bigip': + if self.have.raw_server_type != 'redundant-bigip': + result['server_type'] = 'redundant-bigip' + elif self.want.server_type == 'bigip': + if self.have.raw_server_type != 'single-bigip': + result['server_type'] = 'single-bigip' + else: + result['server_type'] = self.want.server_type + return result + + @property + def server_type_and_devices(self): + """Compares difference between server type and devices list + + These two parameters are linked with each other and, therefore, must be + compared together to ensure that the correct setting is sent to BIG-IP + + :return: + """ + devices_change = self._devices_changed() + server_change = self._server_type_changed() + if not devices_change and not server_change: + return None + tmos = tmos_version(self.client) + if Version(tmos) >= Version('13.0.0'): + result = self._handle_current_server_type_and_devices( + devices_change, server_change + ) + return result + else: + result = self._handle_legacy_server_type_and_devices( + devices_change, server_change + ) + return result + + @property + def state(self): + if self.want.state == 'disabled' and self.have.enabled: + return dict(disabled=True) + elif self.want.state in ['present', 'enabled'] and self.have.disabled: + return dict(enabled=True) + + @property + def monitors(self): + if self.want.monitors is None: + return None + if self.want.monitors == '/Common/bigip' and self.have.monitors == '/Common/bigip': + return None + if self.want.monitors == '/Common/bigip' and self.have.monitors is None: + return None + if self.want.monitors == '/Common/bigip' and len(self.have.monitors) > 0: + return '/Common/bigip' + if self.have.monitors is None: + return self.want.monitors + if self.have.monitors != self.want.monitors: + return self.want.monitors + + @property + def prober_pool(self): + if self.want.prober_pool is None: + return None + if self.have.prober_pool is None: + if self.want.prober_pool == '': + return None + if self.want.prober_pool != self.have.prober_pool: + return self.want.prober_pool + + @property + def prober_preference(self): + if self.want.prober_preference is None: + return None + if self.want.prober_preference == self.have.prober_preference: + return None + if self.want.prober_preference == 'pool' and self.want.prober_pool is None: + raise F5ModuleError( + "A prober_pool needs to be set if prober_preference is set to 'pool'" + ) + if self.want.prober_preference != 'pool' and self.have.prober_preference == 'pool': + if self.want.prober_fallback != 'pool' and self.want.prober_pool != '': + raise F5ModuleError( + "To change prober_preference from {0} to {1}, set prober_pool to an empty string".format( + self.have.prober_preference, + self.want.prober_preference + ) + ) + if self.want.prober_preference == self.want.prober_fallback: + raise F5ModuleError( + "Prober_preference and prober_fallback must not be equal." + ) + if self.want.prober_preference == self.have.prober_fallback: + raise F5ModuleError( + "Cannot set prober_preference to {0} if prober_fallback on device is set to {1}.".format( + self.want.prober_preference, + self.have.prober_fallback + ) + ) + if self.want.prober_preference != self.have.prober_preference: + return self.want.prober_preference + + @property + def prober_fallback(self): + if self.want.prober_fallback is None: + return None + if self.want.prober_fallback == self.have.prober_fallback: + return None + if self.want.prober_fallback == 'pool' and self.want.prober_pool is None: + raise F5ModuleError( + "A prober_pool needs to be set if prober_fallback is set to 'pool'" + ) + if self.want.prober_fallback != 'pool' and self.have.prober_fallback == 'pool': + if self.want.prober_preference != 'pool' and self.want.prober_pool != '': + raise F5ModuleError( + "To change prober_fallback from {0} to {1}, set prober_pool to an empty string".format( + self.have.prober_fallback, + self.want.prober_fallback + ) + ) + if self.want.prober_preference == self.want.prober_fallback: + raise F5ModuleError( + "Prober_preference and prober_fallback must not be equal." + ) + if self.want.prober_fallback == self.have.prober_preference: + raise F5ModuleError( + "Cannot set prober_fallback to {0} if prober_preference on device is set to {1}.".format( + self.want.prober_fallback, + self.have.prober_preference + ) + ) + if self.want.prober_fallback != self.have.prober_fallback: + return self.want.prober_fallback + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def exec_module(self): + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + if self.version_is_less_than('13.0.0'): + manager = self.get_manager('v1') + else: + manager = self.get_manager('v2') + return manager.exec_module() + + def get_manager(self, type): + if type == 'v1': + return V1Manager(**self.kwargs) + elif type == 'v2': + return V2Manager(**self.kwargs) + + def version_is_less_than(self, version): + tmos = tmos_version(self.client) + if Version(tmos) < Version(version): + return True + else: + return False + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.want.update(dict(client=self.client)) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + diff.client = self.client + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ['present', 'enabled', 'disabled']: + changed = self.present() + elif state == "absent": + changed = self.absent() + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _check_link_discovery_requirements(self): + if (self.want.link_discovery in ['enabled', 'enabled-no-delete'] and + self.want.virtual_server_discovery == 'disabled'): + raise F5ModuleError( + "Virtual server discovery must be enabled if link discovery is enabled" + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + if self.want.state == 'disabled': + self.want.update({'disabled': True}) + elif self.want.state in ['present', 'enabled']: + self.want.update({'enabled': True}) + + self.adjust_server_type_by_version() + self.should_update() + + if self.want.devices is None: + raise F5ModuleError( + "You must provide an initial device." + ) + self._assign_creation_defaults() + self.handle_prober_settings() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + if self.exists(): + return True + else: + raise F5ModuleError("Failed to create the server") + + 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/gtm/server/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/server/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/server/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + changed = False + if self.exists(): + changed = self.remove() + return changed + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the server") + return True + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/server/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + +class V1Manager(BaseManager): + def _assign_creation_defaults(self): + if self.want.server_type is None: + if len(self.want.devices) == 0: + raise F5ModuleError( + "You must provide at least one device." + ) + elif len(self.want.devices) == 1: + self.want.update({'server_type': 'single-bigip'}) + else: + self.want.update({'server_type': 'redundant-bigip'}) + if self.want.link_discovery is None: + self.want.update({'link_discovery': 'disabled'}) + if self.want.virtual_server_discovery is None: + self.want.update({'virtual_server_discovery': 'disabled'}) + self._check_link_discovery_requirements() + + def adjust_server_type_by_version(self): + if len(self.want.devices) == 1 and self.want.server_type == 'bigip': + self.want.update({'server_type': 'single-bigip'}) + if len(self.want.devices) > 1 and self.want.server_type == 'bigip': + self.want.update({'server_type': 'redundant-bigip'}) + + def update(self): + self.have = self.read_current_from_device() + self.handle_prober_settings() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def handle_prober_settings(self): + if self.want.prober_preference is not None: + self.want._values.pop('prober_preference') + if self.want.prober_fallback is not None: + self.want._values.pop('prober_fallback') + + +class V2Manager(BaseManager): + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def _assign_creation_defaults(self): + if self.want.server_type is None: + self.want.update({'server_type': 'bigip'}) + if self.want.link_discovery is None: + self.want.update({'link_discovery': 'disabled'}) + if self.want.virtual_server_discovery is None: + self.want.update({'virtual_server_discovery': 'disabled'}) + self._check_link_discovery_requirements() + + def adjust_server_type_by_version(self): + pass + + def handle_prober_settings(self): + if self.want.prober_preference == 'pool' and self.want.prober_pool is None: + raise F5ModuleError( + "A prober_pool needs to be set if prober_preference is set to 'pool'" + ) + if self.want.prober_preference is not None and self.want.prober_fallback is not None: + if self.want.prober_preference == self.want.prober_fallback: + raise F5ModuleError( + "The parameters for prober_preference and prober_fallback must not be the same." + ) + if self.want.prober_fallback == 'pool' and self.want.prober_pool is None: + raise F5ModuleError( + "A prober_pool needs to be set if prober_fallback is set to 'pool'" + ) + + +class ArgumentSpec(object): + def __init__(self): + self.states = ['absent', 'present', 'enabled', 'disabled'] + self.server_types = [ + 'alteon-ace-director', + 'cisco-css', + 'cisco-server-load-balancer', + 'generic-host', + 'radware-wsd', + 'windows-nt-4.0', + 'bigip', + 'cisco-local-director-v2', + 'extreme', + 'generic-load-balancer', + 'sun-solaris', + 'cacheflow', + 'cisco-local-director-v3', + 'foundry-server-iron', + 'netapp', + 'windows-2000-server' + ] + self.supports_check_mode = True + argument_spec = dict( + state=dict( + default='present', + choices=self.states, + ), + name=dict(required=True), + server_type=dict( + choices=self.server_types, + aliases=['product'] + ), + datacenter=dict(), + link_discovery=dict( + choices=['enabled', 'disabled', 'enabled-no-delete'] + ), + virtual_server_discovery=dict( + choices=['enabled', 'disabled', 'enabled-no-delete'] + ), + devices=dict( + type='raw', + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + iquery_options=dict( + type='dict', + options=dict( + allow_path=dict(type='bool'), + allow_service_check=dict(type='bool'), + allow_snmp=dict(type='bool') + ) + ), + availability_requirements=dict( + type='dict', + options=dict( + type=dict( + choices=['all', 'at_least', 'require'], + required=True + ), + at_least=dict(type='int'), + number_of_probes=dict(type='int'), + number_of_probers=dict(type='int') + ), + mutually_exclusive=[ + ['at_least', 'number_of_probes'], + ['at_least', 'number_of_probers'], + ], + required_if=[ + ['type', 'at_least', ['at_least']], + ['type', 'require', ['number_of_probes', 'number_of_probers']] + ] + ), + limits=dict( + type='dict', + options=dict( + bits_enabled=dict(type='bool'), + packets_enabled=dict(type='bool'), + connections_enabled=dict(type='bool'), + cpu_enabled=dict(type='bool'), + memory_enabled=dict(type='bool'), + bits_limit=dict(type='int'), + packets_limit=dict(type='int'), + connections_limit=dict(type='int'), + cpu_limit=dict(type='int'), + memory_limit=dict(type='int'), + ) + ), + monitors=dict( + type='list', + elements='str', + ), + prober_preference=dict( + choices=['inside-datacenter', 'outside-datacenter', 'inherit', 'pool'] + ), + prober_fallback=dict( + choices=['inside-datacenter', 'outside-datacenter', + 'inherit', 'pool', 'any', 'none'] + ), + prober_pool=dict() + + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_topology_record.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_topology_record.py new file mode 100644 index 00000000..1eee6d0a --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_topology_record.py @@ -0,0 +1,1069 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_topology_record +short_description: Manages GTM Topology Records +description: + - Manages GTM (now BIG-IP DNS) Topology Records. Once created, only topology record C(weight) can be modified. +version_added: "1.0.0" +options: + source: + description: + - Specifies the origination of an incoming DNS request. + suboptions: + negate: + description: + - When set to c(yes) the system selects this topology record, when the request source does not match. + type: bool + default: no + subnet: + description: + - An IP address and network mask in the CIDR format. + type: str + region: + description: + - Specifies the name of region already defined in the configuration. + type: str + continent: + description: + - Specifies one of the seven continents, along with the C(Unknown) setting. + - Specifying C(Unknown) forces the system to use a default resolution + if the system cannot determine the location of the local DNS making the request. + - Full continent names and their abbreviated versions are supported. + type: str + country: + description: + - Specifies a country. + - In addition to the country full names, you may also specify their abbreviated + form, such as C(US) instead of C(United States). + - Valid country codes can be found here https://countrycode.org/. + type: str + state: + description: + - Specifies a state in a given country. + - This parameter requires the C(country) option. + type: str + isp: + description: + - Specifies an Internet service provider. + type: str + choices: + - AOL + - BeijingCNC + - CNC + - ChinaEducationNetwork + - ChinaMobilNetwork + - ChinaRailwayTelcom + - ChinaTelecom + - ChinaUnicom + - Comcast + - Earthlink + - ShanghaiCNC + - ShanghaiTelecom + geo_isp: + description: + - Specifies a geolocation ISP. + type: str + type: dict + required: True + destination: + description: + - Specifies where the system directs the incoming DNS request. + suboptions: + negate: + description: + - When set to C(yes) the system selects this topology record, when the request destination does not match. + type: bool + default: no + subnet: + description: + - An IP address and network mask in the CIDR format. + type: str + region: + description: + - Specifies the name of region already defined in the configuration. + type: str + continent: + description: + - Specifies one of the seven continents, along with the C(Unknown) setting. + - Specifying C(Unknown) forces the system to use a default resolution + if the system cannot determine the location of the local DNS making the request. + - Full continent names and their abbreviated versions are supported. + type: str + country: + description: + - Specifies a country. + - Full continent names and their abbreviated versions are supported. + type: str + state: + description: + - Specifies a state in a given country. + - This parameter requires the C(country) option. + type: str + pool: + description: + - Specifies the name of GTM pool already defined in the configuration. + type: str + datacenter: + description: + - Specifies the name of GTM data center already defined in the configuration. + type: str + isp: + description: + - Specifies an Internet service provider. + type: str + choices: + - AOL + - BeijingCNC + - CNC + - ChinaEducationNetwork + - ChinaMobilNetwork + - ChinaRailwayTelcom + - ChinaTelecom + - ChinaUnicom + - Comcast + - Earthlink + - ShanghaiCNC + - ShanghaiTelecom + geo_isp: + description: + - Specifies a geolocation ISP + type: str + type: dict + required: True + weight: + description: + - Specifies the weight of the topology record. + - The system finds the weight of the first topology record that matches the server object (pool or pool member) + and the local DNS. The system then assigns that weight as the topology score for that server object. + - The system load balances to the server object with the highest topology score. + - If the system finds no topology record that matches both the server object and the local DNS, + then the system assigns that server object a zero score. + - If the option is not specified when the record is created the system will set it at a default value of C(1) + - Valid range is (0 - 4294967295) + type: int + partition: + description: + - Device partition to manage resources on. + - Partition parameter is taken into account when used in conjunction with C(pool), C(data_center), + and C(region) parameters, otherwise it is ignored. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures the record exists. + - When C(state) is C(absent), ensures the record is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an IP Subnet and an ISP based topology record + bigip_gtm_topology_record: + source: + - subnet: 192.168.1.0/24 + destination: + - isp: AOL + weight: 10 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a region and a pool based topology record + bigip_gtm_topology_record: + source: + - region: Foo + destination: + - pool: FooPool + partition: FooBar + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a negative region and a negative data center based topology record + bigip_gtm_topology_record: + source: + - region: Baz + - negate: yes + destination: + - datacenter: Baz-DC + - negate: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +weight: + description: The weight of the topology record. + returned: changed + type: int + sample: 20 +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.ipaddress import is_valid_ip_network +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'score': 'weight', + } + + api_attributes = [ + 'score', + ] + + returnables = [ + 'weight', + 'name' + ] + + updatables = [ + 'weight', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + countries = { + 'Afghanistan': 'AF', + 'Aland Islands': 'AX', + 'Albania': 'AL', + 'Algeria': 'DZ', + 'American Samoa': 'AS', + 'Andorra': 'AD', + 'Angola': 'AO', + 'Anguilla': 'AI', + 'Antarctica': 'AQ', + 'Antigua and Barbuda': 'AG', + 'Argentina': 'AR', + 'Armenia': 'AM', + 'Aruba': 'AW', + 'Australia': 'AU', + 'Austria': 'AT', + 'Azerbaijan': 'AZ', + 'Bahamas': 'BS', + 'Bahrain': 'BH', + 'Bangladesh': 'BD', + 'Barbados': 'BB', + 'Belarus': 'BY', + 'Belgium': 'BE', + 'Belize': 'BZ', + 'Benin': 'BJ', + 'Bermuda': 'BM', + 'Bhutan': 'BT', + 'Bolivia': 'BO', + 'Bonaire, Sint Eustatius and Saba': 'BQ', + 'Bosnia and Herzegovina': 'BA', + 'Botswana': 'BW', + 'Bouvet Island': 'BV', + 'Brazil': 'BR', + 'British Indian Ocean Territory': 'IO', + 'Brunei Darussalam': 'BN', + 'Bulgaria': 'BG', + 'Burkina Faso': 'BF', + 'Burundi': 'BI', + 'Cape Verde': 'CV', + 'Cambodia': 'KH', + 'Cameroon': 'CM', + 'Canada': 'CA', + 'Cayman Islands': 'KY', + 'Central African Republic': 'CF', + 'Chad': 'TD', + 'Chile': 'CL', + 'China': 'CN', + 'Christmas Island': 'CX', + 'Cocos (Keeling) Islands': 'CC', + 'Colombia': 'CO', + 'Comoros': 'KM', + 'Congo': 'CG', + 'Congo, The Democratic Republic of the': 'CD', + 'Cook Islands': 'CK', + 'Costa Rica': 'CR', + "Cote D'Ivoire": 'CI', + 'Croatia': 'HR', + 'Cuba': 'CU', + 'Curaçao': 'CW', + 'Cyprus': 'CY', + 'Czech Republic': 'CZ', + 'Denmark': 'DK', + 'Djibouti': 'DJ', + 'Dominica': 'DM', + 'Dominican Republic': 'DO', + 'Ecuador': 'EC', + 'Egypt': 'EG', + 'El Salvador': 'SV', + 'Equatorial Guinea': 'GQ', + 'Eritrea': 'ER', + 'Estonia': 'EE', + 'Ethiopia': 'ET', + 'Falkland Islands (Malvinas)': 'FK', + 'Faroe Islands': 'FO', + 'Fiji': 'FJ', + 'Finland': 'FI', + 'France': 'FR', + 'French Guiana': 'GF', + 'French Polynesia': 'PF', + 'French Southern Territories': 'TF', + 'Gabon': 'GA', + 'Gambia': 'GM', + 'Georgia': 'GE', + 'Germany': 'DE', + 'Ghana': 'GH', + 'Gibraltar': 'GI', + 'Greece': 'GR', + 'Greenland': 'GL', + 'Grenada': 'GD', + 'Guadeloupe': 'GP', + 'Guam': 'GU', + 'Guatemala': 'GT', + 'Guernsey': 'GG', + 'Guinea': 'GN', + 'Guinea-Bissau': 'GW', + 'Guyana': 'GY', + 'Haiti': 'HT', + 'Heard Island and McDonald Islands': 'HM', + 'Holy See (Vatican City State)': 'VA', + 'Honduras': 'HN', + 'Hong Kong': 'HK', + 'Hungary': 'HU', + 'Iceland': 'IS', + 'India': 'IN', + 'Indonesia': 'ID', + 'Iran, Islamic Republic of': 'IR', + 'Iraq': 'IQ', + 'Ireland': 'IE', + 'Isle of Man': 'IM', + 'Israel': 'IL', + 'Italy': 'IT', + 'Jamaica': 'JM', + 'Japan': 'JP', + 'Jersey': 'JE', + 'Jordan': 'JO', + 'Kazakhstan': 'KZ', + 'Kenya': 'KE', + 'Kiribati': 'KI', + "Korea, Democratic People's Republic of": 'KP', + 'Korea, Republic of': 'KR', + 'Kuwait': 'KW', + 'Kyrgyzstan': 'KG', + "Lao People's Democratic Republic": 'LA', + 'Latvia': 'LV', + 'Lebanon': 'LB', + 'Lesotho': 'LS', + 'Liberia': 'LR', + 'Libyan Arab Jamahiriya': 'LY', + 'Liechtenstein': 'LI', + 'Lithuania': 'LT', + 'Luxembourg': 'LU', + 'Macau': 'MO', + 'Macedonia': 'MK', + 'Madagascar': 'MG', + 'Malawi': 'MW', + 'Malaysia': 'MY', + 'Maldives': 'MV', + 'Mali': 'ML', + 'Malta': 'MT', + 'Marshall Islands': 'MH', + 'Martinique': 'MQ', + 'Mauritania': 'MR', + 'Mauritius': 'MU', + 'Mayotte': 'YT', + 'Mexico': 'MX', + 'Micronesia, Federated States of': 'FM', + 'Moldova, Republic of': 'MD', + 'Monaco': 'MC', + 'Mongolia': 'MN', + 'Montenegro': 'ME', + 'Montserrat': 'MS', + 'Morocco': 'MA', + 'Mozambique': 'MZ', + 'Myanmar': 'MM', + 'Namibia': 'NA', + 'Nauru': 'NR', + 'Nepal': 'NP', + 'Netherlands': 'NL', + 'New Caledonia': 'NC', + 'New Zealand': 'NZ', + 'Nicaragua': 'NI', + 'Niger': 'NE', + 'Nigeria': 'NG', + 'Niue': 'NU', + 'Norfolk Island': 'NF', + 'Northern Mariana Islands': 'MP', + 'Norway': 'NO', + 'Oman': 'OM', + 'Pakistan': 'PK', + 'Palau': 'PW', + 'Palestinian Territory': 'PS', + 'Panama': 'PA', + 'Papua New Guinea': 'PG', + 'Paraguay': 'PY', + 'Peru': 'PE', + 'Philippines': 'PH', + 'Pitcairn Islands': 'PN', + 'Poland': 'PL', + 'Portugal': 'PT', + 'Puerto Rico': 'PR', + 'Qatar': 'QA', + 'Reunion': 'RE', + 'Romania': 'RO', + 'Russian Federation': 'RU', + 'Rwanda': 'RW', + 'Saint Barthelemy': 'BL', + 'Saint Helena': 'SH', + 'Saint Kitts and Nevis': 'KN', + 'Saint Lucia': 'LC', + 'Saint Martin': 'MF', + 'Saint Pierre and Miquelon': 'PM', + 'Saint Vincent and the Grenadines': 'VC', + 'Samoa': 'WS', + 'San Marino': 'SM', + 'Sao Tome and Principe': 'ST', + 'Saudi Arabia': 'SA', + 'Senegal': 'SN', + 'Serbia': 'RS', + 'Seychelles': 'SC', + 'Sierra Leone': 'SL', + 'Singapore': 'SG', + 'Sint Maarten (Dutch part)': 'SX', + 'Slovakia': 'SK', + 'Slovenia': 'SI', + 'Solomon Islands': 'SB', + 'Somalia': 'SO', + 'South Africa': 'ZA', + 'South Georgia and the South Sandwich Islands': 'GS', + 'South Sudan': 'SS', + 'Spain': 'ES', + 'Sri Lanka': 'LK', + 'Sudan': 'SD', + 'Suriname': 'SR', + 'Svalbard and Jan Mayen': 'SJ', + 'Swaziland': 'SZ', + 'Sweden': 'SE', + 'Switzerland': 'CH', + 'Syrian Arab Republic': 'SY', + 'Taiwan': 'TW', + 'Tajikistan': 'TJ', + 'Tanzania, United Republic of': 'TZ', + 'Thailand': 'TH', + 'Timor-Leste': 'TL', + 'Togo': 'TG', + 'Tokelau': 'TK', + 'Tonga': 'TO', + 'Trinidad and Tobago': 'TT', + 'Tunisia': 'TN', + 'Turkey': 'TR', + 'Turkmenistan': 'TM', + 'Turks and Caicos Islands': 'TC', + 'Tuvalu': 'TV', + 'Uganda': 'UG', + 'Ukraine': 'UA', + 'United Arab Emirates': 'AE', + 'United Kingdom': 'GB', + 'United States': 'US', + 'United States Minor Outlying Islands': 'UM', + 'Uruguay': 'UY', + 'Uzbekistan': 'UZ', + 'Vanuatu': 'VU', + 'Venezuela': 'VE', + 'Vietnam': 'VN', + 'Virgin Islands, British': 'VG', + 'Virgin Islands, U.S.': 'VI', + 'Wallis and Futuna': 'WF', + 'Western Sahara': 'EH', + 'Yemen': 'YE', + 'Zambia': 'ZM', + 'Zimbabwe': 'ZW', + 'Unrecognized': 'N/A', + 'Asia/Pacific Region': 'AP', + 'Europe': 'EU', + 'Netherlands Antilles': 'AN', + 'France, Metropolitan': 'FX', + 'Anonymous Proxy': 'A1', + 'Satellite Provider': 'A2', + 'Other': 'O1', + } + + continents = { + 'Antarctica': 'AN', + 'Asia': 'AS', + 'Africa': 'AF', + 'Europe': 'EU', + 'North America': 'NA', + 'South America': 'SA', + 'Oceania': 'OC', + 'Unknown': '--', + } + + @property + def src_negate(self): + src_negate = self._values['source'].get('negate', None) + result = flatten_boolean(src_negate) + if result == 'yes': + return 'not' + return None + + @property + def src_subnet(self): + src_subnet = self._values['source'].get('subnet', None) + if src_subnet is None: + return None + if is_valid_ip_network(src_subnet): + return src_subnet + raise F5ModuleError( + "Specified 'subnet' is not a valid subnet." + ) + + @property + def src_region(self): + src_region = self._values['source'].get('region', None) + if src_region is None: + return None + return fq_name(self.partition, src_region) + + @property + def src_continent(self): + src_continent = self._values['source'].get('continent', None) + if src_continent is None: + return None + result = self.continents.get(src_continent, src_continent) + return result + + @property + def src_country(self): + src_country = self._values['source'].get('country', None) + if src_country is None: + return None + result = self.countries.get(src_country, src_country) + return result + + @property + def src_state(self): + src_country = self._values['source'].get('country', None) + src_state = self._values['source'].get('state', None) + if src_state is None: + return None + if src_country is None: + raise F5ModuleError( + 'Country needs to be provided when specifying state' + ) + result = '{0}/{1}'.format(src_country, src_state) + return result + + @property + def src_isp(self): + src_isp = self._values['source'].get('isp', None) + if src_isp is None: + return None + return fq_name('Common', src_isp) + + @property + def src_geo_isp(self): + src_geo_isp = self._values['source'].get('geo_isp', None) + return src_geo_isp + + @property + def dst_negate(self): + dst_negate = self._values['destination'].get('negate', None) + result = flatten_boolean(dst_negate) + if result == 'yes': + return 'not' + return None + + @property + def dst_subnet(self): + dst_subnet = self._values['destination'].get('subnet', None) + if dst_subnet is None: + return None + if is_valid_ip_network(dst_subnet): + return dst_subnet + raise F5ModuleError( + "Specified 'subnet' is not a valid subnet." + ) + + @property + def dst_region(self): + dst_region = self._values['destination'].get('region', None) + if dst_region is None: + return None + return fq_name(self.partition, dst_region) + + @property + def dst_continent(self): + dst_continent = self._values['destination'].get('continent', None) + if dst_continent is None: + return None + result = self.continents.get(dst_continent, dst_continent) + return result + + @property + def dst_country(self): + dst_country = self._values['destination'].get('country', None) + if dst_country is None: + return None + result = self.countries.get(dst_country, dst_country) + return result + + @property + def dst_state(self): + dst_country = self.dst_country + dst_state = self._values['destination'].get('state', None) + if dst_state is None: + return None + if dst_country is None: + raise F5ModuleError( + 'Country needs to be provided when specifying state' + ) + result = '{0}/{1}'.format(dst_country, dst_state) + return result + + @property + def dst_isp(self): + dst_isp = self._values['destination'].get('isp', None) + if dst_isp is None: + return None + return fq_name('Common', dst_isp) + + @property + def dst_geo_isp(self): + dst_geo_isp = self._values['destination'].get('geo_isp', None) + return dst_geo_isp + + @property + def dst_pool(self): + dst_pool = self._values['destination'].get('pool', None) + if dst_pool is None: + return None + return fq_name(self.partition, dst_pool) + + @property + def dst_datacenter(self): + dst_datacenter = self._values['destination'].get('datacenter', None) + if dst_datacenter is None: + return None + return fq_name(self.partition, dst_datacenter) + + @property + def source(self): + options = { + 'negate': self.src_negate, + 'subnet': self.src_subnet, + 'region': self.src_region, + 'continent': self.src_continent, + 'country': self.src_country, + 'state': self.src_state, + 'isp': self.src_isp, + 'geoip-isp': self.src_geo_isp, + } + result = 'ldns: {0}'.format(self._format_options(options)) + return result + + @property + def destination(self): + options = { + 'negate': self.dst_negate, + 'subnet': self.dst_subnet, + 'region': self.dst_region, + 'continent': self.dst_continent, + 'country': self.dst_country, + 'state': self.dst_state, + 'datacenter': self.dst_datacenter, + 'pool': self.dst_pool, + 'isp': self.dst_isp, + 'geoip-isp': self.dst_geo_isp, + } + result = 'server: {0}'.format(self._format_options(options)) + return result + + @property + def name(self): + result = '{0} {1}'.format(self.source, self.destination) + return result + + def _format_options(self, options): + negate = None + cleaned = dict((k, v) for k, v in iteritems(options) if v is not None) + if 'country' in cleaned.keys() and 'state' in cleaned.keys(): + del cleaned['country'] + if 'negate' in cleaned.keys(): + negate = cleaned['negate'] + del cleaned['negate'] + name, value = cleaned.popitem() + if negate: + result = '{0} {1} {2}'.format(negate, name, value) + return result + result = '{0} {1}'.format(name, value) + return result + + @property + def weight(self): + weight = self._values['weight'] + if weight is None: + return None + if 0 <= weight <= 4294967295: + return weight + raise F5ModuleError( + "Valid weight must be in range 0 - 4294967295" + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + name = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + name.replace(' ', '%20').replace('/', '~') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + name = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + name.replace(' ', '%20').replace('/', '~') + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + name = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + name.replace(' ', '%20').replace('/', '~') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + name = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + name.replace(' ', '%20').replace('/', '~') + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.choices = [ + 'AOL', 'BeijingCNC', 'CNC', 'ChinaEducationNetwork', + 'ChinaMobilNetwork', 'ChinaRailwayTelcom', 'ChinaTelecom', + 'ChinaUnicom', 'Comcast', 'Earthlink', 'ShanghaiCNC', + 'ShanghaiTelecom', + ] + argument_spec = dict( + source=dict( + required=True, + type='dict', + options=dict( + subnet=dict(), + region=dict(), + continent=dict(), + country=dict(), + state=dict(), + isp=dict( + choices=self.choices + ), + geo_isp=dict(), + negate=dict( + type='bool', + default='no' + ), + ), + mutually_exclusive=[ + ['subnet', 'region', 'continent', 'country', 'isp', 'geo_isp'] + ] + ), + destination=dict( + required=True, + type='dict', + options=dict( + subnet=dict(), + region=dict(), + continent=dict(), + country=dict(), + state=dict(), + pool=dict(), + datacenter=dict(), + isp=dict( + choices=self.choices + ), + geo_isp=dict(), + negate=dict( + type='bool', + default='no' + ), + ), + mutually_exclusive=[ + ['subnet', 'region', 'continent', 'country', 'pool', 'datacenter', 'isp', 'geo_isp'] + ] + ), + weight=dict(type='int'), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_topology_region.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_topology_region.py new file mode 100644 index 00000000..4b10453d --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_topology_region.py @@ -0,0 +1,880 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_topology_region +short_description: Manages GTM Topology Regions +description: + - Manages GTM (now BIG-IP DNS) Topology Regions. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the region. + type: str + required: True + region_members: + description: + - Specifies the list of region members. + - This list of members is all or nothing, in order to add or remove a member, + you must specify the entire list of members. + - The list will override what is on the device, if different. + - If you specify an empty list, the region members list is removed. + type: list + elements: dict + suboptions: + negate: + description: + - When set to c(yes), the system selects this topology region when the request source does not match. + - Only a single list entry can be specified together with negate. + type: bool + default: no + subnet: + description: + - An IP address and network mask in the CIDR format. + type: str + region: + description: + - Specifies the name of region already defined in the configuration. + type: str + continent: + description: + - Specifies one of the seven continents, along with the C(Unknown) setting. + - Specifying C(Unknown) forces the system to use a default resolution + if the system cannot determine the location of the local DNS making the request. + - Full continent names and their abbreviated versions are supported. + type: str + country: + description: + - The country name or code to use. + - In addition to the country full names, you may also specify their abbreviated + form, such as C(US) instead of C(United States). + - Valid country codes can be found here https://countrycode.org/. + type: str + state: + description: + - Specifies a state in a given country. + type: str + pool: + description: + - Specifies the name of the GTM pool already defined in the configuration. + type: str + datacenter: + description: + - Specifies the name of the GTM data center already defined in the configuration. + type: str + isp: + description: + - Specifies an Internet service provider. + type: str + choices: + - AOL + - BeijingCNC + - CNC + - ChinaEducationNetwork + - ChinaMobilNetwork + - ChinaRailwayTelcom + - ChinaTelecom + - ChinaUnicom + - Comcast + - Earthlink + - ShanghaiCNC + - ShanghaiTelecom + geo_isp: + description: + - Specifies a geolocation ISP. + type: str + partition: + description: + - Device partition to manage resources on. + - Partition parameter is also taken into account when used in conjunction with C(pool), C(data_center), + and C(region) parameters. + type: str + default: Common + state: + description: + - When C(state) is C(present), ensures the region exists. + - When C(state) is C(absent), ensures the region is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + + +EXAMPLES = r''' +- name: Create topology region + bigip_gtm_topology_region: + name: foobar + region_members: + - country: CN + negate: yes + - datacenter: baz + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify topology region + bigip_gtm_topology_region: + name: foobar + region_members: + - continent: EU + - country: PL + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +name: + description: The name value of the GTM region. + returned: changed + type: str + sample: foobar +region_members: + description: The list of members of the GTM region. + returned: changed + type: list + sample: [{"continent": "EU"}, {"country": "PL"}] +''' + +import copy +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.ipaddress import is_valid_ip_network +from ..module_utils.compare import cmp_simple_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'regionMembers': 'region_members', + } + + api_attributes = [ + 'regionMembers', + ] + + returnables = [ + 'region_members', + ] + + updatables = [ + 'region_members', + ] + + +class ApiParameters(Parameters): + @property + def region_members(self): + members = self._values['region_members'] + if members is None: + return None + result = [member['name'] for member in members] + return result + + +class ModuleParameters(Parameters): + countries = { + 'Afghanistan': 'AF', + 'Aland Islands': 'AX', + 'Albania': 'AL', + 'Algeria': 'DZ', + 'American Samoa': 'AS', + 'Andorra': 'AD', + 'Angola': 'AO', + 'Anguilla': 'AI', + 'Antarctica': 'AQ', + 'Antigua and Barbuda': 'AG', + 'Argentina': 'AR', + 'Armenia': 'AM', + 'Aruba': 'AW', + 'Australia': 'AU', + 'Austria': 'AT', + 'Azerbaijan': 'AZ', + 'Bahamas': 'BS', + 'Bahrain': 'BH', + 'Bangladesh': 'BD', + 'Barbados': 'BB', + 'Belarus': 'BY', + 'Belgium': 'BE', + 'Belize': 'BZ', + 'Benin': 'BJ', + 'Bermuda': 'BM', + 'Bhutan': 'BT', + 'Bolivia': 'BO', + 'Bonaire, Sint Eustatius and Saba': 'BQ', + 'Bosnia and Herzegovina': 'BA', + 'Botswana': 'BW', + 'Bouvet Island': 'BV', + 'Brazil': 'BR', + 'British Indian Ocean Territory': 'IO', + 'Brunei Darussalam': 'BN', + 'Bulgaria': 'BG', + 'Burkina Faso': 'BF', + 'Burundi': 'BI', + 'Cape Verde': 'CV', + 'Cambodia': 'KH', + 'Cameroon': 'CM', + 'Canada': 'CA', + 'Cayman Islands': 'KY', + 'Central African Republic': 'CF', + 'Chad': 'TD', + 'Chile': 'CL', + 'China': 'CN', + 'Christmas Island': 'CX', + 'Cocos (Keeling) Islands': 'CC', + 'Colombia': 'CO', + 'Comoros': 'KM', + 'Congo': 'CG', + 'Congo, The Democratic Republic of the': 'CD', + 'Cook Islands': 'CK', + 'Costa Rica': 'CR', + "Cote D'Ivoire": 'CI', + 'Croatia': 'HR', + 'Cuba': 'CU', + 'Curaçao': 'CW', + 'Cyprus': 'CY', + 'Czech Republic': 'CZ', + 'Denmark': 'DK', + 'Djibouti': 'DJ', + 'Dominica': 'DM', + 'Dominican Republic': 'DO', + 'Ecuador': 'EC', + 'Egypt': 'EG', + 'El Salvador': 'SV', + 'Equatorial Guinea': 'GQ', + 'Eritrea': 'ER', + 'Estonia': 'EE', + 'Ethiopia': 'ET', + 'Falkland Islands (Malvinas)': 'FK', + 'Faroe Islands': 'FO', + 'Fiji': 'FJ', + 'Finland': 'FI', + 'France': 'FR', + 'French Guiana': 'GF', + 'French Polynesia': 'PF', + 'French Southern Territories': 'TF', + 'Gabon': 'GA', + 'Gambia': 'GM', + 'Georgia': 'GE', + 'Germany': 'DE', + 'Ghana': 'GH', + 'Gibraltar': 'GI', + 'Greece': 'GR', + 'Greenland': 'GL', + 'Grenada': 'GD', + 'Guadeloupe': 'GP', + 'Guam': 'GU', + 'Guatemala': 'GT', + 'Guernsey': 'GG', + 'Guinea': 'GN', + 'Guinea-Bissau': 'GW', + 'Guyana': 'GY', + 'Haiti': 'HT', + 'Heard Island and McDonald Islands': 'HM', + 'Holy See (Vatican City State)': 'VA', + 'Honduras': 'HN', + 'Hong Kong': 'HK', + 'Hungary': 'HU', + 'Iceland': 'IS', + 'India': 'IN', + 'Indonesia': 'ID', + 'Iran, Islamic Republic of': 'IR', + 'Iraq': 'IQ', + 'Ireland': 'IE', + 'Isle of Man': 'IM', + 'Israel': 'IL', + 'Italy': 'IT', + 'Jamaica': 'JM', + 'Japan': 'JP', + 'Jersey': 'JE', + 'Jordan': 'JO', + 'Kazakhstan': 'KZ', + 'Kenya': 'KE', + 'Kiribati': 'KI', + "Korea, Democratic People's Republic of": 'KP', + 'Korea, Republic of': 'KR', + 'Kuwait': 'KW', + 'Kyrgyzstan': 'KG', + "Lao People's Democratic Republic": 'LA', + 'Latvia': 'LV', + 'Lebanon': 'LB', + 'Lesotho': 'LS', + 'Liberia': 'LR', + 'Libyan Arab Jamahiriya': 'LY', + 'Liechtenstein': 'LI', + 'Lithuania': 'LT', + 'Luxembourg': 'LU', + 'Macau': 'MO', + 'Macedonia': 'MK', + 'Madagascar': 'MG', + 'Malawi': 'MW', + 'Malaysia': 'MY', + 'Maldives': 'MV', + 'Mali': 'ML', + 'Malta': 'MT', + 'Marshall Islands': 'MH', + 'Martinique': 'MQ', + 'Mauritania': 'MR', + 'Mauritius': 'MU', + 'Mayotte': 'YT', + 'Mexico': 'MX', + 'Micronesia, Federated States of': 'FM', + 'Moldova, Republic of': 'MD', + 'Monaco': 'MC', + 'Mongolia': 'MN', + 'Montenegro': 'ME', + 'Montserrat': 'MS', + 'Morocco': 'MA', + 'Mozambique': 'MZ', + 'Myanmar': 'MM', + 'Namibia': 'NA', + 'Nauru': 'NR', + 'Nepal': 'NP', + 'Netherlands': 'NL', + 'New Caledonia': 'NC', + 'New Zealand': 'NZ', + 'Nicaragua': 'NI', + 'Niger': 'NE', + 'Nigeria': 'NG', + 'Niue': 'NU', + 'Norfolk Island': 'NF', + 'Northern Mariana Islands': 'MP', + 'Norway': 'NO', + 'Oman': 'OM', + 'Pakistan': 'PK', + 'Palau': 'PW', + 'Palestinian Territory': 'PS', + 'Panama': 'PA', + 'Papua New Guinea': 'PG', + 'Paraguay': 'PY', + 'Peru': 'PE', + 'Philippines': 'PH', + 'Pitcairn Islands': 'PN', + 'Poland': 'PL', + 'Portugal': 'PT', + 'Puerto Rico': 'PR', + 'Qatar': 'QA', + 'Reunion': 'RE', + 'Romania': 'RO', + 'Russian Federation': 'RU', + 'Rwanda': 'RW', + 'Saint Barthelemy': 'BL', + 'Saint Helena': 'SH', + 'Saint Kitts and Nevis': 'KN', + 'Saint Lucia': 'LC', + 'Saint Martin': 'MF', + 'Saint Pierre and Miquelon': 'PM', + 'Saint Vincent and the Grenadines': 'VC', + 'Samoa': 'WS', + 'San Marino': 'SM', + 'Sao Tome and Principe': 'ST', + 'Saudi Arabia': 'SA', + 'Senegal': 'SN', + 'Serbia': 'RS', + 'Seychelles': 'SC', + 'Sierra Leone': 'SL', + 'Singapore': 'SG', + 'Sint Maarten (Dutch part)': 'SX', + 'Slovakia': 'SK', + 'Slovenia': 'SI', + 'Solomon Islands': 'SB', + 'Somalia': 'SO', + 'South Africa': 'ZA', + 'South Georgia and the South Sandwich Islands': 'GS', + 'South Sudan': 'SS', + 'Spain': 'ES', + 'Sri Lanka': 'LK', + 'Sudan': 'SD', + 'Suriname': 'SR', + 'Svalbard and Jan Mayen': 'SJ', + 'Swaziland': 'SZ', + 'Sweden': 'SE', + 'Switzerland': 'CH', + 'Syrian Arab Republic': 'SY', + 'Taiwan': 'TW', + 'Tajikistan': 'TJ', + 'Tanzania, United Republic of': 'TZ', + 'Thailand': 'TH', + 'Timor-Leste': 'TL', + 'Togo': 'TG', + 'Tokelau': 'TK', + 'Tonga': 'TO', + 'Trinidad and Tobago': 'TT', + 'Tunisia': 'TN', + 'Turkey': 'TR', + 'Turkmenistan': 'TM', + 'Turks and Caicos Islands': 'TC', + 'Tuvalu': 'TV', + 'Uganda': 'UG', + 'Ukraine': 'UA', + 'United Arab Emirates': 'AE', + 'United Kingdom': 'GB', + 'United States': 'US', + 'United States Minor Outlying Islands': 'UM', + 'Uruguay': 'UY', + 'Uzbekistan': 'UZ', + 'Vanuatu': 'VU', + 'Venezuela': 'VE', + 'Vietnam': 'VN', + 'Virgin Islands, British': 'VG', + 'Virgin Islands, U.S.': 'VI', + 'Wallis and Futuna': 'WF', + 'Western Sahara': 'EH', + 'Yemen': 'YE', + 'Zambia': 'ZM', + 'Zimbabwe': 'ZW', + 'Unrecognized': 'N/A', + 'Asia/Pacific Region': 'AP', + 'Europe': 'EU', + 'Netherlands Antilles': 'AN', + 'France, Metropolitan': 'FX', + 'Anonymous Proxy': 'A1', + 'Satellite Provider': 'A2', + 'Other': 'O1', + } + + continents = { + 'Antarctica': 'AN', + 'Asia': 'AS', + 'Africa': 'AF', + 'Europe': 'EU', + 'North America': 'NA', + 'South America': 'SA', + 'Oceania': 'OC', + 'Unknown': '--', + } + + @property + def region_members(self): + result = list() + negate = None + if self._values['region_members'] is None: + return None + if not self._values['region_members']: + return 'none' + members = copy.deepcopy(self._values['region_members']) + for item in members: + member = self._filter_params(item) + if 'negate' in member.keys(): + if len(member.keys()) > 2: + raise F5ModuleError( + 'You cannot specify negate and more than one option together.' + ) + + negate = self._flatten_negate(member) + + for key, value in iteritems(member): + if negate: + output = self._change_value(key, value) + item = "{0} {1} {2}".format(negate, output[0], output[1]) + result.append(item) + negate = None + else: + output = self._change_value(key, value) + item = "{0} {1}".format(output[0], output[1]) + result.append(item) + return result + + def _flatten_negate(self, item): + result = flatten_boolean(item['negate']) + item.pop('negate') + if result == 'yes': + return 'not' + return None + + def _change_value(self, key, value): + if key in ['region', 'pool', 'datacenter']: + return key, fq_name(self.partition, value) + if key == 'isp': + return key, fq_name('Common', value) + if key == 'continent': + return key, self.continents.get(value, value) + if key == 'country': + return key, self.countries.get(value, value) + if key == 'geo_isp': + return 'geoip-isp', value + if key == 'subnet': + return key, self._test_subnet(value) + return key, value + + def _test_subnet(self, item): + if item is None: + return None + if is_valid_ip_network(item): + return item + raise F5ModuleError( + "Specified 'subnet' is not a valid subnet." + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def region_members(self): + members = self._values['region_members'] + if members is None: + return None + if not members: + return 'none' + return ' '.join(members) + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def region_members(self): + return cmp_simple_list(self.want.region_members, self.have.region_members) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/region/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + if self.changes.region_members: + command = 'tmsh create gtm region {0} region-members add {{ {1} }} '.format( + fq_name(self.want.partition, self.want.name), + self.changes.region_members + ) + else: + command = 'tmsh create gtm region {0}'.format( + fq_name(self.want.partition, self.want.name) + ) + payload = { + "command": "run", + "utilCmdArgs": '-c "{0}"'.format(command) + } + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=payload) + try: + response = resp.json() + if 'commandResult' in response: + raise F5ModuleError(response['commandResult']) + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + param = self.changes.region_members + if param: + if param != 'none': + command = 'tmsh modify gtm region {0} region-members replace-all-with {{ {1} }} '.format( + fq_name(self.want.partition, self.want.name), + param + ) + else: + command = 'tmsh modify gtm region {0} region-members {1} '.format( + fq_name(self.want.partition, self.want.name), + param + ) + else: + command = 'tmsh create gtm region {0}'.format( + fq_name(self.want.partition, self.want.name) + ) + + payload = { + "command": "run", + "utilCmdArgs": '-c "{0}"'.format(command) + } + + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=payload) + try: + response = resp.json() + if 'commandResult' in response: + raise F5ModuleError(response['commandResult']) + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/region/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/region/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.choices = [ + 'AOL', 'BeijingCNC', 'CNC', 'ChinaEducationNetwork', + 'ChinaMobilNetwork', 'ChinaRailwayTelcom', 'ChinaTelecom', + 'ChinaUnicom', 'Comcast', 'Earthlink', 'ShanghaiCNC', + 'ShanghaiTelecom', + ] + argument_spec = dict( + name=dict( + required=True + ), + region_members=dict( + type='list', + elements='dict', + options=dict( + subnet=dict(), + region=dict(), + continent=dict(), + country=dict(), + state=dict(), + pool=dict(), + datacenter=dict(), + isp=dict( + choices=self.choices + ), + geo_isp=dict(), + negate=dict( + type='bool', + default='no' + ), + ) + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_virtual_server.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_virtual_server.py new file mode 100644 index 00000000..0c5c7b82 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_virtual_server.py @@ -0,0 +1,1189 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_virtual_server +short_description: Manages F5 BIG-IP GTM virtual servers +description: + - Manages F5 BIG-IP GTM (now BIG-IP DNS) virtual servers. A GTM server can have many virtual servers + associated with it. They are arranged in much the same way that pool members are + to pools. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the virtual server. + type: str + required: True + server_name: + description: + - Specifies the name of the server the virtual server is associated with. + type: str + required: True + address: + description: + - Specifies the IP Address of the virtual server. + - When creating a new GTM virtual server, this parameter is required. + type: str + port: + description: + - Specifies the service port number for the virtual server or pool member. For example, + the HTTP service is typically port 80. + - To specify all ports, use an C(*). + - When creating a new GTM virtual server, if this parameter is not specified, a + default of C(*) will be used. + type: int + translation_address: + description: + - Specifies the translation IP address for the virtual server. + - To unset this parameter, use an empty string (C("")) as a value. + - When creating a new GTM virtual server, if this parameter is not specified, a + default of C(::) will be used. + type: str + translation_port: + description: + - Specifies the translation port number or service name for the virtual server. + - To specify all ports, use an C(*). + - When creating a new GTM virtual server, if this parameter is not specified, a + default of C(*) will be used. + type: str + availability_requirements: + description: + - If you activate more than one health monitor, specifies the number of health + monitors that must receive successful responses in order for the link to be + considered available. + type: dict + suboptions: + type: + description: + - Monitor rule type when C(monitors) is specified. + - When creating a new virtual, if this value is not specified, the default of C(all) will be used. + type: str + required: True + choices: + - all + - at_least + - require + at_least: + description: + - Specifies the minimum number of active health monitors that must be successful + before the link is considered up. + - This parameter is only relevant when a C(type) of C(at_least) is used. + - This parameter will be ignored if a type of either C(all) or C(require) is used. + type: int + number_of_probes: + description: + - Specifies the minimum number of probes that must succeed for this server to be declared up. + - When creating a new virtual server, if this parameter is specified, then the C(number_of_probers) + parameter must also be specified. + - The value of this parameter should always be B(lower) than or B(equal to) + the value of C(number_of_probers). + - This parameter is only relevant when a C(type) of C(require) is used. + - This parameter will be ignored if a type of either C(all) or C(at_least) is used. + type: int + number_of_probers: + description: + - Specifies the number of probers that should be used when running probes. + - When creating a new virtual server, if this parameter is specified, the C(number_of_probes) + parameter must also be specified. + - The value of this parameter should always be B(higher) than or B(equal to) + the value of C(number_of_probers). + - This parameter is only relevant when a C(type) of C(require) is used. + - This parameter will be ignored if a type of either C(all) or C(at_least) is used. + type: int + monitors: + description: + - Specifies the health monitors the system currently uses to monitor this resource. + - When C(availability_requirements.type) is C(require), you may only have a single monitor in the + C(monitors) list. + type: list + elements: str + virtual_server_dependencies: + description: + - Specifies the virtual servers on which the current virtual server depends. + - If any of the specified servers are unavailable, the current virtual server is also listed as unavailable. + type: list + elements: dict + suboptions: + server: + description: + - Server which the dependant virtual server is part of. + type: str + required: True + virtual_server: + description: + - Virtual server to depend on. + type: str + required: True + link: + description: + - Specifies a link to assign to the server or virtual server. + type: str + limits: + description: + - Specifies resource thresholds or limit requirements at the server level. + - When you enable one or more limit settings, the system then uses that data to take servers in and out + of service. + - You can define limits for any or all of the limit settings. However, when a server does not meet the resource + threshold limit requirement, the system marks the entire server as unavailable and directs load balancing + traffic to another resource. + - The limit settings available depend on the type of server. + type: dict + suboptions: + bits_enabled: + description: + - Whether the bits limit is enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + packets_enabled: + description: + - Whether the packets limit is enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + connections_enabled: + description: + - Whether the current connections limit is enabled or not. + - This parameter allows you to switch on or off the effect of the limit. + type: bool + bits_limit: + description: + - Specifies the maximum allowable data throughput rate + for the virtual servers on the server, in bits per second. + - If the network traffic volume exceeds this limit, the system marks the server as unavailable. + type: int + packets_limit: + description: + - Specifies the maximum allowable data transfer rate + for the virtual servers on the server, in packets per second. + - If the network traffic volume exceeds this limit, the system marks the server as unavailable. + type: int + connections_limit: + description: + - Specifies the maximum number of concurrent connections, combined, + for all of the virtual servers on the server. + - If the connections exceed this limit, the system marks the server as unavailable. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + - enabled + - disabled + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Enable virtual server + bigip_gtm_virtual_server: + server_name: server1 + name: my-virtual-server + state: enabled + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +server_name: + description: The server name associated with the virtual server. + returned: changed + type: str + sample: /Common/my-gtm-server +address: + description: The new address of the resource. + returned: changed + type: str + sample: 1.2.3.4 +port: + description: The new port of the resource. + returned: changed + type: int + sample: 500 +translation_address: + description: The new translation address of the resource. + returned: changed + type: int + sample: 500 +translation_port: + description: The new translation port of the resource. + returned: changed + type: int + sample: 500 +availability_requirements: + description: The new availability requirement configurations for the resource. + returned: changed + type: dict + sample: {'type': 'all'} +monitors: + description: The new list of monitors for the resource. + returned: changed + type: list + sample: ['/Common/monitor1', '/Common/monitor2'] +virtual_server_dependencies: + description: The new list of virtual server dependencies for the resource. + returned: changed + type: list + sample: ['/Common/vs1', '/Common/vs2'] +link: + description: The new link value for the resource. + returned: changed + type: str + sample: /Common/my-link +limits: + description: The new limit configurations for the resource. + returned: changed + type: dict + sample: { 'bits_enabled': true, 'bits_limit': 100 } +''' + +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ipaddress import ip_address + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import compare_complex_list +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.ipaddress import ( + validate_ip_v6_address, is_valid_ip +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'limitMaxBps': 'bits_limit', + 'limitMaxBpsStatus': 'bits_enabled', + 'limitMaxConnections': 'connections_limit', + 'limitMaxConnectionsStatus': 'connections_enabled', + 'limitMaxPps': 'packets_limit', + 'limitMaxPpsStatus': 'packets_enabled', + 'translationAddress': 'translation_address', + 'translationPort': 'translation_port', + 'dependsOn': 'virtual_server_dependencies', + 'explicitLinkName': 'link', + 'monitor': 'monitors' + } + + api_attributes = [ + 'dependsOn', + 'destination', + 'disabled', + 'enabled', + 'explicitLinkName', + 'limitMaxBps', + 'limitMaxBpsStatus', + 'limitMaxConnections', + 'limitMaxConnectionsStatus', + 'limitMaxPps', + 'limitMaxPpsStatus', + 'translationAddress', + 'translationPort', + 'monitor', + ] + + returnables = [ + 'bits_enabled', + 'bits_limit', + 'connections_enabled', + 'connections_limit', + 'destination', + 'disabled', + 'enabled', + 'link', + 'monitors', + 'packets_enabled', + 'packets_limit', + 'translation_address', + 'translation_port', + 'virtual_server_dependencies', + 'availability_requirements', + ] + + updatables = [ + 'bits_enabled', + 'bits_limit', + 'connections_enabled', + 'connections_limit', + 'destination', + 'enabled', + 'link', + 'monitors', + 'packets_limit', + 'packets_enabled', + 'translation_address', + 'translation_port', + 'virtual_server_dependencies', + ] + + +class ApiParameters(Parameters): + @property + def address(self): + if self._values['destination'].count(':') >= 2: + # IPv6 + parts = self._values['destination'].split('.') + else: + # IPv4 + parts = self._values['destination'].split(':') + if is_valid_ip(parts[0]): + return str(parts[0]) + raise F5ModuleError( + "'address' parameter from API was not an IP address." + ) + + @property + def port(self): + if self._values['destination'].count(':') >= 2: + # IPv6 + parts = self._values['destination'].split('.') + return parts[1] + # IPv4 + parts = self._values['destination'].split(':') + return int(parts[1]) + + @property + def virtual_server_dependencies(self): + if self._values['virtual_server_dependencies'] is None: + return None + results = [] + for dependency in self._values['virtual_server_dependencies']: + parts = dependency['name'].split(':') + result = dict( + server=parts[0], + virtual_server=parts[1], + ) + results.append(result) + if results: + results = sorted(results, key=lambda k: k['server']) + return results + + @property + def enabled(self): + if 'enabled' in self._values: + return True + else: + return False + + @property + def disabled(self): + if 'disabled' in self._values: + return True + return False + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + elif 'require ' in self._values['monitors']: + return 'require' + else: + return 'all' + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.availability_requirement_type == 'at_least': + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + elif self.availability_requirement_type == 'require': + monitors = ' '.join(monitors) + result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors) + else: + result = ' and '.join(monitors).strip() + + return result + + @property + def number_of_probes(self): + """Returns the probes value from the monitor string. + + The monitor string for a Require monitor looks like this. + + require 1 from 2 { /Common/tcp } + + This method parses out the first of the numeric values. This values represents + the "probes" value that can be updated in the module. + + Returns: + int: The probes value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+(?P\d+)\s+from' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('probes') + + @property + def number_of_probers(self): + """Returns the probers value from the monitor string. + + The monitor string for a Require monitor looks like this. + + require 1 from 2 { /Common/tcp } + + This method parses out the first of the numeric values. This values represents + the "probers" value that can be updated in the module. + + Returns: + int: The probers value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+\d+\s+from\s+(?P\d+)\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('probers') + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + + The monitor string for a Require monitor looks like this. + + min 1 of { /Common/gateway_icmp } + + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('least') + + +class ModuleParameters(Parameters): + def _get_limit_value(self, type): + if self._values['limits'] is None: + return None + if self._values['limits'][type] is None: + return None + return int(self._values['limits'][type]) + + def _get_availability_value(self, type): + if self._values['availability_requirements'] is None: + return None + if self._values['availability_requirements'][type] is None: + return None + return int(self._values['availability_requirements'][type]) + + def _get_limit_status(self, type): + if self._values['limits'] is None: + return None + if self._values['limits'][type] is None: + return None + if self._values['limits'][type]: + return 'enabled' + return 'disabled' + + @property + def address(self): + if self._values['address'] is None: + return None + if is_valid_ip(self._values['address']): + ip = str(ip_address(u'{0}'.format(self._values['address']))) + return ip + raise F5ModuleError( + "Specified 'address' is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + if self._values['port'] == '*': + return 0 + return int(self._values['port']) + + @property + def destination(self): + if self.address is None: + return None + if self.port is None: + return None + if validate_ip_v6_address(self.address): + result = '{0}.{1}'.format(self.address, self.port) + else: + result = '{0}:{1}'.format(self.address, self.port) + return result + + @property + def link(self): + if self._values['link'] is None: + return None + return fq_name(self.partition, self._values['link']) + + @property + def bits_limit(self): + return self._get_limit_value('bits_limit') + + @property + def packets_limit(self): + return self._get_limit_value('packets_limit') + + @property + def connections_limit(self): + return self._get_limit_value('connections_limit') + + @property + def bits_enabled(self): + return self._get_limit_status('bits_enabled') + + @property + def packets_enabled(self): + return self._get_limit_status('packets_enabled') + + @property + def connections_enabled(self): + return self._get_limit_status('connections_enabled') + + @property + def translation_address(self): + if self._values['translation_address'] is None: + return None + if self._values['translation_address'] == '': + return 'none' + return self._values['translation_address'] + + @property + def translation_port(self): + if self._values['translation_port'] is None: + return None + if self._values['translation_port'] in ['*', ""]: + return 0 + return int(self._values['translation_port']) + + @property + def virtual_server_dependencies(self): + if self._values['virtual_server_dependencies'] is None: + return None + results = [] + for dependency in self._values['virtual_server_dependencies']: + result = dict( + server=fq_name(self.partition, dependency['server']), + virtual_server=dependency['virtual_server'] + ) + results.append(result) + if results: + results = sorted(results, key=lambda k: k['server']) + return results + + @property + def enabled(self): + if self._values['state'] == 'enabled': + return True + elif self._values['state'] == 'disabled': + return False + else: + return None + + @property + def disabled(self): + if self._values['state'] == 'enabled': + return False + elif self._values['state'] == 'disabled': + return True + else: + return None + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.availability_requirement_type == 'at_least': + if self.at_least > len(self.monitors_list): + raise F5ModuleError( + "The 'at_least' value must not exceed the number of 'monitors'." + ) + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + elif self.availability_requirement_type == 'require': + monitors = ' '.join(monitors) + if self.number_of_probes > self.number_of_probers: + raise F5ModuleError( + "The 'number_of_probes' must not exceed the 'number_of_probers'." + ) + result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors) + else: + result = ' and '.join(monitors).strip() + + return result + + @property + def availability_requirement_type(self): + if self._values['availability_requirements'] is None: + return None + return self._values['availability_requirements']['type'] + + @property + def number_of_probes(self): + return self._get_availability_value('number_of_probes') + + @property + def number_of_probers(self): + return self._get_availability_value('number_of_probers') + + @property + def at_least(self): + return self._get_availability_value('at_least') + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def virtual_server_dependencies(self): + if self._values['virtual_server_dependencies'] is None: + return None + results = [] + for depend in self._values['virtual_server_dependencies']: + name = '{0}:{1}'.format(depend['server'], depend['virtual_server']) + results.append(dict(name=name)) + return results + + @property + def monitors(self): + monitor_string = self._values['monitors'] + if monitor_string is None: + return None + + if '{' in monitor_string and '}' in monitor_string: + tmp = monitor_string.strip('}').split('{') + monitor = ''.join(tmp).rstrip() + return monitor + + return monitor_string + + +class ReportableChanges(Changes): + @property + def monitors(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + elif 'require ' in self._values['monitors']: + return 'require' + else: + return 'all' + + @property + def number_of_probes(self): + """Returns the probes value from the monitor string. + The monitor string for a Require monitor looks like this. + require 1 from 2 { /Common/tcp } + This method parses out the first of the numeric values. This values represents + the "probes" value that can be updated in the module. + Returns: + int: The probes value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+(?P\d+)\s+from' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('probes')) + + @property + def number_of_probers(self): + """Returns the probers value from the monitor string. + The monitor string for a Require monitor looks like this. + require 1 from 2 { /Common/tcp } + This method parses out the first of the numeric values. This values represents + the "probers" value that can be updated in the module. + Returns: + int: The probers value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'require\s+\d+\s+from\s+(?P\d+)\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('probers')) + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + The monitor string for a Require monitor looks like this. + min 1 of { /Common/gateway_icmp } + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('least')) + + @property + def availability_requirements(self): + if self._values['monitors'] is None: + return None + result = dict() + result['type'] = self.availability_requirement_type + result['at_least'] = self.at_least + result['number_of_probers'] = self.number_of_probers + result['number_of_probes'] = self.number_of_probes + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def destination(self): + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.address is None: + self.want.update({'address': self.have.address}) + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def virtual_server_dependencies(self): + if self.have.virtual_server_dependencies is None: + return self.want.virtual_server_dependencies + if self.want.virtual_server_dependencies is None and self.have.virtual_server_dependencies is None: + return None + if self.want.virtual_server_dependencies is None: + return None + result = compare_complex_list(self.want.virtual_server_dependencies, self.have.virtual_server_dependencies) + return result + + @property + def enabled(self): + if self.want.state == 'enabled' and self.have.disabled: + result = dict( + enabled=True, + disabled=False + ) + return result + elif self.want.state == 'disabled' and self.have.enabled: + result = dict( + enabled=False, + disabled=True + ) + return result + + @property + def monitors(self): + if self.have.monitors is None: + return self.want.monitors + if self.have.monitors != self.want.monitors: + return self.want.monitors + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state in ['present', 'enabled', 'disabled']: + changed = self.present() + elif state == 'absent': + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.port in [None, ""]: + self.want.update({'port': '*'}) + if self.want.translation_port in [None, ""]: + self.want.update({'translation_port': '*'}) + if self.want.translation_address in [None, ""]: + self.want.update({'translation_address': '::'}) + + self._set_changed_options() + + if self.want.address is None: + raise F5ModuleError( + "You must supply an 'address' when creating a new virtual server." + ) + if self.want.availability_requirement_type == 'require' and len(self.want.monitors_list) > 1: + raise F5ModuleError( + "Only one monitor may be specified when using an availability_requirement type of 'require'" + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.server_name), + transform_name(name=self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/gtm/server/{2}/virtual-servers/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.server_name) + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.server_name), + transform_name(name=self.want.name) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.server_name), + transform_name(name=self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.server_name), + transform_name(name=self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + server_name=dict(required=True), + address=dict(), + port=dict(type='int'), + translation_address=dict(), + translation_port=dict(), + availability_requirements=dict( + type='dict', + options=dict( + type=dict( + choices=['all', 'at_least', 'require'], + required=True + ), + at_least=dict(type='int'), + number_of_probes=dict(type='int'), + number_of_probers=dict(type='int') + ), + mutually_exclusive=[ + ['at_least', 'number_of_probes'], + ['at_least', 'number_of_probers'], + ], + required_if=[ + ['type', 'at_least', ['at_least']], + ['type', 'require', ['number_of_probes', 'number_of_probers']] + ] + ), + monitors=dict( + type='list', + elements='str', + ), + virtual_server_dependencies=dict( + type='list', + elements='dict', + options=dict( + server=dict(required=True), + virtual_server=dict(required=True) + ) + ), + link=dict(), + limits=dict( + type='dict', + options=dict( + bits_enabled=dict(type='bool'), + packets_enabled=dict(type='bool'), + connections_enabled=dict(type='bool'), + bits_limit=dict(type='int'), + packets_limit=dict(type='int'), + connections_limit=dict(type='int') + ) + ), + state=dict( + default='present', + choices=['present', 'absent', 'disabled', 'enabled'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_wide_ip.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_wide_ip.py new file mode 100644 index 00000000..08619258 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_gtm_wide_ip.py @@ -0,0 +1,945 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_gtm_wide_ip +short_description: Manages F5 BIG-IP GTM Wide IP +description: + - Manages the F5 BIG-IP GTM (now BIG-IP DNS) Wide IP. +version_added: "1.0.0" +options: + pool_lb_method: + description: + - Specifies the load balancing method used to select a pool in this wide + IP. This setting is relevant only when multiple pools are configured + for a Wide IP. + type: str + aliases: ['lb_method'] + choices: + - round-robin + - ratio + - topology + - global-availability + name: + description: + - Wide IP name. This name must be formatted as a fully qualified + domain name (FQDN). You can also use the alias C(wide_ip) but this + is deprecated and will be removed in a future Ansible version. + type: str + required: True + aliases: + - wide_ip + type: + description: + - Specifies the type of Wide IP. GTM Wide IPs need to be keyed by query + type in addition to name, because pool members need different attributes + depending on the response RDATA they are meant to supply. + type: str + required: True + choices: + - a + - aaaa + - cname + - mx + - naptr + - srv + state: + description: + - When C(present) or C(enabled), ensures the Wide IP exists and + is enabled. + - When C(absent), ensures the Wide IP has been removed. + - When C(disabled), ensures the Wide IP exists and is disabled. + type: str + choices: + - present + - absent + - disabled + - enabled + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + pools: + description: + - The pools you want associated with the Wide IP. + - If C(ratio) is not provided when creating a new Wide IP, it will default + to 1. + type: list + elements: dict + suboptions: + name: + description: + - The name of the pool to include. + type: str + required: True + ratio: + description: + - Ratio for the pool. + - The system uses this number with the Ratio load balancing method. + - When C(ratio) is not provided, the module assigns it value of C(0). + type: int + order: + description: + - Order of the pool in relation to other pools attached to this Wide IP. + - Pool order is significant when the Global Availability load balancing method is used. + - When C(order) is not provided, the module assigns it value of C(0). + type: int + irules: + description: + - List of rules to be applied. + - If you want to remove all existing iRules, specify a single empty value; C(""). + See the documentation for an example. + type: list + elements: str + aliases: + description: + - Specifies alternate domain names for the web site content you are load + balancing. + - You can use the same wildcard characters for aliases as you can for actual + Wide IP names. + type: list + elements: str + last_resort_pool: + description: + - Specifies which GTM pool for the system to use as the last resort pool for + the Wide IP. + - The valid pools for this parameter are those with the C(type) specified in this + module. + type: str + persistence: + description: + - When C(yes), ensures that when a local DNS makes repetitive requests on + behalf of a client, the system reconnects the client to the same resource + as previous requests. + - When C(no), ensures repetitive requests do not reconnect the client + to the same resource. + type: bool + persistence_ttl: + description: + - Specifies the time to maintain a connection between an local DNS and + a particular virtual server. + type: int + persist_cidr_ipv4: + description: + - Specifies a mask used to group IPv4 LDNS addresses. This feature + allows one persistence record to be shared by LDNS addresses + that match within this mask. + type: int + persist_cidr_ipv6: + description: + - Specifies a mask used to group IPv6 LDNS addresses. This feature + allows one persistence record to be shared by LDNS addresses + that match within this mask. + type: int +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Set lb method + bigip_gtm_wide_ip: + pool_lb_method: round-robin + name: my-wide-ip.example.com + type: a + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add iRules to the Wide IP + bigip_gtm_wide_ip: + pool_lb_method: round-robin + name: my-wide-ip.example.com + type: a + irules: + - irule1 + - irule2 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove one iRule from the Virtual Server + bigip_gtm_wide_ip: + pool_lb_method: round-robin + name: my-wide-ip.example.com + type: a + irules: + - irule1 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove all iRules from the Virtual Server + bigip_gtm_wide_ip: + pool_lb_method: round-robin + name: my-wide-ip.example.com + type: a + irules: "" + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Assign a pool with ratio to the Wide IP + bigip_gtm_wide_ip: + pool_lb_method: round-robin + name: my-wide-ip.example.com + type: a + pools: + - name: pool1 + ratio: 100 + order: 2 + - name: pool1 + ratio: 100 + order: 1 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Assign a pool with persistence to the Wide IP + bigip_gtm_wide_ip: + pool_lb_method: round-robin + name: my-wide-ip.example.com + type: a + pools: + - name: pool1 + persistence: yes + persist_cidr_ipv4: 24 + persist_cidr_ipv6: 120 + persistence_ttl: 3500 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +lb_method: + description: The new load balancing method used by the Wide IP. + returned: changed + type: str + sample: topology +state: + description: The new state of the Wide IP. + returned: changed + type: str + sample: disabled +irules: + description: iRules set on the Wide IP. + returned: changed + type: list + sample: ['/Common/irule1', '/Common/irule2'] +aliases: + description: Aliases set on the Wide IP. + returned: changed + type: list + sample: ['alias1.foo.com', '*.wildcard.domain'] +persistence: + description: Whether pool connections will be persisted. + returned: changed + type: bool + sample: False +persist_cidr_ipv4: + description: Specifies a mask used to group IPv4 LDNS addresses. + returned: changed + type: int + sample: 32 +persist_cidr_ipv6: + description: Specifies a mask used to group IPv6 LDNS addresses. + returned: changed + type: int + sample: 128 +persistence_ttl: + description: Specifies the persistence TTL between an local DNS and a particular virtual server. + returned: changed + type: int + sample: 3600 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name, is_valid_fqdn +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'poolLbMode': 'pool_lb_method', + 'rules': 'irules', + 'lastResortPool': 'last_resort_pool', + 'persistCidrIpv4': 'persist_cidr_ipv4', + 'persistCidrIpv6': 'persist_cidr_ipv6', + 'ttlPersistence': 'persistence_ttl', + } + + updatables = [ + 'pool_lb_method', + 'state', + 'pools', + 'irules', + 'enabled', + 'disabled', + 'aliases', + 'last_resort_pool', + 'persist_cidr_ipv4', + 'persist_cidr_ipv6', + 'persistence', + 'persistence_ttl', + ] + + returnables = [ + 'name', + 'pool_lb_method', + 'state', + 'pools', + 'irules', + 'aliases', + 'last_resort_pool', + 'persistence', + 'persist_cidr_ipv4', + 'persist_cidr_ipv6', + 'persistence_ttl', + ] + + api_attributes = [ + 'poolLbMode', + 'enabled', + 'disabled', + 'pools', + 'rules', + 'aliases', + 'lastResortPool', + 'persistence', + 'ttlPersistence', + 'persistCidrIpv4', + 'persistCidrIpv6', + ] + + +class ApiParameters(Parameters): + @property + def disabled(self): + if self._values['disabled'] is True: + return True + return False + + @property + def enabled(self): + if self._values['enabled'] is True: + return True + return False + + @property + def pools(self): + result = [] + if self._values['pools'] is None: + return None + pools = sorted(self._values['pools'], key=lambda x: x['order']) + for item in pools: + pool = dict() + pool.update(item) + name = '/{0}/{1}'.format(item['partition'], item['name']) + del pool['nameReference'] + del pool['name'] + del pool['partition'] + pool['name'] = name + result.append(pool) + return result + + @property + def last_resort_pool(self): + if self._values['last_resort_pool'] in [None, '', 'none']: + return '' + return self._values['last_resort_pool'] + + +class ModuleParameters(Parameters): + @property + def last_resort_pool(self): + if self._values['last_resort_pool'] is None: + return None + if self._values['last_resort_pool'] in ['', 'none']: + return 'none' + return '{0} {1}'.format( + self.type, fq_name(self.partition, self._values['last_resort_pool']) + ) + + @property + def pool_lb_method(self): + if self._values['pool_lb_method'] is None: + return None + lb_method = str(self._values['pool_lb_method']) + return lb_method + + @property + def type(self): + if self._values['type'] is None: + return None + return str(self._values['type']) + + @property + def name(self): + if self._values['name'] is None: + return None + if not is_valid_fqdn(self._values['name']): + raise F5ModuleError( + "The provided name must be a valid FQDN" + ) + return self._values['name'] + + @property + def state(self): + if self._values['state'] == 'enabled': + return 'present' + return self._values['state'] + + @property + def enabled(self): + if self._values['state'] == 'disabled': + return False + elif self._values['state'] in ['present', 'enabled']: + return True + else: + return None + + @property + def disabled(self): + if self._values['state'] == 'disabled': + return True + elif self._values['state'] in ['present', 'enabled']: + return False + else: + return None + + @property + def pools(self): + result = [] + if self._values['pools'] is None: + return None + for item in self._values['pools']: + pool = dict() + if 'name' not in item: + raise F5ModuleError( + "'name' is a required key for items in the list of pools." + ) + if 'ratio' not in item or item['ratio'] is None: + pool['ratio'] = 0 + else: + pool['ratio'] = item['ratio'] + if 'order' not in item or item['order'] is None: + pool['order'] = 0 + else: + pool['order'] = item['order'] + pool['name'] = fq_name(self.partition, item['name']) + result.append(pool) + if result: + pools = sorted(result, key=lambda x: x['order']) + return pools + + @property + def irules(self): + results = [] + if self._values['irules'] is None: + return None + if len(self._values['irules']) == 1 and self._values['irules'][0] == '': + return '' + for irule in self._values['irules']: + result = fq_name(self.partition, irule) + results.append(result) + return results + + @property + def aliases(self): + if self._values['aliases'] is None: + return None + if len(self._values['aliases']) == 1 and self._values['aliases'][0] == '': + return '' + self._values['aliases'].sort() + return self._values['aliases'] + + @property + def persistence(self): + if self._values['persistence'] is None: + return None + result = flatten_boolean(self._values['persistence']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def persistence_ttl(self): + if self._values['persistence_ttl'] is None: + return None + if 0 <= self._values['persistence_ttl'] <= 4294967295: + return self._values['persistence_ttl'] + raise F5ModuleError( + "Valid 'persistence_ttl' must be in range 0 - 4294967295." + ) + + @property + def persist_cidr_ipv4(self): + if self._values['persist_cidr_ipv4'] is None: + return None + if 0 <= self._values['persist_cidr_ipv4'] <= 4294967295: + return self._values['persist_cidr_ipv4'] + raise F5ModuleError( + "Valid 'persist_cidr_ipv4' must be in range 0 - 4294967295." + ) + + @property + def persist_cidr_ipv6(self): + if self._values['persist_cidr_ipv6'] is None: + return None + if 0 <= self._values['persist_cidr_ipv6'] <= 4294967295: + return self._values['persist_cidr_ipv6'] + raise F5ModuleError( + "Valid 'persist_cidr_ipv6' must be in range 0 - 4294967295." + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + change = getattr(self, returnable) + if isinstance(change, dict): + result.update(change) + else: + result[returnable] = change + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def irules(self): + if self._values['irules'] is None: + return None + if self._values['irules'] == '': + return [] + return self._values['irules'] + + +class ReportableChanges(Changes): + @property + def pool_lb_method(self): + result = dict( + lb_method=self._values['pool_lb_method'], + pool_lb_method=self._values['pool_lb_method'], + ) + return result + + @property + def last_resort_pool(self): + if self._values['last_resort_pool'] is None: + return None + if self._values['last_resort_pool'] in ['', 'none']: + return 'none' + return self._values['last_resort_pool'].split(' ')[1] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def to_tuple(self, items): + result = [] + for x in items: + tmp = [(str(k), str(v)) for k, v in iteritems(x)] + result += tmp + return result + + def _diff_complex_items(self, want, have): + if want == [] and have is None: + return None + if want is None: + return None + w = self.to_tuple(want) if isinstance(want, list) else list() + h = self.to_tuple(have) if isinstance(have, list) else list() + if set(w).issubset(set(h)): + return None + else: + return want + + @property + def last_resort_pool(self): + if self.want.last_resort_pool is None: + return None + if self.want.last_resort_pool == 'none' and self.have.last_resort_pool == '': + return None + if self.want.last_resort_pool != self.have.last_resort_pool: + return self.want.last_resort_pool + + @property + def state(self): + if self.want.state == 'disabled' and self.have.enabled: + return self.want.state + elif self.want.state in ['present', 'enabled'] and self.have.disabled: + return self.want.state + + @property + def pools(self): + result = self._diff_complex_items(self.want.pools, self.have.pools) + return result + + @property + def irules(self): + if self.want.irules is None: + return None + if self.want.irules == '' and self.have.irules is None: + return None + if self.want.irules == '' and len(self.have.irules) > 0: + return [] + if self.have.irules is None: + return self.want.irules + if sorted(set(self.want.irules)) != sorted(set(self.have.irules)): + return self.want.irules + + @property + def aliases(self): + if self.want.aliases is None: + return None + if self.want.aliases == '' and self.have.aliases is None: + return None + if self.want.aliases == '' and len(self.have.aliases) > 0: + return [] + if self.have.aliases is None: + return self.want.aliases + if set(self.want.aliases) != set(self.have.aliases): + return self.want.aliases + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + if not module_provisioned(self.client, 'gtm'): + raise F5ModuleError( + "GTM must be provisioned to use this module." + ) + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ["present", "disabled"]: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + if self.want.pool_lb_method is None: + raise F5ModuleError( + "The 'pool_lb_method' option is required when state is 'present'" + ) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the Wide IP") + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + 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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + 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/gtm/wideip/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + params['disabled'] = self.want.disabled + params['enabled'] = self.want.enabled + + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/{2}/".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/wideip/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.type, + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + lb_method_choices = [ + 'round-robin', 'topology', 'ratio', 'global-availability', + ] + self.supports_check_mode = True + argument_spec = dict( + pool_lb_method=dict( + choices=lb_method_choices, + aliases=['lb_method'] + ), + name=dict( + required=True, + aliases=['wide_ip'] + ), + type=dict( + choices=[ + 'a', 'aaaa', 'cname', 'mx', 'naptr', 'srv' + ], + required=True + ), + state=dict( + default='present', + choices=['absent', 'present', 'enabled', 'disabled'] + ), + pools=dict( + type='list', + elements='dict', + options=dict( + name=dict(required=True), + ratio=dict(type='int'), + order=dict(type='int') + ) + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + irules=dict( + type='list', + elements='str', + ), + aliases=dict( + type='list', + elements='str', + ), + last_resort_pool=dict(), + persistence=dict(type='bool'), + persistence_ttl=dict(type='int'), + persist_cidr_ipv4=dict(type='int'), + persist_cidr_ipv6=dict(type='int'), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_hostname.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_hostname.py new file mode 100644 index 00000000..d38bc7ab --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_hostname.py @@ -0,0 +1,293 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_hostname +short_description: Manage the hostname of a BIG-IP +description: + - Manage the hostname of a BIG-IP device. +version_added: "1.0.0" +options: + hostname: + description: + - Hostname of the BIG-IP host. + type: str + required: True +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Matthew Lam (@mryanlam) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Set the hostname of the BIG-IP + bigip_hostname: + hostname: bigip.localhost.localdomain + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +hostname: + description: The new hostname of the device. + returned: changed + type: str + sample: big-ip01.internal +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_attributes = ['hostname'] + updatables = ['hostname'] + returnables = ['hostname'] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + @property + def hostname(self): + if self._values['hostname'] is None: + return None + return str(self._values['hostname']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + pass + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = ApiParameters() + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.update() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _read_global_settings_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/global-settings/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + result = self._read_global_settings_from_device() + uri = "https://{0}:{1}/mgmt/tm/cm/device/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + self_device = next((x['name'] for x in response['items'] if x['selfDevice'] == "true"), None) + result['self_device'] = self_device + return ApiParameters(params=result) + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + params = self.want.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/global-settings/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if self.have.self_device: + uri = "https://{0}:{1}/mgmt/tm/cm/device".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='mv', + name=self.have.self_device, + target=self.want.hostname + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + return True + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + hostname=dict( + required=True + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_iapp_service.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_iapp_service.py new file mode 100644 index 00000000..ce0be267 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_iapp_service.py @@ -0,0 +1,987 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_iapp_service +short_description: Manages TCL iApp services on a BIG-IP +description: + - Manages TCL iApp services on a BIG-IP. + - The API the system uses to communicate with on the BIG-IP is C(/mgmt/tm/sys/application/service/). +version_added: "1.0.0" +options: + name: + description: + - The name of the iApp service you want to deploy. + type: str + required: True + template: + description: + - The iApp template from which to instantiate a new service. This + template must exist on your BIG-IP before you can successfully + create a service. + - When creating a new service, this parameter is required. + type: str + parameters: + description: + - A hash of all the required template variables for the iApp template. + If your parameters are stored in a file (the more common scenario) + we recommend you use either the C(file) or C(template) lookups + to supply the expected parameters. + - These parameters typically consist of the C(lists), C(tables), and + C(variables) fields. + type: dict + force: + description: + - Forces the updating of an iApp service, even if the parameters to the + service have not changed. This option is of particular importance if + the iApp template that underlies the service has been updated in-place. + This option is equivalent to re-configuring the iApp if that template + has changed. + type: bool + default: no + state: + description: + - When C(present), ensures the iApp service is created and running. + When C(absent), ensures the iApp service has been removed. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + strict_updates: + description: + - Indicates whether the application service is tied to the template, + so when the template is updated, the application service changes to + reflect the updates. + - When C(yes), disallows any updates to the resources that the iApp + service has created, if they are not updated directly through the + iApp. + - When C(no), allows updates outside of the iApp. + - If this option is specified in the Ansible task, it takes precedence + over any similar setting in the iApp Service payload that you provide in + the C(parameters) field. + type: bool + traffic_group: + description: + - The traffic group for the iApp service. When creating a new service, if + this value is not specified, the default of C(/Common/traffic-group-1) + is used. + - If this option is specified in the Ansible task, it takes precedence + over any similar setting in the iApp Service payload that you provide in + the C(parameters) field. + type: str + metadata: + description: + - Metadata associated with the iApp service. + - If this option is specified in the Ansible task, it takes precedence + over any similar setting in the iApp Service payload that you provide in + the C(parameters) field. + type: list + elements: raw + description: + description: + - Description of the iApp service. + - If this option is specified in the Ansible task, it takes precedence + over any similar setting in the iApp Service payload that you provide in + the C(parameters) field. + type: str + device_group: + description: + - The device group for the iApp service. + - If this option is specified in the Ansible task, it takes precedence + over any similar setting in the iApp Service payload that you provide in + the C(parameters) field. + type: str +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create HTTP iApp service from iApp template + bigip_iapp_service: + name: foo-service + template: f5.http + parameters: "{{ lookup('file', 'f5.http.parameters.json') }}" + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Upgrade foo-service to v1.2.0rc4 of the f5.http template + bigip_iapp_service: + name: foo-service + template: f5.http.v1.2.0rc4 + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Configure a service using parameters in YAML + bigip_iapp_service: + name: tests + template: web_frontends + state: present + parameters: + variables: + - name: var__vs_address + value: 1.1.1.1 + - name: pm__apache_servers_for_http + value: 2.2.2.1:80 + - name: pm__apache_servers_for_https + value: 2.2.2.2:80 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Re-configure a service whose underlying iApp was updated in place + bigip_iapp_service: + name: tests + template: web_frontends + force: yes + state: present + parameters: + variables: + - name: var__vs_address + value: 1.1.1.1 + - name: pm__apache_servers_for_http + value: 2.2.2.1:80 + - name: pm__apache_servers_for_https + value: 2.2.2.2:80 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Try to remove the iApp template before the associated Service is removed + bigip_iapp_template: + name: web_frontends + state: absent + provider: + user: admin + password: secret + server: lb.mydomain.com + register: result + failed_when: + - result is not success + - "'referenced by one or more applications' not in result.msg" + +- name: Configure a service using more complicated parameters + bigip_iapp_service: + name: tests + template: web_frontends + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + parameters: + variables: + - name: var__vs_address + value: 1.1.1.1 + - name: pm__apache_servers_for_http + value: 2.2.2.1:80 + - name: pm__apache_servers_for_https + value: 2.2.2.2:80 + lists: + - name: irules__irules + value: + - foo + - bar + tables: + - name: basic__snatpool_members + - name: net__snatpool_members + - name: optimizations__hosts + - name: pool__hosts + columnNames: + - name + rows: + - row: + - internal.company.bar + - name: pool__members + columnNames: + - addr + - port + - connection_limit + rows: + - row: + - "none" + - 80 + - 0 + - name: server_pools__servers + delegate_to: localhost + +- name: Override metadata that may or may not exist in parameters + bigip_iapp_service: + name: foo-service + template: f5.http + parameters: "{{ lookup('file', 'f5.http.parameters.json') }}" + metadata: + - persist: yes + name: data 1 + - persist: yes + name: data 2 + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.urls import build_service_uri +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'strictUpdates': 'strict_updates', + 'trafficGroup': 'traffic_group', + 'deviceGroup': 'device_group', + } + + returnables = [ + 'tables', + 'variables', + 'lists', + 'strict_updates', + 'traffic_group', + 'device_group', + 'metadata', + 'template', + 'description', + ] + + api_attributes = [ + 'tables', + 'variables', + 'template', + 'lists', + 'deviceGroup', + 'inheritedDevicegroup', + 'inheritedTrafficGroup', + 'trafficGroup', + 'strictUpdates', + # 'metadata', + 'description', + ] + + updatables = [ + 'tables', + 'variables', + 'lists', + 'strict_updates', + 'device_group', + 'traffic_group', + 'metadata', + 'description', + ] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + def normalize_tables(self, tables): + result = [] + for table in tables: + tmp = dict() + name = table.get('name', None) + if name is None: + raise F5ModuleError( + "One of the provided tables does not have a name" + ) + tmp['name'] = str(name) + columns = table.get('columnNames', None) + if columns: + tmp['columnNames'] = [str(x) for x in columns] + # You cannot have rows without columns + rows = table.get('rows', None) + if rows: + tmp['rows'] = [] + for row in rows: + tmp['rows'].append(dict(row=[str(x) for x in row['row']])) + result.append(tmp) + result = sorted(result, key=lambda k: k['name']) + return result + + def normalize_variables(self, variables): + result = [] + for variable in variables: + tmp = dict((str(k), str(v)) for k, v in iteritems(variable)) + if 'encrypted' not in tmp: + # BIG-IP will inject an 'encrypted' key if you don't provide one. + # If you don't provide one, then we give you the default 'no', by + # default. + tmp['encrypted'] = 'no' + if 'value' not in tmp: + tmp['value'] = '' + + # This seems to happen only on 12.0.0 + elif tmp['value'] == 'none': + tmp['value'] = '' + elif tmp['value'] == 'True': + tmp['value'] = 'yes' + elif tmp['value'] == 'False': + tmp['value'] = 'no' + elif isinstance(tmp['value'], bool): + if tmp['value'] is True: + tmp['value'] = 'yes' + else: + tmp['value'] = 'no' + + if tmp['encrypted'] == 'True': + tmp['encrypted'] = 'yes' + elif tmp['encrypted'] == 'False': + tmp['encrypted'] = 'no' + elif isinstance(tmp['encrypted'], bool): + if tmp['encrypted'] is True: + tmp['encrypted'] = 'yes' + else: + tmp['encrypted'] = 'no' + + result.append(tmp) + result = sorted(result, key=lambda k: k['name']) + return result + + def normalize_list(self, lists): + result = [] + for list in lists: + tmp = dict((str(k), str(v)) for k, v in iteritems(list) if k != 'value') + if 'encrypted' not in list: + # BIG-IP will inject an 'encrypted' key if you don't provide one. + # If you don't provide one, then we give you the default 'no', by + # default. + tmp['encrypted'] = 'no' + if 'value' in list: + if len(list['value']) > 0: + # BIG-IP removes empty values entries, so mimic this behavior + # for user-supplied values. + tmp['value'] = [str(x) for x in list['value']] + + if tmp['encrypted'] == 'True': + tmp['encrypted'] = 'yes' + elif tmp['encrypted'] == 'False': + tmp['encrypted'] = 'no' + elif isinstance(tmp['encrypted'], bool): + if tmp['encrypted'] is True: + tmp['encrypted'] = 'yes' + else: + tmp['encrypted'] = 'no' + + result.append(tmp) + result = sorted(result, key=lambda k: k['name']) + return result + + def normalize_metadata(self, metadata): + result = [] + for item in metadata: + name = item.get('name', None) + persist = flatten_boolean(item.get('persist', "no")) + if persist == "yes": + persist = "true" + else: + persist = "false" + result.append({ + "name": name, + "persist": persist + }) + return result + + +class ApiParameters(Parameters): + @property + def metadata(self): + if self._values['metadata'] is None: + return None + return self._values['metadata'] + + @property + def tables(self): + if self._values['tables'] is None: + return None + return self.normalize_tables(self._values['tables']) + + @property + def lists(self): + if self._values['lists'] is None: + return None + return self.normalize_list(self._values['lists']) + + @property + def variables(self): + if self._values['variables'] is None: + return None + return self.normalize_variables(self._values['variables']) + + @property + def device_group(self): + if self._values['device_group'] in [None, 'none']: + return None + return self._values['device_group'] + + +class ModuleParameters(Parameters): + @property + def param_lists(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('lists', None) + return result + + @property + def param_tables(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('tables', None) + return result + + @property + def param_variables(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('variables', None) + return result + + @property + def param_metadata(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('metadata', None) + return result + + @property + def param_description(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('description', None) + return result + + @property + def param_traffic_group(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('trafficGroup', None) + if not result: + return result + return fq_name(self.partition, result) + + @property + def param_device_group(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('deviceGroup', None) + if not result: + return result + return fq_name(self.partition, result) + + @property + def param_strict_updates(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('strictUpdates', None) + return flatten_boolean(result) + + @property + def tables(self): + if self._values['tables']: + return self.normalize_tables(self._values['tables']) + elif self.param_tables: + return self.normalize_tables(self.param_tables) + return None + + @property + def lists(self): + if self._values['lists']: + return self.normalize_list(self._values['lists']) + elif self.param_lists: + return self.normalize_list(self.param_lists) + return None + + @property + def variables(self): + if self._values['variables']: + return self.normalize_variables(self._values['variables']) + elif self.param_variables: + return self.normalize_variables(self.param_variables) + return None + + @property + def metadata(self): + if self._values['metadata']: + result = self.normalize_metadata(self._values['metadata']) + elif self.param_metadata: + result = self.normalize_metadata(self.param_metadata) + else: + return None + return result + + @property + def template(self): + if self._values['template'] is None: + return None + return fq_name(self.partition, self._values['template']) + + @property + def device_group(self): + if self._values['device_group'] not in [None, 'none']: + result = fq_name(self.partition, self._values['device_group']) + elif self.param_device_group not in [None, 'none']: + result = self.param_device_group + else: + return None + if not result.startswith('/Common/'): + raise F5ModuleError( + "Device groups can only exist in /Common" + ) + return result + + @property + def traffic_group(self): + if self._values['traffic_group']: + result = fq_name(self.partition, self._values['traffic_group']) + elif self.param_traffic_group: + result = self.param_traffic_group + else: + return None + if not result.startswith('/Common/'): + raise F5ModuleError( + "Traffic groups can only exist in /Common" + ) + return result + + @property + def strict_updates(self): + if self._values['strict_updates'] is not None: + result = flatten_boolean(self._values['strict_updates']) + elif self.param_strict_updates is not None: + result = flatten_boolean(self.param_strict_updates) + else: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def description(self): + if self._values['description']: + return self._values['description'] + elif self.param_description: + return self.param_description + return None + + +class Changes(Parameters): + pass + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def metadata(self): + if self.want.metadata is None: + return None + if self.have.metadata is None: + return self.want.metadata + want = [(k, v) for d in self.want.metadata for k, v in iteritems(d)] + have = [(k, v) for d in self.have.metadata for k, v in iteritems(d)] + if set(want) != set(have): + return dict( + metadata=self.want.metadata + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def create(self): + self._set_changed_options() + if self.want.traffic_group is None: + self.want.update({'traffic_group': '/Common/traffic-group-1'}) + if not self.template_exists(): + raise F5ModuleError( + "The specified template does not exist in the provided partition." + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the iApp service") + return True + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update() and not self.want.force: + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exists(self): + base_uri = "https://{0}:{1}/mgmt/tm/sys/application/service/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = build_service_uri(base_uri, 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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + if params or self.want.force: + params['execute-action'] = 'definition' + base_uri = "https://{0}:{1}/mgmt/tm/sys/application/service/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = build_service_uri(base_uri, 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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if self.changes.metadata: + params = dict(metadata=self.changes.metadata) + params.update({'execute-action': 'definition'}) + base_uri = "https://{0}:{1}/mgmt/tm/sys/application/service/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = build_service_uri(base_uri, 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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + return True + + def read_current_from_device(self): + base_uri = "https://{0}:{1}/mgmt/tm/sys/application/service/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = build_service_uri(base_uri, 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def template_exists(self): + name = fq_name(self.want.partition, self.want.template) + parts = name.split('/') + uri = "https://{0}:{1}/mgmt/tm/sys/application/template/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(parts[1], parts[2]) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/application/service/".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if self.changes.metadata: + payload = dict(metadata=self.changes.metadata) + base_uri = "https://{0}:{1}/mgmt/tm/sys/application/service/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = build_service_uri(base_uri, self.want.partition, self.want.name) + resp = self.client.api.patch(uri, json=payload) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + return True + + def remove_from_device(self): + base_uri = "https://{0}:{1}/mgmt/tm/sys/application/service/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = build_service_uri(base_uri, self.want.partition, self.want.name) + + # Metadata needs to be zero'd before the service is removed because + # otherwise, the API will error out saying that "configuration items" + # currently exist. + # + # In other words, the REST API is not able to delete a service while + # there is existing metadata + payload = dict(metadata=[]) + resp = self.client.api.patch(uri, json=payload) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + template=dict(), + description=dict(), + device_group=dict(), + parameters=dict( + type='dict' + ), + state=dict( + default='present', + choices=['absent', 'present'] + ), + force=dict( + default='no', + type='bool' + ), + strict_updates=dict( + type='bool', + ), + metadata=dict( + type='list', + elements='raw', + ), + traffic_group=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_iapp_template.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_iapp_template.py new file mode 100644 index 00000000..f062621f --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_iapp_template.py @@ -0,0 +1,566 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_iapp_template +short_description: Manages TCL iApp templates on a BIG-IP. +description: + - Manages TCL iApp templates on a BIG-IP. This module allows you to + deploy iApp templates to the BIG-IP and manage their lifecycle. The + conventional way to use this module is to import new iApps as needed, + or by extracting the contents of the iApp archive that is provided at + downloads.f5.com, and then importing all the iApps with this module. + This module can also update existing iApps provided the source + of the iApp changed while the name stayed the same. Note that + this module will not reconfigure any services that may have been + created using the C(bigip_iapp_service) module. iApps are normally + not updated in production. Instead, new versions are deployed and then + existing services are changed to consume that new template. As such, + the ability to update templates in-place requires using the C(force) option. +version_added: "1.0.0" +options: + force: + description: + - Specifies whether or not to force the uploading of an iApp. When + C(yes), the system will force update the iApp even if there are iApp services + using it. This will not update the running service, use + C(bigip_iapp_service) to do that. When C(no), the system updates the iApp + only if there are no iApp services using the template. + type: bool + name: + description: + - The name of the iApp template you want to delete. This option + is only available when specifying a C(state) of C(absent) and is + provided as a way to delete templates that you may no longer have + the source of. + type: str + content: + description: + - Sets the contents of an iApp template directly to the specified + value. This is for simple values, but can be used with lookup + plugins for anything complex or with formatting. C(content) must + be provided when creating new templates. + type: str + state: + description: + - Whether the iApp template should exist or not. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add the iApp contained in template iapp.tmpl + bigip_iapp_template: + content: "{{ lookup('template', 'iapp.tmpl') }}" + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Update a template in place + bigip_iapp_template: + content: "{{ lookup('template', 'iapp-new.tmpl') }}" + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Update a template in place that has existing services created from it. + bigip_iapp_template: + content: "{{ lookup('template', 'iapp-new.tmpl') }}" + force: yes + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import re +import uuid +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + upload_file, tmos_version +) +from ..module_utils.teem import send_teem + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + + +class Parameters(AnsibleF5Parameters): + api_attributes = [] + + returnables = [] + + @property + def name(self): + if self._values['name']: + return self._values['name'] + if self._values['content']: + name = self._get_template_name() + return name + return None + + @property + def content(self): + if self._values['content'] is None: + return None + result = self._squash_template_name_prefix() + result = self._replace_template_name(result) + return result + + @property + def checksum(self): + return self._values['tmplChecksum'] + + def _squash_template_name_prefix(self): + """Removes the template name prefix + + This method removes that partition from the name + in the iApp so that comparisons can be done properly and entries + can be created properly when using REST. + + :return string + """ + pattern = r'sys\s+application\s+template\s+/Common/' + replace = 'sys application template ' + return re.sub(pattern, replace, self._values['content']) + + def _replace_template_name(self, template): + """Replaces template name at runtime + + To allow us to do the switch-a-roo with temporary templates and + checksum comparisons, we need to take the template provided to us + and change its name to a temporary value so that BIG-IP will create + a clone for us. + + :return string + """ + pattern = r'sys\s+application\s+template\s+[^ ]+' + + if self._values['name']: + name = self._values['name'] + else: + name = self._get_template_name() + + replace = 'sys application template {0}'.format(fq_name(self.partition, name)) + return re.sub(pattern, replace, template) + + def _get_template_name(self): + pattern = r'sys\s+application\s+template\s+(?P\/[^\{}"\'*?|#]+\/)?(?P[^\{}"\'*?|#]+)' + matches = re.search(pattern, self._values['content']) + try: + result = matches.group('name').strip() + except IndexError: + result = None + if result: + return result + raise F5ModuleError( + "No template name was found in the template" + ) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + changed = False + if self.exists(): + changed = self.remove() + return changed + + def create(self): + if self.module.check_mode: + return True + self.create_on_device() + if self.exists(): + return True + else: + raise F5ModuleError("Failed to create the iApp template") + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the iApp template") + return True + + def update(self): + self.have = self.read_current_from_device() + + if not self.templates_differ(): + return False + + if not self.want.force and self.template_in_use(): + return False + + if self.module.check_mode: + return True + + self._remove_iapp_checksum() + # The same process used for creating (load) can be used for updating + self.create_on_device() + self._generate_template_checksum_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/application/template/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def template_in_use(self): + uri = "https://{0}:{1}/mgmt/tm/sys/application/service/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + name = fq_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 + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + for item in response['items']: + if item['template'] == name: + return True + return False + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + self._generate_template_checksum_on_device() + uri = "https://{0}:{1}/mgmt/tm/sys/application/template/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def _remove_iapp_checksum(self): + """Removes the iApp tmplChecksum + + This is required for updating in place or else the load command will + fail with a "AppTemplate ... content does not match the checksum" + error. + + :return: + """ + uri = "https://{0}:{1}/mgmt/tm/sys/application/template/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + params = dict(tmplChecksum=None) + + resp = self.client.api.patch(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def templates_differ(self): + # BIG-IP can generate checksums of iApps, but the iApp needs to be + # on the box to do this. Additionally, the checksum is MD5, but it + # is not an MD5 of the entire content of the template. Instead, it + # is a hash of some portion of the template that is unknown to me. + # + # The code below is responsible for uploading the provided template + # under a unique name and creating a checksum for it so that that + # checksum can be compared to the one of the existing template. + # + # Using this method we can compare the checksums of the existing + # iApp and the iApp that the user is providing to the module. + backup = self.want.name + + # Override whatever name may have been provided so that we can + # temporarily create a new template to test checksums with + self.want.update({ + 'name': 'ansible-{0}'.format(str(uuid.uuid4())) + }) + + # Create and remove temporary template + temp = self._get_temporary_template() + + # Set the template name back to what it was originally so that + # any future operations only happen on the real template. + self.want.update({ + 'name': backup + }) + if temp.checksum != self.have.checksum: + return True + return False + + def _get_temporary_template(self): + self.create_on_device() + temp = self.read_current_from_device() + self.remove_from_device() + return temp + + def _generate_template_checksum_on_device(self): + command = 'tmsh generate sys application template {0} checksum'.format( + self.want.name + ) + params = dict( + command="run", + utilCmdArgs='-c "{0}"'.format(command) + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def create_on_device(self): + remote_path = "/var/config/rest/downloads/{0}".format(self.want.name) + load_command = 'tmsh load sys application template {0}'.format(remote_path) + + template = StringIO(self.want.content) + self.upload_file_to_device(template, self.want.name) + + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + params = dict( + command="run", + utilCmdArgs='-c "{0}"'.format(load_command) + ) + + resp = self.client.api.post(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'commandResult' in response: + if 'Syntax Error' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + if 'ERROR' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/application/template/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(), + state=dict( + default='present', + choices=['present', 'absent'] + ), + force=dict( + type='bool' + ), + content=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ike_peer.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ike_peer.py new file mode 100644 index 00000000..0bc3a2fe --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ike_peer.py @@ -0,0 +1,824 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_ike_peer +short_description: Manage IPSec IKE Peer configuration on BIG-IP +description: + - Manage IPSec IKE Peer configuration on a BIG-IP device. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the IKE peer. + type: str + required: True + description: + description: + - Description of the IKE peer. + type: str + version: + description: + - Specifies which version of IKE (Internet Key Exchange) is used. + - If the system you are configuring is the IPsec initiator, and you select + both versions, the system tries using IKEv2 for negotiation. If the remote + peer does not support IKEv2, the IPsec tunnel fails. To use IKEv1 in this + case, you must deselect Version 2 and try again. + - If the system you are configuring is the IPsec responder, and you select + both versions, the IPsec initiator system determines which IKE version to use. + - When creating a new IKE peer, this value is required. + type: list + elements: str + choices: + - v1 + - v2 + presented_id_type: + description: + - Specifies the identifier type the local system uses to identify + itself to the peer during IKE Phase 1 negotiations. + type: str + choices: + - address + - asn1dn + - fqdn + - keyid-tag + - user-fqdn + - override + presented_id_value: + description: + - Specifies a value for the identity when using a C(presented_id_type) of + C(override). + - This is a required value when C(version) includes (Cv2). + type: str + verified_id_type: + description: + - Specifies the identifier type the local system uses to identify + the peer during IKE Phase 1 negotiation. + - This is a required value when C(version) includes (Cv2). + - When C(user-fqdn), value of C(verified_id_value) must be in the form of + User @ DNS domain string. + type: str + choices: + - address + - asn1dn + - fqdn + - keyid-tag + - user-fqdn + - override + verified_id_value: + description: + - Specifies a value for the identity when using a C(verified_id_type) of + C(override). + - This is a required value when C(version) includes (Cv2). + type: str + phase1_auth_method: + description: + - Specifies the authentication method for phase 1 negotiation. + - When creating a new IKE peer, if this value is not specified, the default is + C(rsa-signature). + type: str + choices: + - pre-shared-key + - rsa-signature + phase1_lifetime: + description: + - Defines the lifetime in minutes of an IKE SA which will be proposed in the phase 1 negotiations. + - The accepted value range is C(1 - 4294967295) minutes. + - When creating a new IKE peer, if this value is not specified, the default value set by the system is + C(1440) minutes. + type: int + version_added: "1.1.0" + phase1_cert: + description: + - Specifies the digital certificate to use for the RSA signature. + - When creating a new IKE peer, if this value is not specified, and + C(phase1_auth_method) is C(rsa-signature), the default is C(default.crt). + - This parameter is invalid when C(phase1_auth_method) is C(pre-shared-key). + type: str + phase1_key: + description: + - Specifies the public key the digital certificate contains. + - When creating a new IKE peer, if this value is not specified, and + C(phase1_auth_method) is C(rsa-signature), the default is C(default.key). + - This parameter is invalid when C(phase1_auth_method) is C(pre-shared-key). + type: str + phase1_verify_peer_cert: + description: + - In IKEv2, specifies whether the certificate sent by the IKE peer is verified + using the Trusted Certificate Authorities, a CRL, and/or a peer certificate. + - In IKEv1, specifies whether the identifier sent by the peer is verified with + the credentials in the certificate, in the following manner - ASN1DN; specifies + that the entire certificate subject name is compared with the identifier. + Address, FQDN, or User FQDN; specifies that the certificate's subjectAltName is + compared with the identifier. If the two do not match, the negotiation fails. + - When creating a new IKE peer, if this value is not specified, and + C(phase1_auth_method) is C(rsa-signature), the default is C(no). + - This parameter is invalid when C(phase1_auth_method) is C(pre-shared-key). + type: bool + preshared_key: + description: + - Specifies a string the IKE peers share for authenticating each other. + - This parameter is only relevant when C(phase1_auth_method) is C(pre-shared-key). + - This parameter is invalid when C(phase1_auth_method) is C(rsa-signature). + type: str + remote_address: + description: + - Displays the IP address of the BIG-IP system that is remote to the system + you are configuring. + type: str + phase1_encryption_algorithm: + description: + - Specifies the algorithm to use for IKE encryption. + - IKE C(version) C(v2) does not support C(blowfish), C(camellia), or C(cast128). + type: str + choices: + - 3des + - des + - blowfish + - cast128 + - aes128 + - aes192 + - aes256 + - camellia + phase1_hash_algorithm: + description: + - Specifies the algorithm to use for IKE authentication. + type: str + choices: + - sha1 + - md5 + - sha256 + - sha384 + - sha512 + phase1_perfect_forward_secrecy: + description: + - Specifies the Diffie-Hellman group to use for IKE Phase 1 and Phase 2 negotiations. + type: str + choices: + - ecp256 + - ecp384 + - ecp521 + - modp768 + - modp1024 + - modp1536 + - modp2048 + - modp3072 + - modp4096 + - modp6144 + - modp8192 + update_password: + description: + - C(always) allows updating passwords if the user chooses to do so. + C(on_create) only sets the password for newly created IKE peers. + type: str + choices: + - always + - on_create + default: always + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create new IKE peer + bigip_ike_peer: + name: ike1 + remote_address: 1.2.3.4 + version: + - v1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Change presented id type - keyid-tag + bigip_ike_peer: + name: ike1 + presented_id_type: keyid-tag + presented_id_value: key1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove IKE peer + bigip_ike_peer: + name: ike1 + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +presented_id_type: + description: The new Presented ID Type value of the resource. + returned: changed + type: str + sample: address +verified_id_type: + description: The new Verified ID Type value of the resource. + returned: changed + type: str + sample: address +phase1_auth_method: + description: The new IKE Phase 1 Credentials Authentication Method value of the resource. + returned: changed + type: str + sample: rsa-signature +remote_address: + description: The new Remote Address value of the resource. + returned: changed + type: str + sample: 1.2.2.1 +version: + description: The new list of IKE versions. + returned: changed + type: list + sample: ['v1', 'v2'] +phase1_encryption_algorithm: + description: The new IKE Phase 1 Encryption Algorithm. + returned: changed + type: str + sample: 3des +phase1_hash_algorithm: + description: The new IKE Phase 1 Authentication Algorithm. + returned: changed + type: str + sample: sha256 +phase1_perfect_forward_secrecy: + description: The new IKE Phase 1 Perfect Forward Secrecy. + returned: changed + type: str + sample: modp1024 +phase1_cert: + description: The new IKE Phase 1 Certificate Credentials. + returned: changed + type: str + sample: /Common/cert1.crt +phase1_key: + description: The new IKE Phase 1 Key Credentials. + returned: changed + type: str + sample: /Common/cert1.key +phase1_verify_peer_cert: + description: The new IKE Phase 1 Key Verify Peer Certificate setting. + returned: changed + type: bool + sample: yes +verified_id_value: + description: The new Verified ID Value setting for the Verified ID Type. + returned: changed + type: str + sample: 1.2.3.1 +presented_id_value: + description: The new Presented ID Value setting for the Presented ID Type. + returned: changed + type: str + sample: 1.2.3.1 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'myIdType': 'presented_id_type', + 'peersIdType': 'verified_id_type', + 'phase1AuthMethod': 'phase1_auth_method', + 'presharedKeyEncrypted': 'preshared_key', + 'remoteAddress': 'remote_address', + 'version': 'version', + 'phase1EncryptAlgorithm': 'phase1_encryption_algorithm', + 'phase1HashAlgorithm': 'phase1_hash_algorithm', + 'phase1PerfectForwardSecrecy': 'phase1_perfect_forward_secrecy', + 'myCertFile': 'phase1_cert', + 'myCertKeyFile': 'phase1_key', + 'verifyCert': 'phase1_verify_peer_cert', + 'peersIdValue': 'verified_id_value', + 'myIdValue': 'presented_id_value', + 'lifetime': 'phase1_lifetime', + } + + api_attributes = [ + 'myIdType', + 'peersIdType', + 'phase1AuthMethod', + 'presharedKeyEncrypted', + 'remoteAddress', + 'version', + 'phase1EncryptAlgorithm', + 'phase1HashAlgorithm', + 'phase1PerfectForwardSecrecy', + 'myCertFile', + 'myCertKeyFile', + 'verifyCert', + 'peersIdValue', + 'myIdValue', + 'description', + 'lifetime', + ] + + returnables = [ + 'presented_id_type', + 'verified_id_type', + 'phase1_auth_method', + 'preshared_key', + 'remote_address', + 'version', + 'phase1_encryption_algorithm', + 'phase1_hash_algorithm', + 'phase1_perfect_forward_secrecy', + 'phase1_cert', + 'phase1_key', + 'phase1_verify_peer_cert', + 'verified_id_value', + 'presented_id_value', + 'description', + 'phase1_lifetime', + ] + + updatables = [ + 'presented_id_type', + 'verified_id_type', + 'phase1_auth_method', + 'preshared_key', + 'remote_address', + 'version', + 'phase1_encryption_algorithm', + 'phase1_hash_algorithm', + 'phase1_perfect_forward_secrecy', + 'phase1_cert', + 'phase1_key', + 'phase1_verify_peer_cert', + 'verified_id_value', + 'presented_id_value', + 'description', + 'phase1_lifetime', + ] + + @property + def phase1_verify_peer_cert(self): + return flatten_boolean(self._values['phase1_verify_peer_cert']) + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def phase1_cert(self): + if self._values['phase1_cert'] is None: + return None + if self._values['phase1_cert'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['phase1_cert']) + + @property + def phase1_key(self): + if self._values['phase1_key'] is None: + return None + if self._values['phase1_key'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['phase1_key']) + + @property + def phase1_lifetime(self): + if self._values['phase1_lifetime'] is None: + return None + if 1 <= int(self._values['phase1_lifetime']) <= 4294967295: + return int(self._values['phase1_lifetime']) + raise F5ModuleError( + "Valid 'phase1_lifetime' must be in range 1 - 4294967295." + ) + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def phase1_verify_peer_cert(self): + if self._values['phase1_verify_peer_cert'] is None: + return None + elif self._values['phase1_verify_peer_cert'] == 'yes': + return 'true' + else: + return 'false' + + +class ReportableChanges(Changes): + @property + def phase1_verify_peer_cert(self): + return flatten_boolean(self._values['phase1_verify_peer_cert']) + + @property + def preshared_key(self): + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/ike-peer/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + + if self.changes.version is not None and len(self.changes.version) == 0: + raise F5ModuleError( + "At least one version value must be specified." + ) + + if self.changes.phase1_auth_method == 'pre-shared-key': + if self.changes.preshared_key is None and self.have.preshared_key is None: + raise F5ModuleError( + "A 'preshared_key' must be specified when changing 'phase1_auth_method' " + "to 'pre-shared-key'." + ) + + if self.want.update_password == 'always': + self.want.update({'preshared_key': self.want.preshared_key}) + else: + if self.want.preshared_key: + del self.want._values['preshared_key'] + + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.changes.version is None: + raise F5ModuleError( + "The 'version' parameter is required when creating a new IKE peer." + ) + if self.changes.phase1_auth_method is None: + self.changes.update({'phase1_auth_method': 'rsa-signature'}) + if self.changes.phase1_cert is None: + self.changes.update({'phase1_cert': 'default.crt'}) + if self.changes.phase1_key is None: + self.changes.update({'phase1_key': 'default.key'}) + + if self.module.check_mode: + return True + self.create_on_device() + 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/net/ipsec/ike-peer/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/ike-peer/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/ike-peer/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/ike-peer/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + presented_id_type=dict( + choices=['address', 'asn1dn', 'fqdn', 'keyid-tag', 'user-fqdn', 'override'] + ), + presented_id_value=dict(), + verified_id_type=dict( + choices=['address', 'asn1dn', 'fqdn', 'keyid-tag', 'user-fqdn', 'override'] + ), + verified_id_value=dict(), + phase1_auth_method=dict( + choices=[ + 'pre-shared-key', 'rsa-signature' + ] + ), + preshared_key=dict(no_log=True), + remote_address=dict(), + version=dict( + type='list', + elements='str', + choices=['v1', 'v2'] + ), + phase1_lifetime=dict(type='int'), + phase1_encryption_algorithm=dict( + choices=[ + '3des', 'des', 'blowfish', 'cast128', 'aes128', 'aes192', + 'aes256', 'camellia' + ] + ), + phase1_hash_algorithm=dict( + choices=[ + 'sha1', 'md5', 'sha256', 'sha384', 'sha512' + ] + ), + phase1_perfect_forward_secrecy=dict( + choices=[ + 'ecp256', 'ecp384', 'ecp521', 'modp768', 'modp1024', 'modp1536', + 'modp2048', 'modp3072', 'modp4096', 'modp6144', 'modp8192' + ] + ), + phase1_cert=dict(), + phase1_key=dict(), + phase1_verify_peer_cert=dict(type='bool'), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + description=dict(), + state=dict(default='present', choices=['absent', 'present']), + 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.required_if = [ + ['presented_id_type', 'fqdn', ['presented_id_value']], + ['presented_id_type', 'keyid-tag', ['presented_id_value']], + ['presented_id_type', 'user-fqdn', ['presented_id_value']], + ['presented_id_type', 'override', ['presented_id_value']], + + ['verified_id_type', 'fqdn', ['verified_id_value']], + ['verified_id_type', 'keyid-tag', ['verified_id_value']], + ['verified_id_type', 'user-fqdn', ['verified_id_value']], + ['verified_id_type', 'override', ['verified_id_value']], + ] + self.required_together = [ + ['phase1_cert', 'phase1_key'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if, + required_together=spec.required_together, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_imish_config.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_imish_config.py new file mode 100644 index 00000000..82c959d5 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_imish_config.py @@ -0,0 +1,847 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_imish_config +short_description: Manage BIG-IP advanced routing configuration sections +description: + - This module provides an implementation for working with advanced routing + configuration sections in a deterministic way. +version_added: "1.0.0" +options: + route_domain: + description: + - Route domain on which to manage the BGP configuration. + type: str + default: 0 + lines: + description: + - The ordered set of commands that should be configured in the + section. + - The commands must be the exact same as those found in the device + running-config. + - Be sure to note the configuration command syntax, as some commands + are automatically modified by the device config parser. + type: list + elements: str + aliases: ['commands'] + parents: + description: + - The ordered set of parents that uniquely identify the section or hierarchy + the commands should be checked against. + - If the C(parents) argument is omitted, the commands are checked against + the set of top level or global commands. + type: list + elements: str + src: + description: + - The I(src) argument provides a path to the configuration file + to load into the remote system. + - The path can either be a full system path to the configuration + file if the value starts with /, or relative to the root of the + implemented role or playbook. + - This argument is mutually exclusive with the I(lines) and + I(parents) arguments. + type: path + before: + description: + - The ordered set of commands to push onto the command stack if + a change needs to be made. + - This allows the playbook designer the opportunity to perform + configuration commands prior to pushing any changes, without + affecting how the set of commands are matched against the system. + type: list + elements: str + after: + description: + - The ordered set of commands to append to the end of the command + stack if a change needs to be made. + - Just like with I(before), this allows the playbook designer to + append a set of commands to be executed after the command set. + type: list + elements: str + match: + description: + - Instructs the module on the way to perform the matching of + the set of commands against the current device config. + - If match is set to I(line), commands are matched line by line. + - If match is set to I(strict), command lines are matched with respect + to position. + - If match is set to I(exact), command lines must be an equal match. + - Finally, if match is set to I(none), the module will not attempt to + compare the source configuration with the running configuration on + the remote device. + type: str + choices: + - line + - strict + - exact + - none + default: line + replace: + description: + - Instructs the module on the way to perform the configuration + on the device. + - If the replace argument is set to I(line), the modified lines + are pushed to the device in configuration mode. + - If the replace argument is set to I(block), the entire + command block is pushed to the device in configuration mode if any + line is not correct. + type: str + choices: + - line + - block + default: line + backup: + description: + - This argument will cause the module to create a full backup of + the current C(running-config) from the remote device before any + changes are made. + - The backup file is written to the C(backup) folder in the playbook + root directory or role root directory, if playbook is part of an + Ansible role. If the directory does not exist, it is created. + type: bool + default: no + running_config: + description: + - By default, the module will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. + - There are times when you do not want to have the task get the + current running-config for every task in a playbook. + - The I(running_config) argument allows the implementer to pass in + the configuration to use as the base config for comparison. + type: str + aliases: ['config'] + save_when: + description: + - When changes are made to the device running-configuration, the + changes are not copied to non-volatile storage by default. + - If the argument is set to I(always), the running-config will + always be copied to the startup-config and the I(modified) flag will + always be set to C(True). + - If the argument is set to I(modified), the running-config + will only be copied to the startup-config if it has changed since + the last save to startup-config. + - If the argument is set to I(never), the running-config will never be + copied to the startup-config. + - If the argument is set to I(changed), the running-config + will only be copied to the startup-config if the task has made a change. + type: str + choices: + - always + - never + - modified + - changed + default: never + diff_against: + description: + - When using the C(ansible-playbook --diff) command line argument, + the module can generate diffs against different sources. + - When this option is configured as I(startup), the module will return + the diff of the running-config against the startup-config. + - When this option is configured as I(intended), the module will + return the diff of the running-config against the configuration + provided in the C(intended_config) argument. + - When this option is configured as I(running), the module will + return the before and after diff of the running-config with respect + to any changes made to the device configuration. + type: str + choices: + - startup + - intended + - running + default: startup + diff_ignore_lines: + description: + - Use this argument to specify one or more lines that should be + ignored during the diff. + - This is used for lines in the configuration that are automatically + updated by the system. + - This argument takes a list of regular expressions or exact line matches. + type: list + elements: str + intended_config: + description: + - The C(intended_config) provides the master configuration + the node should conform to and is used to check the final + running-config against. + - This argument will not modify any settings on the remote device and + is strictly used to check the compliance of the current device's + configuration against. + - When specifying this argument, the task should also modify the + C(diff_against) value and set it to I(intended). + type: str + backup_options: + description: + - This is a dict object containing configurable options related to backup file path. + The value of this option is read-only when C(backup) is set to I(yes). If C(backup) is set + to I(no), this option will be silently ignored. + suboptions: + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given, it will be generated based on the hostname, current time and date + in the format defined by _config.@ + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file will be stored. If the directory does not exist, it will be first + created and the filename is either the value of C(filename) or default filename + as described in C(filename) options description. If the path value is not given, + a I(backup) directory will be created in the current working directory + and backup configuration will be copied in C(filename) within the I(backup) directory. + type: path + type: dict + allow_duplicates: + description: + - Allows duplicate commands to be sent to the device. This is to accommodate scenarios where + address families are configured. + - Only used with the C(lines) parameter. + type: bool + default: no + version_added: "1.2.0" +notes: + - Abbreviated commands are NOT idempotent +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: configure top level configuration and save it + bigip_imish_config: + lines: bfd slow-timer 2000 + save_when: modified + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: diff the running-config against a provided config + bigip_imish_config: + diff_against: intended + intended_config: "{{ lookup('file', 'master.cfg') }}" + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add config to a parent block + bigip_imish_config: + lines: + - bgp graceful-restart restart-time 120 + - redistribute kernel route-map rhi + - neighbor 10.10.10.11 remote-as 65000 + - neighbor 10.10.10.11 fall-over bfd + - neighbor 10.10.10.11 remote-as 65000 + - neighbor 10.10.10.11 fall-over bfd + parents: router bgp 64664 + match: exact + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove an existing acl before writing it + bigip_imish_config: + lines: + - access-list 10 permit 20.20.20.20 + - access-list 10 permit 20.20.20.21 + - access-list 10 deny any + before: no access-list 10 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: for idempotency, use full-form commands + bigip_imish_config: + lines: + # - desc My interface + - description My Interface + # parents: int ANYCAST-P2P-2 + parents: interface ANYCAST-P2P-2 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: configurable backup path + bigip_imish_config: + lines: bfd slow-timer 2000 + backup: yes + provider: + user: admin + password: secret + server: lb.mydomain.com + backup_options: + filename: backup.cfg + dir_path: /home/user + delegate_to: localhost +''' + +RETURN = r''' +commands: + description: The set of commands that will be pushed to the remote device. + returned: always + type: list + sample: ['interface ANYCAST-P2P-2', 'neighbor 20.20.20.21 remote-as 65000', 'neighbor 20.20.20.21 fall-over bfd'] +updates: + description: The set of commands that will be pushed to the remote device. + returned: always + type: list + sample: ['interface ANYCAST-P2P-2', 'neighbor 20.20.20.21 remote-as 65000', 'neighbor 20.20.20.21 fall-over bfd'] +backup_path: + description: The full path to the backup file. + returned: when backup is yes + type: str + sample: /playbooks/ansible/backup/bigip_imish_config.2016-07-16@22:28:34 +''' + + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + + +import os +import tempfile +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import dumps +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, ImishConfig, f5_argument_spec +) +from ..module_utils.icontrol import ( + upload_file, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + + ] + + returnables = [ + '__backup__', + 'commands', + 'updates' + ] + + updatables = [ + + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + result = dict(changed=False) + config = None + contents = None + + if self.want.backup or (self.module._diff and self.want.diff_against == 'running'): + contents = self.read_current_from_device() + config = ImishConfig(indent=1, contents=contents) + if self.want.backup: + # The backup file is created in the bigip_imish_config action plugin. Refer + # to that if you have questions. The key below is removed by the action plugin. + result['__backup__'] = contents + + if any((self.want.src, self.want.lines)): + match = self.want.match + replace = self.want.replace + + candidate = self.get_candidate() + running = self.get_running_config(contents) + + response = self.get_diff( + candidate=candidate, + running=running, + diff_match=match, + diff_ignore_lines=self.want.diff_ignore_lines, + path=self.want.parents, + diff_replace=replace + ) + + config_diff = response['config_diff'] + + if config_diff: + commands = config_diff.split('\n') + + if self.want.before: + commands[:0] = self.want.before + + if self.want.after: + commands.extend(self.want.after) + + result['commands'] = commands + result['updates'] = commands + + if not self.module.check_mode: + self.load_config(commands) + + result['changed'] = True + + running_config = self.want.running_config + startup_config = None + + if self.want.save_when == 'always': + self.save_config(result) + elif self.want.save_when == 'modified': + output = self.execute_show_commands(['show running-config', 'show startup-config']) + + running_config = ImishConfig(indent=1, contents=output[0], ignore_lines=self.want.diff_ignore_lines) + startup_config = ImishConfig(indent=1, contents=output[1], ignore_lines=self.want.diff_ignore_lines) + + if running_config.sha1 != startup_config.sha1: + self.save_config(result) + elif self.want.save_when == 'changed' and result['changed']: + self.save_on_device() + + if self.module._diff: + if not running_config: + output = self.execute_show_commands('show running-config') + contents = output[0] + else: + contents = running_config + + # recreate the object in order to process diff_ignore_lines + running_config = ImishConfig(indent=1, contents=contents, ignore_lines=self.want.diff_ignore_lines) + + if self.want.diff_against == 'running': + if self.module.check_mode: + self.module.warn("unable to perform diff against running-config due to check mode") + contents = None + else: + contents = config.config_text + + elif self.want.diff_against == 'startup': + if not startup_config: + output = self.execute_show_commands('show startup-config') + contents = output[0] + else: + contents = startup_config.config_text + + elif self.want.diff_against == 'intended': + contents = self.want.intended_config + + if contents is not None: + base_config = ImishConfig(indent=1, contents=contents, ignore_lines=self.want.diff_ignore_lines) + + if running_config.sha1 != base_config.sha1: + if self.want.diff_against == 'intended': + before = running_config + after = base_config + elif self.want.diff_against in ('startup', 'running'): + before = base_config + after = running_config + + result.update({ + 'changed': True, + 'diff': {'before': str(before), 'after': str(after)} + }) + self.changes.update(result) + return result['changed'] + + def load_config(self, commands): + # Add space to command list so that it won't chop last character from last command + commands = ['{0} '.format(x) for x in commands] + content = StringIO("\n".join(commands)) + + file = tempfile.NamedTemporaryFile() + name = os.path.basename(file.name) + + self.upload_file_to_device(content, name) + self.load_config_on_device(name) + self.remove_uploaded_file_from_device(name) + + def remove_uploaded_file_from_device(self, name): + filepath = '/var/config/rest/downloads/{0}'.format(name) + params = { + "command": "run", + "utilCmdArgs": filepath + } + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def load_config_on_device(self, name): + filepath = '/var/config/rest/downloads/{0}'.format(name) + command = 'imish -r {0} -f {1}'.format(self.want.route_domain, filepath) + + params = { + "command": "run", + "utilCmdArgs": '-c "{0}"'.format(command) + } + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'commandResult' in response: + if 'Dynamic routing is not enabled' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + command = 'imish -r {0} -e \\\"show running-config\\\"'.format(self.want.route_domain) + + params = { + "command": "run", + "utilCmdArgs": '-c "{0}"'.format(command) + } + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'commandResult' in response: + if 'Dynamic routing is not enabled' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + return response['commandResult'] + raise F5ModuleError(resp.content) + + def save_on_device(self): + command = 'imish -r {0} -e write'.format(self.want.route_domain) + params = { + "command": "run", + "utilCmdArgs": '-c "{0}"'.format(command) + } + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def get_diff(self, candidate=None, running=None, diff_match='line', + diff_ignore_lines=None, path=None, diff_replace='line'): + diff = {} + + # prepare candidate configuration + candidate_obj = ImishConfig(indent=1) + candidate_obj.load(candidate) + + if running and diff_match != 'none' and diff_replace != 'config': + # running configuration + running_obj = ImishConfig(indent=1, contents=running, ignore_lines=diff_ignore_lines) + configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace) + else: + configdiffobjs = candidate_obj.items + + diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else '' + return diff + + def get_running_config(self, config=None): + contents = self.want.running_config + if not contents: + if config: + contents = config + else: + contents = self.read_current_from_device() + return contents + + def get_candidate(self): + candidate = '' + if self.want.src: + candidate = self.want.src + + elif self.want.lines: + candidate_obj = ImishConfig(indent=1) + parents = self.want.parents or list() + if self.want.allow_duplicates: + candidate_obj.add(self.want.lines, parents=parents, duplicates=True) + else: + candidate_obj.add(self.want.lines, parents=parents) + candidate = dumps(candidate_obj, 'raw') + return candidate + + def execute_show_commands(self, commands): + body = [] + + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + for command in to_list(commands): + command = 'imish -r {0} -e \\\"{1}\\\"'.format(self.want.route_domain, command) + params = { + "command": "run", + "utilCmdArgs": '-c "{0}"'.format(command) + } + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'commandResult' in response: + if 'Dynamic routing is not enabled' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + body.append(response['commandResult']) + + return body + + def save_config(self, result): + result['changed'] = True + if self.module.check_mode: + self.module.warn( + 'Skipping command `copy running-config startup-config` ' + 'due to check_mode. Configuration not copied to ' + 'non-volatile storage' + ) + return + self.save_on_device() + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + backup_spec = dict( + filename=dict(), + dir_path=dict(type='path') + ) + argument_spec = dict( + route_domain=dict(default=0), + src=dict(type='path'), + lines=dict( + type='list', + elements='str', + aliases=['commands'], + ), + parents=dict( + type='list', + elements='str', + ), + before=dict( + type='list', + elements='str', + ), + after=dict( + type='list', + elements='str', + ), + match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), + replace=dict(default='line', choices=['line', 'block']), + running_config=dict(aliases=['config']), + intended_config=dict(), + backup=dict(type='bool', default='no'), + backup_options=dict(type='dict', options=backup_spec), + save_when=dict(choices=['always', 'never', 'modified', 'changed'], default='never'), + diff_against=dict(choices=['running', 'startup', 'intended'], default='startup'), + diff_ignore_lines=dict( + type='list', + elements='str', + ), + allow_duplicates=dict(type='bool', default='no') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.mutually_exclusive = [ + ('lines', 'src'), + ('parents', 'src'), + ] + self.required_if = [ + ('match', 'strict', ['lines']), + ('match', 'exact', ['lines']), + ('replace', 'block', ['lines']), + ('diff_against', 'intended', ['intended_config']) + ] + self.add_file_common_args = True + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + required_if=spec.required_if, + add_file_common_args=spec.add_file_common_args, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_interface.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_interface.py new file mode 100644 index 00000000..ebda3a97 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_interface.py @@ -0,0 +1,932 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_interface +short_description: Module to manage BIG-IP physical interfaces. +description: + - Module to manage BIG-IP physical interfaces. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the interface to manage. + type: str + required: True + description: + description: + - User defined description. + type: str + enabled: + description: + - Specifies the current status of the interface. + - When C(yes), enables the interface to pass traffic. + - When C(no), disables the interface from passing traffic. + type: bool + bundle: + description: + - Enables or disables bundle capability. + - This option is only supported on select hardware platforms and interfaces. + - "Attempting to enable this option on a C(VE) or any other unsupported platform/interface + will result in module run failure." + type: str + choices: + - enabled + - disabled + - not-supported + bundle_speed: + description: + - Sets the bundle speed, which is applicable only when the bundle is C(yes). + - This option is only supported on selected hardware platforms and interfaces. + - "Attempting to enable this option on a C(VE) or any other unsupported platform/interface + will result in module run failure." + type: str + choices: + - 100G + - 40G + - not-supported + force_gigabit_fiber: + description: + - Enables or disables forcing of gigabit fiber media. + - When C(yes) for a gigabit fiber interface, the media setting will be forced, and no auto-negotiation will be + performed. + - When C(no) auto-negotiation will be performed with just a single gigabit fiber option advertised. + type: bool + prefer_port: + description: + - Indicates which side of a combo port the interface uses, if both sides have the potential for an external link. + - The default value for a combo port is sfp. Do not use this option for non-combo ports. + type: str + choices: + - sfp + - fixed + media_fixed: + description: + - "Specifies the settings for a fixed (non-pluggable) interface." + - Use this option only with a combo port to specify the media type for the fixed interface, + when it is not the preferred port. + type: str + choices: + - 100000-FD + - 100000LR4-FD + - 10000LR-FD + - 10000T-FD + - 1000SX-FD + - 100TX-FD + - 10T-HD + - 20000-FD + - 40000LR4-FD + - 100000AR4-FD + - 100000SR4-FD + - 10000SFPCU-FD + - 1000CX-FD + - 1000T-FD + - 100TX-HD + - 12000-FD + - 21000-FD + - 40000SR4-FD + - 100000CR4-FD + - 10000ER-FD + - 10000SR-FD + - 1000LX-FD + - 1000T-HD + - 10T-FD + - 16000-FD + - 40000-FD + - 42000-FD + - auto + - no-phy + media_sfp: + description: + - "Specifies the settings for an SFP (pluggable) interface." + - Use this option only with a combo port to specify the media type for the SFP interface, + when it is not the preferred port. + type: str + choices: + - 100000-FD + - 100000LR4-FD + - 10000LR-FD + - 10000T-FD + - 1000SX-FD + - 100TX-FD + - 10T-HD + - 20000-FD + - 40000LR4-FD + - 100000AR4-FD + - 100000SR4-FD + - 10000SFPCU-FD + - 1000CX-FD + - 1000T-FD + - 100TX-HD + - 12000-FD + - 21000-FD + - 40000SR4-FD + - 100000CR4-FD + - 10000ER-FD + - 10000SR-FD + - 1000LX-FD + - 1000T-HD + - 10T-FD + - 16000-FD + - 40000-FD + - 42000-FD + - auto + - no-phy + flow_control: + description: + - Specifies how the system controls the sending of PAUSE frames. + - When C(tx-rx), the interface honors pause frames from its partner, + and also generates pause frames when necessary. + - When C(tx), the interface ignores pause frames from its partner, and generates pause frames when necessary. + - When C(rx), the interface honors pause frames from its partner, but does not generate pause frames. + - When (none), the flow control is disabled on the interface. + type: str + choices: + - none + - rx + - tx + - tx-rx + forward_error_correction: + description: + - "Enables or disables IEEE 802.3bm Clause 91 Reed-Solomon Forward Error Correction on 100G interfaces. Not valid + for LR4 media." + - This option is only supported on selected hardware platforms and interfaces. + - "Attempting to enable this option on a C(VE) or any other unsupported platform/interface + will result in module run failure." + type: str + choices: + - enabled + - disabled + - not-supported + - auto + port_fwd_mode: + description: + - Specifies the operation mode. + type: str + choices: + - l3 + - passive + - virtual-wire + lldp_admin: + description: + - Specifies LLDP settings on an interface level. + - When C(disabled), the interface neither transmits (sends) LLDP messages to nor receives LLDP messages + from neighboring devices. + - When C(txonly), the interface transmits LLDP messages to neighbor devices, but does not receive LLDP messages + from neighbor devices. + - When C(rxonly), the interface receives LLDP messages from neighbor devices, but does not transmit LLDP messages + to neighbor devices. + - When C(txrx), the interface transmits LLDP messages to and receives LLDP messages from neighboring devices. + type: str + choices: + - disable + - rxonly + - txonly + - txrx + lldp_tlvmap: + description: + - Specifies the content of an LLDP message being sent or received. + - "Each LLDP attribute specified with this setting is optional and is in the form of Type, Length, Value + (TLV)." + - "The three mandatory TLVs not taken into account when calculating this value are: C(Chassis ID), C(Port ID), + and C(TTL)." + - The optional attributes that are available have a specific TLV numeric value mapped to them. + - The C(Port Description) attribute has a TLV value of C(8). + - The C(System Name) attribute has a TLV value of C(16). + - The C(System Description) attribute has a TLV value of C(32). + - The C(System Capabilities) attribute has a TLV value of C(64). + - The C(Management Address) attribute has a TLV value of C(128). + - The C(Port VLAN ID) attribute has a TLV value of C(256). + - The C(VLAN Name) attribute has a TLV value of C(512). + - The C(Port and Protocol VLAN ID) attribute has a TLV value of C(1024). + - The C(Protocol Identity) attribute has a TLV value of C(2048). + - "The C(MAC/PHY Config Status) attribute has a TLV value of C(4096)." + - The C(Link Aggregation) attribute has a TLV value of C(8192). + - The C(Max Frame Size) attribute has a TLV value of C(32768). + - The C(Product Model) attribute has a TLV value of C(65536). + - The C(lldp_tlvmap) is a numeric value that is a sum of all TLV values of selected attributes. + - Setting C(lldp_tlvmap) to C(0) will remove all attributes from the interface. + - Setting C(lldp_tlvmap) to C(114680) will add all attributes to the interface. + type: int + stp: + description: + - Enables or disables STP. + type: bool + stp_auto_edge_port: + description: + - Sets STP automatic edge port detection for the interface. + - "When C(yes), the system monitors the interface for incoming STP, RSTP, or MSTP packets. If no such packets are + received for a sufficient period of time (about three seconds), the interface is automatically given edge port + status." + - When C(no), the system never gives the interface edge port status automatically. Any STP setting set on a + per-interface basis applies to all spanning tree instances. + type: bool + stp_edge_port: + description: + - Specifies whether the interface connects to an end station instead of another spanning tree bridge. + type: bool + stp_link_type: + description: + - Specifies the STP link type for the interface. + type: str + choices: + - auto + - p2p + - shared + sflow: + description: + - Specifies sFlow settings for the interface. + suboptions: + poll_interval: + description: + - Specifies the maximum interval between two pollings, in seconds. + - For this setting to take effect, C(poll_interval_global) must be set to C(no). + - The valid range is 0 - 4294967295. + type: int + poll_interval_global: + description: + - Specifies whether the global interface C(poll_interval) setting overrides the object-level + C(poll_interval) setting. + - When C(yes) the C(poll_interval) setting does not take effect. + type: bool + type: dict +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Update Interface Settings + bigip_interface: + name: 1.1 + stp: yes + stp_auto_edge_port: no + stp_edge_port: yes + stp_link_type: shared + description: my description + flow_control: tx + lldp_admin: txrx + lldp_tlvmap: 8 + force_gigabit_fiber: no + sflow: + - poll_interval: 10 + - poll_interval_global: no + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Disable Interface + bigip_interface: + name: 1.1 + enabled: no + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Change sflow interface settings + bigip_interface: + name: 1.1 + sflow: + - poll_interval: 0 + - poll_interval_global: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: User defined description. + returned: changed + type: str + sample: my description +enabled: + description: The current status of the interface. + returned: changed + type: bool + sample: yes +bundle: + description: Enables or disables bundle capability. + returned: changed + type: str + sample: not-supported +bundle_speed: + description: The bundle speed. + returned: changed + type: str + sample: 100G +force_gigabit_fiber: + description: Enables or disables forcing of gigabit fiber media. + returned: changed + type: bool + sample: yes +prefer_port: + description: The side of a combo port the interface uses. + returned: changed + type: str + sample: fixed +media_fixed: + description: The settings for a fixed interface. + returned: changed + type: str + sample: 100000-FD +media_sfp: + description: The settings for a SFP interface. + returned: changed + type: str + sample: 100000-FD +flow_control: + description: Specifies how the system controls the sending of PAUSE frames. + returned: changed + type: str + sample: tx +forward_error_correction: + description: Enables or disables Forward Error Correction. + returned: changed + type: str + sample: auto +port_fwd_mode: + description: The operation mode. + returned: changed + type: str + sample: passive +lldp_admin: + description: The LLDP settings on an interface level. + returned: changed + type: str + sample: txrx +lldp_tlvmap: + description: The content of an LLDP message being sent or received. + returned: changed + type: int + sample: 136 +stp: + description: Enables or disables STP. + returned: changed + type: bool + sample: no +stp_auto_edge_port: + description: Sets STP automatic edge port detection for the interface. + returned: changed + type: bool + sample: yes +stp_edge_port: + description: Specifies whether the interface connects to an end station instead of another spanning tree bridge. + returned: changed + type: bool + sample: no +stp_link_type: + description: The STP link type for the interface. + returned: changed + type: str + sample: shared +sflow: + description: Specifies sFlow settings for the interface. + type: complex + returned: changed + contains: + poll_interval_global: + description: The global sFlow settings override. + returned: changed + type: bool + sample: yes + poll_interval: + description: The maximum interval in seconds between two pollings. + returned: changed + type: int + sample: 128 + sample: hash/dictionary of values +''' + +import copy +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean, + transform_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'bundleSpeed': 'bundle_speed', + 'flowControl': 'flow_control', + 'forceGigabitFiber': 'force_gigabit_fiber', + 'forwardErrorCorrection': 'forward_error_correction', + 'lldpAdmin': 'lldp_admin', + 'lldpTlvmap': 'lldp_tlvmap', + 'mediaFixed': 'media_fixed', + 'mediaSfp': 'media_sfp', + 'portFwdMode': 'port_fwd_mode', + 'preferPort': 'prefer_port', + 'stpAutoEdgePort': 'stp_auto_edge_port', + 'stpEdgePort': 'stp_edge_port', + 'stpLinkType': 'stp_link_type', + } + + api_attributes = [ + 'bundle', + 'bundleSpeed', + 'description', + 'enabled', + 'disabled', + 'flowControl', + 'forceGigabitFiber', + 'forwardErrorCorrection', + 'lldpAdmin', + 'lldpTlvmap', + 'mediaFixed', + 'mediaSfp', + 'portFwdMode', + 'preferPort', + 'stp', + 'stpAutoEdgePort', + 'stpEdgePort', + 'stpLinkType', + 'sflow', + ] + + returnables = [ + 'description', + 'enabled', + 'disabled', + 'bundle', + 'bundle_speed', + 'force_gigabit_fiber', + 'prefer_port', + 'media_fixed', + 'media_sfp', + 'flow_control', + 'forward_error_correction', + 'port_fwd_mode', + 'lldp_tlvmap', + 'lldp_admin', + 'stp', + 'stp_auto_edge_port', + 'stp_edge_port', + 'stp_link_type', + 'sflow_interval', + 'sflow_global', + ] + + updatables = [ + 'description', + 'enabled', + 'disabled', + 'bundle', + 'bundle_speed', + 'force_gigabit_fiber', + 'prefer_port', + 'media_fixed', + 'media_sfp', + 'flow_control', + 'forward_error_correction', + 'port_fwd_mode', + 'lldp_tlvmap', + 'lldp_admin', + 'stp', + 'stp_auto_edge_port', + 'stp_edge_port', + 'stp_link_type', + 'sflow_interval', + 'sflow_global', + ] + + +class ApiParameters(Parameters): + @property + def sflow_interval(self): + if self._values['sflow'] is None: + return None + return self._values['sflow']['pollInterval'] + + @property + def sflow_global(self): + if self._values['sflow'] is None: + return None + return self._values['sflow']['pollIntervalGlobal'] + + +class ModuleParameters(Parameters): + @property + def enabled(self): + result = flatten_boolean(self._values['enabled']) + if result == 'yes': + return True + + @property + def disabled(self): + result = flatten_boolean(self._values['enabled']) + if result == 'no': + return True + + @property + def lldp_tlvmap(self): + if self._values['lldp_tlvmap'] is None: + return None + if self._values['lldp_tlvmap'] == 0: + return self._values['lldp_tlvmap'] + if 8 <= self._values['lldp_tlvmap'] <= 114680: + return self._values['lldp_tlvmap'] + raise F5ModuleError( + "TLV value {0} is out of valid range of: 8 - 114680." + ) + + @property + def force_gigabit_fiber(self): + result = flatten_boolean(self._values['force_gigabit_fiber']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def stp(self): + result = flatten_boolean(self._values['stp']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def stp_auto_edge_port(self): + result = flatten_boolean(self._values['stp_auto_edge_port']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def stp_edge_port(self): + result = flatten_boolean(self._values['stp_edge_port']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + + @property + def sflow_interval(self): + if self._values['sflow'] is None: + return None + if self._values['sflow']['poll_interval'] is None: + return None + if 0 <= self._values['sflow']['poll_interval'] <= 4294967295: + return self._values['sflow']['poll_interval'] + raise F5ModuleError( + "Valid 'poll_interval' must be in range 0 - 4294967295." + ) + + @property + def sflow_global(self): + if self._values['sflow'] is None: + return None + result = flatten_boolean(self._values['sflow']['poll_interval_global']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def sflow(self): + to_filter = dict( + pollInterval=self._values['sflow_interval'], + pollIntervalGlobal=self._values['sflow_global'], + ) + result = self._filter_params(to_filter) + if result: + return result + + +class ReportableChanges(Changes): + returnables = [ + 'description', + 'enabled', + 'bundle', + 'bundle_speed', + 'force_gigabit_fiber', + 'prefer_port', + 'media_fixed', + 'media_sfp', + 'flow_control', + 'forward_error_correction', + 'port_fwd_mode', + 'lldp_tlvmap', + 'lldp_admin', + 'stp', + 'stp_auto_edge_port', + 'stp_edge_port', + 'stp_link_type', + 'sflow', + ] + + @property + def enabled(self): + enabled = self._values.get('enabled', None) + disabled = self._values.get('disabled', None) + if enabled: + return 'yes' + if disabled: + return 'no' + + @property + def stp(self): + result = flatten_boolean(self._values['stp']) + return result + + @property + def stp_auto_edge_port(self): + result = flatten_boolean(self._values['stp_auto_edge_port']) + return result + + @property + def stp_edge_port(self): + result = flatten_boolean(self._values['stp_edge_port']) + return result + + @property + def sflow(self): + to_filter = dict( + poll_interval=self._values['sflow_interval'], + poll_interval_global=self._values['sflow_global'], + ) + result = self._filter_params(to_filter) + if result: + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + result = cmp_str_with_none(self.want.description, self.have.description) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + raise F5ModuleError( + "The specified interface: {0} does not exist.".format(self.want.name) + ) + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/interface/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + 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/net/interface/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=self.want.name) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/interface/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.choices = [ + '100000-FD', '100000LR4-FD', '10000LR-FD', '10000T-FD', '1000SX-FD', '100TX-FD', '10T-HD', '20000-FD', + '40000LR4-FD', '100000AR4-FD', '100000SR4-FD', '10000SFPCU-FD', '1000CX-FD', '1000T-FD', '100TX-HD', + '12000-FD', '21000-FD', '40000SR4-FD', '100000CR4-FD', '10000ER-FD', '10000SR-FD', '1000LX-FD', '1000T-HD', + '10T-FD', '16000-FD', '40000-FD', '42000-FD', 'auto', 'no-phy' + ] + self.bundle = ['disabled', 'enabled', 'not-supported'] + self.fec = copy.copy(self.bundle) + self.fec.append('auto') + argument_spec = dict( + name=dict( + required=True + ), + description=dict(), + enabled=dict( + type='bool' + ), + bundle=dict(choices=self.bundle), + bundle_speed=dict( + choices=[ + '100G', '40G', 'not-supported' + ] + ), + force_gigabit_fiber=dict(type='bool'), + prefer_port=dict( + choices=[ + 'fixed', 'sfp' + ] + ), + media_fixed=dict(choices=self.choices), + media_sfp=dict(choices=self.choices), + flow_control=dict( + choices=[ + 'none', 'rx', 'tx', 'tx-rx' + ] + ), + forward_error_correction=dict(choices=self.fec), + port_fwd_mode=dict( + choices=[ + 'l3', 'passive', 'virtual-wire' + ] + ), + lldp_tlvmap=dict(type='int'), + lldp_admin=dict( + choices=[ + 'disable', 'rxonly', 'txonly', 'txrx' + ] + ), + stp=dict(type='bool'), + stp_auto_edge_port=dict(type='bool'), + stp_edge_port=dict(type='bool'), + stp_link_type=dict( + choices=['auto', 'p2p', 'shared'] + ), + sflow=dict( + type='dict', + options=dict( + poll_interval=dict(type='int'), + poll_interval_global=dict(type='bool'), + ) + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ipsec_policy.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ipsec_policy.py new file mode 100644 index 00000000..77d62af0 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ipsec_policy.py @@ -0,0 +1,771 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_ipsec_policy +short_description: Manage IPSec policies on a BIG-IP +description: + - Manage IPSec policies on a BIG-IP device. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the IPSec policy. + type: str + required: True + description: + description: + - Description of the policy + type: str + protocol: + description: + - Specifies the IPsec protocol. + - Options include ESP (Encapsulating Security Protocol) or AH (Authentication Header). + type: str + choices: + - esp + - ah + mode: + description: + - Specifies the processing mode. + - When C(transport), specifies a mode that encapsulates only the payload (adding + an ESP header, trailer, and authentication tag). + - When C(tunnel), specifies a mode that includes encapsulation of the header as + well as the payload (adding a new IP header, in addition to adding an ESP header, + trailer, and authentication tag). If you select this option, you must also + provide IP addresses for the local and remote endpoints of the IPsec tunnel. + - When C(isession), specifies the use of iSession over an IPsec tunnel. To use + this option, you must also configure the iSession endpoints with IPsec in the + Acceleration section of the user interface. + - When C(interface), specifies the IPsec policy can be used in the tunnel + profile for network interfaces. + type: str + choices: + - transport + - interface + - isession + - tunnel + tunnel_local_address: + description: + - Specifies the local endpoint IP address of the IPsec tunnel. + - This parameter is only valid when C(mode) is C(tunnel). + type: str + tunnel_remote_address: + description: + - Specifies the remote endpoint IP address of the IPsec tunnel. + - This parameter is only valid when C(mode) is C(tunnel). + type: str + encrypt_algorithm: + description: + - Specifies the algorithm to use for IKE encryption. + type: str + choices: + - none + - 3des + - aes128 + - aes192 + - aes256 + - aes-gmac256 + - aes-gmac192 + - aes-gmac128 + - aes-gcm256 + - aes-gcm192 + - aes-gcm256 + - aes-gcm128 + route_domain: + description: + - Specifies the route domain, when C(interface) is selected for the C(mode) setting. + type: int + auth_algorithm: + description: + - Specifies the algorithm to use for IKE authentication. + type: str + choices: + - sha1 + - sha256 + - sha384 + - sha512 + - aes-gcm128 + - aes-gcm192 + - aes-gcm256 + - aes-gmac128 + - aes-gmac192 + - aes-gmac256 + ipcomp: + description: + - Specifies whether to use IPComp encapsulation. + - When C(none), specifies IPComp is disabled. + - When C(deflate), specifies IPComp is enabled and uses the Deflate + compression algorithm. + type: str + choices: + - none + - "null" + - deflate + lifetime: + description: + - Specifies the length of time before the IKE security association expires, + in minutes. + type: int + kb_lifetime: + description: + - Specifies the length of time before the IKE security association, in kilobytes. + expires. + type: int + perfect_forward_secrecy: + description: + - Specifies the Diffie-Hellman group to use for IKE Phase 2 negotiation. + type: str + choices: + - none + - modp768 + - modp1024 + - modp1536 + - modp2048 + - modp3072 + - modp4096 + - modp6144 + - modp8192 + ipv4_interface: + description: + - When C(mode) is C(interface), indicates if the IPv4 C(any) address should be used. + By default C(BIG-IP) assumes C(any6) address for tunnel addresses when C(mode) is C(interface). + - This option takes effect only when C(mode) is set to C(interface). + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a IPSec policy + bigip_ipsec_policy: + name: policy1 + mode: tunnel + tunnel_local_address: 1.1.1.1 + tunnel_remote_address: 2.2.2. + auth_algorithm: sha1 + encrypt_algorithm: 3des + protocol: esp + perfect_forward_secrecy: modp1024 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +auth_algorithm: + description: The new IKE Phase 2 Authentication Algorithm value. + returned: changed + type: str + sample: sha512 +encrypt_algorithm: + description: The new IKE Phase 2 Encryption Algorithm value. + returned: changed + type: str + sample: aes256 +lifetime: + description: The new IKE Phase 2 Lifetime value. + returned: changed + type: int + sample: 1440 +kb_lifetime: + description: The new IKE Phase 2 KB Lifetime value. + returned: changed + type: int + sample: 0 +perfect_forward_secrecy: + description: The new IKE Phase 2 Perfect Forward Secrecy value. + returned: changed + type: str + sample: modp2048 +tunnel_local_address: + description: The new Tunnel Local Address value. + returned: changed + type: str + sample: 1.2.2.1 +tunnel_remote_address: + description: The new Tunnel Remote Address value. + returned: changed + type: str + sample: 2.1.1.2 +mode: + description: The new Mode value. + returned: changed + type: str + sample: tunnel +protocol: + description: The new IPsec Protocol value. + returned: changed + type: str + sample: ah +ipcomp: + description: The new IKE Phase 2 IPComp value. + returned: changed + type: str + sample: deflate +description: + description: The new description value. + returned: changed + type: str + sample: My policy +route_domain: + description: The new Route Domain value when in Tunnel mode. + returned: changed + type: int + sample: 2 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'ikePhase2AuthAlgorithm': 'auth_algorithm', + 'ikePhase2EncryptAlgorithm': 'encrypt_algorithm', + 'ikePhase2Lifetime': 'lifetime', + 'ikePhase2LifetimeKilobytes': 'kb_lifetime', + 'ikePhase2PerfectForwardSecrecy': 'perfect_forward_secrecy', + 'tunnelLocalAddress': 'tunnel_local_address', + 'tunnelRemoteAddress': 'tunnel_remote_address', + } + + api_attributes = [ + 'ikePhase2AuthAlgorithm', + 'ikePhase2EncryptAlgorithm', + 'ikePhase2Lifetime', + 'ikePhase2LifetimeKilobytes', + 'ikePhase2PerfectForwardSecrecy', + 'tunnelLocalAddress', + 'tunnelRemoteAddress', + 'mode', + 'protocol', + 'ipcomp', + 'description', + ] + + returnables = [ + 'auth_algorithm', + 'encrypt_algorithm', + 'lifetime', + 'kb_lifetime', + 'perfect_forward_secrecy', + 'tunnel_local_address', + 'tunnel_remote_address', + 'mode', + 'protocol', + 'ipcomp', + 'description', + 'route_domain', + ] + + updatables = [ + 'auth_algorithm', + 'encrypt_algorithm', + 'lifetime', + 'kb_lifetime', + 'perfect_forward_secrecy', + 'tunnel_local_address', + 'tunnel_remote_address', + 'mode', + 'protocol', + 'ipcomp', + 'description', + 'route_domain', + ] + + @property + def tunnel_local_address(self): + if self._values['tunnel_local_address'] is None: + return None + result = self._values['tunnel_local_address'].split('%')[0] + return result + + @property + def tunnel_remote_address(self): + if self._values['tunnel_remote_address'] is None: + return None + result = self._values['tunnel_remote_address'].split('%')[0] + return result + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def encrypt_algorithm(self): + if self._values['encrypt_algorithm'] is None: + return None + elif self._values['encrypt_algorithm'] == 'null': + return 'none' + return self._values['encrypt_algorithm'] + + @property + def route_domain(self): + if self._values['tunnel_local_address'] is None and self._values['tunnel_remote_address'] is None: + return None + elif self._values['tunnel_local_address'] is None and self._values['tunnel_remote_address'] is not None: + if self._values['tunnel_remote_address'] == 'any6': + result = 'any6' + elif self._values['tunnel_remote_address'] == 'any': + result = 'any' + else: + result = int(self._values['tunnel_remote_address'].split('%')[1]) + elif self._values['tunnel_remote_address'] is None and self._values['tunnel_local_address'] is not None: + if self._values['tunnel_local_address'] == 'any6': + result = 'any6' + elif self._values['tunnel_local_address'] == 'any': + result = 'any' + else: + result = int(self._values['tunnel_local_address'].split('%')[1]) + else: + try: + result = int(self._values['tunnel_local_address'].split('%')[1]) + except Exception: + if self._values['tunnel_local_address'] in ['any6', 'any']: + return 0 + return None + try: + if result in ['any6', 'any']: + return 0 + return int(self._values['tunnel_local_address'].split('%')[1]) + except Exception: + return None + + +class ModuleParameters(Parameters): + @property + def ipv4_interface(self): + result = flatten_boolean(self._values['ipv4_interface']) + if result == 'yes': + return True + return False + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def encrypt_algorithm(self): + if self._values['encrypt_algorithm'] is None: + return None + elif self._values['encrypt_algorithm'] == 'none': + return 'null' + return self._values['encrypt_algorithm'] + + @property + def tunnel_local_address(self): + if self._values['tunnel_local_address'] is None: + return None + if self._values['route_domain'] and len(self._values['tunnel_local_address'].split('%')) == 1: + result = '{0}%{1}'.format(self._values['tunnel_local_address'], self._values['route_domain']) + return result + return self._values['tunnel_local_address'] + + @property + def tunnel_remote_address(self): + if self._values['tunnel_remote_address'] is None: + return None + if self._values['route_domain'] and len(self._values['tunnel_remote_address'].split('%')) == 1: + result = '{0}%{1}'.format(self._values['tunnel_remote_address'], self._values['route_domain']) + return result + return self._values['tunnel_remote_address'] + + +class ReportableChanges(Changes): + @property + def encrypt_algorithm(self): + if self._values['encrypt_algorithm'] is None: + return None + elif self._values['encrypt_algorithm'] == 'null': + return 'none' + return self._values['encrypt_algorithm'] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def route_domain(self): + if self.want.route_domain is None: + return None + if self.have.route_domain != self.want.route_domain: + if self.want.route_domain == 0 and self.want.ipv4_interface: + return dict( + tunnel_local_address='any', + tunnel_remote_address='any', + route_domain=self.want.route_domain, + ) + elif self.want.route_domain == 0 and not self.want.ipv4_interface: + return dict( + tunnel_local_address='any6', + tunnel_remote_address='any6', + route_domain=self.want.route_domain, + ) + else: + return dict( + tunnel_local_address='any%{0}'.format(self.want.route_domain), + tunnel_remote_address='any%{0}'.format(self.want.route_domain), + route_domain=self.want.route_domain, + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/ipsec-policy/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.mode == 'interface': + if self.want.ipv4_interface: + self._set_any_on_interface(ip='ipv4') + else: + self._set_any_on_interface() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _set_any_on_interface(self, ip='ipv6'): + if ip == 'ipv4': + self.want.update({'tunnel_local_address': 'any'}) + self.want.update({'tunnel_remote_address': 'any'}) + else: + self.want.update({'tunnel_local_address': 'any6'}) + self.want.update({'tunnel_remote_address': 'any6'}) + + 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/net/ipsec/ipsec-policy/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/ipsec-policy/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/ipsec-policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/ipsec-policy/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + protocol=dict( + choices=['esp', 'ah'] + ), + mode=dict( + choices=['transport', 'interface', 'isession', 'tunnel'] + ), + ipv4_interface=dict(type='bool'), + tunnel_local_address=dict(), + tunnel_remote_address=dict(), + encrypt_algorithm=dict( + choices=[ + 'none', '3des', 'aes128', 'aes192', 'aes256', 'aes-gmac256', + 'aes-gmac192', 'aes-gmac128', 'aes-gcm256', 'aes-gcm192', + 'aes-gcm256', 'aes-gcm128' + ] + ), + route_domain=dict(type='int'), + auth_algorithm=dict( + choices=[ + 'sha1', 'sha256', 'sha384', 'sha512', 'aes-gcm128', + 'aes-gcm192', 'aes-gcm256', 'aes-gmac128', 'aes-gmac192', + 'aes-gmac256', + ] + ), + ipcomp=dict( + choices=['none', 'null', 'deflate'] + ), + lifetime=dict(type='int'), + kb_lifetime=dict(type='int'), + perfect_forward_secrecy=dict( + choices=[ + 'none', 'modp768', 'modp1024', 'modp1536', 'modp2048', 'modp3072', + 'modp4096', 'modp6144', 'modp8192' + ] + ), + state=dict(default='present', choices=['absent', 'present']), + 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.required_if = [ + ['mode', 'tunnel', ['tunnel_local_address', 'tunnel_remote_address']], + ['mode', 'interface', ['route_domain']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_irule.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_irule.py new file mode 100644 index 00000000..4c7ba1a7 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_irule.py @@ -0,0 +1,568 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_irule +short_description: Manage iRules across different modules on a BIG-IP +description: + - Manage iRules across different modules on a BIG-IP device. +version_added: "1.0.0" +options: + content: + description: + - When used instead of B(src), sets the contents of an iRule directly to + the specified value. This is for simple values, but can be used with + lookup plugins for anything complex or with formatting. Either one + of C(src) or C(content) must be provided. + type: str + module: + description: + - The BIG-IP module to which the iRule should be added. + type: str + required: True + choices: + - ltm + - gtm + name: + description: + - The name of the iRule. + type: str + required: True + src: + description: + - The iRule file to interpret and upload to the BIG-IP. Either one + of C(src) or C(content) must be provided. + type: path + state: + description: + - Whether the iRule should exist or not. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add the iRule contained in template irule.tcl to the LTM module + bigip_irule: + content: "{{ lookup('template', 'irule.tcl') }}" + module: ltm + name: MyiRule + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add the iRule contained in static file irule.tcl to the LTM module + bigip_irule: + module: ltm + name: MyiRule + src: irule.tcl + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +module: + description: The module that the iRule was added to. + returned: changed and success + type: str + sample: gtm +src: + description: The filename that included the iRule source. + returned: changed and success, when provided + type: str + sample: /opt/src/irules/example1.tcl +content: + description: The content of the iRule that was managed. + returned: changed and success + type: str + sample: "when LB_FAILED { set wipHost [LB::server addr] }" +''' + +import os +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'apiAnonymous': 'content', + } + + updatables = [ + 'content', + ] + + api_attributes = [ + 'apiAnonymous', + ] + + returnables = [ + 'content', 'src', 'module', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def content(self): + if self._values['content'] is None: + result = self.src_content + else: + result = self._values['content'] + + return str(result).strip() + + @property + def src(self): + if self._values['src'] is None: + return None + return self._values['src'] + + @property + def src_content(self): + if not os.path.exists(self._values['src']): + raise F5ModuleError( + "The specified 'src' was not found." + ) + with open(self._values['src']) as f: + result = f.read() + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + self.kwargs = kwargs + + def exec_module(self): + if self.module.params['module'] == 'ltm': + manager = self.get_manager('ltm') + elif self.module.params['module'] == 'gtm': + manager = self.get_manager('gtm') + else: + raise F5ModuleError( + "An unknown iRule module type was specified" + ) + return manager.exec_module() + + def get_manager(self, type): + if type == 'ltm': + return LtmManager(**self.kwargs) + elif type == 'gtm': + return GtmManager(**self.kwargs) + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ["present"]: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if not self.want.content and not self.want.src: + raise F5ModuleError( + "Either 'content' or 'src' must be provided" + ) + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + if not self.exists(): + raise F5ModuleError("Failed to create the iRule") + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the iRule") + return True + + +class LtmManager(BaseManager): + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/rule/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + 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/ltm/rule/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + 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/ltm/rule/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/rule/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/rule/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + +class GtmManager(BaseManager): + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/rule/{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 update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/gtm/rule/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + 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/gtm/rule/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/rule/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/gtm/rule/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + content=dict(), + src=dict( + type='path', + ), + name=dict(required=True), + module=dict( + required=True, + choices=['gtm', 'ltm'] + ), + 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 = [ + ['content', 'src'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_log_destination.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_log_destination.py new file mode 100644 index 00000000..27746a0c --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_log_destination.py @@ -0,0 +1,1254 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_log_destination +short_description: Manages log destinations on a BIG-IP. +description: + - Manages log destinations on a BIG-IP device. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the log destination. + type: str + required: True + type: + description: + - Specifies the type of log destination. + - Once created, this parameter cannot be changed. + type: str + choices: + - remote-high-speed-log + - remote-syslog + - arcsight + - splunk + - management-port + - ipfix + required: True + description: + description: + - The description of the log destination. + type: str + 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 the same. + - The C(bsd-syslog) and C(rfc3164) choices are the same. + type: str + choices: + - bsd-syslog + - syslog + - legacy-bigip + - rfc5424 + - rfc3164 + 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. + type: str + 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. + type: str + 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. + type: str + choices: + - tcp + - udp + - ipfix + - netflow-9 + 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). + type: str + choices: + - adaptive + - balanced + - replicated + 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. + type: str + 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. + type: int + 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). + type: str + 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). + type: str + 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). + type: int + 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. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a high-speed logging destination + bigip_log_destination: + name: foo + type: remote-high-speed-log + 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_format: rfc5424 + forward_to: my-destination + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +forward_to: + description: The new Forward To value. + returned: changed + type: str + sample: /Common/dest1 +pool: + description: The new Pool value. + returned: changed + type: str + sample: /Common/pool1 +distribution: + description: The new Distribution Method value. + returned: changed + type: str + sample: balanced +protocol: + description: The new Protocol value. + returned: changed + type: str + sample: tcp +syslog_format: + description: The new Syslog format value. + returned: changed + type: str + sample: syslog +address: + description: The new Address value. + returned: changed + type: str + 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: str + sample: /Common/tcp +server_ssl_profile: + description: The new Server SSL Profile value. + returned: changed + type: str + sample: /Common/serverssl +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(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 ModuleParameters(Parameters): + @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 ApiParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def type(self): + if self.want.type != self.have.type: + raise F5ModuleError( + "'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): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = None + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _validate_creation_parameters(self): + pass + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._validate_creation_parameters() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + +class V1Manager(BaseManager): + """Manages remote-syslog settings + + """ + + def _validate_creation_parameters(self): + if self.want.syslog_format is None: + self.want.update({'syslog_format': 'bsd-syslog'}) + if self.want.forward_to is None: + raise F5ModuleError( + "'forward_to' is required when creating a new remote-syslog destination." + ) + + def exists(self): + 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) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + 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) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + 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) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + 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) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response['type'] = 'remote-syslog' + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class V2Manager(BaseManager): + """Manages remote-high-speed-log settings + + """ + def _validate_creation_parameters(self): + if self.want.protocol is None: + self.want.update({'protocol': 'tcp'}) + if self.want.distribution is None: + self.want.update({'distribution': 'adaptive'}) + if self.want.pool is None: + raise F5ModuleError( + "'pool' is required when creating a new remote-high-speed-log destination." + ) + + def exists(self): + 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) + ) + 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/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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + 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) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + 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) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + 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) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response['type'] = 'remote-high-speed-log' + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class V3Manager(BaseManager): + 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 exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + 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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + 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) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response['type'] = 'arcsight' + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class V4Manager(BaseManager): + """Manager for Splunk + """ + + 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 exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + 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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + 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) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response['type'] = 'splunk' + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class V5Manager(BaseManager): + """Manager for Management Port + """ + 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 exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + 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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + 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) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response['type'] = 'management-port' + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class V6Manager(BaseManager): + """Manager for IPFIX + """ + 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 exists(self): + errors = [401, 403, 409, 500, 501, 502, 503, 504] + 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 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 errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + 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) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + 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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response['type'] = 'ipfix' + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + self.module = kwargs.get('module', None) + + def exec_module(self): + if self.module.params['type'] == 'remote-syslog': + 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 + + def get_manager(self, type): + if type == 'v1': + 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): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + type=dict( + required=True, + choices=[ + 'arcsight', + 'remote-high-speed-log', + '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'), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_log_publisher.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_log_publisher.py new file mode 100644 index 00000000..c4b73cf7 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_log_publisher.py @@ -0,0 +1,422 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_log_publisher +short_description: Manages log publishers on a BIG-IP +description: + - Manages log publishers on a BIG-IP device. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the log publisher. + type: str + required: True + description: + description: + - Specifies a description for the log publisher. + type: str + destinations: + description: + - Specifies log destinations for this log publisher to use. + type: list + elements: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a log publisher for use in high speed loggins + bigip_log_publisher: + name: publisher1 + destinations: + - hsl1 + - security-log-servers-logging + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the resource. + returned: changed + type: str + sample: "Security log publisher" +destinations: + description: The new list of destinations for the resource. + returned: changed + type: list + sample: ['/Common/destination1', '/Common/destination2'] +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_simple_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + 'destinations', + 'description', + ] + + returnables = [ + 'destinations', + 'description', + ] + + updatables = [ + 'destinations', + 'description', + ] + + +class ApiParameters(Parameters): + @property + def destinations(self): + if self._values['destinations'] is None: + return None + results = [] + for destination in self._values['destinations']: + result = fq_name(destination['partition'], destination['name']) + results.append(result) + results.sort() + return results + + +class ModuleParameters(Parameters): + @property + def destinations(self): + if self._values['destinations'] is None: + return None + if len(self._values['destinations']) == 1 and self._values['destinations'][0] == '': + return '' + result = [fq_name(self.partition, x) for x in self._values['destinations']] + result = list(set(result)) + result.sort() + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def destinations(self): + result = cmp_simple_list(self.want.destinations, self.have.destinations) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/publisher/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + 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/publisher/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/publisher/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + 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/publisher/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/log-config/publisher/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + destinations=dict( + type='list', + elements='str', + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ltm_global.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ltm_global.py new file mode 100644 index 00000000..7336f43d --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ltm_global.py @@ -0,0 +1,332 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2020, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: bigip_ltm_global +short_description: Manages global LTM settings +description: + - Manages global BIG-IP LTM settings. These settings include connection related settings. +version_added: "1.16.0" +options: + connection: + description: + - Specifies the connection related general LTM settings. + type: dict + required: True + suboptions: + default_vs_syn_challenge_tresh: + description: + - Specifies the default value of per-virtual server SYN Cookie activation threshold. + - "The valid range is 128 - 1048576, or infinite (encoded as 0)." + type: int + global_syn_challenge_tresh: + description: + - Specifies the default value of the global SYN Cookie activation threshold. + - "The valid range is 2048 - 4194304, or infinite (encoded as 0)." + type: int +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Modify ltm global settings + bigip_ltm_global: + connection: + default_vs_syn_challenge_tresh: 9123 + global_syn_challenge_tresh: 20000 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +default_vs_syn_challenge_tresh: + description: The default value of per-virtual server SYN Cookie activation threshold. + returned: changed + type: int + sample: 0 +global_syn_challenge_tresh: + description: The default value of the global SYN Cookie activation threshold. + returned: changed + type: int + sample: 64000 +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultVsSynChallengeThreshold': 'default_vs_syn_challenge_tresh', + 'globalSynChallengeThreshold': 'global_syn_challenge_tresh', + } + + api_attributes = [ + 'defaultVsSynChallengeThreshold', + 'globalSynChallengeThreshold', + ] + + returnables = [ + 'default_vs_syn_challenge_tresh', + 'global_syn_challenge_tresh' + ] + + updatables = [ + 'default_vs_syn_challenge_tresh', + 'global_syn_challenge_tresh' + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def default_vs_syn_challenge_tresh(self): + if self._values['connection'] is None: + return None + value = self._values['connection']['default_vs_syn_challenge_tresh'] + if value is None: + return None + if value == 0: + return 'infinite' + if 128 < value > 1048576: + raise F5ModuleError( + "Specified number is out of valid range, correct range is between 128 and 1048576, or 0 for infinite." + ) + return str(value) + + @property + def global_syn_challenge_tresh(self): + if self._values['connection'] is None: + return None + value = self._values['connection']['global_syn_challenge_tresh'] + if value is None: + return None + if value == 0: + return 'infinite' + if 2048 < value > 4194304: + raise F5ModuleError( + "Specified number is out of valid range, correct range is between 2048 and 4194304, or 0 for infinite." + ) + return str(value) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def default_vs_syn_challenge_tresh(self): + if self._values['default_vs_syn_challenge_tresh'] is None: + return None + value = self._values['default_vs_syn_challenge_tresh'] + if value is None: + return None + if value == 'infinite': + return 0 + return int(value) + + @property + def global_syn_challenge_tresh(self): + value = self._values['global_syn_challenge_tresh'] + if value is None: + return None + if value == 'infinite': + return 0 + return int(value) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + return self.update() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/global-settings/connection".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/global-settings/connection".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + connection=dict( + type='dict', + required=True, + options=dict( + default_vs_syn_challenge_tresh=dict(type='int'), + global_syn_challenge_tresh=dict(type='int') + ), + required_one_of=[ + ['default_vs_syn_challenge_tresh', 'global_syn_challenge_tresh'] + ] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_lx_package.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_lx_package.py new file mode 100644 index 00000000..600b930e --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_lx_package.py @@ -0,0 +1,522 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_lx_package +short_description: Manages Javascript LX packages on a BIG-IP +description: + - Manages Javascript LX packages on a BIG-IP. This module allows + you to deploy LX packages to the BIG-IP and manage their lifecycle. +version_added: "1.0.0" +options: + package: + description: + - The LX package that you want to upload or remove. When C(state) is C(present), + and you intend to use this module in a C(role), we recommend you use + the C({{ role_path }}) variable. An example is provided in the C(EXAMPLES) section. + - When C(state) is C(absent), it is not necessary for the package to exist on the + Ansible controller. If the full path to the package is provided, the fileame will + specifically be cherry-picked from it to properly remove the package. + type: path + state: + description: + - Whether the LX package should exist or not. + type: str + default: present + choices: + - present + - absent + retain_package_file: + description: + - Specifies whether the install file should be deleted on successful installation of the package. + type: bool + default: no + version_added: "1.4.0" +notes: + - Requires the RPM tool be installed on the host. This can be accomplished in + different ways on each platform. On Debian based systems with C(apt); + C(apt-get install rpm). On Mac with C(brew); C(brew install rpm). + This command is already present on RedHat based systems. + - Requires BIG-IP >= 12.1.0, because the required functionality is missing + on prior versions. + - The module name C(bigip_iapplx_package) has been deprecated in favor of C(bigip_lx_package). +requirements: + - Requires BIG-IP >= 12.1.0 + - The 'rpm' tool installed on the Ansible controller +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Install AS3 + bigip_lx_package: + package: f5-appsvcs-3.5.0-3.noarch.rpm + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Add an LX package stored in a role + bigip_lx_package: + package: "{{ roles_path }}/files/MyApp-0.1.0-0001.noarch.rpm'" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove an LX package + bigip_lx_package: + package: MyApp-0.1.0-0001.noarch.rpm + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Install AS3 and don't delete package file + bigip_lx_package: + package: f5-appsvcs-3.5.0-3.noarch.rpm + retain_package_file: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import os +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) +from ansible.module_utils.urls import urlparse + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean +) +from ..module_utils.icontrol import ( + tmos_version, upload_file +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_attributes = [] + returnables = [] + + @property + def package(self): + if self._values['package'] is None: + return None + return self._values['package'] + + @property + def package_file(self): + if self._values['package'] is None: + return None + return os.path.basename(self._values['package']) + + @property + def package_name(self): + """Return a valid name for the package + + BIG-IP determines the package name by the content of the RPM info. + It does not use the filename. Therefore, we do the same. This method + is only used though when the file actually exists on your Ansible + controller. + + If the package does not exist, then we instead use the filename + portion of the 'package' argument that is provided. + + Non-existence typically occurs when using 'state' = 'absent' + + :return: + """ + cmd = ['rpm', '-qp', '--queryformat', '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}', self.package] + rc, out, err = self._module.run_command(cmd) + if not out: + return str(self.package_file) + return out + + @property + def package_root(self): + if self._values['package'] is None: + return None + base = os.path.basename(self._values['package']) + result = os.path.splitext(base) + return result[0] + + @property + def retain_package_file(self): + return flatten_boolean(self._values['retain_package_file']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(module=self.module, params=self.module.params) + self.changes = UsableChanges() + + def exec_module(self): + result = dict() + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + state = self.want.state + if Version(version) <= Version('12.0.0'): + raise F5ModuleError( + "This version of BIG-IP is not supported." + ) + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def absent(self): + changed = False + if self.exists(): + changed = self.remove() + return changed + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the LX package.") + return True + + def create(self): + if self.module.check_mode: + return True + if not os.path.exists(self.want.package): + if self.want.package.startswith('/'): + raise F5ModuleError( + "The specified LX package was not found at {0}.".format(self.want.package) + ) + else: + raise F5ModuleError( + "The specified LX package was not found in {0}.".format(os.getcwd()) + ) + if not self.check_file_exists_on_device(): + self.upload_to_device() + self.create_on_device() + self.enable_iapplx_on_device() + if self.want.retain_package_file == 'no': + self.remove_package_file_from_device() + if self.exists(): + return True + else: + raise F5ModuleError("Failed to install LX package.") + + def exists(self): + exists = False + packages = self.get_installed_packages_on_device() + if os.path.exists(self.want.package): + exists = True + for package in packages: + if exists: + if self.want.package_name == package['packageName']: + return True + else: + if self.want.package_root == package['packageName']: + return True + return False + + def get_installed_packages_on_device(self): + uri = "https://{0}:{1}/mgmt/shared/iapp/package-management-tasks".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + params = dict(operation='QUERY') + resp = self.client.api.post(uri, json=params) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201, 202] or 'code' in response and response['code'] not in [200, 201, 202]: + raise F5ModuleError(resp.content) + + path = urlparse(response["selfLink"]).path + task = self._wait_for_task(path) + + if task['status'] == 'FINISHED': + return task['queryResponse'] + raise F5ModuleError( + "Failed to find the installed packages on the device." + ) + + def _wait_for_task(self, path): + task = None + for x in range(0, 60): + task = self.check_task_on_device(path) + if task['status'] in ['FINISHED', 'FAILED']: + return task + time.sleep(1) + return task + + def check_task_on_device(self, path): + uri = "https://{0}:{1}{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + path + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201, 202] or 'code' in response and response['code'] in [200, 201, 202]: + return response + raise F5ModuleError(resp.content) + + def upload_to_device(self): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, self.want.package) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def check_file_exists_on_device(self): + params = dict( + command="run", + utilCmdArgs="/var/config/rest/downloads/{0}".format(self.want.package_file) + ) + uri = "https://{0}:{1}/mgmt/tm/util/unix-ls".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'commandResult' in response: + if 'No such file or directory' in response['commandResult']: + return False + elif self.want.package_file in response['commandResult']: + return True + raise F5ModuleError(resp.content) + + def remove_package_file_from_device(self): + params = dict( + command="run", + utilCmdArgs="/var/config/rest/downloads/{0}".format(self.want.package_file) + ) + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".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 resp.status in [200, 201, 202] or 'code' in response and response['code'] in [200, 201, 202]: + return True + raise F5ModuleError(resp.content) + + def create_on_device(self): + remote_path = "/var/config/rest/downloads/{0}".format(self.want.package_file) + params = dict( + operation='INSTALL', packageFilePath=remote_path + ) + uri = "https://{0}:{1}/mgmt/shared/iapp/package-management-tasks".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 resp.status not in [200, 201, 202] or 'code' in response and response['code'] not in [200, 201, 202]: + raise F5ModuleError(resp.content) + + path = urlparse(response["selfLink"]).path + task = self._wait_for_task(path) + + if task['status'] == 'FINISHED': + return True + else: + raise F5ModuleError(task['errorMessage']) + + def remove_from_device(self): + params = dict( + operation='UNINSTALL', + packageName=self.want.package_root + ) + uri = "https://{0}:{1}/mgmt/shared/iapp/package-management-tasks".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 resp.status not in [200, 201, 202] or 'code' in response and response['code'] not in [200, 201, 202]: + raise F5ModuleError(resp.content) + + path = urlparse(response["selfLink"]).path + task = self._wait_for_task(path) + + if task['status'] == 'FINISHED': + return True + return False + + def enable_iapplx_on_device(self): + params = dict( + command="run", + utilCmdArgs='-c "touch /var/config/rest/iapps/enable"' + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status in [200, 201, 202] or 'code' in response and response['code'] in [200, 201, 202]: + return True + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + state=dict( + default='present', + choices=['present', 'absent'] + ), + package=dict(type='path'), + retain_package_file=dict( + default='no', + type='bool' + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_if = [ + ['state', 'present', ['package']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_management_route.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_management_route.py new file mode 100644 index 00000000..792d7a91 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_management_route.py @@ -0,0 +1,453 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_management_route +short_description: Manage system management routes on a BIG-IP +description: + - Configures route settings for the management interface of a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the management route. + type: str + required: True + description: + description: + - Description of the management route. + type: str + gateway: + description: + - Specifies the system forwards packets to the destination through the + gateway with the specified IP address. + type: str + network: + description: + - The subnet and netmask for the route. + - To specify the route is the default route for the system, provide the + value C(default). + - Only one C(default) entry is allowed. + - This parameter cannot be changed after it is set. Therefore, if you do need to change + it, you must delete it and create a new route. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a management route + bigip_management_route: + name: tacacs + description: Route to TACACS + gateway: 10.10.10.10 + network: 11.11.11.0/24 + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the management route. + returned: changed + type: str + sample: Route to TACACS +gateway: + description: The new gateway of the management route. + returned: changed + type: str + sample: 10.10.10.10 +network: + description: The new network to use for the management route. + returned: changed + type: str + sample: default +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ipaddress import ip_network + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, transform_name +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + 'description', + 'gateway', + 'network', + ] + + returnables = [ + 'description', + 'gateway', + 'network', + ] + + updatables = [ + 'description', + 'gateway', + 'network', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def network(self): + if self._values['network'] is None: + return None + if self._values['network'] == 'default': + return 'default' + try: + addr = ip_network(u"{0}".format(str(self._values['network']))) + return str(addr) + except ValueError: + raise F5ModuleError( + "The 'network' must either be a network address (with CIDR) or the word 'default'." + ) + + @property + def gateway(self): + if self._values['gateway'] is None: + return None + if is_valid_ip(self._values['gateway']): + return self._values['gateway'] + else: + raise F5ModuleError( + "The 'gateway' must an IP address." + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def network(self): + if self.want.network is None: + return None + if self.want.network == '0.0.0.0/0' and self.have.network == 'default': + return None + if self.want.network != self.have.network: + raise F5ModuleError( + "'network' cannot be changed after it is set." + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + changed = False + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/management-route/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/management-route/".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) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/management-route/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/management-route/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/management-route/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + gateway=dict(), + network=dict(), + description=dict(), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_peer.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_peer.py new file mode 100644 index 00000000..823b2b58 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_peer.py @@ -0,0 +1,663 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_message_routing_peer +short_description: Manage peers for routing generic message protocol messages +description: + - Manage peers for routing generic message protocol messages. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the peer to manage. + type: str + required: True + description: + description: + - The user-defined description of the peer. + type: str + type: + description: + - Parameter used to specify the type of the peer to manage. + - Default setting is C(generic) with more options coming. + type: str + choices: + - generic + default: generic + auto_init: + description: + - If C(yes), the BIG-IP automatically creates outbound connections to the active pool members in the + specified C(pool) using the configuration of the specified C(transport_config). + - For auto-initialization to attempt to create a connection, the peer must be included in a route that is attached + to a router instance. For each router instance the peer is contained in, a connection is initiated. + - The C(auto_init) logic verifies at C(auto_init_interval) if the a connection exists between + the BIG-IP and the pool members of the pool. If a connection does not exist, it attempts to reestablish one. + type: bool + auto_init_interval: + description: + - Specifies the interval at which attempts to initiate a connection occur. + - The default value upon peer object creation, that is supplied by the system is C(5000) milliseconds. + - The accepted range is between 0 and 4294967295 inclusive. + type: int + connection_mode: + description: + - Specifies how the number of connections per host are to be limited. + type: str + choices: + - per-blade + - per-client + - per-peer + - per-tmm + number_of_connections: + description: + - Specifies the distribution of connections between the BIG-IP and a remote host. + - The accepted range is between 0 and 65535 inclusive. + type: int + pool: + description: + - Specifies the name of the pool that messages are routed towards. + - The specified pool must be on the same partition as the peer. + type: str + ratio: + description: + - Specifies the ratio to be used for selection of a peer within a list of peers in a LTM route. + - The accepted range is between 0 and 4294967295 inclusive. + type: int + transport_config: + description: + - The name of the LTM virtual or LTM transport-config to use for creating an outgoing connection. + - The resource must exist on the same partition as the peer object. + type: str + partition: + description: + - Device partition to create peer object on. + type: str + default: Common + state: + description: + - When C(present), ensures the peer exists. + - When C(absent), ensures the peer is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP >= 14.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a simple peer + bigip_message_routing_peer: + name: foobar + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create message routing peer with additional settings + bigip_message_routing_peer: + name: foobar + connection_mode: per-blade + pool: /baz/bar + partition: baz + transport_config: foovirtual + ratio: 10 + auto_init: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify message routing peer settings + bigip_message_routing_peer: + name: foobar + partition: baz + ratio: 20 + auto_init_interval: 2000 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove message routing peer + bigip_message_routing_peer: + name: foobar + partition: baz + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +auto_init: + description: Enables creation of outbound connections to the active pool members. + returned: changed + type: bool + sample: yes +auto_init_interval: + description: The interval at which attempts to initiate a connection occur. + returned: changed + type: int + sample: 2000 +connection_mode: + description: Specifies how the number of connections per host are to be limited. + returned: changed + type: str + sample: per-peer +number_of_connections: + description: The distribution of connections between the BIG-IP and a remote host. + returned: changed + type: int + sample: 2000 +transport_config: + description: The LTM virtual or LTM transport-config to use for creating an outgoing connection. + returned: changed + type: str + sample: /Common/foobar +description: + description: The user defined description of the peer. + returned: changed + type: str + sample: Some description +pool: + description: The name of the pool that messages are routed towards. + returned: changed + type: str + sample: /Bazbar/foobar +ratio: + description: The ratio to be used for selection of a peer within a list of peers in a LTM route. + returned: changed + type: int + sample: 500 +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'autoInitialization': 'auto_init', + 'autoInitializationInterval': 'auto_init_interval', + 'connectionMode': 'connection_mode', + 'numberConnections': 'number_of_connections', + 'transportConfig': 'transport_config', + } + + api_attributes = [ + 'autoInitialization', + 'autoInitializationInterval', + 'connectionMode', + 'description', + 'numberConnections', + 'pool', + 'ratio', + 'transportConfig', + ] + + returnables = [ + 'auto_init', + 'auto_init_interval', + 'connection_mode', + 'number_of_connections', + 'transport_config', + 'description', + 'pool', + 'ratio', + ] + + updatables = [ + 'auto_init', + 'auto_init_interval', + 'connection_mode', + 'number_of_connections', + 'transport_config', + 'description', + 'pool', + 'ratio', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def auto_init(self): + result = flatten_boolean(self._values['auto_init']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def auto_init_interval(self): + if self._values['auto_init_interval'] is None: + return None + if 0 <= self._values['auto_init_interval'] <= 4294967295: + return self._values['auto_init_interval'] + raise F5ModuleError( + "Valid 'auto_init_interval' must be in range 0 - 4294967295 milliseconds." + ) + + @property + def number_of_connections(self): + if self._values['number_of_connections'] is None: + return None + if 0 <= self._values['number_of_connections'] <= 65535: + return self._values['number_of_connections'] + raise F5ModuleError( + "Valid 'number_of_connections' must be in range 0 - 65535." + ) + + @property + def ratio(self): + if self._values['ratio'] is None: + return None + if 0 <= self._values['ratio'] <= 4294967295: + return self._values['ratio'] + raise F5ModuleError( + "Valid 'ratio' must be in range 0 - 4294967295." + ) + + @property + def pool(self): + if self._values['pool'] is None: + return None + if self._values['pool'] == "": + return "" + result = fq_name(self.partition, self._values['pool']) + return result + + @property + def transport_config(self): + if self._values['transport_config'] is None: + return None + if self._values['transport_config'] == "": + return "" + result = fq_name(self.partition, self._values['transport_config']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def auto_init(self): + result = flatten_boolean(self._values['auto_init']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + result = cmp_str_with_none(self.want.description, self.have.description) + return result + + @property + def transport_config(self): + result = cmp_str_with_none(self.want.transport_config, self.have.transport_config) + return result + + @property + def pool(self): + result = cmp_str_with_none(self.want.pool, self.have.pool) + return result + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + +class GenericModuleManager(BaseManager): + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/peer/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/message-routing/generic/peer/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/peer/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/peer/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/peer/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def version_less_than_14(self): + version = tmos_version(self.client) + if Version(version) < Version('14.0.0'): + return True + return False + + def exec_module(self): + if self.version_less_than_14(): + raise F5ModuleError('Message routing is not supported on TMOS version below 14.x') + if self.module.params['type'] == 'generic': + manager = self.get_manager('generic') + else: + raise F5ModuleError( + "Unknown type specified." + ) + return manager.exec_module() + + def get_manager(self, type): + if type == 'generic': + return GenericModuleManager(**self.kwargs) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + auto_init=dict(type='bool'), + auto_init_interval=dict(type='int'), + connection_mode=dict( + choices=['per-blade', 'per-client', 'per-peer', 'per-tmm'] + ), + description=dict(), + number_of_connections=dict(type='int'), + pool=dict(), + ratio=dict(type='int'), + transport_config=dict(), + type=dict( + choices=['generic'], + default='generic' + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.required_if = [ + ['auto_init', True, ['transport_config']] + ] + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_protocol.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_protocol.py new file mode 100644 index 00000000..144ebad9 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_protocol.py @@ -0,0 +1,569 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_message_routing_protocol +short_description: Manage the generic message parser profile. +description: + - Manages the generic message parser profile for use with the message routing framework. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the generic parser profile. + required: True + type: str + description: + description: + - The user-defined description of the generic parser profile. + type: str + parent: + description: + - The parent template of this parser profile. Once this value has been set, it cannot be changed. + - When creating a new profile, if this parameter is not specified, + the default is the system-supplied C(genericmsg) profile. + type: str + disable_parser: + description: + - When C(yes), the generic message parser is disabled, ignoring all incoming packets and not directly + send message data. + - This mode supports iRule script protocol implementations that generates messages from the incoming transport + stream and sends outgoing messages on the outgoing transport stream. + type: bool + max_egress_buffer: + description: + - Specifies the maximum size of the send buffer in bytes. If the number of bytes in the send buffer for a + connection exceeds this value, the generic message protocol will stop receiving outgoing messages from the + router until the size of the size of the buffer drops below this setting. + - The accepted range is between 0 and 4294967295 inclusive. + type: int + max_msg_size: + description: + - Specifies the maximum size of a received message. If a message exceeds this size, the connection will be reset. + - The accepted range is between 0 and 4294967295 inclusive. + type: int + msg_terminator: + description: + - The string of characters used to terminate a message. If the message-terminator is not specified, + the generic message parser will not separate the input stream into messages. + type: str + no_response: + description: + - When set, matching of responses to requests is disabled. + type: bool + partition: + description: + - Device partition to create route object on. + type: str + default: Common + state: + description: + - When C(present), ensures the route exists. + - When C(absent), ensures the route is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP >= 14.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a generic parser + bigip_message_routing_protocol: + name: foo + description: 'This is parser' + no_response: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify a generic parser + bigip_message_routing_protocol: + name: foo + no_response: no + max_egress_buffer: 10000 + max_msg_size: 2000 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove generic parser + bigip_message_routing_protocol: + name: foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The user-defined description of the parser profile. + returned: changed + type: str + sample: My description +parent: + description: The parent template of this parser profile. + returned: changed + type: str + sample: /Common/genericmsg +disable_parser: + description: Disables generic message parser. + returned: changed + type: bool + sample: yes +max_egress_buffer: + description: The maximum size of the send buffer in bytes. + returned: changed + type: int + sample: 10000 +max_msg_size: + description: The maximum size of a received message. + returned: changed + type: int + sample: 4000 +msg_terminator: + description: The string of characters used to terminate a message. + returned: changed + type: str + sample: '%%%%' +no_response: + description: Disables matching of responses to requests. + returned: changed + type: bool + sample: yes +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'disableParser': 'disable_parser', + 'maxEgressBuffer': 'max_egress_buffer', + 'maxMessageSize': 'max_msg_size', + 'messageTerminator': 'msg_terminator', + 'noResponse': 'no_response', + + } + + api_attributes = [ + 'description', + 'defaultsFrom', + 'disableParser', + 'maxEgressBuffer', + 'maxMessageSize', + 'messageTerminator', + 'noResponse', + ] + + returnables = [ + 'description', + 'parent', + 'disable_parser', + 'max_egress_buffer', + 'max_msg_size', + 'msg_terminator', + 'no_response', + ] + + updatables = [ + 'description', + 'parent', + 'disable_parser', + 'max_egress_buffer', + 'max_msg_size', + 'msg_terminator', + 'no_response', + ] + + @property + def no_response(self): + return flatten_boolean(self._values['no_response']) + + @property + def disable_parser(self): + return flatten_boolean(self._values['disable_parser']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def max_msg_size(self): + if self._values['max_msg_size'] is None: + return None + if 0 <= self._values['max_msg_size'] <= 4294967295: + return self._values['max_msg_size'] + raise F5ModuleError( + "Valid 'max_msg_size' must be in range 0 - 4294967295." + ) + + @property + def max_egress_buffer(self): + if self._values['max_egress_buffer'] is None: + return None + if 0 <= self._values['max_egress_buffer'] <= 4294967295: + return self._values['max_egress_buffer'] + raise F5ModuleError( + "Valid 'max_egress_buffer' must be in range 0 - 4294967295." + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def parent(self): + if self.want.parent is None: + return None + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent router profile cannot be changed." + ) + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def msg_terminator(self): + return cmp_str_with_none(self.want.msg_terminator, self.have.msg_terminator) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def version_less_than_14(self, version): + if Version(version) < Version('14.0.0'): + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if self.version_less_than_14(version): + raise F5ModuleError('Message routing is not supported on TMOS version below 14.x') + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/protocol/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/message-routing/generic/protocol/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/protocol/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/protocol/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/protocol/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + parent=dict(), + disable_parser=dict(type='bool'), + max_egress_buffer=dict(type='int'), + max_msg_size=dict(type='int'), + msg_terminator=dict(), + no_response=dict(type='bool'), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_route.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_route.py new file mode 100644 index 00000000..f8173544 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_route.py @@ -0,0 +1,558 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_message_routing_route +short_description: Manages static routes for routing message protocol messages +description: + - Manages static routes for routing message protocol messages. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the static route. + required: True + type: str + description: + description: + - The user-defined description of the static route. + type: str + type: + description: + - Parameter used to specify the type of the route to manage. + - Default setting is C(generic) with more options coming. + type: str + choices: + - generic + default: generic + src_address: + description: + - Specifies the source address of the route. + - Setting the attribute to an empty string will create a wildcard matching all message source-addresses, which is + the default when creating a new route. + type: str + dst_address: + description: + - Specifies the destination address of the route. + - Setting the attribute to an empty string will create a wildcard matching all message destination-addresses, + which is the default when creating a new route. + type: str + peer_selection_mode: + description: + - Specifies the method to use when selecting a peer from the provided list of C(peers). + type: str + choices: + - ratio + - sequential + peers: + description: + - Specifies a list of ltm messagerouting-peer objects. + - The specified peer must be on the same partition as the route. + type: list + elements: str + partition: + description: + - Device partition to create route object on. + type: str + default: Common + state: + description: + - When C(present), ensures the route exists. + - When C(absent), ensures the route is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP >= 14.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a simple generic route + bigip_message_routing_route: + name: foobar + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify a generic route + bigip_message_routing_route: + name: foobar + peers: + - peer1 + - peer2 + peer_selection_mode: ratio + src_address: annoying_user + dst_address: blackhole + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove a generic + bigip_message_routing_route: + name: foobar + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The user-defined description of the route. + returned: changed + type: str + sample: Some description +src_address: + description: The source address of the route. + returned: changed + type: str + sample: annyoing_user +dst_address: + description: The destination address of the route. + returned: changed + type: str + sample: blackhole +peer_selection_mode: + description: The method to use when selecting a peer. + returned: changed + type: str + sample: ratio +peers: + description: The list of ltm messagerouting-peer object. + returned: changed + type: list + sample: ['/Common/peer1', '/Common/peer2'] +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, is_empty_list, fq_name +) +from ..module_utils.compare import ( + cmp_simple_list, cmp_str_with_none +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'peerSelectionMode': 'peer_selection_mode', + 'sourceAddress': 'src_address', + 'destinationAddress': 'dst_address', + } + + api_attributes = [ + 'description', + 'peerSelectionMode', + 'peers', + 'sourceAddress', + 'destinationAddress', + ] + + returnables = [ + 'peer_selection_mode', + 'peers', + 'description', + 'src_address', + 'dst_address' + ] + + updatables = [ + 'peer_selection_mode', + 'peers', + 'description', + 'src_address', + 'dst_address' + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def peers(self): + if self._values['peers'] is None: + return None + if is_empty_list(self._values['peers']): + return "" + result = [fq_name(self.partition, peer) for peer in self._values['peers']] + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + result = cmp_str_with_none(self.want.description, self.have.description) + return result + + @property + def dst_address(self): + result = cmp_str_with_none(self.want.dst_address, self.have.dst_address) + return result + + @property + def src_address(self): + result = cmp_str_with_none(self.want.src_address, self.have.src_address) + return result + + @property + def peers(self): + result = cmp_simple_list(self.want.peers, self.have.peers) + return result + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + +class GenericModuleManager(BaseManager): + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/message-routing/generic/route/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def version_less_than_14(self): + version = tmos_version(self.client) + if Version(version) < Version('14.0.0'): + return True + return False + + def exec_module(self): + if self.version_less_than_14(): + raise F5ModuleError('Message routing is not supported on TMOS version below 14.x') + if self.module.params['type'] == 'generic': + manager = self.get_manager('generic') + else: + raise F5ModuleError( + "Unknown type specified." + ) + return manager.exec_module() + + def get_manager(self, type): + if type == 'generic': + return GenericModuleManager(**self.kwargs) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + src_address=dict(), + dst_address=dict(), + peer_selection_mode=dict( + choices=['ratio', 'sequential'] + ), + peers=dict( + type='list', + elements='str', + ), + type=dict( + choices=['generic'], + default='generic' + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_router.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_router.py new file mode 100644 index 00000000..7636f889 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_router.py @@ -0,0 +1,759 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_message_routing_router +short_description: Manages router profiles for message-routing protocols +description: + - Manages router profiles for message-routing protocols. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the router profile. + required: True + type: str + description: + description: + - The user-defined description of the router profile. + type: str + type: + description: + - Parameter used to specify the type of the router profile to manage. + - Default setting is C(generic) with more options coming. + type: str + choices: + - generic + default: generic + parent: + description: + - The parent template of this router profile. Once this value has been set, it cannot be changed. + - The default values are set by the system if not specified and they correspond to the router type created, + for example, C(/Common/messagerouter) for C(generic) C(type) and so on. + type: str + ignore_client_port: + description: + - When C(yes), the remote port on clientside connections (connections where the peer connected to the BIG-IP) + is ignored when searching for an existing connection. + type: bool + inherited_traffic_group: + description: + - When set to C(yes), the C(traffic_group) will be inherited from the containing folder. When not specified the + system sets this to C(no) when creating new router profile. + type: bool + traffic_group: + description: + - Specifies the traffic-group of the router profile. + - Setting the C(traffic_group) to an empty string value C("") will cause the device to inherit from containing + folder, which means the value of C(inherited_traffic_group) on device will be C(yes). + type: str + use_local_connection: + description: + - If C(yes), the router will route a message to an existing connection on the same TMM as the message was + received. + type: bool + max_pending_bytes: + description: + - The maximum number of bytes worth of pending messages that will be held while waiting for a connection to a + peer to be created. Once reached, any additional messages to the peer will be flagged as undeliverable + and returned to the originator. + - The accepted range is between 0 and 4294967295 inclusive. + type: int + max_pending_messages: + description: + - The maximum number of pending messages that will be held while waiting for a connection to a peer to be created. + Once reached, any additional messages to the peer will be flagged as undeliverable and returned + to the originator. + - The accepted range is between 0 and 65535 inclusive. + type: int + max_retries: + description: + - Sets the maximum number of time a message may be resubmitted for rerouting by the C(MR::retry) iRule command. + - The accepted range is between 0 and 4294967295 inclusive. + type: int + mirror: + description: + - Enables or disables state mirroring. State mirroring can be used to maintain the same state information in the + standby unit that is in the active unit. + type: bool + mirrored_msg_sweeper_interval: + description: + - Specifies the maximum time in milliseconds that a message will be held on the standby device as it waits for + the active device to route the message. + - Messages on the standby device held for longer than the configurable sweeper interval, will be dropped. + - The acceptable range is between 0 and 4294967295 inclusive. + type: int + routes: + description: + - Specifies a list of static routes for the router instance to use. + - The route must be on the same partition as router profile. + type: list + elements: str + partition: + description: + - Device partition to create router profile on. + type: str + default: Common + state: + description: + - When C(present), ensures the router profile exists. + - When C(absent), ensures the router profile is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP >= 14.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a generic router profile + bigip_message_routing_router: + name: foo + max_retries: 10 + ignore_client_port: yes + routes: + - /Common/route1 + - /Common/route2 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify a generic router profile + bigip_message_routing_router: + name: foo + ignore_client_port: no + mirror: yes + mirrored_msg_sweeper_interval: 4000 + traffic_group: /Common/traffic-group-2 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove a generic router profile + bigip_message_routing_router: + name: foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The user-defined description of the router profile. + returned: changed + type: str + sample: My description +parent: + description: The parent template of this router profile. + returned: changed + type: str + sample: /Common/messagerouter +ignore_client_port: + description: Enables ignoring of the remote port on clientside connections when searching for an existing connection. + returned: changed + type: bool + sample: no +inherited_traffic_group: + description: Specifies if a traffic-group should be inherited from containing folder. + returned: changed + type: bool + sample: yes +traffic_group: + description: The traffic-group of the router profile. + returned: changed + type: str + sample: /Common/traffic-group-1 +use_local_connection: + description: Enables routing of messages to an existing connection on the same TMM as the message was received. + returned: changed + type: bool + sample: yes +max_pending_bytes: + description: The maximum number of bytes worth of pending messages that will be held. + returned: changed + type: int + sample: 10000 +max_pending_messages: + description: The maximum number of pending messages that will be held. + returned: changed + type: int + sample: 64 +max_retries: + description: The maximum number of time a message may be resubmitted for rerouting. + returned: changed + type: int + sample: 10 +mirror: + description: Enables or disables state mirroring. + returned: changed + type: bool + sample: yes +mirrored_msg_sweeper_interval: + description: The maximum time in milliseconds that a message will be held on the standby device. + returned: changed + type: int + sample: 2000 +routes: + description: The list of static routes for the router instance to use. + returned: changed + type: list + sample: ['/Common/route1', '/Common/route2'] +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import ( + cmp_str_with_none, cmp_simple_list +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'useLocalConnection': 'use_local_connection', + 'ignoreClientPort': 'ignore_client_port', + 'inheritedTrafficGroup': 'inherited_traffic_group', + 'maxPendingBytes': 'max_pending_bytes', + 'maxPendingMessages': 'max_pending_messages', + 'maxRetries': 'max_retries', + 'mirroredMessageSweeperInterval': 'mirrored_msg_sweeper_interval', + 'trafficGroup': 'traffic_group', + } + + api_attributes = [ + 'description', + 'useLocalConnection', + 'ignoreClientPort', + 'inheritedTrafficGroup', + 'maxPendingBytes', + 'maxPendingMessages', + 'maxRetries', + 'mirror', + 'mirroredMessageSweeperInterval', + 'trafficGroup', + 'routes', + 'defaultsFrom', + ] + + returnables = [ + 'parent', + 'description', + 'use_local_connection', + 'ignore_client_port', + 'inherited_traffic_group', + 'max_pending_bytes', + 'max_pending_messages', + 'max_retries', + 'mirrored_msg_sweeper_interval', + 'traffic_group', + 'mirror', + 'routes', + ] + + updatables = [ + 'description', + 'use_local_connection', + 'ignore_client_port', + 'inherited_traffic_group', + 'max_pending_bytes', + 'max_pending_messages', + 'max_retries', + 'mirrored_msg_sweeper_interval', + 'traffic_group', + 'mirror', + 'routes', + 'parent', + ] + + @property + def ignore_client_port(self): + return flatten_boolean(self._values['ignore_client_port']) + + @property + def use_local_connection(self): + return flatten_boolean(self._values['use_local_connection']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def inherited_traffic_group(self): + result = flatten_boolean(self._values['inherited_traffic_group']) + if result is None: + return None + if result == 'yes': + return 'true' + return 'false' + + @property + def mirror(self): + result = flatten_boolean(self._values['mirror']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def max_pending_bytes(self): + if self._values['max_pending_bytes'] is None: + return None + if 0 <= self._values['max_pending_bytes'] <= 4294967295: + return self._values['max_pending_bytes'] + raise F5ModuleError( + "Valid 'max_pending_bytes' must be in range 0 - 4294967295 bytes." + ) + + @property + def max_retries(self): + if self._values['max_retries'] is None: + return None + if 0 <= self._values['max_retries'] <= 4294967295: + return self._values['max_retries'] + raise F5ModuleError( + "Valid 'max_retries' must be in range 0 - 4294967295." + ) + + @property + def max_pending_messages(self): + if self._values['max_pending_messages'] is None: + return None + if 0 <= self._values['max_pending_messages'] <= 65535: + return self._values['max_pending_messages'] + raise F5ModuleError( + "Valid 'max_pending_messages' must be in range 0 - 65535 messages." + ) + + @property + def mirrored_msg_sweeper_interval(self): + if self._values['mirrored_msg_sweeper_interval'] is None: + return None + if 0 <= self._values['mirrored_msg_sweeper_interval'] <= 4294967295: + return self._values['mirrored_msg_sweeper_interval'] + raise F5ModuleError( + "Valid 'mirrored_msg_sweeper_interval' must be in range 0 - 4294967295 milliseconds." + ) + + @property + def routes(self): + if self._values['routes'] is None: + return None + if len(self._values['routes']) == 1 and self._values['routes'][0] == "": + return "" + result = [fq_name(self.partition, peer) for peer in self._values['routes']] + return result + + @property + def traffic_group(self): + if self._values['traffic_group'] is None: + return None + if self._values['traffic_group'] == "": + return "" + result = fq_name('Common', self._values['traffic_group']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def mirror(self): + result = flatten_boolean(self._values['mirror']) + return result + + @property + def inherited_traffic_group(self): + result = self._values['inherited_traffic_group'] + if result == 'true': + return 'yes' + if result == 'false': + return 'no' + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def parent(self): + if self.want.parent is None: + return None + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent router profile cannot be changed." + ) + + @property + def routes(self): + result = cmp_simple_list(self.want.routes, self.have.routes) + return result + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + +class GenericModuleManager(BaseManager): + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/router/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/message-routing/generic/router/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/router/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/router/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/router/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def version_less_than_14(self): + version = tmos_version(self.client) + if Version(version) < Version('14.0.0'): + return True + return False + + def exec_module(self): + if self.version_less_than_14(): + raise F5ModuleError('Message routing is not supported on TMOS version below 14.x') + if self.module.params['type'] == 'generic': + manager = self.get_manager('generic') + else: + raise F5ModuleError( + "Unknown type specified." + ) + return manager.exec_module() + + def get_manager(self, type): + if type == 'generic': + return GenericModuleManager(**self.kwargs) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + parent=dict(), + ignore_client_port=dict(type='bool'), + inherited_traffic_group=dict(type='bool'), + use_local_connection=dict(type='bool'), + max_pending_bytes=dict(type='int'), + max_pending_messages=dict(type='int'), + max_retries=dict(type='int'), + mirror=dict(type='bool'), + mirrored_msg_sweeper_interval=dict(type='int'), + routes=dict( + type='list', + elements='str', + ), + traffic_group=dict(), + type=dict( + choices=['generic'], + default='generic' + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_transport_config.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_transport_config.py new file mode 100644 index 00000000..e14be35b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_message_routing_transport_config.py @@ -0,0 +1,679 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_message_routing_transport_config +short_description: Manages configuration for an outgoing connection +description: + - Manages configuration for an outgoing connection in BIG-IP message routing. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the transport config to manage. + type: str + required: True + description: + description: + - The user-defined description of the transport config. + type: str + profiles: + description: + - Specifies a list of profiles for the outgoing connection to use to direct and manage traffic. + type: list + elements: str + src_addr_translation: + description: + - Specifies the type of source address translation enabled for the transport config and the pool + the source address translation will use. + suboptions: + type: + description: + - Specifies the type of source address translation associated with the specified transport config. + - When set to C(snat), the C(pool) parameter needs to contain a name for a valid LSN or SNAT pool. + type: str + choices: + - snat + - none + - automap + pool: + description: + - Specifies the name of a LSN or SNAT pool used by the specified transport config. + - "Name can also be specified in C(fullPath) format: C(/Common/foobar)." + - When C(type) is C(none) or C(automap), the pool parameter will be replaced by C(none) keyword, + thus any defined C(pool) parameter will be ignored. + type: str + type: dict + src_port: + description: + - Specifies the source port for the connection being created. + - If no value is specified an ephemeral port is chosen for the connection being created. + - The acceptable range is between 0 and 65535 inclusive. + type: int + rules: + description: + - The iRules you want run on this transport config. iRules help automate the intercepting, processing, + and routing of application traffic. + type: list + elements: str + type: + description: + - Parameter used to specify the type of transport-config object to manage. + - Default setting is C(generic) with more options coming. + type: str + choices: + - generic + default: generic + partition: + description: + - Device partition to create transport-config object on. + type: str + default: Common + state: + description: + - When C(present), ensures the transport-config object exists. + - When C(absent), ensures the transport-config object is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP >= 14.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create generic transport config + bigip_message_routing_transport_config: + name: foo + profiles: + transport: genericmsg + tcp: tcp-lan-optimized + description: new_transport + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify generic transport config + bigip_message_routing_transport_config: + name: foo + rules: + - rule_1 + - rule_2 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove generic transport config + bigip_message_routing_transport_config: + name: foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The user-defined description of the router profile. + returned: changed + type: str + sample: My description +rules: + description: The iRules running on transport config. + returned: changed + type: list + sample: ['/Common/rule1', '/Common/rule2'] +profiles: + description: The profiles for the outgoing connection . + returned: changed + type: list + sample: ['/Common/profile1', '/Common/profile2'] +src_addr_translation: + description: The type of source address translation enabled for the transport config. + type: complex + returned: changed + contains: + type: + description: the type of source address translation associated with the specified transport config. + type: str + returned: changed + sample: automap + pool: + description: The name of a LSN or SNAT pool used by the specified transport config. + type: str + returned: changed + sample: /Common/pool1 + sample: hash/dictionary of values +source_port: + description: The source port for the connection being created. + returned: changed + type: int + sample: 10041 +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import ( + cmp_str_with_none, cmp_simple_list +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'sourceAddressTranslation': 'src_addr_translation', + 'sourcePort': 'src_port', + } + + api_attributes = [ + 'description', + 'sourceAddressTranslation', + 'sourcePort', + 'profiles', + 'rules', + ] + + returnables = [ + 'description', + 'snat_pool', + 'snat_type', + 'profiles', + 'rules', + 'src_port', + ] + + updatables = [ + 'description', + 'snat_pool', + 'snat_type', + 'profiles', + 'rules', + 'src_port', + ] + + +class ApiParameters(Parameters): + @property + def profiles(self): + if 'profilesReference' not in self._values: + return None + if 'items' not in self._values['profilesReference']: + return None + result = [item['fullPath'] for item in self._values['profilesReference']['items']] + return result + + @property + def snat_pool(self): + if self._values['src_addr_translation'] is None: + return None + if 'pool' in self._values['src_addr_translation']: + return self._values['src_addr_translation']['pool'] + + @property + def snat_type(self): + if self._values['src_addr_translation'] is None: + return None + return self._values['src_addr_translation']['type'] + + +class ModuleParameters(Parameters): + @property + def profiles(self): + if self._values['profiles'] is None: + return None + result = [fq_name(self.partition, p) for p in self._values['profiles']] + return result + + @property + def rules(self): + if self._values['rules'] is None: + return None + result = [fq_name(self.partition, rule) for rule in self._values['rules']] + return result + + @property + def snat_pool(self): + if self._values['src_addr_translation'] is None: + return None + if self._values['src_addr_translation']['pool']: + result = fq_name(self.partition, self._values['src_addr_translation']['pool']) + return result + + @property + def snat_type(self): + if self._values['src_addr_translation'] is None: + return None + return self._values['src_addr_translation']['type'] + + @property + def src_port(self): + if self._values['src_port'] is None: + return None + if 0 <= self._values['src_port'] <= 65535: + return self._values['src_port'] + raise F5ModuleError( + "Valid 'src_port' must be in range 0 - 65535 inclusive." + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def src_addr_translation(self): + if self._values['snat_type'] is None: + return None + if self._values['snat_type'] in ['none', 'automap']: + result = dict( + pool='none', + type=self._values['snat_type'] + ) + return result + + result = dict( + pool=self._values['snat_pool'], + type=self._values['snat_type'] + ) + return result + + +class ReportableChanges(Changes): + returnables = [ + 'description', + 'src_addr_translation', + 'rules', + 'src_port', + 'profiles', + ] + + @property + def src_addr_translation(self): + if self._values['snat_type'] is None: + return None + to_filter = dict( + pool=self._values['snat_pool'], + type=self._values['snat_type'] + ) + result = self._filter_params(to_filter) + if result: + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def profiles(self): + result = cmp_simple_list(self.want.profiles, self.have.profiles) + return result + + @property + def rules(self): + result = cmp_simple_list(self.want.rules, self.have.rules) + return result + + @property + def description(self): + result = cmp_str_with_none(self.want.description, self.have.description) + return result + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.profiles is None: + raise F5ModuleError( + 'Profiles parameter needs to be specified when creating transport config.' + ) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + +class GenericModuleManager(BaseManager): + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/transport-config/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/message-routing/generic/transport-config/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/transport-config/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/transport-config/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/transport-config/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + query = '?expandSubcollections=true' + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def version_less_than_14(self): + version = tmos_version(self.client) + if Version(version) < Version('14.0.0'): + return True + return False + + def exec_module(self): + if self.version_less_than_14(): + raise F5ModuleError('Message routing is not supported on TMOS version below 14.x') + if self.module.params['type'] == 'generic': + manager = self.get_manager('generic') + else: + raise F5ModuleError( + "Unknown type specified." + ) + return manager.exec_module() + + def get_manager(self, type): + if type == 'generic': + return GenericModuleManager(**self.kwargs) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + profiles=dict( + type='list', + elements='str', + ), + src_port=dict(type='int'), + src_addr_translation=dict( + type='dict', + options=dict( + type=dict( + choices=['none', 'automap', 'snat'] + ), + pool=dict(), + ), + required_if=[ + ['type', 'snat', ['pool']] + ] + ), + rules=dict( + type='list', + elements='str', + ), + type=dict( + choices=['generic'], + default='generic' + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_dns.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_dns.py new file mode 100644 index 00000000..587d346a --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_dns.py @@ -0,0 +1,1020 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_dns +short_description: Manage DNS monitors on a BIG-IP +description: + - Manages DNS health monitors on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the monitor. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(dns) + parent on the C(Common) partition. + type: str + default: /Common/dns + description: + description: + - The description of the monitor. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. + - This value B(must) be less than the C(timeout) value. + - When creating a new monitor, if this parameter is not provided, the + default C(5) will be used. + type: int + up_interval: + description: + - Specifies the interval for the system to use to perform the health check + when a resource is up. + - When C(0), specifies the system uses the interval specified in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval to + use when checking the health of a resource that is up. + - When creating a new monitor, if this parameter is not provided, the + default C(0) will be used. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. + - If the target responds within the set time period, it is considered up. + - If the target does not respond within the set time period, it is considered down. + - You can change this to any number, however, it should be 3 times the + interval number of seconds plus 1 second. + - If this parameter is not provided when creating a new monitor, the default + value will be C(16). + type: int + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + - Monitors in transparent mode can monitor pool members through firewalls. + - When creating a new monitor, if this parameter is not provided, the default + value will be C(no). + type: bool + reverse: + description: + - Specifies whether the monitor operates in reverse mode. + - When the monitor is in reverse mode, a successful receive string match + marks the monitored object down instead of up. You can use the + this mode only if you configure the C(receive) option. + - This parameter is not compatible with the C(time_until_up) parameter. If + C(time_until_up) is specified, it must be C(0). Or, if it already exists, it + must be C(0). + type: bool + receive: + description: + - Specifies the IP address the monitor uses from the resource record sections + of the DNS response. + - The IP address should be specified in the dotted-decimal notation or IPv6 notation. + type: str + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node will be marked up. + - A value of 0 will cause a node to be marked up immediately after a valid + response is received from the node. + - If this parameter is not provided when creating a new monitor, the default + value will be C(0). + type: int + manual_resume: + description: + - Specifies whether the system automatically changes the status of a resource + to B(enabled) at the next successful monitor check. + - If C(yes), you must manually re-enable the resource + before the system can use it for load balancing connections. + - When creating a new monitor, if this parameter is not specified, the default + value is C(no). + - When C(yes), specifies you must manually re-enable the resource after an + unsuccessful monitor check. + - When C(no), specifies the system automatically changes the status of a + resource to B(enabled) at the next successful monitor check. + type: bool + ip: + description: + - IP address part of the IP/port definition. + - If this parameter is not provided when creating a new monitor, the + default value will be C(*). + type: str + port: + description: + - Port address part of the IP/port definition. + - If this parameter is not provided when creating a new monitor, the default + value will be C(*). + - Note that if specifying an IP address, you must use a value between 1 and 65535. + type: str + query_name: + description: + - Specifies a query name for the monitor to use in a DNS query. + type: str + query_type: + description: + - Specifies the type of DNS query the monitor sends. + - When creating a new monitor, if this parameter is not specified, the default + value is C(a). + - When C(a), specifies the monitor will send a DNS query of type A. + - When C(aaaa), specifies the monitor will send a DNS query of type AAAA. + type: str + choices: + - a + - aaaa + answer_section_contains: + description: + - Specifies the type of DNS query the monitor sends. + - When creating a new monitor, if this value is not specified, the default + value is C(query-type). + - When C(query-type), specifies that the response should contain at least one + answer of which the resource record type matches the query type. + - When C(any-type), specifies the DNS message should contain at least one answer. + - When C(anything), specifies an empty answer is enough to mark the status of + the node up. + type: str + choices: + - any-type + - anything + - query-type + accept_rcode: + description: + - Specifies the RCODE required in the response for an up status. + - When creating a new monitor, if this parameter is not specified, the default + value is C(no-error). + - When C(no-error), specifies the status of the node will be marked up if + the received DNS message has no error. + - When C(anything), specifies the status of the node will be marked up + irrespective of the RCODE in the DNS message received. + - If this parameter is set to C(anything), it will disregard the C(receive) + string, and nullify it if the monitor is being updated. + type: str + choices: + - no-error + - anything + adaptive: + description: + - Specifies whether adaptive response time monitoring is enabled for this monitor. + - When C(yes), the monitor determines the state of a service based on how divergent + from the mean latency a monitor probe for that service is allowed to be. + Also, values for the C(allowed_divergence), C(adaptive_limit), and + and C(sampling_timespan) will be enforced. + - When C(disabled), the monitor determines the state of a service based on the + C(interval), C(up_interval), C(time_until_up), and C(timeout) monitor settings. + type: bool + allowed_divergence_type: + description: + - When specifying a new monitor, if C(adaptive) is C(yes), the default is + C(relative). + - When C(absolute), the number of milliseconds the latency of a monitor probe + can exceed the mean latency of a monitor probe for the service being probed. + In typical cases, if the monitor detects three probes in a row that miss the + latency value you set, the pool member or node is marked down. + - When C(relative), the percentage of deviation the latency of a monitor probe + can exceed the mean latency of a monitor probe for the service being probed. + type: str + choices: + - relative + - absolute + allowed_divergence_value: + description: + - When specifying a new monitor, if C(adaptive) is C(yes), and C(type) is + C(relative), the default is C(25) percent. + type: int + adaptive_limit: + description: + - Specifies the absolute number of milliseconds that may not be exceeded by a monitor + probe, regardless of C(allowed_divergence) setting, for a probe to be + considered successful. + - This value applies regardless of the value of the C(allowed_divergence) setting. + - While this value can be configured when C(adaptive) is C(no), it will not take + effect on the system until C(adaptive) is C(yes). + type: int + sampling_timespan: + description: + - Specifies the length, in seconds, of the probe history window the system + uses to calculate the mean latency and standard deviation of a monitor probe. + - While this value can be configured when C(adaptive) is C(no), it will not take + effect on the system until C(adaptive) is C(yes). + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a DNS monitor + bigip_monitor_dns: + name: DNS-UDP-V6 + interval: 2 + query_name: localhost + query_type: aaaa + up_interval: 5 + adaptive: no + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: http +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +adaptive: + description: Whether adaptive is enabled or not. + returned: changed + type: bool + sample: yes +accept_rcode: + description: RCODE required in the response for an up status. + returned: changed + type: str + sample: no-error +allowed_divergence_type: + description: Type of divergence used for adaptive response time monitoring. + returned: changed + type: str + sample: absolute +allowed_divergence_value: + description: + - Value of the type of divergence used for adaptive response time monitoring. + - May be C(percent) or C(ms) depending on whether C(relative) or C(absolute). + returned: changed + type: int + sample: 25 +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +adaptive_limit: + description: Absolute number of milliseconds that may not be exceeded by a monitor probe. + returned: changed + type: int + sample: 200 +sampling_timespan: + description: Absolute number of milliseconds that may not be exceeded by a monitor probe. + returned: changed + type: int + sample: 200 +answer_section_contains: + description: Type of DNS query that the monitor sends. + returned: changed + type: str + sample: query-type +manual_resume: + description: + - Whether the system automatically changes the status of a resource to enabled at the + next successful monitor check. + returned: changed + type: str + sample: query-type +up_interval: + description: Interval for the system to use to perform the health check when a resource is up. + returned: changed + type: int + sample: 0 +query_name: + description: Query name for the monitor to use in a DNS query. + returned: changed + type: str + sample: foo +query_type: + description: Type of DNS query the monitor sends. Either C(a) or C(aaaa). + returned: changed + type: str + sample: aaaa +receive: + description: IP address the monitor uses from the resource record sections of the DNS response. + returned: changed + type: str + sample: 2.3.2.4 +reverse: + description: Whether the monitor operates in reverse mode. + returned: changed + type: bool + sample: yes +port: + description: + - Alias port or service for the monitor to check, on behalf of the pools or pool + members with which the monitor is associated. + returned: changed + type: str + sample: 80 +transparent: + description: Whether the monitor operates in transparent mode. + returned: changed + type: bool + sample: no +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import ( + is_valid_ip, validate_ip_v6_address, validate_ip_address +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'acceptRcode': 'accept_rcode', + 'adaptiveDivergenceType': 'allowed_divergence_type', + 'adaptiveDivergenceValue': 'allowed_divergence_value', + 'adaptiveLimit': 'adaptive_limit', + 'adaptiveSamplingTimespan': 'sampling_timespan', + 'answerContains': 'answer_section_contains', + 'manualResume': 'manual_resume', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + 'qname': 'query_name', + 'qtype': 'query_type', + 'recv': 'receive', + 'defaultsFrom': 'parent', + } + + api_attributes = [ + 'adaptive', + 'acceptRcode', + 'adaptiveDivergenceType', + 'adaptiveDivergenceValue', + 'adaptiveLimit', + 'adaptiveSamplingTimespan', + 'answerContains', + 'defaultsFrom', + 'description', + 'destination', + 'interval', + 'manualResume', + 'qname', + 'qtype', + 'recv', + 'reverse', + 'timeout', + 'timeUntilUp', + 'transparent', + 'upInterval', + 'destination', + ] + + returnables = [ + 'adaptive', + 'accept_rcode', + 'allowed_divergence_type', + 'allowed_divergence_value', + 'description', + 'adaptive_limit', + 'sampling_timespan', + 'answer_section_contains', + 'manual_resume', + 'time_until_up', + 'up_interval', + 'query_name', + 'query_type', + 'receive', + 'reverse', + 'timeout', + 'interval', + 'transparent', + 'parent', + 'ip', + 'port', + ] + + updatables = [ + 'adaptive', + 'accept_rcode', + 'allowed_divergence_type', + 'allowed_divergence_value', + 'adaptive_limit', + 'sampling_timespan', + 'answer_section_contains', + 'description', + 'manual_resume', + 'time_until_up', + 'up_interval', + 'query_name', + 'query_type', + 'receive', + 'reverse', + 'timeout', + 'transparent', + 'parent', + 'destination', + 'interval', + ] + + @property + def type(self): + return 'dns' + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, d, port = value.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = value.rpartition(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + + # Per BZ617284, the BIG-IP UI does not raise a warning about this. + # So I do + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def receive(self): + if self._values['receive'] is None: + return None + if self._values['receive'] == '': + return '' + if is_valid_ip(self._values['receive']): + return self._values['receive'] + raise F5ModuleError( + "The specified 'receive' parameter must be either an IPv4 or v6 address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def manual_resume(self): + if self._values['manual_resume'] is None: + return None + elif self._values['manual_resume'] is True: + return 'enabled' + return 'disabled' + + @property + def reverse(self): + if self._values['reverse'] is None: + return None + elif self._values['reverse'] is True: + return 'enabled' + return 'disabled' + + @property + def transparent(self): + if self._values['transparent'] is None: + return None + elif self._values['transparent'] is True: + return 'enabled' + return 'disabled' + + @property + def adaptive(self): + if self._values['adaptive'] is None: + return None + elif self._values['adaptive'] is True: + return 'enabled' + return 'disabled' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def reverse(self): + return flatten_boolean(self._values['reverse']) + + @property + def transparent(self): + return flatten_boolean(self._values['transparent']) + + @property + def adaptive(self): + return flatten_boolean(self._values['adaptive']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/dns/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def _address_type_matches_query_type(self, type, validator): + if self.want.query_type == type and self.have.query_type == type: + if self.want.receive is not None and validator(self.want.receive): + return True + if self.have.receive is not None and validator(self.have.receive): + return True + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.want.reverse == 'enabled': + if not self.want.receive and not self.have.receive: + raise F5ModuleError( + "A 'receive' string must be specified when setting 'reverse'." + ) + if self.want.time_until_up != 0 and self.have.time_until_up != 0: + raise F5ModuleError( + "Monitors with the 'reverse' attribute are not currently compatible with 'time_until_up'." + ) + if self._address_type_matches_query_type('a', validate_ip_v6_address): + raise F5ModuleError( + "Monitor has a IPv6 address. Only a 'query_type' of 'aaaa' is supported for IPv6." + ) + elif self._address_type_matches_query_type('aaaa', validate_ip_address): + raise F5ModuleError( + "Monitor has a IPv4 address. Only a 'query_type' of 'a' is supported for IPv4." + ) + + if self.want.accept_rcode == 'anything': + if self.want.receive is not None and is_valid_ip(self.want.receive) and self.have.receive is not None: + raise F5ModuleError( + "No 'receive' string may be specified, or exist, when 'accept_rcode' is 'anything'." + ) + elif self.want.receive is None and self.have.receive is not None: + self.want.update({'receive': ''}) + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.want.reverse == 'enabled': + if self.want.time_until_up != 0: + raise F5ModuleError( + "Monitors with the 'reverse' attribute are not currently compatible with 'time_until_up'." + ) + if not self.want.receive: + raise F5ModuleError( + "A 'receive' string must be specified when setting 'reverse'." + ) + + if self.want.receive is not None and validate_ip_v6_address(self.want.receive) and self.want.query_type == 'a': + raise F5ModuleError( + "Monitor has a IPv6 address. Only a 'query_type' of 'aaaa' is supported for IPv6." + ) + elif (self.want.receive is not None and validate_ip_address(self.want.receive) and + self.want.query_type == 'aaaa'): + raise F5ModuleError( + "Monitor has a IPv4 address. Only a 'query_type' of 'a' is supported for IPv4." + ) + + if self.want.accept_rcode == 'anything': + if self.want.receive is not None and is_valid_ip(self.want.receive): + raise F5ModuleError( + "No 'receive' string may be specified, or exist, when 'accept_rcode' is 'anything'." + ) + elif self.want.receive is None: + self.want.update({'receive': ''}) + + if self.want.query_name is None: + raise F5ModuleError( + "'query_name' is required when creating a new DNS monitor." + ) + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/monitor/dns/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/dns/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/dns/{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/ltm/monitor/dns/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/dns'), + receive=dict(), + ip=dict(), + description=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + manual_resume=dict(type='bool'), + reverse=dict(type='bool'), + transparent=dict(type='bool'), + time_until_up=dict(type='int'), + up_interval=dict(type='int'), + accept_rcode=dict(choices=['no-error', 'anything']), + adaptive=dict(type='bool'), + sampling_timespan=dict(type='int'), + adaptive_limit=dict(type='int'), + answer_section_contains=dict( + choices=['any-type', 'anything', 'query-type'] + ), + query_name=dict(), + query_type=dict(choices=['a', 'aaaa']), + allowed_divergence_type=dict(choices=['relative', 'absolute']), + allowed_divergence_value=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_external.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_external.py new file mode 100644 index 00000000..3e0a82a3 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_external.py @@ -0,0 +1,734 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_external +short_description: Manages external LTM monitors on a BIG-IP +description: + - Manages external LTM monitors on a BIG-IP device. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the monitor. + type: str + required: True + description: + description: + - The description of the monitor. + type: str + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(external) + parent on the C(Common) partition. + type: str + default: /Common/external + arguments: + description: + - Specifies any command-line arguments the script requires. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value will be + '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, then the default value will be + '*'. If specifying an IP address, you must use a value between 1 and 65535. + type: str + external_program: + description: + - Specifies the name of the file for the monitor to use. In order to reference + a file, you must first import it using options on the B(System > File Management > External + Monitor Program File List > Import) screen. The BIG-IP system automatically + places the file in the proper location on the file system. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. If this parameter is not provided when creating + a new monitor, the default value will be 5. This value B(must) + be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. + - If the target responds within the set time period, it is considered up. + - If the target does not respond within the set time period, it is considered + down. + - You can change this to any number, however, it should be + 3 times the interval number of seconds plus 1 second. + - If this parameter is not provided when creating a new monitor, the + default value will be C(16). + type: int + variables: + description: + - Specifies any variables the script requires. + - Note that double quotes in values will be suppressed. + type: dict + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an external monitor + bigip_monitor_external: + name: foo + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Create an external monitor with variables + bigip_monitor_external: + name: foo + timeout: 10 + variables: + var1: foo + var2: bar + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add a variable to an existing set + bigip_monitor_external: + name: foo + timeout: 10 + variables: + var1: foo + var2: bar + cat: dog + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: external +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +''' +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import ( + compare_dictionary, cmp_str_with_none +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'apiRawValues': 'variables', + 'run': 'external_program', + 'args': 'arguments', + } + + api_attributes = [ + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'run', + 'args', + 'description', + ] + + returnables = [ + 'parent', + 'ip', + 'port', + 'interval', + 'timeout', + 'variables', + 'external_program', + 'arguments', + 'description', + ] + + updatables = [ + 'destination', + 'interval', + 'timeout', + 'variables', + 'external_program', + 'arguments', + 'description', + ] + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, d, port = value.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = value.rpartition(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + + # Per BZ617284, the BIG-IP UI does not raise a warning about this. + # So I do + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def type(self): + return 'external' + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def variables(self): + if self._values['variables'] is None: + return None + pattern = r'^userDefined\s(?P.*)' + result = {} + for k, v in iteritems(self._values['variables']): + matches = re.match(pattern, k) + if not matches: + raise F5ModuleError( + "Unable to find the variable 'key' in the API payload." + ) + key = matches.group('key') + result[key] = v + return result + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def variables(self): + if self._values['variables'] is None: + return None + result = {} + for k, v in iteritems(self._values['variables']): + result[k] = str(v).replace('"', '') + return result + + @property + def external_program(self): + if self._values['external_program'] is None: + return None + return fq_name(self.partition, self._values['external_program']) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + @property + def variables(self): + if self.want.variables is None: + return None + if self.have.variables is None: + return dict( + variables=self.want.variables + ) + result = dict() + different = compare_dictionary(self.want.variables, self.have.variables) + if not different: + return None + + for k, v in iteritems(self.want.variables): + if k in self.have.variables and v != self.have.variables[k]: + result[k] = v + elif k not in self.have.variables: + result[k] = v + for k, v in iteritems(self.have.variables): + if k not in self.want.variables: + result[k] = "none" + if result: + result = dict( + variables=result + ) + return result + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/external/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 16}) + if self.want.interval is None: + self.want.update({'interval': 5}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + + 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/ltm/monitor/external/".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if self.want.variables: + self.set_variable_on_device(self.want.variables) + + def set_variable_on_device(self, commands): + command = ' '.join(['user-defined {0} \\\"{1}\\\"'.format(k, v) for k, v in iteritems(commands)]) + command = 'tmsh modify ltm monitor external {0} {1}'.format(self.want.name, command) + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(command) + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + if params: + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/external/{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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if self.changes.variables: + self.set_variable_on_device(self.changes.variables) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/external/{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/ltm/monitor/external/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/external'), + description=dict(), + arguments=dict(), + ip=dict(), + port=dict(), + external_program=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + state=dict( + default='present', + choices=['present', 'absent'] + ), + variables=dict(type='dict'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_ftp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_ftp.py new file mode 100644 index 00000000..4eba1a88 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_ftp.py @@ -0,0 +1,817 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_ftp +short_description: Manage FTP monitors on a BIG-IP +description: + - Manage FTP monitors on a BIG-IP device. +version_added: "1.1.0" +options: + name: + description: + - Specifies the name of the monitor. + type: str + required: True + app_service: + description: + - The iApp service to be associated with this profile. When no service is + specified, the default is None. + type: str + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. + - When creating a new monitor, if this parameter is not specified, the default + is the system-supplied C(ftp) monitor. + type: str + description: + description: + - The description of the monitor. + type: str + debug: + description: + - Specifies whether the monitor sends error messages and additional information to a log file created and + labeled specifically for this monitor. + - "When C(yes) the system redirects error messages and additional information to the + C(/var/log/monitors/--.log) file." + type: bool + mode: + description: + - Specifies the data transfer process (DTP) mode. + - When C(passive), the monitor sends a data transfer request to the FTP server. When the FTP server receives the + request, the FTP server initiates and establishes the data connection. + - When C(port), the monitor initiates and establishes the data connection with the FTP server. + type: str + choices: + - passive + - port + filename: + description: + - Specifies the full path and file name of the file the system attempts to download. The health check + is successful if the system can download the file. + type: str + target_username: + description: + - Specifies the user name, if the monitored target requires authentication. + type: str + target_password: + description: + - Specifies the password, if the monitored target requires authentication. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + If specifying an IP address, you must use a value between 1 and 65535. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. + - This value B(must) be less than the C(timeout) value. + - When creating a new monitor, if this parameter is not provided, the + default C(5) will be used. + type: int + up_interval: + description: + - Specifies the interval at which the system performs the health check + when a resource is up. + - When C(0), specifies the system uses the interval specified in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval to + use when checking the health of a resource that is up. + - When creating a new monitor, if this parameter is not provided, the + default C(0) will be used. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. + - If the target responds within the set time period, it is considered up. + - If the target does not respond within the set time period, it is considered down. + - You can change this to any number, however, it should be 3 times the + interval number of seconds plus 1 second. + - If this parameter is not provided when creating a new monitor, then the default + value will be C(31). + type: int + manual_resume: + description: + - Specifies whether the system automatically changes the status of a resource + to B(enabled) at the next successful monitor check. + - If you set this option to C(yes), you must manually re-enable the resource + before the system can use it for load balancing connections. + - When creating a new monitor, if this parameter is not specified, the default + value is C(no). + - When C(yes), specifies you must manually re-enable the resource after an + unsuccessful monitor check. + - When C(no), specifies the system automatically changes the status of a + resource to B(enabled) at the next successful monitor check. + type: bool + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node will be marked up. + - A value of C(0) will cause a node to be marked up immediately after a valid + response is received from the node. + - If this parameter is not provided when creating a new monitor, then the default + value will be C(0). + type: int + update_password: + description: + - C(always) will update passwords if the C(target_password) is specified. + - C(on_create) will only set the password for newly created monitors. + type: str + choices: + - always + - on_create + default: always + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create FTP Monitor + bigip_monitor_ftp: + state: present + ip: 10.10.10.10 + name: my_ft_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove FTP Monitor + bigip_monitor_ftp: + state: absent + name: my_ftp_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Include a username and password in the FTP monitor + bigip_monitor_ftp: + state: absent + name: my_ftp_monitor + target_username: monitor_user + target_password: monitor_pass + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +app_service: + description: The iApp service associated with this monitor. + returned: changed + type: str + sample: /Common/good_service.app/good_service +parent: + description: The parent monitor. + returned: changed + type: str + sample: /Common/foo_ftp +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +debug: + description: + - Whether the monitor sends error messages and additional information to a log file created and + labeled specifically for this monitor. + returned: changed + type: bool + sample: no +mode: + description: Specifies the data transfer process (DTP) mode. + returned: changed + type: str + sample: passive +filename: + description: Specifies the full path and file name of the file the system attempts to download. + returned: changed + type: str + sample: /ftp/var/health.txt +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +port: + description: + - Alias port or service for the monitor to check, on behalf of the pools or pool + members with which the monitor is associated. + returned: changed + type: str + sample: 80 +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +up_interval: + description: Interval for the system to use to perform the health check when a resource is up. + returned: changed + type: int + sample: 0 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to up at the next successful monitor check. + returned: changed + type: bool + sample: yes +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'appService': 'app_service', + 'defaultsFrom': 'parent', + 'timeUntilUp': 'time_until_up', + 'manualResume': 'manual_resume', + 'upInterval': 'up_interval', + 'username': 'target_username', + 'password': 'target_password', + } + + api_attributes = [ + 'defaultsFrom', + 'description', + 'debug', + 'mode', + 'filename', + 'username', + 'password', + 'destination', + 'interval', + 'upInterval', + 'timeout', + 'manualResume', + 'timeUntilUp', + ] + + returnables = [ + 'app_service', + 'parent', + 'description', + 'debug', + 'mode', + 'filename', + 'ip', + 'port', + 'interval', + 'up_interval', + 'timeout', + 'manual_resume', + 'time_until_up', + 'target_username', + 'destination', + ] + + updatables = [ + 'app_service', + 'description', + 'debug', + 'mode', + 'filename', + 'target_username', + 'target_password', + 'ip', + 'port', + 'interval', + 'up_interval', + 'timeout', + 'manual_resume', + 'time_until_up', + 'destination', + ] + + +class ApiParameters(Parameters): + @property + def ip(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return ip + + @property + def port(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return int(port) if port.isnumeric() else port + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > self._values['interval'] > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400." + ) + return self._values['interval'] + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + if self._values['timeout'] is None: + return None + if 1 > self._values['timeout'] > 86400: + raise F5ModuleError( + "Timeout value must be between 1 and 86400." + ) + return self._values['timeout'] + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, port = value.split(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + if self._values['time_until_up'] is None: + return None + if 0 > self._values['time_until_up'] > 86400: + raise F5ModuleError( + "Time_until_up value must be between 0 and 86400." + ) + return self._values['time_until_up'] + + @property + def manual_resume(self): + result = flatten_boolean(self._values['manual_resume']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def debug(self): + result = flatten_boolean(self._values['debug']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def debug(self): + return flatten_boolean(self._values['debug']) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified." + ) + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def target_password(self): + if self.want.target_password != self.have.target_password: + if self.want.update_password == 'always': + result = self.want.target_password + return result + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/ftp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/monitor/ftp/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/ftp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/ftp/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/ftp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + app_service=dict(), + parent=dict(), + description=dict(), + debug=dict(type='bool'), + mode=dict( + choices=['passive', 'port'] + ), + filename=dict(), + target_username=dict(), + target_password=dict(no_log=True), + ip=dict(), + port=dict(), + interval=dict(type='int'), + up_interval=dict(type='int'), + timeout=dict(type='int'), + manual_resume=dict(type='bool'), + time_until_up=dict(type='int'), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_gateway_icmp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_gateway_icmp.py new file mode 100644 index 00000000..3a715e51 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_gateway_icmp.py @@ -0,0 +1,792 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_gateway_icmp +short_description: Manages F5 BIG-IP LTM gateway ICMP monitors +description: + - Manages gateway ICMP monitors on a BIG-IP LTM. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the + C(gateway_icmp) parent on the C(Common) partition. + type: str + default: /Common/gateway_icmp + description: + description: + - The description of the monitor. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. If + specifying an IP address, you must use a value between 1 and 65535. + type: str + interval: + description: + - Specifies, in seconds, the frequency at which the system issues the + monitor check when either the resource is down or the status of the + resource is unknown. + type: int + timeout: + description: + - Specifies the number of seconds the target has in which to respond to + the monitor request. + - If the target responds within the set time period, it is considered 'up'. + If the target does not respond within the set time period, it is considered + 'down'. When this value is set to 0 (zero), the system uses the interval + from the parent monitor. + - Note that C(timeout) and C(time_until_up) combine to control when a + resource is set to up. + type: int + time_until_up: + description: + - Specifies the number of seconds to wait after a resource first responds + correctly to the monitor before setting the resource to 'up'. + - During the interval, all responses from the resource must be correct. + - When the interval expires, the resource is marked 'up'. + - A value of C(0) means the resource is marked up immediately upon + receipt of the first correct response. + type: int + up_interval: + description: + - Specifies the interval for the system to use to perform the health check + when a resource is up. + - When C(0), specifies the system uses the interval specified in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval to + use when checking the health of a resource that is up. + type: int + manual_resume: + description: + - Specifies whether the system automatically changes the status of a resource + to B(enabled) at the next successful monitor check. + - If you set this option to C(yes), you must manually re-enable the resource + before the system can use it for load balancing connections. + - When C(yes), specifies you must manually re-enable the resource after an + unsuccessful monitor check. + - When C(no), specifies the system automatically changes the status of a + resource to B(enabled) at the next successful monitor check. + type: bool + adaptive: + description: + - Specifies whether adaptive response time monitoring is enabled for this monitor. + - When C(yes), the monitor determines the state of a service based on how divergent + from the mean latency a monitor probe for that service is allowed to be. + Also, values for the C(allowed_divergence), C(adaptive_limit), and + and C(sampling_timespan) will be enforced. + - When C(disabled), the monitor determines the state of a service based on the + C(interval), C(up_interval), C(time_until_up), and C(timeout) monitor settings. + type: bool + allowed_divergence_type: + description: + - When specifying a new monitor, if C(adaptive) is C(yes), the default is + C(relative). + - When C(absolute), the number of milliseconds the latency of a monitor probe + can exceed the mean latency of a monitor probe for the service being probed. + In typical cases, if the monitor detects three probes in a row that miss the + latency value you set, the pool member or node is marked down. + - When C(relative), the percentage of deviation the latency of a monitor probe + can exceed the mean latency of a monitor probe for the service being probed. + type: str + choices: + - relative + - absolute + allowed_divergence_value: + description: + - When specifying a new monitor, if C(adaptive) is C(yes), and C(type) is + C(relative), the default is C(25) percent. + type: int + adaptive_limit: + description: + - Specifies the absolute number of milliseconds that may not be exceeded by a monitor + probe, regardless of C(allowed_divergence) setting, for a probe to be + considered successful. + - This value applies regardless of the value of the C(allowed_divergence) setting. + - While this value can be configured when C(adaptive) is C(no), it will not take + effect on the system until C(adaptive) is C(yes). + type: int + sampling_timespan: + description: + - Specifies the length, in seconds, of the probe history window that the system + uses to calculate the mean latency and standard deviation of a monitor probe. + - While this value can be configured when C(adaptive) is C(no), it will not take + effect on the system until C(adaptive) is C(yes). + type: int + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + - A monitor in transparent mode directs traffic through the associated pool members + or nodes (usually a router or firewall) to the aliased destination (that is, it + probes the C(ip)-C(port) combination specified in the monitor). + - If the monitor cannot successfully reach the aliased destination, the pool member + or node through which the monitor traffic was sent is marked down. + - When creating a new monitor, if this parameter is not provided, then the default + value will be whatever is provided by the C(parent). + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures that the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a monitor + bigip_monitor_gateway_icmp: + name: gw1 + adaptive: no + interval: 1 + time_until_up: 0 + timeout: 3 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: gateway-icmp +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +adaptive: + description: Whether adaptive is enabled or not. + returned: changed + type: bool + sample: yes +allowed_divergence_type: + description: Type of divergence used for adaptive response time monitoring. + returned: changed + type: str + sample: absolute +allowed_divergence_value: + description: + - Value of the type of divergence used for adaptive response time monitoring. + - May be C(percent) or C(ms) depending on whether C(relative) or C(absolute). + returned: changed + type: int + sample: 25 +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +adaptive_limit: + description: Absolute number of milliseconds that may not be exceeded by a monitor probe. + returned: changed + type: int + sample: 200 +sampling_timespan: + description: Absolute number of milliseconds that may not be exceeded by a monitor probe. + returned: changed + type: int + sample: 200 +up_interval: + description: Interval for the system to use to perform the health check when a resource is up. + returned: changed + type: int + sample: 0 +port: + description: + - Alias port or service for the monitor to check, on behalf of the pools or pool + members with which the monitor is associated. + returned: changed + type: str + sample: 80 +transparent: + description: Whether the monitor operates in transparent mode. + returned: changed + type: bool + sample: no +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'adaptiveDivergenceType': 'allowed_divergence_type', + 'adaptiveDivergenceValue': 'allowed_divergence_value', + 'adaptiveLimit': 'adaptive_limit', + 'adaptiveSamplingTimespan': 'sampling_timespan', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + 'defaultsFrom': 'parent', + } + + api_attributes = [ + 'adaptive', + 'adaptiveDivergenceType', + 'adaptiveDivergenceValue', + 'adaptiveLimit', + 'adaptiveSamplingTimespan', + 'defaultsFrom', + 'description', + 'destination', + 'interval', + 'manualResume', + 'timeout', + 'timeUntilUp', + 'transparent', + 'upInterval', + 'destination', + ] + + returnables = [ + 'adaptive', + 'allowed_divergence_type', + 'allowed_divergence_value', + 'description', + 'adaptive_limit', + 'sampling_timespan', + 'manual_resume', + 'time_until_up', + 'up_interval', + 'timeout', + 'interval', + 'transparent', + 'parent', + 'ip', + 'port', + ] + + updatables = [ + 'adaptive', + 'allowed_divergence_type', + 'allowed_divergence_value', + 'adaptive_limit', + 'sampling_timespan', + 'description', + 'manual_resume', + 'time_until_up', + 'up_interval', + 'timeout', + 'interval', + 'transparent', + 'parent', + 'destination', + 'interval', + ] + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, d, port = value.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = value.rpartition(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + + # Per BZ617284, the BIG-IP UI does not raise a warning about this. + # So I do + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def manual_resume(self): + if self._values['manual_resume'] is None: + return None + elif self._values['manual_resume'] is True: + return 'enabled' + return 'disabled' + + @property + def transparent(self): + if self._values['transparent'] is None: + return None + elif self._values['transparent'] is True: + return 'enabled' + return 'disabled' + + @property + def adaptive(self): + if self._values['adaptive'] is None: + return None + elif self._values['adaptive'] is True: + return 'enabled' + return 'disabled' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def transparent(self): + return flatten_boolean(self._values['transparent']) + + @property + def adaptive(self): + return flatten_boolean(self._values['adaptive']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/gateway-icmp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/monitor/gateway-icmp/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/gateway-icmp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/gateway-icmp/{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/ltm/monitor/gateway-icmp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/gateway_icmp'), + ip=dict(), + description=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + time_until_up=dict(type='int'), + up_interval=dict(type='int'), + manual_resume=dict(type='bool'), + adaptive=dict(type='bool'), + allowed_divergence_type=dict(choices=['relative', 'absolute']), + allowed_divergence_value=dict(type='int'), + adaptive_limit=dict(type='int'), + sampling_timespan=dict(type='int'), + transparent=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_http.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_http.py new file mode 100644 index 00000000..796cd8c0 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_http.py @@ -0,0 +1,766 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_http +short_description: Manages F5 BIG-IP LTM HTTP monitors +description: Manages F5 BIG-IP LTM HTTP monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(http) + parent on the C(Common) partition. + type: str + default: /Common/http + description: + description: + - The description of the monitor. + type: str + send: + description: + - The Send string for the monitor call. When creating a new monitor, if + this value is not provided, the default C(GET /\r\n) is used. + type: str + receive: + description: + - The Receive string for the monitor call. + type: str + receive_disable: + description: + - This setting works like C(receive), except the system marks the node + or pool member disabled when its response matches the C(receive_disable) + string but not C(receive). To use this setting, you must specify both + C(receive_disable) and C(receive). + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is + '*'. If specifying an IP address, you must specify a value between 1 and 65535. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. If this parameter is not provided when creating + a new monitor, the default value is 5. This value B(must) + be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + to any number, however, it should be 3 times the + interval number of seconds plus 1 second. If this parameter is not + provided when creating a new monitor, the default value is 16. + type: int + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node is marked up. A value of 0 causes a + node to be marked up immediately after a valid response is received + from the node. If this parameter is not provided when creating + a new monitor, the default value is 0. + type: int + target_username: + description: + - Specifies the user name, if the monitored target requires authentication. + type: str + target_password: + description: + - Specifies the password, if the monitored target requires authentication. + type: str + reverse: + description: + - Specifies whether the monitor operates in reverse mode. + - When the monitor is in reverse mode, a successful receive string match + marks the monitored object down instead of up. You can use the + this mode only if you configure the C(receive) option. + - This parameter is not compatible with the C(time_until_up) parameter. If + C(time_until_up) is specified, it must be C(0). Or, if it already exists, it + must be C(0). + type: bool + up_interval: + description: + - Specifies the interval for the system to use to perform the health check + when a resource is up. + - When C(0), specifies the system uses the interval specified in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval + when checking the health of a resource that is up. + type: int + version_added: "1.22.0" + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create HTTP Monitor + bigip_monitor_http: + state: present + ip: 10.10.10.10 + name: my_http_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove HTTP Monitor + bigip_monitor_http: + state: absent + name: my_http_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Include a username and password in the HTTP monitor + bigip_monitor_http: + state: absent + name: my_http_monitor + target_username: monitor_user + target_password: monitor_pass + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: http +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important_Monitor +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +reverse: + description: Whether the monitor operates in reverse mode. + returned: changed + type: bool + sample: yes +up_interval: + description: Interval for the system to use to perform the health check when a resource is up. + returned: changed + type: int + sample: 0 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'timeUntilUp': 'time_until_up', + 'defaultsFrom': 'parent', + 'recv': 'receive', + 'recvDisable': 'receive_disable', + 'upInterval': 'up_interval', + } + + api_attributes = [ + 'timeUntilUp', + 'defaultsFrom', + 'interval', + 'timeout', + 'recv', + 'send', + 'destination', + 'username', + 'password', + 'recvDisable', + 'description', + 'reverse', + 'upInterval', + ] + + returnables = [ + 'parent', + 'send', + 'receive', + 'ip', + 'port', + 'interval', + 'timeout', + 'time_until_up', + 'receive_disable', + 'description', + 'reverse', + 'up_interval', + ] + + updatables = [ + 'destination', + 'send', + 'receive', + 'interval', + 'timeout', + 'time_until_up', + 'target_username', + 'target_password', + 'receive_disable', + 'description', + 'reverse', + 'up_interval', + ] + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, d, port = value.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = value.rpartition(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + + # Per BZ617284, the BIG-IP UI does not raise a warning about this. + # So I do + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def type(self): + return 'http' + + @property + def username(self): + return self._values['target_username'] + + @property + def password(self): + return self._values['target_password'] + + @property + def reverse(self): + return flatten_boolean(self._values['reverse']) + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def reverse(self): + if self._values['reverse'] is None: + return None + elif self._values['reverse'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def reverse(self): + return flatten_boolean(self._values['reverse']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def receive(self): + return cmp_str_with_none(self.want.receive, self.have.receive) + + @property + def receive_disable(self): + return cmp_str_with_none(self.want.receive_disable, self.have.receive_disable) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/http/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.want.reverse == 'enabled': + if not self.want.receive and not self.have.receive: + raise F5ModuleError( + "A 'receive' string must be specified when setting 'reverse'." + ) + if self.want.time_until_up != 0 and self.have.time_until_up != 0: + raise F5ModuleError( + "Monitors with the 'reverse' attribute are not currently compatible with 'time_until_up'." + ) + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the monitor.") + return True + + def create(self): + self._set_changed_options() + if self.want.reverse == 'enabled': + if self.want.time_until_up != 0: + raise F5ModuleError( + "Monitors with the 'reverse' attribute are not currently compatible with 'time_until_up'." + ) + if not self.want.receive: + raise F5ModuleError( + "A 'receive' string must be specified when setting 'reverse'." + ) + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 16}) + if self.want.interval is None: + self.want.update({'interval': 5}) + if self.want.time_until_up is None: + self.want.update({'time_until_up': 0}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + if self.want.send is None: + self.want.update({'send': 'GET /\r\n'}) + + 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/ltm/monitor/http/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/http/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/http/{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/ltm/monitor/http/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/http'), + description=dict(), + send=dict(), + receive=dict(), + receive_disable=dict(), + ip=dict(), + up_interval=dict(type='int'), + port=dict(), + interval=dict(type='int'), + reverse=dict(type='bool'), + timeout=dict(type='int'), + time_until_up=dict(type='int'), + target_username=dict(), + target_password=dict(no_log=True), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_https.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_https.py new file mode 100644 index 00000000..1c4395ae --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_https.py @@ -0,0 +1,800 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_https +short_description: Manages F5 BIG-IP LTM HTTPS monitors +description: Manages F5 BIG-IP LTM HTTPS monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + description: + description: + - The description of the monitor. + type: str + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(https) + parent on the C(Common) partition. + type: str + default: /Common/https + send: + description: + - The Send string for the monitor call. When creating a new monitor, if + this value is not provided, the default C(GET /\\r\\n) is used. + type: str + receive: + description: + - The Receive string for the monitor call. + type: str + receive_disable: + description: + - This setting works like C(receive), except the system marks the node + or pool member disabled when its response matches the C(receive_disable) + string but not C(receive). To use this setting, you must specify both + C(receive_disable) and C(receive). + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is + '*'. If specifying an IP address, you must specify a value between 1 and 65535. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template runs. If this parameter is not provided when creating + a new monitor, the default value is 5. This value B(must) + be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + to any number, however, it should be 3 times the + interval number of seconds plus 1 second. If this parameter is not + provided when creating a new monitor, the default value is 16. + type: int + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node is marked up. A value of 0 causes a + node to be marked up immediately after a valid response is received + from the node. If this parameter is not provided when creating + a new monitor, then the default value is 0. + type: int + target_username: + description: + - Specifies the user name, if the monitored target requires authentication. + type: str + target_password: + description: + - Specifies the password, if the monitored target requires authentication. + type: str + reverse: + description: + - Specifies whether the monitor operates in reverse mode. + - When the monitor is in reverse mode, a successful receive string match + marks the monitored object down instead of up. You can use the + this mode only if you configure the C(receive) option. + - This parameter is not compatible with the C(time_until_up) parameter. If + C(time_until_up) is specified, it must be C(0). Or, if it already exists, it + must be C(0). + type: bool + version_added: "1.12.0" + ssl_profile: + description: + - Specifies the SSL profile to use for the HTTPS monitor. + - Defining SSL profiles enables refined customization of the SSL attributes + for an HTTPS monitor. + - This parameter is only supported on BIG-IP versions 13.x and later. + type: str + up_interval: + description: + - Specifies the interval for the system to use to perform the health check + when a resource is up. + - When C(0), specifies the system uses the interval specified in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval + when checking the health of a resource that is up. + type: int + cipher_list: + description: + - Specifies the list of ciphers for this monitor. + - The items in the cipher list are separated with a colon C(:). + type: str + version_added: "1.3.0" + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create HTTPS Monitor + bigip_monitor_https: + name: my_http_monitor + state: present + ip: 10.10.10.10 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove HTTPS Monitor + bigip_monitor_https: + name: my_http_monitor + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: https +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +reverse: + description: Whether the monitor operates in reverse mode. + returned: changed + type: bool + sample: yes +up_interval: + description: Interval for the system to use to perform the health check when a resource is up. + returned: changed + type: int + sample: 0 +cipher_list: + description: The new value for the cipher list. + returned: changed + type: str + sample: +3DES:+kEDH +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'timeUntilUp': 'time_until_up', + 'defaultsFrom': 'parent', + 'recv': 'receive', + 'recvDisable': 'receive_disable', + 'sslProfile': 'ssl_profile', + 'upInterval': 'up_interval', + 'cipherlist': 'cipher_list', + } + + api_attributes = [ + 'timeUntilUp', + 'defaultsFrom', + 'interval', + 'timeout', + 'recv', + 'send', + 'destination', + 'username', + 'password', + 'recvDisable', + 'description', + 'reverse', + 'sslProfile', + 'upInterval', + 'cipherlist', + ] + + returnables = [ + 'parent', + 'send', + 'receive', + 'ip', + 'port', + 'interval', + 'timeout', + 'time_until_up', + 'receive_disable', + 'description', + 'reverse', + 'ssl_profile', + 'up_interval', + 'cipher_list', + ] + + updatables = [ + 'destination', + 'send', + 'receive', + 'interval', + 'timeout', + 'time_until_up', + 'target_username', + 'target_password', + 'receive_disable', + 'description', + 'reverse', + 'ssl_profile', + 'up_interval', + 'cipher_list', + ] + + @property + def username(self): + return self._values['target_username'] + + @property + def password(self): + return self._values['target_password'] + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, d, port = value.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = value.rpartition(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + + # Per BZ617284, the BIG-IP UI does not raise a warning about this. + # So I do + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + elif self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def type(self): + return 'https' + + @property + def reverse(self): + return flatten_boolean(self._values['reverse']) + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def ssl_profile(self): + if self._values['ssl_profile'] is None: + return None + if self._values['ssl_profile'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['ssl_profile']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def reverse(self): + if self._values['reverse'] is None: + return None + elif self._values['reverse'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def reverse(self): + return flatten_boolean(self._values['reverse']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + @property + def ssl_profile(self): + if self.want.ssl_profile is None: + return None + if self.want.ssl_profile == '' and self.have.ssl_profile is None: + return None + if self.want.ssl_profile != self.have.ssl_profile: + return self.want.ssl_profile + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def receive(self): + return cmp_str_with_none(self.want.receive, self.have.receive) + + @property + def receive_disable(self): + return cmp_str_with_none(self.want.receive_disable, self.have.receive_disable) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/https/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.want.reverse == 'enabled': + if not self.want.receive and not self.have.receive: + raise F5ModuleError( + "A 'receive' string must be specified when setting 'reverse'." + ) + if self.want.time_until_up != 0 and self.have.time_until_up != 0: + raise F5ModuleError( + "Monitors with the 'reverse' attribute are not currently compatible with 'time_until_up'." + ) + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.want.reverse == 'enabled': + if self.want.time_until_up != 0: + raise F5ModuleError( + "Monitors with the 'reverse' attribute are not currently compatible with 'time_until_up'." + ) + if not self.want.receive: + raise F5ModuleError( + "A 'receive' string must be specified when setting 'reverse'." + ) + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 16}) + if self.want.interval is None: + self.want.update({'interval': 5}) + if self.want.time_until_up is None: + self.want.update({'time_until_up': 0}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + if self.want.send is None: + self.want.update({'send': 'GET /\r\n'}) + + 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/ltm/monitor/https/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/https/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/https/{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/ltm/monitor/https/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/https'), + description=dict(), + send=dict(), + receive=dict(), + receive_disable=dict(), + ip=dict(), + up_interval=dict(type='int'), + port=dict(), + interval=dict(type='int'), + reverse=dict(type='bool'), + timeout=dict(type='int'), + time_until_up=dict(type='int'), + target_username=dict(), + target_password=dict(no_log=True), + ssl_profile=dict(), + cipher_list=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_icmp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_icmp.py new file mode 100644 index 00000000..782ded9e --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_icmp.py @@ -0,0 +1,787 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_icmp +short_description: Manages F5 BIG-IP LTM ICMP monitors +description: + - Manages ICMP monitors on a BIG-IP. +version_added: "1.1.0" +options: + name: + description: + - Specifies the name of the monitor. + type: str + required: True + app_service: + description: + - The iApp service to be associated with this profile. When no service is + specified, the default is None. + type: str + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. + - When creating a new monitor, if this parameter is not specified, the default + is the system-supplied C(icmp) monitor. + type: str + description: + description: + - The description of the monitor. + type: str + ip: + description: + - Specifies the IP address of the resource that is the destination of this monitor. + - When set to B(*), the device performs a health check on the IP address of the node. + - "When set to an B() the device performs a health check on that IP address and marks the associated node up + or down as a result of the response. This option is set by the device by default when not defined during monitor + creation." + - "When set to an B() and C(transparent) is C(yes), the device performs a health check on that IP address, + routes the check through the associated node IP address, and marks the associated node IP address up or down + accordingly." + type: str + interval: + description: + - Specifies the frequency, in seconds, at which the system issues the + monitor check when either the resource is down or the status of the + resource is unknown. + type: int + timeout: + description: + - Specifies the number of seconds the target has in which to respond to + the monitor request. + - If the target responds within the set time period, it is considered 'up'. + If the target does not respond within the set time period, it is considered + 'down'. When this value is set to 0 (zero), the system uses the interval + from the parent monitor. + - Note that C(timeout) and C(time_until_up) combine to control when a + resource is set to up. + type: int + time_until_up: + description: + - Specifies the number of seconds to wait after a resource first responds + correctly to the monitor before setting the resource to 'up'. + - During the interval, all responses from the resource must be correct. + - When the interval expires, the resource is marked 'up'. + - A value of 0 means the resource is marked up immediately upon + receipt of the first correct response. + type: int + up_interval: + description: + - Specifies the interval for the system to use to perform the health check + when a resource is up. + - When C(0), specifies the system uses the interval specified in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval to + use when checking the health of a resource that is up. + type: int + manual_resume: + description: + - Specifies whether the system automatically changes the status of a resource + to B(enabled) at the next successful monitor check. + - If you set this option to C(yes), you must manually re-enable the resource + before the system can use it for load balancing connections. + - When C(yes), specifies you must manually re-enable the resource after an + unsuccessful monitor check. + - When C(no), specifies the system automatically changes the status of a + resource to B(enabled) at the next successful monitor check. + type: bool + adaptive: + description: + - Specifies whether adaptive response time monitoring is enabled for this monitor. + - When C(yes), the monitor determines the state of a service based on how divergent + from the mean latency a monitor probe for that service is allowed to be. + Also, values for the C(allowed_divergence), C(adaptive_limit), and + and C(sampling_timespan) will be enforced. + - When C(disabled), the monitor determines the state of a service based on the + C(interval), C(up_interval), C(time_until_up), and C(timeout) monitor settings. + type: bool + allowed_divergence_type: + description: + - When specifying a new monitor, if C(adaptive) is C(yes), the default is + C(relative). + - When C(absolute), the number of milliseconds the latency of a monitor probe + can exceed the mean latency of a monitor probe for the service being probed. + In typical cases, if the monitor detects three probes in a row that miss the + latency value you set, the pool member or node is marked down. + - When C(relative), the percentage of deviation the latency of a monitor probe + can exceed the mean latency of a monitor probe for the service being probed. + type: str + choices: + - relative + - absolute + allowed_divergence_value: + description: + - When specifying a new monitor, if C(adaptive) is C(yes), and C(type) is + C(relative), the default is C(25) percent. + type: int + adaptive_limit: + description: + - Specifies the absolute number of milliseconds that may not be exceeded by a monitor + probe, regardless of C(allowed_divergence) setting, for a probe to be + considered successful. + - This value applies regardless of the value of the C(allowed_divergence) setting. + - While this value can be configured when C(adaptive) is C(no), it will not take + effect on the system until C(adaptive) is C(yes). + type: int + sampling_timespan: + description: + - Specifies the length, in seconds, of the probe history window the system + uses to calculate the mean latency and standard deviation of a monitor probe. + - While this value can be configured when C(adaptive) is C(no), it will not take + effect on the system until C(adaptive) is C(yes). + type: int + transparent: + description: + - Specifies whether the monitor operates in transparent mode. + - A monitor in transparent mode directs traffic through the associated pool members + or nodes (usually a router or firewall) to the aliased destination (that is, it + probes the C(ip)-C(port) combination specified in the monitor). + - If the monitor cannot successfully reach the aliased destination, the pool member + or node through which the monitor traffic was sent is marked down. + - When creating a new monitor, if this parameter is not provided, the default + value will be whatever is provided by the C(parent). + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures that the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an ICMP monitor + bigip_monitor_icmp: + name: icmp1 + adaptive: no + interval: 1 + time_until_up: 0 + timeout: 3 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Update an ICMP monitor + bigip_monitor_icmp: + name: icmp1 + manual_resume: yes + interval: 5 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove an ICMP monitor + bigip_monitor_icmp: + name: icmp1 + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +app_service: + description: The iApp service associated with this monitor. + returned: changed + type: str + sample: /Common/good_service.app/good_service +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: gateway-icmp +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +adaptive: + description: Whether adaptive is enabled or not. + returned: changed + type: bool + sample: yes +allowed_divergence_type: + description: Type of divergence used for adaptive response time monitoring. + returned: changed + type: str + sample: absolute +allowed_divergence_value: + description: + - Value of the type of divergence used for adaptive response time monitoring. + - May be C(percent) or C(ms) depending on whether C(relative) or C(absolute). + returned: changed + type: int + sample: 25 +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +adaptive_limit: + description: Absolute number of milliseconds that may not be exceeded by a monitor probe. + returned: changed + type: int + sample: 200 +sampling_timespan: + description: Absolute number of milliseconds that may not be exceeded by a monitor probe. + returned: changed + type: int + sample: 200 +up_interval: + description: Interval for the system to use to perform the health check when a resource is up. + returned: changed + type: int + sample: 0 +port: + description: + - Alias port or service for the monitor to check, on behalf of the pools or pool + members with which the monitor is associated. + returned: changed + type: str + sample: 80 +transparent: + description: Whether the monitor operates in transparent mode. + returned: changed + type: bool + sample: no +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'appService': 'app_service', + 'adaptiveDivergenceType': 'allowed_divergence_type', + 'adaptiveDivergenceValue': 'allowed_divergence_value', + 'adaptiveLimit': 'adaptive_limit', + 'adaptiveSamplingTimespan': 'sampling_timespan', + 'timeUntilUp': 'time_until_up', + 'upInterval': 'up_interval', + 'defaultsFrom': 'parent', + 'destination': 'ip', + } + + api_attributes = [ + 'adaptive', + 'adaptiveDivergenceType', + 'adaptiveDivergenceValue', + 'adaptiveLimit', + 'adaptiveSamplingTimespan', + 'defaultsFrom', + 'description', + 'destination', + 'interval', + 'manualResume', + 'timeout', + 'timeUntilUp', + 'transparent', + 'upInterval', + 'destination', + 'appService', + ] + + returnables = [ + 'app_service', + 'adaptive', + 'allowed_divergence_type', + 'allowed_divergence_value', + 'description', + 'adaptive_limit', + 'sampling_timespan', + 'manual_resume', + 'time_until_up', + 'up_interval', + 'timeout', + 'interval', + 'transparent', + 'parent', + 'ip', + ] + + updatables = [ + 'app_service', + 'adaptive', + 'allowed_divergence_type', + 'allowed_divergence_value', + 'adaptive_limit', + 'sampling_timespan', + 'description', + 'manual_resume', + 'time_until_up', + 'up_interval', + 'timeout', + 'interval', + 'transparent', + 'ip', + 'interval', + ] + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > self._values['interval'] > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400." + ) + return self._values['interval'] + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + if self._values['timeout'] is None: + return None + if 1 > self._values['timeout'] > 86400: + raise F5ModuleError( + "Timeout value must be between 1 and 86400." + ) + return self._values['timeout'] + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + if self._values['time_until_up'] is None: + return None + if 0 > self._values['time_until_up'] > 86400: + raise F5ModuleError( + "Time_until_up value must be between 0 and 86400." + ) + return self._values['time_until_up'] + + @property + def manual_resume(self): + result = flatten_boolean(self._values['manual_resume']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def transparent(self): + result = flatten_boolean(self._values['transparent']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def adaptive(self): + result = flatten_boolean(self._values['adaptive']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def transparent(self): + return flatten_boolean(self._values['transparent']) + + @property + def adaptive(self): + return flatten_boolean(self._values['adaptive']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/icmp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/monitor/icmp/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/icmp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/icmp/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/icmp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + app_service=dict(), + parent=dict(), + ip=dict(), + description=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + time_until_up=dict(type='int'), + up_interval=dict(type='int'), + manual_resume=dict(type='bool'), + adaptive=dict(type='bool'), + allowed_divergence_type=dict(choices=['relative', 'absolute']), + allowed_divergence_value=dict(type='int'), + adaptive_limit=dict(type='int'), + sampling_timespan=dict(type='int'), + transparent=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_ldap.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_ldap.py new file mode 100644 index 00000000..b5bfd6bd --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_ldap.py @@ -0,0 +1,829 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_ldap +short_description: Manages BIG-IP LDAP monitors +description: + - Manages BIG-IP LDAP monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + description: + description: + - Specifies descriptive text that identifies the monitor. + type: str + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. + - By default, this value is the C(ldap) parent on the C(Common) partition. + type: str + default: "/Common/ldap" + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + - Note that if specifying an IP address, you must specify a value between 1 and 65535. + type: str + interval: + description: + - Specifies the frequency, in seconds, at which the system issues the + monitor check when either the resource is down or the status of the + resource is unknown. + type: int + timeout: + description: + - Specifies the number of seconds the target has in which to respond to + the monitor request. + - If the target responds within the set time period, it is considered 'up'. + If the target does not respond within the set time period, it is considered + 'down'. When this value is set to 0 (zero), the system uses the interval + from the parent monitor. + - Note that C(timeout) and C(time_until_up) combine to control when a + resource is set to up. + type: int + time_until_up: + description: + - Specifies the number of seconds to wait after a resource first responds + correctly to the monitor before setting the resource to 'up'. + - During the interval, all responses from the resource must be correct. + - When the interval expires, the resource is marked 'up'. + - A value of 0 means the resource is marked up immediately upon + receipt of the first correct response. + type: int + up_interval: + description: + - Specifies the interval for the system to use to perform the health check + when a resource is up. + - When C(0), specifies the system uses the interval specified in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval to + use when checking the health of a resource that is up. + type: int + manual_resume: + description: + - Specifies whether the system automatically changes the status of a resource + to B(enabled) at the next successful monitor check. + - If you set this option to C(yes), you must manually re-enable the resource + before the system can use it for load balancing connections. + - When C(yes), specifies you must manually re-enable the resource after an + unsuccessful monitor check. + - When C(no), specifies the system automatically changes the status of a + resource to B(enabled) at the next successful monitor check. + type: bool + target_username: + description: + - Specifies the user name, if the monitored target requires authentication. + type: str + target_password: + description: + - Specifies the password, if the monitored target requires authentication. + type: str + base: + description: + - Specifies the location in the LDAP tree from which the monitor starts the + health check. + type: str + filter: + description: + - Specifies an LDAP key for which the monitor searches. + type: str + security: + description: + - Specifies the secure protocol type for communication with the target. + type: str + choices: + - none + - ssl + - tls + mandatory_attributes: + description: + - Specifies whether the target must include attributes in its response to be + considered up. + type: bool + chase_referrals: + description: + - Upon receipt of an LDAP referral entry, specifies whether the target + follows (or chases) that referral. + type: bool + debug: + description: + - Specifies whether the monitor sends error messages and additional information + to a log file created and labeled specifically for this monitor. + type: bool + update_password: + description: + - C(always) will update passwords if the C(target_password) is specified. + - C(on_create) will only set the password for newly created monitors. + type: str + choices: + - always + - on_create + default: always + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Greg Crosby (@crosbygw) +''' + +EXAMPLES = r''' +- name: Create a LDAP monitor + bigip_monitor_ldap: + name: foo + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: ldap +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important_Monitor +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +security: + description: The new Security setting of the resource. + returned: changed + type: str + sample: ssl +debug: + description: The new Debug setting of the resource. + returned: changed + type: bool + sample: yes +mandatory_attributes: + description: The new Mandatory Attributes setting of the resource. + returned: changed + type: bool + sample: no +chase_referrals: + description: The new Chase Referrals setting of the resource. + returned: changed + type: bool + sample: yes +manual_resume: + description: The new Manual Resume setting of the resource. + returned: changed + type: bool + sample: no +filter: + description: The new LDAP Filter setting of the resource. + returned: changed + type: str + sample: filter1 +base: + description: The new LDAP Base setting of the resource. + returned: changed + type: str + sample: base +''' + +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'timeUntilUp': 'time_until_up', + 'defaultsFrom': 'parent', + 'mandatoryAttributes': 'mandatory_attributes', + 'chaseReferrals': 'chase_referrals', + 'manualResume': 'manual_resume', + 'username': 'target_username', + 'password': 'target_password', + } + + api_attributes = [ + 'timeUntilUp', + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'description', + 'security', + 'mandatoryAttributes', + 'chaseReferrals', + 'debug', + 'manualResume', + 'username', + 'password', + 'filter', + 'base', + ] + + returnables = [ + 'parent', + 'ip', + 'destination', + 'port', + 'interval', + 'timeout', + 'time_until_up', + 'description', + 'security', + 'debug', + 'mandatory_attributes', + 'chase_referrals', + 'manual_resume', + 'target_username', + 'target_password', + 'filter', + 'base', + ] + + updatables = [ + 'destination', + 'interval', + 'timeout', + 'time_until_up', + 'description', + 'security', + 'debug', + 'mandatory_attributes', + 'chase_referrals', + 'manual_resume', + 'target_username', + 'target_password', + 'filter', + 'base', + ] + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def mandatory_attributes(self): + return flatten_boolean(self._values['mandatory_attributes']) + + @property + def chase_referrals(self): + return flatten_boolean(self._values['chase_referrals']) + + @property + def debug(self): + return flatten_boolean(self._values['debug']) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + +class ApiParameters(Parameters): + @property + def ip(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return ip + + @property + def port(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return int(port) if port.isnumeric() else port + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def security(self): + if self._values['security'] in ['none', None]: + return '' + return self._values['security'] + + +class ModuleParameters(Parameters): + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, port = value.split(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def type(self): + return 'ldap' + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def security(self): + if self._values['security'] in ['none', None]: + return '' + return self._values['security'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def manual_resume(self): + if self._values['manual_resume'] is None: + return None + if self._values['manual_resume'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def ip(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return ip + + @property + def port(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return int(port) if port.isnumeric() else port + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def target_password(self): + if self.want.target_password != self.have.target_password: + if self.want.update_password == 'always': + result = self.want.target_password + return result + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def _set_default_creation_values(self): + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/ldap/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/monitor/ldap/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/ldap/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/ldap/{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/ltm/monitor/ldap/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/ldap'), + ip=dict(), + description=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + target_username=dict(), + target_password=dict(no_log=True), + debug=dict(type='bool'), + security=dict( + choices=['none', 'ssl', 'tls'] + ), + manual_resume=dict(type='bool'), + time_until_up=dict(type='int'), + up_interval=dict(type='int'), + filter=dict(), + base=dict(), + mandatory_attributes=dict(type='bool'), + chase_referrals=dict(type='bool'), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_mysql.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_mysql.py new file mode 100644 index 00000000..c968d5bd --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_mysql.py @@ -0,0 +1,892 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_mysql +short_description: Manages BIG-IP MySQL monitors +description: + - Manages BIG-IP MySQL monitors. +version_added: "1.3.0" +options: + name: + description: + - Monitor name. + type: str + required: True + app_service: + description: + - The iApp service to be associated with this profile. When no service is + specified, the default is None. + type: str + description: + description: + - Specifies descriptive text that identifies the monitor. + type: str + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. + - By default, this value is the C(mysql) parent on the C(Common) partition. + type: str + count: + description: + - Specifies the number of monitor probes after which the connection to the database is terminated. + type: int + database: + description: + - Specifies the name of the database that the monitor tries to access. + type: str + debug: + description: + - Specifies whether the monitor sends error messages + and additional information to a log file created and labeled specifically for this monitor. + type: bool + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, then the default value is '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + - If specifying an IP address, you must specify a value between 1 and 65535. + type: str + target_password: + description: + - Specifies the password if the monitored target requires authentication. + type: str + target_username: + description: + - Specifies the user name if the monitored target requires authentication. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + interval: + description: + - Specifies the frequency at which the system issues the monitor check. + type: int + timeout: + description: + - Specifies the number of seconds the target has in which to respond to + the monitor request. + - If the target responds within the set time period, it is considered 'up'. + If the target does not respond within the set time period, it is considered + 'down'. When this value is set to 0 (zero), the system uses the interval + from the parent monitor. + - Note that C(timeout) and C(time_until_up) combine to control when a + resource is set to up. + type: int + time_until_up: + description: + - Specifies the number of seconds to wait after a resource first responds + correctly to the monitor before setting the resource to 'up'. + - During the interval, all responses from the resource must be correct. + - When the interval expires, the resource is marked 'up'. + - A value of 0, means that the resource is marked up immediately upon + receipt of the first correct response. + type: int + up_interval: + description: + - Specifies the interval for the system to use to perform the health check + when a resource is up. + - When C(0), specifies the system uses the interval specified in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval to + use when checking the health of a resource that is up. + type: int + manual_resume: + description: + - Specifies whether the system automatically changes the status of a resource + to B(enabled) at the next successful monitor check. + - If you set this option to C(yes), you must manually re-enable the resource + before the system can use it for load balancing connections. + - When C(yes), specifies you must manually re-enable the resource after an + unsuccessful monitor check. + - When C(no), specifies the system automatically changes the status of a + resource to B(enabled) at the next successful monitor check. + type: bool + recv: + description: + - Specifies the text string the monitor looks for in the returned resource. + - The most common receive expressions contain a text string + that is included in a field in your database. + - If you do not specify both a Send String and a Receive String, + the monitor performs a simple service check and connect only. + type: str + recv_column: + description: + - Specifies the column in the database where the system expects + the specified Receive String to be located. + - This is an optional setting, and is applicable only + if you configure the C(send) and C(recv) options. + type: str + recv_row: + description: + - Specifies the row in the database where the system expects the specified Receive String to be located. + - This is an optional setting, and is applicable only if you configure the C(send) and C(recv) options. + type: str + send: + description: + - Specifies the SQL query the monitor sends to the target object. + - Because the string may have special characters, the system may require the string be enclosed with single + quotation marks. If this value is C(none), then a valid connection suffices to determine that the service is up. + In this case, the system does not need the recv, recv-row, and recv-column options and ignores them + even if not C(none). + type: str + update_password: + description: + - C(always) will update passwords if the C(target_password) is specified. + - C(on_create) will only set the password for newly created monitors. + type: str + choices: + - always + - on_create + default: always + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Andrey Kashcheev (@andreykashcheev) +''' + +EXAMPLES = r''' +- name: Create an mysql monitor + bigip_monitor_mysql: + ip: 10.10.10.10 + port: 10923 + name: my_mysql_monitor + send: "SELECT status FROM v$instance" + recv: OPEN + recv_column: 2 + recv_row: 1 + database: primary1 + target_username: bigip + target_password: secret + update_password: on_create + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Modify an mysql monitor + bigip_monitor_mysql: + name: my_mysql_monitor + recv_column: 4 + recv_row: 3 + database: primary2 + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove mysql monitor + bigip_monitor_mysql: + state: absent + name: my_mysql_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +app_service: + description: The iApp service associated with this monitor. + returned: changed + type: str + sample: /Common/good_service.app/good_service +parent: + description: The parent monitor. + returned: changed + type: str + sample: /Common/foo_mysql +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +debug: + description: + - Whether the monitor sends error messages and additional information to a log file created and + labeled specifically for this monitor. + returned: changed + type: bool + sample: no +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +port: + description: + - Alias port or service for the monitor to check, on behalf of the pools or pool + members with which the monitor is associated. + returned: changed + type: str + sample: 80 +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +up_interval: + description: Interval for the system to use to perform the health check when a resource is up. + returned: changed + type: int + sample: 0 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to up at the next successful monitor check. + returned: changed + type: bool + sample: yes +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +recv: + description: The text string the monitor looks for in the returned resource. + returned: changed + type: str + sample: OPEN +send: + description: The SQL query the monitor sends to the target object. + returned: changed + type: str + sample: "SELECT status FROM v$instance" +database: + description: The name of the database the monitor tries to access. + returned: changed + type: str + sample: primary1 +target_username: + description: The user name for the the monitored target. + returned: changed + type: str + sample: bigip +recv_column: + description: The column in the database where the specified string should be located. + returned: changed + type: str + sample: 2 +recv_row: + description: The row in the database where the specified string should be located. + returned: changed + type: str + sample: 1 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'appService': 'app_service', + 'upInterval': 'up_interval', + 'timeUntilUp': 'time_until_up', + 'password': 'target_password', + 'username': 'target_username', + 'defaultsFrom': 'parent', + 'manualResume': 'manual_resume', + 'recvColumn': 'recv_column', + 'recvRow': 'recv_row' + } + + api_attributes = [ + 'recvColumn', + 'recvRow', + 'timeUntilUp', + 'username', + 'count', + 'password', + 'timeout', + 'destination', + 'send', + 'recv', + 'debug', + 'interval', + 'upInterval', + 'manualResume', + 'database' + ] + + returnables = [ + 'app_service', + 'count', + 'timeout', + 'debug', + 'ip', + 'port', + 'destination', + 'time_until_up', + 'up_interval', + 'interval', + 'upInterval', + 'manual_resume', + 'target_username', + 'target_password', + 'description', + 'send', + 'recv', + 'recv_column', + 'recv_row', + 'database' + ] + + updatables = [ + 'app_service', + 'destination', + 'count', + 'timeout', + 'debug', + 'time_until_up', + 'up_interval', + 'manual_resume', + 'interval', + 'upInterval', + 'target_username', + 'target_password', + 'send', + 'recv', + 'recv_column', + 'recv_row', + 'database' + ] + + +class ApiParameters(Parameters): + @property + def ip(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return ip + + @property + def port(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return port + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def count(self): + if self._values['count'] is None: + return None + result = int(self._values['count']) + return result + + +class ModuleParameters(Parameters): + @property + def timeout(self): + if self._values['timeout'] is None: + return None + if self._values['timeout'] is None: + return None + if 1 > self._values['timeout'] > 86400: + raise F5ModuleError( + "Timeout value must be between 1 and 86400." + ) + return self._values['timeout'] + + @property + def manual_resume(self): + result = flatten_boolean(self._values['manual_resume']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def debug(self): + return flatten_boolean(self._values['debug']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, port = value.split(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + if self._values['time_until_up'] is None: + return None + if 0 > self._values['time_until_up'] > 86400: + raise F5ModuleError( + "Time_until_up value must be between 0 and 86400." + ) + return self._values['time_until_up'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def count(self): + if self._values['count'] is None: + return None + result = str(self._values['count']) + return result + + +class ReportableChanges(Changes): + @property + def ip(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return ip + + @property + def port(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return int(port) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def count(self): + if self._values['count'] is None: + return None + result = int(self._values['count']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def target_password(self): + if self.want.target_password != self.have.target_password: + if self.want.update_password == 'always': + result = self.want.target_password + return result + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/mysql/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/monitor/mysql/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/mysql/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/mysql/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/mysql/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + count=dict(type='int'), + app_service=dict(), + name=dict(required=True), + parent=dict(), + ip=dict(), + description=dict(), + port=dict(), + database=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + target_username=dict(), + target_password=dict(no_log=True), + debug=dict(type='bool'), + time_until_up=dict(type='int'), + up_interval=dict(type='int'), + manual_resume=dict(type='bool'), + send=dict(), + recv=dict(), + recv_row=dict(), + recv_column=dict(), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_oracle.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_oracle.py new file mode 100644 index 00000000..4295dcd2 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_oracle.py @@ -0,0 +1,880 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_oracle +short_description: Manages BIG-IP Oracle monitors +description: + - Manages BIG-IP Oracle monitors. +version_added: "1.3.0" +options: + name: + description: + - Monitor name. + type: str + required: True + app_service: + description: + - The iApp service to be associated with this profile. When no service is + specified, the default is None. + type: str + description: + description: + - Specifies descriptive text that identifies the monitor. + type: str + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. + - By default, this value is the C(oracle) parent on the C(Common) partition. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + - If specifying an IP address, you must specify a value between 1 and 65535. + type: str + interval: + description: + - Specifies the frequency, in seconds, at which the system issues the + monitor check when either the resource is down or the status of the + resource is unknown. + type: int + timeout: + description: + - Specifies the number of seconds the target has in which to respond to + the monitor request. + - If the target responds within the set time period, it is considered 'up'. + If the target does not respond within the set time period, it is considered + 'down'. When this value is set to 0 (zero), the system uses the interval + from the parent monitor. + - Note that C(timeout) and C(time_until_up) combine to control when a + resource is set to up. + type: int + time_until_up: + description: + - Specifies the number of seconds to wait after a resource first responds + correctly to the monitor before setting the resource to 'up'. + - During the interval, all responses from the resource must be correct. + - When the interval expires, the resource is marked 'up'. + - A value of 0 means the resource is marked up immediately upon + receipt of the first correct response. + type: int + up_interval: + description: + - Specifies the interval for the system to use to perform the health check + when a resource is up. + - When C(0), specifies the system uses the interval in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval to + use when checking the health of a resource that is up. + type: int + manual_resume: + description: + - Specifies whether the system automatically changes the status of a resource + to B(enabled) at the next successful monitor check. + - If you set this option to C(yes), you must manually re-enable the resource + before the system can use it for load balancing connections. + - When C(yes), specifies you must manually re-enable the resource after an + unsuccessful monitor check. + - When C(no), specifies the system automatically changes the status of a + resource to B(enabled) at the next successful monitor check. + type: bool + recv: + description: + - Specifies the text string that the monitor looks for in the returned resource. + - The most common receive expressions contain a text string that is included in a field in your database. + - If you do not specify both C(send) and a C(recv) parameters, the monitor performs a simple service check + and connect only. + type: str + recv_column: + description: + - Specifies the column in the database where the specified C(recv) string should be located. + - This is an optional setting and is applicable only if you configure the C(send) and the C(recv) parameters. + type: str + recv_row: + description: + - Specifies the row in the database where the specified C(recv) string should be located. + - This is an optional setting, and is applicable only if you configure the C(send) and the C(recv) parameters. + type: str + send: + description: + - Specifies the SQL query the monitor sends to the target object. + - Since the string may have special characters, the system may require the string be enclosed with single + quotation marks. If this value is C(none), a valid connection suffices to determine the service is up. + In this case, the system does not need the recv, recv-row, and recv-column options and ignores them + even if not C(none). + type: str + database: + description: + - Specifies the name of the database the monitor tries to access. + type: str + count: + description: + - Specifies the number of monitor probes after which the connection to the database will be terminated. + - Count value of zero indicates that the connection will never be terminated. + type: int + target_username: + description: + - Specifies the user name, if the monitored target requires authentication. + type: str + target_password: + description: + - Specifies the password, if the monitored target requires authentication. + type: str + debug: + description: + - Specifies whether the monitor sends error messages and additional information + to a log file created and labeled specifically for this monitor. + type: bool + update_password: + description: + - C(always) will update passwords if the C(target_password) is specified. + - C(on_create) will only set the password for newly created monitors. + type: str + choices: + - always + - on_create + default: always + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an oracle monitor + bigip_monitor_oracle: + ip: 10.10.10.10 + port: 10923 + name: my_oracle_monitor + send: "SELECT status FROM v$instance" + recv: OPEN + recv_column: 2 + recv_row: 1 + database: primary1 + target_username: bigip + target_password: secret + update_password: on_create + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Modify an oracle monitor + bigip_monitor_oracle: + name: my_oracle_monitor + recv_column: 4 + recv_row: 3 + database: primary2 + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove oracle monitor + bigip_monitor_oracle: + state: absent + name: my_oracle_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +app_service: + description: The iApp service associated with this monitor. + returned: changed + type: str + sample: /Common/good_service.app/good_service +parent: + description: The parent monitor. + returned: changed + type: str + sample: /Common/foo_oracle +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +debug: + description: + - Whether the monitor sends error messages and additional information to a log file created and + labeled specifically for this monitor. + returned: changed + type: bool + sample: no +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +port: + description: + - Alias port or service for the monitor to check, on behalf of the pools or pool + members with which the monitor is associated. + returned: changed + type: str + sample: 80 +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +up_interval: + description: Interval for the system to use to perform the health check when a resource is up. + returned: changed + type: int + sample: 0 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to up at the next successful monitor check. + returned: changed + type: bool + sample: yes +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +recv: + description: The text string that the monitor looks for in the returned resource. + returned: changed + type: str + sample: OPEN +send: + description: The SQL query the monitor sends to the target object. + returned: changed + type: str + sample: "SELECT status FROM v$instance" +database: + description: The name of the database that the monitor tries to access. + returned: changed + type: str + sample: primary1 +target_username: + description: The user name for the the monitored target. + returned: changed + type: str + sample: bigip +recv_column: + description: The column in the database where the specified string should be located. + returned: changed + type: str + sample: 2 +recv_row: + description: The row in the database where the specified string should be located. + returned: changed + type: str + sample: 1 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'appService': 'app_service', + 'defaultsFrom': 'parent', + 'timeUntilUp': 'time_until_up', + 'manualResume': 'manual_resume', + 'upInterval': 'up_interval', + 'recvColumn': 'recv_column', + 'recvRow': 'recv_row', + 'username': 'target_username', + 'password': 'target_password', + } + + api_attributes = [ + 'database', + 'defaultsFrom', + 'debug', + 'description', + 'destination', + 'interval', + 'manualResume', + 'recv', + 'recvColumn', + 'recvRow', + 'send', + 'timeout', + 'timeUntilUp', + 'upInterval', + 'username', + 'password', + 'count', + ] + + returnables = [ + 'app_service', + 'parent', + 'description', + 'destination', + 'debug', + 'ip', + 'port', + 'interval', + 'up_interval', + 'timeout', + 'manual_resume', + 'time_until_up', + 'recv_column', + 'recv_row', + 'count', + 'send', + 'recv', + 'database', + 'target_username', + ] + + updatables = [ + 'app_service', + 'parent', + 'description', + 'destination', + 'debug', + 'ip', + 'port', + 'interval', + 'up_interval', + 'timeout', + 'manual_resume', + 'time_until_up', + 'recv_column', + 'recv_row', + 'count', + 'send', + 'recv', + 'database', + 'target_username', + 'target_password', + ] + + +class ApiParameters(Parameters): + @property + def ip(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return ip + + @property + def port(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return port + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def count(self): + if self._values['count'] is None: + return None + result = int(self._values['count']) + return result + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > self._values['interval'] > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400." + ) + return self._values['interval'] + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + if self._values['timeout'] is None: + return None + if 1 > self._values['timeout'] > 86400: + raise F5ModuleError( + "Timeout value must be between 1 and 86400." + ) + return self._values['timeout'] + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, port = value.split(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + if self._values['time_until_up'] is None: + return None + if 0 > self._values['time_until_up'] > 86400: + raise F5ModuleError( + "Time_until_up value must be between 0 and 86400." + ) + return self._values['time_until_up'] + + @property + def manual_resume(self): + result = flatten_boolean(self._values['manual_resume']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def debug(self): + result = flatten_boolean(self._values['debug']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def count(self): + if self._values['count'] is None: + return None + result = str(self._values['count']) + return result + + +class ReportableChanges(Changes): + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + @property + def count(self): + if self._values['count'] is None: + return None + result = int(self._values['count']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified." + ) + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + @property + def target_password(self): + if self.want.target_password != self.have.target_password: + if self.want.update_password == 'always': + result = self.want.target_password + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/oracle/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/monitor/oracle/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/oracle/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/oracle/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/oracle/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + app_service=dict(), + parent=dict(), + description=dict(), + debug=dict(type='bool'), + database=dict(), + count=dict(type='int'), + ip=dict(), + port=dict(), + interval=dict(type='int'), + up_interval=dict(type='int'), + timeout=dict(type='int'), + manual_resume=dict(type='bool'), + time_until_up=dict(type='int'), + recv=dict(), + recv_column=dict(), + recv_row=dict(), + send=dict(), + target_username=dict(), + target_password=dict(no_log=True), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_smtp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_smtp.py new file mode 100644 index 00000000..53200b3b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_smtp.py @@ -0,0 +1,765 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_smtp +short_description: Manage SMTP monitors on a BIG-IP +description: + - Manage SMTP monitors on a BIG-IP. +version_added: "1.1.0" +options: + name: + description: + - Specifies the name of the monitor. + type: str + required: True + app_service: + description: + - The iApp service to be associated with this profile. When no service is + specified, the default is None. + type: str + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. + - When creating a new monitor, if this parameter is not specified, the default + is the system-supplied C(smtp) monitor. + type: str + description: + description: + - The description of the monitor. + type: str + debug: + description: + - Specifies whether the monitor sends error messages and additional information to a log file created and + labeled specifically for this monitor. + - "When C(yes) the system redirects error messages and additional information to the + C(/var/log/monitors/--.log) file." + type: bool + domain: + description: + - Specifies the domain name to check. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + If specifying an IP address, you must specify a value between 1 and 65535. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. + - This value B(must) be less than the C(timeout) value. + - When creating a new monitor, if this parameter is not provided, the + default C(5) will be used. + type: int + up_interval: + description: + - Specifies the interval for the system to use to perform the health check + when a resource is up. + - When C(0), specifies the system uses the interval specified in + C(interval) to check the health of the resource. + - When any other number, enables you to specify a different interval to + use when checking the health of a resource that is up. + - When creating a new monitor, if this parameter is not provided, the + default C(0) will be used. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. + - If the target responds within the set time period, it is considered up. + - If the target does not respond within the set time period, it is considered down. + - You can change this to any number, however, it should be 3 times the + interval number of seconds plus 1 second. + - If this parameter is not provided when creating a new monitor, the default + value will be C(31). + type: int + manual_resume: + description: + - Specifies whether the system automatically changes the status of a resource + to B(enabled) at the next successful monitor check. + - If you set this option to C(yes), you must manually re-enable the resource + before the system can use it for load balancing connections. + - When creating a new monitor, if this parameter is not specified, the default + value is C(no). + - When C(yes), specifies you must manually re-enable the resource after an + unsuccessful monitor check. + - When C(no), specifies the system automatically changes the status of a + resource to B(enabled) at the next successful monitor check. + type: bool + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node will be marked up. + - A value of C(0) will cause a node to be marked up immediately after a valid + response is received from the node. + - If this parameter is not provided when creating a new monitor, then the default + value will be C(0). + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create SMTP Monitor + bigip_monitor_smtp: + state: present + ip: 10.10.10.10 + name: my_smtp_monitor + domain: foo.com + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove SMTP Monitor + bigip_monitor_smtp: + state: absent + name: my_smtp_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +app_service: + description: The iApp service associated with this monitor. + returned: changed + type: str + sample: /Common/good_service.app/good_service +parent: + description: The parent monitor. + returned: changed + type: str + sample: /Common/foo_smtp +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +debug: + description: + - Whether the monitor sends error messages and additional information to a log file created and + labeled specifically for this monitor. + returned: changed + type: bool + sample: no +domain: + description: Specifies the domain name to check. + returned: changed + type: str + sample: bigipinternal.com +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +port: + description: + - Alias port or service for the monitor to check, on behalf of the pools or pool + members with which the monitor is associated. + returned: changed + type: str + sample: 80 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +up_interval: + description: Interval for the system to use to perform the health check when a resource is up. + returned: changed + type: int + sample: 0 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +manual_resume: + description: + - Specifies whether the system automatically changes the status of a + resource to up at the next successful monitor check. + returned: changed + type: bool + sample: yes +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'appService': 'app_service', + 'defaultsFrom': 'parent', + 'timeUntilUp': 'time_until_up', + 'manualResume': 'manual_resume', + 'upInterval': 'up_interval', + } + + api_attributes = [ + 'defaultsFrom', + 'debug', + 'description', + 'destination', + 'domain', + 'interval', + 'upInterval', + 'timeout', + 'manualResume', + 'timeUntilUp', + ] + + returnables = [ + 'app_service', + 'parent', + 'description', + 'destination', + 'debug', + 'domain', + 'ip', + 'port', + 'interval', + 'up_interval', + 'timeout', + 'manual_resume', + 'time_until_up', + + ] + + updatables = [ + 'app_service', + 'description', + 'debug', + 'destination', + 'domain', + 'ip', + 'port', + 'interval', + 'up_interval', + 'timeout', + 'manual_resume', + 'time_until_up', + ] + + +class ApiParameters(Parameters): + @property + def ip(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return ip + + @property + def port(self): + des = self._values['destination'] + ip, d, port = des.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = des.rpartition(':') + return int(port) if port.isnumeric() else port + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def domain(self): + if self._values['domain'] in [None, 'none']: + return None + return self._values['domain'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def domain(self): + if self._values['domain'] is None: + return None + elif self._values['domain'] in ['none', '']: + return '' + return self._values['domain'] + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > self._values['interval'] > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400." + ) + return self._values['interval'] + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + if self._values['timeout'] is None: + return None + if 1 > self._values['timeout'] > 86400: + raise F5ModuleError( + "Timeout value must be between 1 and 86400." + ) + return self._values['timeout'] + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, port = value.split(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + if self._values['time_until_up'] is None: + return None + if 0 > self._values['time_until_up'] > 86400: + raise F5ModuleError( + "Time_until_up value must be between 0 and 86400." + ) + return self._values['time_until_up'] + + @property + def manual_resume(self): + result = flatten_boolean(self._values['manual_resume']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def debug(self): + result = flatten_boolean(self._values['debug']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def debug(self): + return flatten_boolean(self._values['debug']) + + @property + def manual_resume(self): + return flatten_boolean(self._values['manual_resume']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified." + ) + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def domain(self): + return cmp_str_with_none(self.want.domain, self.have.domain) + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/smtp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/monitor/smtp/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/smtp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/smtp/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/smtp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + app_service=dict(), + parent=dict(), + description=dict(), + debug=dict(type='bool'), + domain=dict(), + ip=dict(), + port=dict(), + interval=dict(type='int'), + up_interval=dict(type='int'), + timeout=dict(type='int'), + manual_resume=dict(type='bool'), + time_until_up=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_snmp_dca.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_snmp_dca.py new file mode 100644 index 00000000..0e49889d --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_snmp_dca.py @@ -0,0 +1,757 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_snmp_dca +short_description: Manages BIG-IP SNMP data collecting agent (DCA) monitors +description: + - The BIG-IP has an SNMP data collecting agent (DCA) that can query remote + SNMP agents of various types, including the UC Davis agent (UCD) and the + Windows 2000 Server agent (WIN2000). +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + description: + description: + - Specifies descriptive text that identifies the monitor. + type: str + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(snmp_dca) + parent on the C(Common) partition. + type: str + default: "/Common/snmp_dca" + interval: + description: + - Specifies the frequency, in seconds, at which the system issues the + monitor check when either the resource is down or the status of the + resource is unknown. When creating a new monitor, the default is C(10). + type: int + timeout: + description: + - Specifies the number of seconds the target has in which to respond to + the monitor request. When creating a new monitor, the default is C(30) + seconds. If the target responds within the set time period, it is + considered 'up'. If the target does not respond within the set time + period, it is considered 'down'. When this value is set to 0 (zero), + the system uses the interval from the parent monitor. Note that + C(timeout) and C(time_until_up) combine to control when a resource is + set to up. + type: int + time_until_up: + description: + - Specifies the number of seconds to wait after a resource first responds + correctly to the monitor before setting the resource to 'up'. During the + interval, all responses from the resource must be correct. When the + interval expires, the resource is marked 'up'. A value of 0, means + that the resource is marked up immediately upon receipt of the first + correct response. When creating a new monitor, the default is C(0). + type: int + community: + description: + - Specifies the community name the system must use to authenticate + with the host server through SNMP. When creating a new monitor, the + default value is C(public). This value is case sensitive. + type: str + version: + description: + - Specifies the version of SNMP the host server uses. When creating + a new monitor, the default is C(v1). When C(v1), specifies the + host server uses SNMP version 1. When C(v2c), specifies that the host + server uses SNMP version 2c. + type: str + choices: + - v1 + - v2c + agent_type: + description: + - Specifies the SNMP agent running on the monitored server. When creating + a new monitor, the default is C(UCD) (UC-Davis). + type: str + choices: + - UCD + - WIN2000 + - GENERIC + cpu_coefficient: + description: + - Specifies the coefficient the system uses to calculate the weight + of the CPU threshold in the dynamic ratio load balancing algorithm. + When creating a new monitor, the default is C(1.5). + type: str + cpu_threshold: + description: + - Specifies the maximum acceptable CPU usage on the target server. When + creating a new monitor, the default is C(80) percent. + type: int + memory_coefficient: + description: + - Specifies the coefficient the system uses to calculate the weight + of the memory threshold in the dynamic ratio load balancing algorithm. + When creating a new monitor, the default is C(1.0). + type: str + memory_threshold: + description: + - Specifies the maximum acceptable memory usage on the target server. + When creating a new monitor, the default is C(70) percent. + type: int + disk_coefficient: + description: + - Specifies the coefficient the system uses to calculate the weight + of the disk threshold in the dynamic ratio load balancing algorithm. + When creating a new monitor, the default is C(2.0). + type: str + disk_threshold: + description: + - Specifies the maximum acceptable disk usage on the target server. When + creating a new monitor, the default is C(90) percent. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 + - This module does not support the C(variables) option because it + is broken in the REST API and does not function correctly in C(tmsh); for + example you cannot remove user-defined params. Therefore, there is no way + to automatically configure it. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create SNMP DCS monitor + bigip_monitor_snmp_dca: + name: my_monitor + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove TCP Echo Monitor + bigip_monitor_snmp_dca: + name: my_monitor + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: snmp_dca +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +community: + description: The new community for the monitor. + returned: changed + type: str + sample: foobar +version: + description: The new new SNMP version to be used by the monitor. + returned: changed + type: str + sample: v2c +agent_type: + description: The new agent type to be used by the monitor. + returned: changed + type: str + sample: UCD +cpu_coefficient: + description: The new CPU coefficient. + returned: changed + type: float + sample: 2.4 +cpu_threshold: + description: The new CPU threshold. + returned: changed + type: int + sample: 85 +memory_coefficient: + description: The new memory coefficient. + returned: changed + type: float + sample: 6.4 +memory_threshold: + description: The new memory threshold. + returned: changed + type: int + sample: 50 +disk_coefficient: + description: The new disk coefficient. + returned: changed + type: float + sample: 10.2 +disk_threshold: + description: The new disk threshold. + returned: changed + type: int + sample: 34 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'timeUntilUp': 'time_until_up', + 'defaultsFrom': 'parent', + 'agentType': 'agent_type', + 'cpuCoefficient': 'cpu_coefficient', + 'cpuThreshold': 'cpu_threshold', + 'memoryCoefficient': 'memory_coefficient', + 'memoryThreshold': 'memory_threshold', + 'diskCoefficient': 'disk_coefficient', + 'diskThreshold': 'disk_threshold', + } + + api_attributes = [ + 'timeUntilUp', + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'community', + 'version', + 'agentType', + 'cpuCoefficient', + 'cpuThreshold', + 'memoryCoefficient', + 'memoryThreshold', + 'diskCoefficient', + 'diskThreshold', + 'description', + ] + + returnables = [ + 'parent', + 'ip', + 'interval', + 'timeout', + 'time_until_up', + 'description', + 'community', + 'version', + 'agent_type', + 'cpu_coefficient', + 'cpu_threshold', + 'memory_coefficient', + 'memory_threshold', + 'disk_coefficient', + 'disk_threshold', + ] + + updatables = [ + 'ip', + 'interval', + 'timeout', + 'time_until_up', + 'description', + 'community', + 'version', + 'agent_type', + 'cpu_coefficient', + 'cpu_threshold', + 'memory_coefficient', + 'memory_threshold', + 'disk_coefficient', + 'disk_threshold', + ] + + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def cpu_coefficient(self): + result = self._get_numeric_property('cpu_coefficient') + return result + + @property + def cpu_threshold(self): + result = self._get_numeric_property('cpu_threshold') + return result + + @property + def memory_coefficient(self): + result = self._get_numeric_property('memory_coefficient') + return result + + @property + def memory_threshold(self): + result = self._get_numeric_property('memory_threshold') + return result + + @property + def disk_coefficient(self): + result = self._get_numeric_property('disk_coefficient') + return result + + @property + def disk_threshold(self): + result = self._get_numeric_property('disk_threshold') + return result + + def _get_numeric_property(self, property): + if self._values[property] is None: + return None + try: + fvar = float(self._values[property]) + except ValueError: + raise F5ModuleError( + "Provided {0} must be a valid number".format(property) + ) + return fvar + + @property + def type(self): + return 'snmp_dca' + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/snmp-dca/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 30}) + if self.want.interval is None: + self.want.update({'interval': 10}) + if self.want.time_until_up is None: + self.want.update({'time_until_up': 0}) + if self.want.community is None: + self.want.update({'community': 'public'}) + if self.want.version is None: + self.want.update({'version': 'v1'}) + if self.want.agent_type is None: + self.want.update({'agent_type': 'UCD'}) + if self.want.cpu_coefficient is None: + self.want.update({'cpu_coefficient': '1.5'}) + if self.want.cpu_threshold is None: + self.want.update({'cpu_threshold': '80'}) + if self.want.memory_coefficient is None: + self.want.update({'memory_coefficient': '1.0'}) + if self.want.memory_threshold is None: + self.want.update({'memory_threshold': '70'}) + if self.want.disk_coefficient is None: + self.want.update({'disk_coefficient': '2.0'}) + if self.want.disk_threshold is None: + self.want.update({'disk_threshold': '90'}) + + 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/ltm/monitor/snmp-dca/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/snmp-dca/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/snmp-dca/{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/ltm/monitor/snmp-dca/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + parent=dict(default='/Common/snmp_dca'), + interval=dict(type='int'), + timeout=dict(type='int'), + time_until_up=dict(type='int'), + community=dict(), + version=dict(choices=['v1', 'v2c']), + agent_type=dict( + choices=['UCD', 'WIN2000', 'GENERIC'] + ), + cpu_coefficient=dict(), + cpu_threshold=dict(type='int'), + memory_coefficient=dict(), + memory_threshold=dict(type='int'), + disk_coefficient=dict(), + disk_threshold=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp.py new file mode 100644 index 00000000..d0836bc1 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp.py @@ -0,0 +1,684 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_tcp +short_description: Manages F5 BIG-IP LTM TCP monitors +description: Manages F5 BIG-IP LTM TCP monitors via iControl REST API. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(tcp) + parent on the C(Common) partition. + type: str + default: /Common/tcp + description: + description: + - The description of the monitor. + type: str + send: + description: + - The Send string for the monitor call. + type: str + receive: + description: + - The Receive string for the monitor call. + type: str + receive_disable: + description: + - The Receive Disable string for the monitor call. + This setting works like C(receive), except the system marks the node + or pool member disabled when its response matches the C(receive_disable) + string but not C(receive). To use this setting, you must specify both + C(receive_disable) and C(receive). + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + - If this value is an IP address, and the C(type) is C(tcp) (the default), + then a C(port) number must be specified. + - In BIG IP Management UI, this field is B(Alias Address). + aliases: + - alias_address + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + If specifying an IP address, you must specify a value between 1 and 65535. + - This argument is not supported for TCP Echo types. + - In BIG IP Management UI, this field is B(Alias Service Port). + type: str + aliases: + - alias_service_port + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. If this parameter is not provided when creating + a new monitor, the default value is 5. This value B(must) + be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + number to any number you want, however, it should be 3 times the + interval number of seconds plus 1 second. If this parameter is not + provided when creating a new monitor, the default value is 16. + type: int + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node will be marked up. A value of C(0) causes a + node to be marked up immediately after a valid response is received + from the node. If this parameter is not provided when creating + a new monitor, the default value is C(0). + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create TCP Monitor + bigip_monitor_tcp: + state: present + name: my_tcp_monitor + send: tcp string to send + receive: tcp string to receive + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove TCP Monitor + bigip_monitor_tcp: + state: absent + name: my_tcp_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: tcp +send: + description: The new Send string for this monitor. + returned: changed + type: str + sample: tcp string to send +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +receive: + description: The new Receive string for this monitor. + returned: changed + type: str + sample: tcp string to receive +receive_disable: + description: The new Receive Disable string for this monitor. + returned: changed + type: str + sample: tcp string to receive +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +port: + description: The new port of IP/port definition. + returned: changed + type: str + sample: admin@root.local +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'timeUntilUp': 'time_until_up', + 'defaultsFrom': 'parent', + 'recv': 'receive', + 'recvDisable': 'receive_disable' + } + + api_attributes = [ + 'timeUntilUp', + 'defaultsFrom', + 'interval', + 'timeout', + 'recv', + 'recvDisable', + 'send', + 'destination', + 'description', + ] + + returnables = [ + 'parent', + 'send', + 'receive', + 'receive_disable', + 'ip', + 'port', + 'interval', + 'timeout', + 'time_until_up', + 'description', + ] + + updatables = [ + 'destination', + 'send', + 'receive', + 'receive_disable', + 'interval', + 'timeout', + 'time_until_up', + 'description', + ] + + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + if is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, d, port = value.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = value.rpartition(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def type(self): + return 'tcp' + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 16}) + if self.want.interval is None: + self.want.update({'interval': 5}) + if self.want.time_until_up is None: + self.want.update({'time_until_up': 0}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + + 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/ltm/monitor/tcp/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp/{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/ltm/monitor/tcp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/tcp'), + description=dict(), + send=dict(), + receive=dict(), + receive_disable=dict(), + ip=dict( + aliases=['alias_address'] + ), + port=dict( + aliases=['alias_service_port'] + ), + interval=dict(type='int'), + timeout=dict(type='int'), + time_until_up=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp_echo.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp_echo.py new file mode 100644 index 00000000..859ad1f5 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp_echo.py @@ -0,0 +1,590 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_tcp_echo +short_description: Manages F5 BIG-IP LTM TCP echo monitors +description: Manages F5 BIG-IP LTM TCP echo monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(tcp_echo) + parent on the C(Common) partition. + type: str + default: /Common/tcp_echo + description: + description: + - The description of the monitor. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. If this parameter is not provided when creating + a new monitor, the default value is 5. This value B(must) + be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + to any number, however, it should be 3 times the + interval number of seconds plus 1 second. If this parameter is not + provided when creating a new monitor, the default value is 16. + type: int + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node will be marked up. A value of C(0) will cause a + node to be marked up immediately after a valid response is received + from the node. If this parameter is not provided when creating + a new monitor, the default value is be C(0). + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create TCP Echo Monitor + bigip_monitor_tcp_echo: + state: present + ip: 10.10.10.10 + name: my_tcp_monitor + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove TCP Echo Monitor + bigip_monitor_tcp_echo: + state: absent + name: my_tcp_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: tcp +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +''' +import os +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'timeUntilUp': 'time_until_up', + 'defaultsFrom': 'parent', + } + + api_attributes = [ + 'timeUntilUp', + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'description', + ] + + returnables = [ + 'parent', + 'ip', + 'interval', + 'timeout', + 'time_until_up', + 'description', + ] + + updatables = [ + 'ip', + 'interval', + 'timeout', + 'time_until_up', + 'description', + ] + + @property + def interval(self): + if self._values['interval'] is None: + return None + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def destination(self): + return self.ip + + @destination.setter + def destination(self, value): + self._values['ip'] = value + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + if self._values['parent'].startswith('/'): + parent = os.path.basename(self._values['parent']) + result = '/{0}/{1}'.format(self.partition, parent) + else: + result = '/{0}/{1}'.format(self.partition, self._values['parent']) + return result + + @property + def type(self): + return 'tcp_echo' + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None: + return None + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp-echo/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 16}) + if self.want.interval is None: + self.want.update({'interval': 5}) + if self.want.time_until_up is None: + self.want.update({'time_until_up': 0}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + + 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/ltm/monitor/tcp-echo/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp-echo/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp-echo/{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/ltm/monitor/tcp-echo/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/tcp_echo'), + description=dict(), + ip=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + time_until_up=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp_half_open.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp_half_open.py new file mode 100644 index 00000000..672371ef --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_tcp_half_open.py @@ -0,0 +1,641 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_tcp_half_open +short_description: Manages F5 BIG-IP LTM TCP half-open monitors +description: Manages F5 BIG-IP LTM TCP half-open monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(tcp_half_open) + parent on the C(Common) partition. + type: str + default: /Common/tcp_half_open + description: + description: + - The description of the monitor. + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + - If this value is an IP address, and the C(type) is C(tcp) (the default), + then a C(port) number must be specified. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + If specifying an IP address, you must specify a value between 1 and 65535. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. If this parameter is not provided when creating + a new monitor, the default value is 5. This value B(must) + be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + to any number, however, it should be 3 times the + interval number of seconds plus 1 second. If this parameter is not + provided when creating a new monitor, then the default value is 16. + type: int + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node will be marked up. A value of C(0) will cause a + node to be marked up immediately after a valid response is received + from the node. If this parameter is not provided when creating + a new monitor, the default value is be 0. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create TCP half-open Monitor + bigip_monitor_tcp_half_open: + state: present + ip: 10.10.10.10 + name: my_tcp_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove TCP half-open Monitor + bigip_monitor_tcp_half_open: + state: absent + name: my_tcp_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add half-open monitor for all addresses, port 514 + bigip_monitor_tcp_half_open: + port: 514 + name: my_tcp_monitor + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: tcp +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval in which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +''' + +import os +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'timeUntilUp': 'time_until_up', + 'defaultsFrom': 'parent', + 'recv': 'receive', + } + + api_attributes = [ + 'timeUntilUp', + 'defaultsFrom', + 'interval', + 'timeout', + 'destination', + 'description', + ] + + returnables = [ + 'parent', + 'ip', + 'port', + 'interval', + 'timeout', + 'time_until_up', + 'description', + ] + + updatables = [ + 'destination', + 'interval', + 'timeout', + 'time_until_up', + 'description', + ] + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, d, port = value.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = value.rpartition(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + + # Per BZ617284, the BIG-IP UI does not raise a warning about this. + # So I raise the error instead. + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + elif self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + if self._values['parent'].startswith('/'): + parent = os.path.basename(self._values['parent']) + result = '/{0}/{1}'.format(self.partition, parent) + else: + result = '/{0}/{1}'.format(self.partition, self._values['parent']) + return result + + @property + def type(self): + return 'tcp_half_open' + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp-half-open/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 16}) + if self.want.interval is None: + self.want.update({'interval': 5}) + if self.want.time_until_up is None: + self.want.update({'time_until_up': 0}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + + 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/ltm/monitor/tcp-half-open/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp-half-open/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/tcp-half-open/{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/ltm/monitor/tcp-half-open/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/tcp_half_open'), + description=dict(), + ip=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + time_until_up=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_udp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_udp.py new file mode 100644 index 00000000..992cdad6 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_monitor_udp.py @@ -0,0 +1,651 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_monitor_udp +short_description: Manages F5 BIG-IP LTM UDP monitors +description: Manages F5 BIG-IP LTM UDP monitors. +version_added: "1.0.0" +options: + name: + description: + - Monitor name. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(udp) + parent on the C(Common) partition. + type: str + default: /Common/udp + description: + description: + - The description of the monitor. + type: str + send: + description: + - The Send string for the monitor call. When creating a new monitor, if + this value is not provided, the default C(default send string) is used. + type: str + receive: + description: + - The Receive string for the monitor call. + type: str + receive_disable: + description: + - This setting works like C(receive), except the system marks the node + or pool member disabled when its response matches the C(receive_disable) + string but not C(receive). To use this setting, you must specify both + C(receive_disable) and C(receive). + type: str + ip: + description: + - IP address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + type: str + port: + description: + - Port address part of the IP/port definition. If this parameter is not + provided when creating a new monitor, the default value is '*'. + If specifying an IP address, you must specify a value between 1 and 65535. + type: str + interval: + description: + - The interval specifying how frequently the monitor instance of this + template will run. If this parameter is not provided when creating + a new monitor, the default value is 5. This value B(must) + be less than the C(timeout) value. + type: int + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered 'up'. If the target does not respond within + the set time period, it is considered 'down'. You can change this + to any number, however it should be 3 times the + interval number of seconds plus 1 second. If this parameter is not + provided when creating a new monitor, the default value is 16. + type: int + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node will be marked up. A value of C(0) causes a + node to be marked up immediately after a valid response is received + from the node. If this parameter is not provided when creating + a new monitor, the default value is C(0). + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the monitor exists. + - When C(absent), ensures the monitor is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create UDP Monitor + bigip_monitor_udp: + state: present + ip: 10.10.10.10 + name: my_udp_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove UDP Monitor + bigip_monitor_udp: + state: absent + name: my_udp_monitor + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: New parent template of the monitor. + returned: changed + type: str + sample: http +description: + description: The description of the monitor. + returned: changed + type: str + sample: Important Monitor +ip: + description: The new IP of IP/port definition. + returned: changed + type: str + sample: 10.12.13.14 +interval: + description: The new interval at which to run the monitor check. + returned: changed + type: int + sample: 2 +timeout: + description: The new timeout in which the remote system must respond to the monitor. + returned: changed + type: int + sample: 10 +time_until_up: + description: The new time in which to mark a system as up after first successful response. + returned: changed + type: int + sample: 2 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'timeUntilUp': 'time_until_up', + 'defaultsFrom': 'parent', + 'recv': 'receive', + } + + api_attributes = [ + 'timeUntilUp', + 'defaultsFrom', + 'interval', + 'timeout', + 'recv', + 'send', + 'destination', + 'description', + ] + + returnables = [ + 'parent', + 'send', + 'receive', + 'ip', + 'port', + 'interval', + 'timeout', + 'time_until_up', + 'description', + ] + + updatables = [ + 'destination', + 'send', + 'receive', + 'interval', + 'timeout', + 'time_until_up', + 'description', + ] + + @property + def destination(self): + if self.ip is None and self.port is None: + return None + destination = '{0}:{1}'.format(self.ip, self.port) + return destination + + @destination.setter + def destination(self, value): + ip, d, port = value.rpartition('.') + if not is_valid_ip(ip) and ip != '*': + ip, d, port = value.rpartition(':') + self._values['ip'] = ip + self._values['port'] = port + + @property + def interval(self): + if self._values['interval'] is None: + return None + + # Per BZ617284, the BIG-IP UI does not raise a warning about this. + # So I do + if 1 > int(self._values['interval']) > 86400: + raise F5ModuleError( + "Interval value must be between 1 and 86400" + ) + return int(self._values['interval']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def ip(self): + if self._values['ip'] is None: + return None + if self._values['ip'] in ['*', '0.0.0.0']: + return '*' + elif is_valid_ip(self._values['ip']) or is_valid_ip(self._values['ip'].split("%")[0]): + return self._values['ip'] + else: + raise F5ModuleError( + "The provided 'ip' parameter is not an IP address." + ) + + @property + def port(self): + if self._values['port'] is None: + return None + elif self._values['port'] == '*': + return '*' + return int(self._values['port']) + + @property + def time_until_up(self): + if self._values['time_until_up'] is None: + return None + return int(self._values['time_until_up']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def type(self): + return 'udp' + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + @property + def parent(self): + if self.want.parent != self.have.parent: + raise F5ModuleError( + "The parent monitor cannot be changed" + ) + + @property + def destination(self): + if self.want.ip is None and self.want.port is None: + return None + if self.want.port is None: + self.want.update({'port': self.have.port}) + if self.want.ip is None: + self.want.update({'ip': self.have.ip}) + + if self.want.port in [None, '*'] and self.want.ip != '*': + raise F5ModuleError( + "Specifying an IP address requires that a port number be specified" + ) + + if self.want.destination != self.have.destination: + return self.want.destination + + @property + def interval(self): + if self.want.timeout is not None and self.want.interval is not None: + if self.want.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.timeout is not None: + if self.have.interval >= self.want.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + elif self.want.interval is not None: + if self.want.interval >= self.have.timeout: + raise F5ModuleError( + "Parameter 'interval' must be less than 'timeout'." + ) + if self.want.interval != self.have.interval: + return self.want.interval + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/udp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + self._set_default_creation_values() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _set_default_creation_values(self): + if self.want.timeout is None: + self.want.update({'timeout': 16}) + if self.want.interval is None: + self.want.update({'interval': 5}) + if self.want.time_until_up is None: + self.want.update({'time_until_up': 0}) + if self.want.ip is None: + self.want.update({'ip': '*'}) + if self.want.port is None: + self.want.update({'port': '*'}) + if self.want.send is None: + self.want.update({'send': 'default send string'}) + + 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/ltm/monitor/udp/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/udp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/monitor/udp/{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/ltm/monitor/udp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='/Common/udp'), + description=dict(), + send=dict(), + receive=dict(), + receive_disable=dict(required=False), + ip=dict(), + port=dict(), + interval=dict(type='int'), + timeout=dict(type='int'), + time_until_up=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_network_globals.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_network_globals.py new file mode 100644 index 00000000..9ea371e5 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_network_globals.py @@ -0,0 +1,1508 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_network_globals +short_description: Manage network global settings on BIG-IP +description: + - Module to manage STP, Multicast, DAG, LLDP and Self Allow global settings on a BIG-IP. +version_added: "1.0.0" +options: + stp: + description: + - Manage global settings for STP on BIG-IP. + type: dict + suboptions: + config_name: + description: + - Specifies the configuration name. The accepted length is from 1 to 32 characters. + - Only has effect when the C(mode) is C(mstp). + type: str + config_revision: + description: + - Specifies the revision level of the MSTP configuration, when C(mode) is C(mstp). + - You must specify a number in the range of 0 to 65535. + type: int + description: + description: + - User-defined description. + type: str + fwd_delay: + description: + - The number of seconds for which an interface was blocked from forwarding network traffic after a + reconfiguration of the spanning tree topology. This parameter has no effect when C(rstp) or C(mstp) modes + are used, as long as all bridges in the spanning tree use the RSTP or MSTP protocol. + - If any legacy STP bridges are present, neighboring bridges must fall back to the old protocol, + whose reconfiguration time is affected by the forward delay value. + - The valid range is 4 to 30. + type: int + hello_time: + description: + - Specifies the time interval in seconds between the periodic transmissions that communicate spanning tree + information to the adjacent bridges in the network. + - The hello time set by default on the device is optimal in virtually all cases. F5 recommends that you do + not change the hello time. + - The valid range is 1 to 10. + type: int + max_age: + description: + - Specifies the number of seconds for which spanning tree information received from other bridges is + considered valid. + - The valid range is 6 to 40 seconds. + type: int + max_hops: + description: + - Specifies the maximum number of hops an MSTP packet may travel before it is discarded. + - This option only takes effect when C(mode) is C(mstp). + - The number of hops must be in the range of 1 to 255. + type: int + mode: + description: + - Specifies the spanning tree mode. + - "The C(mstp), C(rstp) and C(stp) options are only supported on hardware platforms. Attempting to set these + modes on VE type platforms will result in failure. The only valid options on VE type platforms are: + C(passthru) and C(disabled)." + type: str + choices: + - disabled + - mstp + - passthru + - rstp + - stp + transmit_hold: + description: + - Specifies the absolute limit on the number of spanning tree protocol packets the traffic management system + may transmit on a port in any hello time interval. + - The valid range is 1 to 10 packets. + type: int + multicast: + description: + - Manage multicast traffic configuration options. + type: dict + suboptions: + max_pending_packets: + description: + - Specifies the maximum number of packet queued on behalf of a single incomplete MFC entry. + - The valid range is 0 - 4294967295. + type: int + max_pending_routes: + description: + - Specifies the number of incomplete MFC entries each TMM will allow to exist at one time. + - The valid range is 0 - 4294967295. + type: int + route_lookup_timeout: + description: + - Specifies maximum lifetime of an incomplete MFC entry, in seconds. + - The valid range is 0 - 4294967295. + type: int + rate_limit: + description: + - When C(yes), the DB variable C(switchboard.maxmcastrate) setting controls the multicast packet per second rate + limiting in the switch. + type: bool + dag: + description: + - Manage global disaggregation settings. + type: dict + suboptions: + round_robin_mode: + description: + - Specifies whether the round robin disaggregator (DAG) on a blade can disaggregate packets to all the TMMs + in the system or only to the TMMs local to the blade. + - When C(global), the DAG will disaggregate packets to all TMMs in the system. + - When C(local), the DAG will disaggregate packets only to the TMMs local to the blade. + type: str + choices: + - global + - local + icmp_hash: + description: + - Specifies the ICMP hash for ICMP echo request and ICMP echo reply in SW DAG. + - When C(icmp), ICMP echo request and ICMP echo reply are disaggregated based on ICMP id. + - When C(ipicmp), ICMP echo request and ICMP echo reply are disaggregated based on ICMP id and IP addresses. + - This option is only available in C(TMOS) version C(13.x) and above. + type: str + choices: + - icmp + - ipicmp + dag_ipv6_prefix_len: + description: + - Specifies whether SPDAG or IPv6 prefix DAG should be used to disaggregate IPv6 traffic when vlan cmp hash + is set to C(src-ip) or C(dst-ip). + - The valid value range is 0 - 128, with C(128) value SPAG is in use. + - This option is only available in TMOS version C(13.x) and above. + type: int + lldp: + description: + - Manage LLDP configuration options. + type: dict + suboptions: + enabled: + description: + - Specifies the current status of LLDP. + - When C(yes), the LLDP is enabled globally on the device. + - When C(no), the LLDP is disabled globally on the device. + type: bool + max_neighbors_per_port: + description: + - Specifies the maximum number of neighbors per port. + - The valid value range is 0 - 65535. + type: int + reinit_delay: + description: + - Specifies the maximum number of seconds to wait after reaching the TTL interval before resetting TTL timer. + - The valid value range is 0 - 65535. + type: int + tx_delay: + description: + - Specifies the number of seconds to wait for LLDP to initialize on an interface before sending LLDP message. + - The valid value range is 0 - 65535. + type: int + tx_hold: + description: + - "Specifies the multiplier that determines the LLDP Time to Live (TTL). TTL is determined by multiplying + this value and C(tx_interval)." + - The valid value range is 0 - 65535. + type: int + tx_interval: + description: + - Specifies the interval devices use to send LLDP information from each of their interfaces. + - The valid value range is 0 - 65535. + type: int + self_allow: + description: + - Manage Self Allow global configuration options. + type: dict + suboptions: + defaults: + description: + - The default set of protocols and ports allowed by a self IP if the self IP allow-service setting is + B(default). + type: list + elements: dict + suboptions: + protocol: + description: + - The protocol name to be set. + type: str + port: + description: + - The port number to be set. + - The valid value range is 0 - 65535. + type: int + all: + description: + - Sets B(all) or B(none) ports and protocols as a system wide C(self_allow) setting. + - When C(yes), the self_allow allows all protocols and ports. This is the equivalent of setting B(all) option + in C(TMSH). + - When C(no), the self_allow allows no protocols and ports. This is the equivalent of setting B(none) option + in C(TMSH). + type: bool + version_added: "1.1.0" +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Update STP settings + bigip_network_globals: + stp: + config_name: foobar + config_revision: 1 + max_hops: 20 + mode: mstp + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Update DAG settings + bigip_network_globals: + dag: + icmp_hash: ipicmp + round_robin_mode: local + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Update multiple settings + bigip_network_globals: + stp: + config_name: foobar + config_revision: 1 + max_hops: 20 + mode: mstp + dag: + icmp_hash: ipicmp + round_robin_mode: local + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +stp: + description: Manage global settings for STP on BIG-IP. + type: complex + returned: changed + contains: + config_name: + description: The configuration name. + returned: changed + type: str + sample: foobar + config_revision: + description: The revision level of the MSTP configuration. + returned: changed + type: int + sample: 2 + description: + description: User-defined description. + returned: changed + type: str + sample: My description + fwd_delay: + description: The number of seconds for which an interface was blocked from forwarding network traffic. + returned: changed + type: int + sample: 4 + hello_time: + description: The time interval at seconds between the periodic transmissions of spanning tree information. + returned: changed + type: int + sample: 2 + max_age: + description: The number of seconds that spanning tree information received from other bridges is considered valid. + returned: changed + type: int + sample: 30 + max_hops: + description: The maximum number of hops an MSTP packet may travel before it is discarded. + returned: changed + type: int + sample: 15 + mode: + description: The spanning tree mode. + returned: changed + type: str + sample: mstp + transmit_hold: + description: + - The limit on the number of STP the traffic management system may transmit on a port in any hello + time interval. + returned: changed + type: int + sample: 5 + sample: hash/dictionary of values +multicast: + description: Manage multicast traffic configuration options. + type: complex + returned: changed + contains: + max_pending_packets: + description: The maximum number of packet queued on behalf of a single incomplete MFC entry. + returned: changed + type: int + sample: 3000 + max_pending_routes: + description: The number of incomplete MFC entries each TMM will allow to exist at one time. + returned: changed + type: int + sample: 50 + route_lookup_timeout: + description: The maximum lifetime of an incomplete MFC entry, in seconds. + returned: changed + type: int + sample: 20 + rate_limit: + description: Enables DB variable control over multicast packet per second rate limiting in the switch. + returned: changed + type: bool + sample: yes + sample: hash/dictionary of values +dag: + description: Manage multicast traffic configuration options. + type: complex + returned: changed + contains: + round_robin_mode: + description: The mode of operation of the DAG on a blade. + returned: changed + type: str + sample: local + icmp_hash: + description: Specifies the ICMP hash for the ICMP echo request and ICMP echo reply in SW DAG. + returned: changed + type: str + sample: ipicmp + dag_ipv6_prefix_len: + description: Specifies whether SPDAG or IPv6 prefix DAG should be used to disaggregate IPv6 traffic. + returned: changed + type: int + sample: 128 + sample: hash/dictionary of values +lldp: + description: Manage multicast traffic configuration options. + type: complex + returned: changed + contains: + enabled: + description: The current status of LLDP. + returned: changed + type: bool + sample: yes + max_neighbors_per_port: + description: The maximum number of neighbors per port. + returned: changed + type: int + sample: 128 + reinit_delay: + description: The maximum number of seconds to wait before resetting the TTL timer after reaching the TTL interval. + returned: changed + type: int + sample: 30 + tx_delay: + description: The number of seconds to wait for LLDP to initialize on an interface before sending LLDP message. + returned: changed + type: int + sample: 500 + tx_hold: + description: The multiplier that determines the LLDP Time to Live. + returned: changed + type: int + sample: 10 + tx_interval: + description: The interval devices use to send LLDP information from each of their interfaces. + returned: changed + type: int + sample: 240 + sample: hash/dictionary of values +self_allow: + description: Manages self_allow system wide settings. + type: complex + returned: changed + contains: + defaults: + description: The default set of protocols and ports allowed by a self IP. + type: complex + returned: changed + contains: + protocol: + description: The protocol name to be set. + returned: changed + type: str + sample: tcp + port: + description: The port number to be set. + returned: changed + type: int + sample: 443 + sample: hash/dictionary of values + all: + description: Allows all or none ports and protocols as a system wide self_allow setting. + returned: changed + type: bool + sample: yes + sample: hash/dictionary of values +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean +) +from ..module_utils.compare import ( + cmp_str_with_none, cmp_simple_list +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = {} + + api_attributes = [] + + returnables = [ + 'stp', + 'dag', + 'multicast', + 'lldp', + 'self_allow', + ] + + updatables = [ + 'stp_config_name', + 'stp_config_revision', + 'stp_description', + 'stp_fwd_delay', + 'stp_hello_time', + 'stp_max_age', + 'stp_max_hops', + 'stp_mode', + 'stp_transmit_hold', + 'mcast_max_pending_packets', + 'mcast_max_pending_routes', + 'mcast_rate_limit', + 'mcast_route_lookup_timeout', + 'dag_round_robin', + 'dag_ipv6_prefix_len', + 'dag_icmp_hash', + 'lldp_enabled', + 'lldp_disabled', + 'lldp_max_neighbors_per_port', + 'lldp_reinit_delay', + 'lldp_tx_delay', + 'lldp_tx_hold', + 'lldp_tx_interval', + 'self_allow_all', + 'self_allow_defaults', + ] + + +class ApiParameters(Parameters): + @property + def stp_config_name(self): + if self._values['stp'] is None: + return None + return self._values['stp'].get('configName', None) + + @property + def stp_config_revision(self): + if self._values['stp'] is None: + return None + return self._values['stp']['configRevision'] + + @property + def stp_description(self): + if self._values['stp'] is None: + return None + return self._values['stp'].get('description', None) + + @property + def stp_fwd_delay(self): + if self._values['stp'] is None: + return None + return self._values['stp']['fwdDelay'] + + @property + def stp_hello_time(self): + if self._values['stp'] is None: + return None + return self._values['stp']['helloTime'] + + @property + def stp_max_age(self): + if self._values['stp'] is None: + return None + return self._values['stp']['maxAge'] + + @property + def stp_max_hops(self): + if self._values['stp'] is None: + return None + return self._values['stp']['maxHops'] + + @property + def stp_mode(self): + if self._values['stp'] is None: + return None + return self._values['stp']['mode'] + + @property + def stp_transmit_hold(self): + if self._values['stp'] is None: + return None + return self._values['stp']['transmitHold'] + + @property + def mcast_max_pending_packets(self): + if self._values['multicast'] is None: + return None + return self._values['multicast']['maxPendingPackets'] + + @property + def mcast_max_pending_routes(self): + if self._values['multicast'] is None: + return None + return self._values['multicast']['maxPendingRoutes'] + + @property + def mcast_rate_limit(self): + if self._values['multicast'] is None: + return None + result = flatten_boolean(self._values['multicast']['rateLimit']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def mcast_route_lookup_timeout(self): + if self._values['multicast'] is None: + return None + return self._values['multicast']['routeLookupTimeout'] + + @property + def dag_round_robin_mode(self): + if self._values['dag'] is None: + return None + return self._values['dag']['roundRobinMode'] + + @property + def dag_ipv6_prefix_len(self): + if self._values['dag'] is None: + return None + return self._values['dag'].get('dagIpv6PrefixLen', None) + + @property + def dag_icmp_hash(self): + if self._values['dag'] is None: + return None + return self._values['dag'].get('icmpHash', None) + + @property + def lldp_enabled(self): + if self._values['lldp'] is None: + return None + if 'enabled' in self._values['lldp']: + result = flatten_boolean(self._values['lldp']['enabled']) + if result == 'yes': + return True + + @property + def lldp_disabled(self): + if self._values['lldp'] is None: + return None + if 'disabled' in self._values['lldp']: + result = flatten_boolean(self._values['lldp']['disabled']) + if result == 'yes': + return True + + @property + def lldp_max_neighbors_per_port(self): + if self._values['lldp'] is None: + return None + return self._values['lldp']['maxNeighborsPerPort'] + + @property + def lldp_reinit_delay(self): + if self._values['lldp'] is None: + return None + return self._values['lldp']['reinitDelay'] + + @property + def lldp_tx_delay(self): + if self._values['lldp'] is None: + return None + return self._values['lldp']['txDelay'] + + @property + def lldp_tx_hold(self): + if self._values['lldp'] is None: + return None + return self._values['lldp']['txHold'] + + @property + def lldp_tx_interval(self): + if self._values['lldp'] is None: + return None + return self._values['lldp']['txInterval'] + + @property + def self_allow_all(self): + if self.self_allow_defaults is None: + return 'no' + if self.self_allow_defaults == 'all': + return 'yes' + + @property + def self_allow_defaults(self): + if self._values['self_allow'] is None: + return None + return self._values['self_allow'].get('defaults', None) + + +class ModuleParameters(Parameters): + @property + def stp_config_name(self): + if self._values['stp'] is None: + return None + if self._values['stp']['config_name'] is None: + return None + if len(self._values['stp']['config_name']) > 32: + raise F5ModuleError( + "The 'config_name' cannot be more than 32 characters in length." + ) + return self._values['stp']['config_name'] + + @property + def stp_config_revision(self): + if self._values['stp'] is None: + return None + if self._values['stp']['config_revision'] is None: + return None + if 0 <= self._values['stp']['config_revision'] <= 65535: + return self._values['stp']['config_revision'] + raise F5ModuleError( + "Valid 'config_revision' must be in range 0 - 65535." + ) + + @property + def stp_description(self): + if self._values['stp'] is None: + return None + if self._values['stp']['description'] is None: + return None + return self._values['stp']['description'] + + @property + def stp_fwd_delay(self): + if self._values['stp'] is None: + return None + if self._values['stp']['fwd_delay'] is None: + return None + if 4 <= self._values['stp']['fwd_delay'] <= 30: + return self._values['stp']['fwd_delay'] + raise F5ModuleError( + "Valid 'fwd_delay' must be in range 4 - 30 seconds." + ) + + @property + def stp_hello_time(self): + if self._values['stp'] is None: + return None + if self._values['stp']['hello_time'] is None: + return None + if 1 <= self._values['stp']['hello_time'] <= 10: + return self._values['stp']['hello_time'] + raise F5ModuleError( + "Valid 'hello_time' must be in range 1 - 10 seconds." + ) + + @property + def stp_max_age(self): + if self._values['stp'] is None: + return None + if self._values['stp']['max_age'] is None: + return None + if 6 <= self._values['stp']['max_age'] <= 40: + return self._values['stp']['max_age'] + raise F5ModuleError( + "Valid 'hello_time' must be in range 6 - 40 seconds." + ) + + @property + def stp_max_hops(self): + if self._values['stp'] is None: + return None + if self._values['stp']['max_hops'] is None: + return None + if 1 <= self._values['stp']['max_hops'] <= 255: + return self._values['stp']['max_hops'] + raise F5ModuleError( + "Valid 'max_hops' must be in range 1 - 255." + ) + + @property + def stp_mode(self): + if self._values['stp'] is None: + return None + if self._values['stp']['mode'] is None: + return None + return self._values['stp']['mode'] + + @property + def stp_transmit_hold(self): + if self._values['stp'] is None: + return None + if self._values['stp']['transmit_hold'] is None: + return None + if 1 <= self._values['stp']['transmit_hold'] <= 10: + return self._values['stp']['transmit_hold'] + raise F5ModuleError( + "Valid 'transmit_hold' must be in range 1 - 10 packets." + ) + + @property + def mcast_max_pending_packets(self): + if self._values['multicast'] is None: + return None + if self._values['multicast']['max_pending_packets'] is None: + return None + if 0 <= self._values['multicast']['max_pending_packets'] <= 4294967295: + return self._values['multicast']['max_pending_packets'] + raise F5ModuleError( + "Valid 'max_pending_packets' must be in range 0 - 4294967295." + ) + + @property + def mcast_max_pending_routes(self): + if self._values['multicast'] is None: + return None + if self._values['multicast']['max_pending_routes'] is None: + return None + if 0 <= self._values['multicast']['max_pending_routes'] <= 4294967295: + return self._values['multicast']['max_pending_routes'] + raise F5ModuleError( + "Valid 'max_pending_routes' must be in range 0 - 4294967295." + ) + + @property + def mcast_rate_limit(self): + if self._values['multicast'] is None: + return None + result = flatten_boolean(self._values['multicast']['rate_limit']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def mcast_route_lookup_timeout(self): + if self._values['multicast'] is None: + return None + if self._values['multicast']['route_lookup_timeout'] is None: + return None + if 0 <= self._values['multicast']['route_lookup_timeout'] <= 4294967295: + return self._values['multicast']['route_lookup_timeout'] + raise F5ModuleError( + "Valid 'route_lookup_timeout' must be in range 0 - 4294967295." + ) + + @property + def dag_round_robin_mode(self): + if self._values['dag'] is None: + return None + if self._values['dag']['round_robin_mode'] is None: + return None + return self._values['dag']['round_robin_mode'] + + @property + def dag_ipv6_prefix_len(self): + if self._values['dag'] is None: + return None + if self._values['dag']['dag_ipv6_prefix_len'] is None: + return None + if 0 <= self._values['dag']['dag_ipv6_prefix_len'] <= 128: + return self._values['dag']['dag_ipv6_prefix_len'] + raise F5ModuleError( + "Valid 'dag_ipv6_prefix_len' must be in range 0 - 128." + ) + + @property + def dag_icmp_hash(self): + if self._values['dag'] is None: + return None + if self._values['dag']['icmp_hash'] is None: + return None + return self._values['dag']['icmp_hash'] + + @property + def lldp_enabled(self): + if self._values['lldp'] is None: + return None + result = flatten_boolean(self._values['lldp']['enabled']) + if result == 'yes': + return True + return None + + @property + def lldp_disabled(self): + if self._values['lldp'] is None: + return None + result = flatten_boolean(self._values['lldp']['enabled']) + if result == 'no': + return True + return None + + @property + def lldp_max_neighbors_per_port(self): + if self._values['lldp'] is None: + return None + if self._values['lldp']['max_neighbors_per_port'] is None: + return None + if 0 <= self._values['lldp']['max_neighbors_per_port'] <= 65535: + return self._values['lldp']['max_neighbors_per_port'] + raise F5ModuleError( + "Valid 'max_neighbors_per_port' must be in range 0 - 65535." + ) + + @property + def lldp_reinit_delay(self): + if self._values['lldp'] is None: + return None + if self._values['lldp']['reinit_delay'] is None: + return None + if 0 <= self._values['lldp']['reinit_delay'] <= 65535: + return self._values['lldp']['reinit_delay'] + raise F5ModuleError( + "Valid 'reinit_delay' must be in range 0 - 65535 seconds." + ) + + @property + def lldp_tx_delay(self): + if self._values['lldp'] is None: + return None + if self._values['lldp']['tx_delay'] is None: + return None + if 0 <= self._values['lldp']['tx_delay'] <= 65535: + return self._values['lldp']['tx_delay'] + raise F5ModuleError( + "Valid 'tx_delay' must be in range 0 - 65535 seconds." + ) + + @property + def lldp_tx_hold(self): + if self._values['lldp'] is None: + return None + if self._values['lldp']['tx_hold'] is None: + return None + if 0 <= self._values['lldp']['tx_hold'] <= 65535: + return self._values['lldp']['tx_hold'] + raise F5ModuleError( + "Valid 'tx_hold' must be in range 0 - 65535." + ) + + @property + def lldp_tx_interval(self): + if self._values['lldp'] is None: + return None + if self._values['lldp']['tx_interval'] is None: + return None + if 0 <= self._values['lldp']['tx_interval'] <= 65535: + return self._values['lldp']['tx_interval'] + raise F5ModuleError( + "Valid 'tx_interval' must be in range 0 - 65535 seconds." + ) + + @property + def self_allow_defaults(self): + if self._values['self_allow'] is None: + return None + if self.self_allow_all == 'yes': + return 'all' + if self.self_allow_all == 'no': + return 'none' + result = list() + for item in self._values['self_allow']['defaults']: + if not 0 <= item['port'] or not item['port'] <= 65535: + raise F5ModuleError( + "Valid self_allow_defaults port must be in range 0 - 65535." + ) + to_append = "{0}:{1}".format(item['protocol'], item['port']) + result.append(to_append) + return result + + @property + def self_allow_all(self): + if self._values['self_allow'] is None: + return None + result = flatten_boolean(self._values['self_allow']['all']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def stp(self): + to_filter = dict( + configName=self._values['stp_config_name'], + configRevision=self._values['stp_config_revision'], + description=self._values['stp_description'], + fwdDelay=self._values['stp_fwd_delay'], + helloTime=self._values['stp_hello_time'], + maxAge=self._values['stp_max_age'], + maxHops=self._values['stp_max_hops'], + mode=self._values['stp_mode'], + transmitHold=self._values['stp_transmit_hold'] + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def multicast(self): + to_filter = dict( + maxPendingPackets=self._values['mcast_max_pending_packets'], + maxPendingRoutes=self._values['mcast_max_pending_routes'], + rateLimit=self._values['mcast_rate_limit'], + routeLookupTimeout=self._values['mcast_route_lookup_timeout'], + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def dag(self): + to_filter = dict( + roundRobinMode=self._values['dag_round_robin_mode'], + dagIpv6PrefixLen=self._values['dag_ipv6_prefix_len'], + icmpHash=self._values['dag_icmp_hash'], + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def lldp(self): + to_filter = dict( + enabled=self._values['lldp_enabled'], + disabled=self._values['lldp_disabled'], + maxNeighborsPerPort=self._values['lldp_max_neighbors_per_port'], + reinitDelay=self._values['lldp_reinit_delay'], + txDelay=self._values['lldp_tx_delay'], + txHold=self._values['lldp_tx_hold'], + txInterval=self._values['lldp_tx_interval'], + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def self_allow(self): + to_filter = dict( + defaults=self._values['self_allow_defaults'], + ) + result = self._filter_params(to_filter) + if result: + return result + + +class ReportableChanges(Changes): + @property + def stp(self): + if self._values['stp'] is None: + return None + to_filter = dict( + config_name=self._values['stp'].get('configName', None), + config_revision=self._values['stp'].get('configRevision', None), + description=self._values['stp'].get('description', None), + fwd_delay=self._values['stp'].get('fwdDelay', None), + hello_time=self._values['stp'].get('helloTime', None), + max_age=self._values['stp'].get('maxAge', None), + max_hops=self._values['stp'].get('maxHops', None), + mode=self._values['stp'].get('mode', None), + transmit_hold=self._values['stp'].get('transmitHold', None), + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def multicast(self): + if self._values['multicast'] is None: + return None + to_filter = dict( + max_pending_packets=self._values['multicast'].get('maxPendingPackets', None), + max_pending_routes=self._values['multicast'].get('maxPendingRoutes', None), + rate_limit=flatten_boolean(self._values['multicast'].get('rateLimit', None)), + route_lookup_timeout=self._values['multicast'].get('routeLookupTimeout', None) + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def dag(self): + if self._values['dag'] is None: + return None + to_filter = dict( + round_robin_mode=self._values['dag'].get('roundRobinMode', None), + dag_ipv6_prefix_len=self._values['dag'].get('dagIpv6PrefixLen', None), + icmp_hash=self._values['dag'].get('icmpHash', None), + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def lldp(self): + if self._values['lldp'] is None: + return None + to_filter = dict( + enabled=self.enabled, + max_neighbors_per_port=self._values['lldp'].get('maxNeighborsPerPort', None), + reinit_delay=self._values['lldp'].get('reinitDelay', None), + tx_delay=self._values['lldp'].get('txDelay', None), + tx_hold=self._values['lldp'].get('txHold', None), + tx_interval=self._values['lldp'].get('txInterval', None) + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def enabled(self): + enabled = self._values['lldp'].get('enabled', None) + disabled = self._values['lldp'].get('disabled', None) + if enabled: + return 'yes' + if disabled: + return 'no' + + @property + def self_allow(self): + if self._values['self_allow'] is None: + return None + to_filter = dict( + defaults=self._parse_self_defaults(), + all=self._get_all_value(), + ) + result = self._filter_params(to_filter) + if result: + return result + + def _parse_self_defaults(self): + items = self._values['self_allow'].get('defaults') + if isinstance(items, list): + result = dict() + for item in items: + result['protocol'] = item.split(':')[0] + result['port'] = item.split(':')[1] + return result + + def _get_all_value(self): + items = self._values['self_allow'].get('defaults') + if isinstance(items, string_types): + if items == 'none': + return 'no' + if items == 'all': + return 'yes' + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def stp_config_name(self): + result = cmp_str_with_none(self.want.stp_config_name, self.have.stp_config_name) + return result + + @property + def stp_description(self): + result = cmp_str_with_none(self.want.stp_description, self.have.stp_description) + return result + + @property + def self_allow_defaults(self): + if self.want.self_allow_defaults is None: + return None + if self.want.self_allow_defaults == 'none' and self.have.self_allow_defaults is None: + return None + if self.want.self_allow_defaults in ['all', 'none']: + if isinstance(self.have.self_allow_defaults, string_types): + if self.want.self_allow_defaults != self.have.self_allow_defaults: + return self.want.self_allow_defaults + else: + return None + if isinstance(self.have.self_allow_defaults, list): + return self.want.self_allow_defaults + result = cmp_simple_list(self.want.self_allow_defaults, self.have.self_allow_defaults) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + return self.update() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + params = self.changes.to_return() + if self.changes.stp: + self.update_on_device_stp(params['stp']) + if self.changes.multicast: + self.update_on_device_mcast(params['multicast']) + if self.changes.dag: + self.update_on_device_dag(params['dag']) + if self.changes.lldp: + self.update_on_device_lldp(params['lldp']) + if self.changes.self_allow: + self.update_on_device_self(params['self_allow']) + + def update_on_device_stp(self, params): + uri = "https://{0}:{1}/mgmt/tm/net/stp-globals/".format( + self.client.provider['server'], + self.client.provider['server_port'], + + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device_mcast(self, params): + uri = "https://{0}:{1}/mgmt/tm/net/multicast-globals/".format( + self.client.provider['server'], + self.client.provider['server_port'], + + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device_dag(self, params): + uri = "https://{0}:{1}/mgmt/tm/net/dag-globals/".format( + self.client.provider['server'], + self.client.provider['server_port'], + + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device_lldp(self, params): + uri = "https://{0}:{1}/mgmt/tm/net/lldp-globals/".format( + self.client.provider['server'], + self.client.provider['server_port'], + + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device_self(self, params): + uri = "https://{0}:{1}/mgmt/tm/net/self-allow/".format( + self.client.provider['server'], + self.client.provider['server_port'], + + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + response = dict( + stp=None, + multicast=None, + dag=None, + lldp=None, + self_allow=None, + ) + if self.want.stp: + response['stp'] = self.read_current_from_device_stp() + if self.want.multicast: + response['multicast'] = self.read_current_from_device_mcast() + if self.want.dag: + response['dag'] = self.read_current_from_device_dag() + if self.want.lldp: + response['lldp'] = self.read_current_from_device_lldp() + if self.want.self_allow: + response['self_allow'] = self.read_current_from_device_self() + return ApiParameters(params=response) + + def read_current_from_device_stp(self): + uri = "https://{0}:{1}/mgmt/tm/net/stp-globals/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response + raise F5ModuleError(resp.content) + + def read_current_from_device_mcast(self): + uri = "https://{0}:{1}/mgmt/tm/net/multicast-globals/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response + raise F5ModuleError(resp.content) + + def read_current_from_device_dag(self): + uri = "https://{0}:{1}/mgmt/tm/net/dag-globals/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response + raise F5ModuleError(resp.content) + + def read_current_from_device_lldp(self): + uri = "https://{0}:{1}/mgmt/tm/net/lldp-globals/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response + raise F5ModuleError(resp.content) + + def read_current_from_device_self(self): + uri = "https://{0}:{1}/mgmt/tm/net/self-allow/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + stp=dict( + type='dict', + options=dict( + config_name=dict(), + config_revision=dict(type='int'), + description=dict(), + fwd_delay=dict(type='int'), + hello_time=dict(type='int'), + max_age=dict(type='int'), + max_hops=dict(type='int'), + mode=dict( + choices=['disabled', 'mstp', 'passthru', 'rstp', 'stp'] + ), + transmit_hold=dict(type='int') + ) + ), + multicast=dict( + type='dict', + options=dict( + max_pending_packets=dict(type='int'), + max_pending_routes=dict(type='int'), + rate_limit=dict(type='bool'), + route_lookup_timeout=dict(type='int'), + ) + ), + dag=dict( + type='dict', + options=dict( + dag_ipv6_prefix_len=dict(type='int'), + icmp_hash=dict( + choices=['icmp', 'ipicmp'] + ), + round_robin_mode=dict( + choices=['global', 'local'] + ), + ) + ), + lldp=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + max_neighbors_per_port=dict(type='int'), + reinit_delay=dict(type='int'), + tx_delay=dict(type='int'), + tx_hold=dict(type='int'), + tx_interval=dict(type='int') + ) + ), + self_allow=dict( + type='dict', + options=dict( + defaults=dict( + type='list', + elements='dict', + options=dict( + protocol=dict(), + port=dict(type='int'), + ), + required_together=[ + ['protocol', 'port'] + ] + ), + all=dict(type='bool'), + ), + mutually_exclusive=[ + ['defaults', 'all'] + ], + required_one_of=[ + ['defaults', 'all'] + ], + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_one_of = [ + ['stp', 'multicast', 'dag', 'lldp', 'self_allow'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_one_of=spec.required_one_of + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_node.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_node.py new file mode 100644 index 00000000..887a0050 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_node.py @@ -0,0 +1,1113 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2016, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_node +short_description: Manages F5 BIG-IP LTM nodes +description: + - Manages F5 BIG-IP LTM nodes. +version_added: "1.0.0" +options: + state: + description: + - Specifies the current state of the node. C(enabled) (All traffic + allowed), specifies the system sends traffic to this node regardless + of the node's state. C(disabled) (Only persistent or active connections + allowed), specifies the node can handle only persistent or + active connections. C(offline) (Only active connections allowed), + specifies the node can handle only active connections. In all + cases except C(absent), the node will be created if it does not yet + exist. + - Be particularly careful about changing the status of a node whose FQDN + cannot be resolved. These situations disable your ability to change their + C(state) to C(disabled) or C(offline). They will remain in an + *Unavailable - Enabled* state. + type: str + choices: + - present + - absent + - enabled + - disabled + - offline + default: present + name: + description: + - Specifies the name of the node. + type: str + required: True + monitors: + description: + - Specifies the health monitors the system currently uses to + monitor this node. + type: list + elements: str + address: + description: + - IP address of the node. This can be either IPv4 or IPv6. When creating a + new node, you must provide one of either C(address) or C(fqdn). This + parameter cannot be updated after it is set. + type: str + aliases: + - ip + - host + fqdn: + description: + - FQDN name of the node. This can be any name that is a valid RFC 1123 DNS + name. Therefore, the only characters that can be used are "A" to "Z", + "a" to "z", "0" to "9", the hyphen ("-") and the period ("."). + - FQDN names must include at least one period; delineating the host from + the domain. For example, C(host.domain). + - FQDN names must end with a letter or a number. + - When creating a new node, you must provide one of either C(address) or C(fqdn) provided. + This parameter cannot be updated after it is set. + type: str + aliases: + - hostname + fqdn_address_type: + description: + - Specifies whether the FQDN of the node resolves to an IPv4 or IPv6 address. + - When creating a new node, if this parameter is not specified and C(fqdn) is + specified, this parameter will default to C(ipv4). + - This parameter cannot be changed after it has been set. + type: str + choices: + - ipv4 + - ipv6 + - all + fqdn_auto_populate: + description: + - Specifies whether the system automatically creates ephemeral nodes using + the IP addresses returned by the resolution of a DNS query for a node defined + by an FQDN. + - When C(yes), the system generates an ephemeral node for each IP address + returned in response to a DNS query for the FQDN of the node. Additionally, + when a DNS response indicates the IP address of an ephemeral node no longer + exists, the system deletes the ephemeral node. + - When C(no), the system resolves a DNS query for the FQDN of the node with the + single IP address associated with the FQDN. + - When creating a new node, if this parameter is not specified and C(fqdn) is + specified, this parameter will default to C(yes). + - This parameter cannot be changed after it has been set. + type: bool + fqdn_up_interval: + description: + - Specifies the interval at which a query occurs, when the DNS server is up. + The associated monitor attempts to probe three times, and marks the server + down if it there is no response within the span of three times the interval + value, in seconds. + - This parameter accepts a value of C(ttl) to query, based off of the TTL of + the FQDN. The default TTL interval is similar to specifying C(3600). + - When creating a new node, if this parameter is not specified and C(fqdn) is + specified, this parameter will default to C(3600). + type: str + fqdn_down_interval: + description: + - Specifies the interval in which a query occurs, when the DNS server is down. + The associated monitor continues polling as long as the DNS server is down. + - When creating a new node, if this parameter is not specified and C(fqdn) is + specified, this parameter will default to C(5). + type: int + description: + description: + - Specifies descriptive text that identifies the node. + - You can remove a description by either specifying an empty string, or by + specifying the special value C(none). + type: str + connection_limit: + description: + - Node connection limit. Setting this to C(0) disables the limit. + type: int + rate_limit: + description: + - Node rate limit (connections-per-second). Setting this to C(0) disables the limit. + type: int + ratio: + description: + - Node ratio weight. Valid values range from 1 through 100. + - When creating a new node, if this parameter is not specified, the default of + C(1) will be used. + type: int + dynamic_ratio: + description: + - The dynamic ratio number for the node. Used for dynamic ratio load balancing. + - When creating a new node, if this parameter is not specified, the default of + C(1) will be used. + type: int + availability_requirements: + description: + - If you activate more than one health monitor, specifies the number of health + monitors that must receive successful responses in order for the link to be + considered available. + type: dict + suboptions: + type: + description: + - Monitor rule type when C(monitors) is specified. + - When creating a new pool, if this value is not specified, the default of + 'all' will be used. + type: str + required: True + choices: + - all + - at_least + at_least: + description: + - Specifies the minimum number of active health monitors that must be successful + before the link is considered up. + - This parameter is only relevant when a C(type) of C(at_least) is used. + - This parameter will be ignored if a type of C(all) is used. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add node + bigip_node: + host: 10.20.30.40 + name: 10.20.30.40 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add node with a single 'ping' monitor + bigip_node: + host: 10.20.30.40 + name: mytestserver + monitors: + - /Common/icmp + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Modify node description + bigip_node: + name: 10.20.30.40 + description: Our best server yet + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Delete node + bigip_node: + state: absent + name: 10.20.30.40 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Force node offline + bigip_node: + state: disabled + name: 10.20.30.40 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add node by their FQDN + bigip_node: + fqdn: foo.bar.com + name: foobar.net + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +monitors: + description: + - Changed list of monitors for the node. + returned: changed and success + type: list + sample: ['icmp', 'tcp_echo'] +description: + description: + - Changed value for the description of the node. + returned: changed and success + type: str + sample: E-Commerce webserver in ORD +session: + description: + - Changed value for the internal session of the node. + returned: changed and success + type: str + sample: user-disabled +state: + description: + - Changed value for the internal state of the node. + returned: changed and success + type: str + sample: user-down +''' + +import re +import time +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.parsing.convert_bool import ( + BOOLEANS_FALSE, BOOLEANS_TRUE +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'monitor': 'monitors', + 'connectionLimit': 'connection_limit', + 'rateLimit': 'rate_limit' + } + + api_attributes = [ + 'description', + 'address', + 'fqdn', + 'ratio', + 'connectionLimit', + 'rateLimit', + 'monitor', + + # Used for changing state + # + # user-enabled (enabled) + # user-disabled (disabled) + # user-disabled (offline) + 'session', + + # Used for changing state + # user-down (offline) + 'state' + ] + + returnables = [ + 'monitors', + 'description', + 'fqdn', + 'address', + 'session', + 'state', + 'fqdn_auto_populate', + 'fqdn_address_type', + 'fqdn_up_interval', + 'fqdn_down_interval', + 'fqdn_name', + 'connection_limit', + 'ratio', + 'rate_limit', + 'availability_requirements' + ] + + updatables = [ + 'monitors', + 'description', + 'state', + 'fqdn_up_interval', + 'fqdn_down_interval', + 'tmName', + 'fqdn_auto_populate', + 'fqdn_address_type', + 'connection_limit', + 'ratio', + 'rate_limit', + ] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + except Exception: + return result + + @property + def rate_limit(self): + if self._values['rate_limit'] is None: + return None + if self._values['rate_limit'] == 'disabled': + return 0 + return int(self._values['rate_limit']) + + +class Changes(Parameters): + pass + + +class UsableChanges(Changes): + @property + def fqdn(self): + result = dict() + if self._values['fqdn_up_interval'] is not None: + result['interval'] = self._values['fqdn_up_interval'] + if self._values['fqdn_down_interval'] is not None: + result['downInterval'] = self._values['fqdn_down_interval'] + if self._values['fqdn_auto_populate'] is not None: + result['autopopulate'] = self._values['fqdn_auto_populate'] + if self._values['fqdn_name'] is not None: + result['tmName'] = self._values['fqdn_name'] + if self._values['fqdn_address_type'] is not None: + result['addressFamily'] = self._values['fqdn_address_type'] + if not result: + return None + return result + + @property + def monitors(self): + monitor_string = self._values['monitors'] + if monitor_string is None: + return None + if '{' in monitor_string and '}' in monitor_string: + tmp = monitor_string.strip('}').split('{') + monitor = ''.join(tmp).rstrip() + return monitor + return monitor_string + + +class ReportableChanges(Changes): + @property + def monitors(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + else: + return 'all' + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + The monitor string for a Require monitor looks like this. + min 1 of { /Common/gateway_icmp } + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('least')) + + @property + def availability_requirements(self): + if self._values['monitors'] is None: + return None + result = dict() + result['type'] = self.availability_requirement_type + result['at_least'] = self.at_least + return result + + +class ModuleParameters(Parameters): + def _get_availability_value(self, type): + if self._values['availability_requirements'] is None: + return None + if self._values['availability_requirements'][type] is None: + return None + return self._values['availability_requirements'][type] + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + except Exception: + result = self._values['monitors'] + result.sort() + return result + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + if self._values['monitors'] == 'default': + return 'default' + if len(self._values['monitors']) == 1 and self._values['monitors'][0] == '': + return '/Common/none' + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if len(self.monitors_list) > 1: + if self.availability_requirement_type == 'at_least': + if self.at_least > len(self.monitors_list): + raise F5ModuleError( + "The 'at_least' value must not exceed the number of 'monitors'." + ) + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + else: + result = ' and '.join(monitors).strip() + return result + if len(self.monitors_list) == 1: + return monitors[0] + + @property + def availability_requirement_type(self): + if self._values['availability_requirements'] is None: + return None + return self._values['availability_requirements']['type'] + + @property + def at_least(self): + return self._get_availability_value('at_least') + + @property + def fqdn_up_interval(self): + if self._values['fqdn_up_interval'] is None: + return None + return str(self._values['fqdn_up_interval']) + + @property + def fqdn_down_interval(self): + if self._values['fqdn_down_interval'] is None: + return None + return str(self._values['fqdn_down_interval']) + + @property + def fqdn_auto_populate(self): + auto_populate = self._values.get('fqdn_auto_populate', None) + if auto_populate in BOOLEANS_TRUE: + return 'enabled' + elif auto_populate in BOOLEANS_FALSE: + return 'disabled' + + @property + def fqdn_name(self): + return self._values.get('fqdn', None) + + @property + def fqdn(self): + if self._values['fqdn'] is None: + return None + result = dict( + addressFamily=self._values.get('fqdn_address_type', None), + downInterval=self._values.get('fqdn_down_interval', None), + interval=self._values.get('fqdn_up_interval', None), + autopopulate=None, + tmName=self._values.get('fqdn', None) + ) + auto_populate = self._values.get('fqdn_auto_populate', None) + if auto_populate in BOOLEANS_TRUE: + result['autopopulate'] = 'enabled' + elif auto_populate in BOOLEANS_FALSE: + result['autopopulate'] = 'disabled' + return result + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class ApiParameters(Parameters): + @property + def fqdn_up_interval(self): + if self._values['fqdn'] is None: + return None + if 'interval' in self._values['fqdn']: + return str(self._values['fqdn']['interval']) + + @property + def fqdn_down_interval(self): + if self._values['fqdn'] is None: + return None + if 'downInterval' in self._values['fqdn']: + return str(self._values['fqdn']['downInterval']) + + @property + def fqdn_address_type(self): + if self._values['fqdn'] is None: + return None + if 'addressFamily' in self._values['fqdn']: + return str(self._values['fqdn']['addressFamily']) + + @property + def fqdn_auto_populate(self): + if self._values['fqdn'] is None: + return None + if 'autopopulate' in self._values['fqdn']: + return str(self._values['fqdn']['autopopulate']) + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + else: + return 'all' + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + except Exception: + result = self._values['monitors'] + result.sort() + return result + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + if self._values['monitors'] == 'default': + return 'default' + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if len(self.monitors_list) > 1: + if self.availability_requirement_type == 'at_least': + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + else: + result = ' and '.join(monitors).strip() + return result + if len(self.monitors_list) == 1: + return monitors[0] + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + + The monitor string for a Require monitor looks like this. + + min 1 of { /Common/gateway_icmp } + + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('least') + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def monitors(self): + if self.want.monitors is None: + return None + if self.want.monitors == 'default' and self.have.monitors == 'default': + return None + if self.want.monitors == 'default' and self.have.monitors is None: + return None + if self.want.monitors == '/Common/none' and self.have.monitors == '/Common/none': + return None + if self.want.monitors == 'default' and len(self.have.monitors) > 0: + return 'default' + if self.have.monitors is None: + return self.want.monitors + if self.have.monitors != self.want.monitors: + return self.want.monitors + + @property + def state(self): + result = None + if self.want.state in ['present', 'enabled']: + if self.have.session not in ['user-enabled', 'monitor-enabled']: + result = dict( + session='user-enabled', + state='user-up', + ) + elif self.want.state == 'disabled': + if self.have.session != 'user-disabled' or self.have.state == 'user-down': + result = dict( + session='user-disabled', + state='user-up' + ) + elif self.want.state == 'offline': + if self.have.state != 'user-down': + result = dict( + session='user-disabled', + state='user-down' + ) + return result + + @property + def description(self): + if self.want.description is None: + return None + if self.have.description is None and self.want.description == '': + return None + if self.want.description != self.have.description: + return self.want.description + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self): + warnings = [] + if self.want: + warnings += self.want._values.get('__warnings', []) + if self.have: + warnings += self.have._values.get('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + try: + if state in ['present', 'enabled', 'disabled', 'offline']: + changed = self.present() + elif state == "absent": + changed = self.absent() + except IOError as e: + raise F5ModuleError(str(e)) + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations() + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def _check_required_creation_vars(self): + if self.want.address is None and self.want.fqdn is None: + raise F5ModuleError( + "At least one of 'address' or 'fqdn' is required when creating a node" + ) + elif self.want.address is not None and self.want.fqdn is not None: + raise F5ModuleError( + "Only one of 'address' or 'fqdn' can be provided when creating a node" + ) + elif self.want.fqdn is not None: + self.want.update(dict(address='any6')) + + def _munge_creation_state_for_device(self): + # Modifying the state before sending to BIG-IP + # + # The 'state' must be set to None to exclude the values (accepted by this + # module) from being sent to the BIG-IP because for specific Ansible states, + # BIG-IP will consider those state values invalid. + if self.want.state in ['present', 'enabled']: + self.want.update(dict( + session='user-enabled', + state='user-up', + )) + elif self.want.state in 'disabled': + self.want.update(dict( + session='user-disabled', + state='user-up' + )) + else: + # State 'offline' + # Offline state will result in the monitors stopping for the node + self.want.update(dict( + session='user-disabled', + + # only a valid state can be specified. The module's value is "offline", + # but this is an invalid value for the BIG-IP. Therefore set it to user-down. + state='user-down', + + # Even user-down wil not work when _creating_ a node, so we register another + # want value (that is not sent to the API). This is checked for later to + # determine if we have to PATCH the node to be offline. + is_offline=True + )) + + def create(self): + self._check_required_creation_vars() + self._munge_creation_state_for_device() + + if self.want.fqdn_name: + if self.want.fqdn_auto_populate is None: + self.want.update({'fqdn_auto_populate': True}) + if self.want.fqdn_address_type is None: + self.want.update({'fqdn_address_type': 'ipv4'}) + if self.want.fqdn_up_interval is None: + self.want.update({'fqdn_up_interval': 3600}) + if self.want.fqdn_down_interval is None: + self.want.update({'fqdn_down_interval': 5}) + if self.want.ratio is None: + self.want.update({'ratio': 1}) + if self.want.dynamic_ratio is None: + self.want.update({'dynamic_ratio': 1}) + + self._set_changed_options() + if self.module.check_mode: + return True + + # These are being set here because the ``create_on_device`` method + # uses ``self.changes`` (to get formatting of parameters correct) + # but these two parameters here cannot be changed and also it is + # not easy to get the current versions of them for comparison. + if self.want.address: + self.changes.update({'address': self.want.address}) + if self.want.fqdn_up_interval is not None: + self.changes.update({'fqdn_up_interval': self.want.fqdn_up_interval}) + if self.want.fqdn_down_interval is not None: + self.changes.update({'fqdn_down_interval': self.want.fqdn_down_interval}) + if self.want.fqdn_auto_populate is not None: + self.changes.update({'fqdn_auto_populate': self.want.fqdn_auto_populate}) + if self.want.fqdn_name is not None: + self.changes.update({'fqdn_name': self.want.fqdn_name}) + if self.want.fqdn_address_type is not None: + self.changes.update({'fqdn_address_type': self.want.fqdn_address_type}) + + self.create_on_device() + if not self.exists(): + raise F5ModuleError("Failed to create the node") + # It appears that you cannot create a node in an 'offline' state, so instead + # we update its status to offline after we create it. + if self.want.is_offline: + self.update_node_offline_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + + if self.want.fqdn_auto_populate is not None: + if self.want.fqdn_auto_populate != self.have.fqdn_auto_populate: + raise F5ModuleError( + "The 'fqdn_auto_populate' parameter cannot be changed." + ) + if self.want.fqdn_address_type is not None: + if self.want.fqdn_address_type != self.have.fqdn_address_type: + raise F5ModuleError( + "The 'fqdn_address_type' parameter cannot be changed." + ) + + if self.module.check_mode: + return True + + self.update_on_device() + if self.want.state == 'offline': + self.update_node_offline_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the node.") + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/node/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/node/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update_node_offline_on_device(self): + params = dict( + session="user-disabled", + state="user-down" + ) + uri = "https://{0}:{1}/mgmt/tm/ltm/node/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + if params: + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + 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/ltm/node/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + self._wait_for_fqdn_checks() + return True + raise F5ModuleError(resp.content) + + def _wait_for_fqdn_checks(self): + while True: + have = self.read_current_from_device() + if have.state == 'fqdn-checking': + time.sleep(1) + else: + break + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/node/{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 + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + address=dict( + aliases=['host', 'ip'] + ), + fqdn=dict( + aliases=['hostname'] + ), + description=dict(), + state=dict( + choices=['absent', 'present', 'enabled', 'disabled', 'offline'], + default='present' + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + fqdn_address_type=dict( + choices=['ipv4', 'ipv6', 'all'] + ), + fqdn_auto_populate=dict(type='bool'), + fqdn_up_interval=dict(), + fqdn_down_interval=dict(type='int'), + connection_limit=dict(type='int'), + rate_limit=dict(type='int'), + ratio=dict(type='int'), + dynamic_ratio=dict(type='int'), + availability_requirements=dict( + type='dict', + options=dict( + type=dict( + choices=['all', 'at_least'], + required=True + ), + at_least=dict(type='int'), + ), + required_if=[ + ['type', 'at_least', ['at_least']], + ] + ), + monitors=dict( + type='list', + elements='str', + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_partition.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_partition.py new file mode 100644 index 00000000..39234996 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_partition.py @@ -0,0 +1,500 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_partition +short_description: Manage BIG-IP partitions +description: + - Manage partitions on the BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Name of the partition. + type: str + required: True + description: + description: + - The description to attach to the partition. + type: str + route_domain: + description: + - The default Route Domain to assign to the partition. If no route domain + is specified, the default route domain for the system (typically + zero) will be used only when creating a new partition. + type: int + state: + description: + - Whether the partition should exist or not. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create partition "foo" using the default route domain + bigip_partition: + name: foo + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create partition "bar" using a custom route domain + bigip_partition: + name: bar + route_domain: 3 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Change route domain of partition "foo" + bigip_partition: + name: foo + route_domain: 8 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set a description for partition "foo" + bigip_partition: + name: foo + description: Tenant CompanyA + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Delete the "foo" partition + bigip_partition: + name: foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +route_domain: + description: Name of the route domain associated with the partition. + returned: changed and success + type: int + sample: 0 +description: + description: The description of the partition. + returned: changed and success + type: str + sample: Example partition +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultRouteDomain': 'route_domain', + } + + api_attributes = [ + 'description', + 'defaultRouteDomain', + ] + + returnables = [ + 'description', + 'route_domain', + 'folder_description', + ] + + updatables = [ + 'description', + 'route_domain', + 'folder_description', + ] + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def partition(self): + # Cannot create a partition in a partition, so nullify this + return None + + @property + def route_domain(self): + if self._values['route_domain'] is None: + return None + return int(self._values['route_domain']) + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + if cmp_str_with_none(self.want.description, self.have.description) is None: + return cmp_str_with_none(self.want.description, self.have.folder_description) + else: + return self.want.description + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + if self.changes.description: + self.update_folder_on_device() + if not self.exists(): + raise F5ModuleError("Failed to create the partition.") + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + if self.changes.description: + self.update_folder_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the partition.") + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/partition/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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) + result = ApiParameters(params=response) + uri = "https://{0}:{1}/mgmt/tm/sys/folder/~{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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) + result.update({'folder_description': response.get('description', None)}) + return result + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/auth/partition/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + uri = "https://{0}:{1}/mgmt/tm/auth/partition/".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, 409]: + 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/auth/partition/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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 update_folder_on_device(self): + params = dict(description=self.changes.description) + uri = "https://{0}:{1}/mgmt/tm/sys/folder/~{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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/auth/partition/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.delete(uri) + if resp.status == 200: + return True + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + route_domain=dict(type='int'), + state=dict( + choices=['absent', 'present'], + default='present' + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_password_policy.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_password_policy.py new file mode 100644 index 00000000..4b884368 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_password_policy.py @@ -0,0 +1,428 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_password_policy +short_description: Manages the authentication password policy on a BIG-IP +description: + - Manages the authentication password policy on a BIG-IP device. +version_added: "1.0.0" +options: + expiration_warning: + description: + - Specifies the number of days before a password expires. + - This value determines when the BIG-IP system automatically + warns users their password is about to expire. + type: int + max_duration: + description: + - Specifies the maximum number of days a password is valid. + type: int + max_login_failures: + description: + - Specifies the number of consecutive unsuccessful login attempts + the system allows before locking out the user. + - Specify zero (0) to disable this parameter. + type: int + min_duration: + description: + - Specifies the minimum number of days a password is valid. + type: int + min_length: + description: + - Specifies the minimum number of characters in a valid password. + - This value must be between 6 and 255. + type: int + policy_enforcement: + description: + - Enables or disables the password policy on the BIG-IP system. + type: bool + required_lowercase: + description: + - Specifies the number of lowercase alpha characters that must be + present in a password for the password to be valid. + type: int + required_numeric: + description: + - Specifies the number of numeric characters that must be present in + a password for the password to be valid. + type: int + required_special: + description: + - Specifies the number of special characters that must be present in + a password for the password to be valid. + type: int + required_uppercase: + description: + - Specifies the number of uppercase alpha characters that must be + present in a password for the password to be valid. + type: int + password_memory: + description: + - Specifies whether the user has configured the BIG-IP system to + remember a password on a specific computer and how many passwords + to remember. + type: int +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Change password policy to require 2 numeric characters + bigip_password_policy: + required_numeric: 2 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +expiration_warning: + description: The new expiration warning. + returned: changed + type: int + sample: 7 +max_duration: + description: The new max duration. + returned: changed + type: int + sample: 99999 +max_login_failures: + description: The new max login failures. + returned: changed + type: int + sample: 0 +min_duration: + description: The new minimum duration. + returned: changed + type: int + sample: 0 +min_length: + description: The new minimum password length. + returned: changed + type: int + sample: 6 +policy_enforcement: + description: The new policy enforcement setting. + returned: changed + type: bool + sample: yes +required_lowercase: + description: The lowercase requirement. + returned: changed + type: int + sample: 1 +required_numeric: + description: The numeric requirement. + returned: changed + type: int + sample: 2 +required_special: + description: The special character requirement. + returned: changed + type: int + sample: 1 +required_uppercase: + description: The uppercase character requirement. + returned: changed + type: int + sample: 1 +password_memory: + description: The new number of remembered passwords + returned: changed + type: int + sample: 0 +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'expirationWarning': 'expiration_warning', + 'maxDuration': 'max_duration', + 'maxLoginFailures': 'max_login_failures', + 'minDuration': 'min_duration', + 'minimumLength': 'min_length', + 'passwordMemory': 'password_memory', + 'policyEnforcement': 'policy_enforcement', + 'requiredLowercase': 'required_lowercase', + 'requiredNumeric': 'required_numeric', + 'requiredSpecial': 'required_special', + 'requiredUppercase': 'required_uppercase', + } + + api_attributes = [ + 'expirationWarning', + 'maxDuration', + 'maxLoginFailures', + 'minDuration', + 'minimumLength', + 'passwordMemory', + 'policyEnforcement', + 'requiredLowercase', + 'requiredNumeric', + 'requiredSpecial', + 'requiredUppercase', + ] + + returnables = [ + 'expiration_warning', + 'max_duration', + 'max_login_failures', + 'min_duration', + 'min_length', + 'password_memory', + 'policy_enforcement', + 'required_lowercase', + 'required_numeric', + 'required_special', + 'required_uppercase', + ] + + updatables = [ + 'expiration_warning', + 'max_duration', + 'max_login_failures', + 'min_duration', + 'min_length', + 'password_memory', + 'policy_enforcement', + 'required_lowercase', + 'required_numeric', + 'required_special', + 'required_uppercase', + ] + + @property + def policy_enforcement(self): + return flatten_boolean(self._values['policy_enforcement']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def policy_enforcement(self): + if self._values['policy_enforcement'] is None: + return None + if self._values['policy_enforcement'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def policy_enforcement(self): + return flatten_boolean(self._values['policy_enforcement']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + return self.update() + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/auth/password-policy".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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 read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/password-policy".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + expiration_warning=dict(type='int'), + max_duration=dict(type='int'), + max_login_failures=dict(type='int'), + min_duration=dict(type='int'), + min_length=dict(type='int'), + password_memory=dict(type='int'), + policy_enforcement=dict(type='bool'), + required_lowercase=dict(type='int'), + required_numeric=dict(type='int'), + required_special=dict(type='int'), + required_uppercase=dict(type='int'), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_policy.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_policy.py new file mode 100644 index 00000000..9d968f6a --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_policy.py @@ -0,0 +1,1154 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_policy +short_description: Manage general policy configuration on a BIG-IP +description: + - Manages general policy configuration on a BIG-IP. This module is best + used in conjunction with the C(bigip_policy_rule) module. This module + can handle general configuration, like setting the draft state of the policy, + the description, and items unrelated to the policy rules themselves. + It is also the first module that should be used when creating rules, as + the C(bigip_policy_rule) module requires a policy parameter. +version_added: "1.0.0" +options: + description: + description: + - The description to attach to the policy. + - This parameter is only supported on versions of BIG-IP >= 12.1.0. On earlier + versions it is simply ignored. + type: str + name: + description: + - The name of the policy to create. + type: str + required: True + state: + description: + - When C(state) is C(present), ensures the policy exists and is + published. When C(state) is C(absent), ensures the policy is removed, + even if it is currently drafted. + - When C(state) is C(draft), ensures the policy exists and is drafted. + When modifying rules, it is required that policies first be in a draft. + - Drafting is only supported on versions of BIG-IP >= 12.1.0. On versions + prior to that, specifying a C(state) of C(draft) will raise an error. + type: str + choices: + - present + - absent + - draft + default: present + strategy: + description: + - Specifies the method to determine which actions get executed when + there are multiple rules that match. When creating new + policies, the default is C(first). + - This module does not allow you to specify the C(best) strategy to use. + It will choose the system default (C(/Common/best-match)) instead. + type: str + choices: + - first + - all + - best + rules: + description: + - Specifies a list of rules you want associated with this policy. + The order of this list is the order they will be evaluated by BIG-IP. + If the specified rules do not exist (for example when creating a new + policy) they will be created. + - The C(conditions) for a default rule are C(all). + - The C(actions) for a default rule are C(ignore). + - The C(bigip_policy_rule) module can be used to create and edit existing + and new rules. + type: list + elements: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create policy which is immediately published + bigip_policy: + name: Policy-Foo + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add a rule to the new policy - Immediately published + bigip_policy_rule: + policy: Policy-Foo + name: ABC + conditions: + - type: http_uri + path_starts_with: + - /ABC + - foo + - bar + path_ends_with: + - baz + actions: + - forward: yes + select: yes + pool: pool-svrs + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add multiple rules to the new policy - Added in the order they are specified + bigip_policy_rule: + policy: Policy-Foo + name: "{{ item.name }}" + conditions: "{{ item.conditions }}" + actions: "{{ item.actions }}" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + loop: + - name: rule1 + actions: + - type: forward + pool: pool-svrs + conditions: + - type: http_uri + path_starts_with: /euro + - name: HomePage + actions: + - type: forward + pool: pool-svrs + conditions: + - type: http_uri + path_starts_with: /HomePage/ + +- name: Create policy specify default rules - Immediately published + bigip_policy: + name: Policy-Bar + state: present + rules: + - rule1 + - rule2 + - rule3 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create policy specify default rules - Left in a draft + bigip_policy: + name: Policy-Baz + state: draft + rules: + - rule1 + - rule2 + - rule3 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +strategy: + description: The new strategy set on the policy. + returned: changed and success + type: int + sample: first-match +description: + description: + - The new description of the policy. + - This value is only returned for BIG-IP devices >= 12.1.0. + returned: changed and success + type: str + sample: This is my description +rules: + description: List of the rules, and their order, applied to the policy. + returned: changed and success + type: list + sample: ['/Common/rule1', '/Common/rule2'] +''' +import re +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +try: + from urllib import quote_plus +except ImportError: + from urllib.parse import quote_plus + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + def to_return(self): + result = {} + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + + @property + def strategy(self): + if self._values['strategy'] is None: + return None + + # Look for 'first' from Ansible or REST + elif self._values['strategy'] == 'first': + return self._get_builtin_strategy('first') + elif 'first-match' in self._values['strategy']: + return str(self._values['strategy']) + + # Look for 'all' from Ansible or REST + elif self._values['strategy'] == 'all': + return self._get_builtin_strategy('all') + elif 'all-match' in self._values['strategy']: + return str(self._values['strategy']) + + else: + # Look for 'best' from Ansible or REST + if self._values['strategy'] == 'best': + return self._get_builtin_strategy('best') + elif 'best-match' in self._values['strategy']: + return str(self._values['strategy']) + else: + # These are custom strategies. The strategy may include the + # partition, but if it does not, then we add the partition + # that is provided to the module. + return self._get_custom_strategy_name() + + def _get_builtin_strategy(self, strategy): + return '/Common/{0}-match'.format(strategy) + + def _get_custom_strategy_name(self): + strategy = self._values['strategy'] + if re.match(r'(\/[a-zA-Z_0-9.-]+){2}', strategy): + return strategy + elif re.match(r'[a-zA-Z_0-9.-]+', strategy): + return '/{0}/{1}'.format(self.partition, strategy) + else: + raise F5ModuleError( + "The provided strategy name is invalid!" + ) + + @property + def rules(self): + if self._values['rules'] is None: + return None + # In case rule values are unicode (as they may be coming from the API + result = [str(x) for x in self._values['rules']] + return result + + +class SimpleParameters(Parameters): + api_attributes = [ + 'strategy', + ] + + updatables = [ + 'strategy', + 'rules', + ] + + returnables = [ + 'strategy', + 'rules', + ] + + +class ComplexParameters(Parameters): + api_attributes = [ + 'strategy', + 'description', + ] + + updatables = [ + 'strategy', + 'description', + 'rules', + ] + + returnables = [ + 'strategy', + 'description', + 'rules', + ] + + +class SimpleChanges(SimpleParameters): + api_attributes = [ + 'strategy' + ] + + updatables = [ + 'strategy', 'rules' + ] + + returnables = [ + 'strategy', 'rules' + ] + + +class ComplexChanges(ComplexParameters): + api_attributes = [ + 'strategy', 'description' + ] + + updatables = [ + 'strategy', 'description', 'rules' + ] + + returnables = [ + 'strategy', 'description', 'rules' + ] + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = Parameters(params=self.module.params) + + def _announce_deprecations(self): + warnings = [] + if self.want: + warnings += self.want._values.get('__deprecated', []) + if self.have: + warnings += self.have._values.get('__deprecated', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _announce_warnings(self): + warnings = [] + if self.want: + warnings += self.want._values.get('__warning', []) + if self.have: + warnings += self.have._values.get('__warning', []) + for warning in warnings: + self.module.warn(warning['msg']) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def _validate_creation_parameters(self): + if self.want.strategy is None: + self.want.update(dict(strategy='first')) + + def _get_rule_names(self, rules): + if 'items' in rules: + rules['items'].sort(key=lambda x: x['ordinal']) + result = [x['name'] for x in rules['items']] + return result + else: + return [] + + def _read_rule_from_device(self, rule_name, draft=False): + if draft: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name, sub_path='Drafts'), + quote_plus(rule_name) + ) + else: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + 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) + return response['ordinal'] + + def _create_rule_on_device(self, rule_name, idx, draft=False): + params = dict(name=rule_name, ordinal=idx) + if draft: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name, sub_path='Drafts'), + ) + else: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + 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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def _modify_rule_on_device(self, rule_name, idx, draft=False): + params = dict(ordinal=idx) + if draft: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name, sub_path='Drafts'), + quote_plus(rule_name) + ) + else: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + 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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def _rule_exists_on_device(self, rule_name, draft=False): + if draft: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name, sub_path='Drafts'), + quote_plus(rule_name) + ) + else: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + 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 _remove_rule_on_device(self, rule_name, draft=False): + if draft: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name, sub_path='Drafts'), + quote_plus(rule_name) + ) + else: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + self.want.name + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def _upsert_policy_rules_on_device(self, draft=False): + rules = self.changes.rules + if rules is None: + rules = [] + for idx, rule in enumerate(rules): + if self._rule_exists_on_device(rule, draft): + ordinal = self._read_rule_from_device(rule, draft) + if int(ordinal) != idx: + self._modify_rule_on_device(rule, idx, draft) + else: + self._create_rule_on_device(rule, idx, draft) + self._remove_rule_difference(rules, draft) + + def _remove_rule_difference(self, rules, draft=False): + if not rules or not self.have.rules: + return + have_rules = set(self.have.rules) + want_rules = set(rules) + removable = have_rules.difference(want_rules) + for remove in removable: + self._remove_rule_on_device(remove, draft) + + +class SimpleManager(BaseManager): + def __init__(self, *args, **kwargs): + super(SimpleManager, self).__init__(**kwargs) + self.want = SimpleParameters(params=self.module.params) + self.have = SimpleParameters() + self.changes = SimpleChanges() + + def _set_changed_options(self): + changed = {} + for key in SimpleParameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = SimpleChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = SimpleParameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + changed[k] = change + if changed: + self.changes = SimpleChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == 'draft': + raise F5ModuleError( + "The 'draft' status is not available on BIG-IP versions < 12.1.0" + ) + if state == 'present': + changed = self.present() + elif state == 'absent': + changed = self.absent() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations() + self._announce_warnings() + send_teem(start, self.client, self.module, version) + return result + + def create(self): + self._validate_creation_parameters() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def absent(self): + changed = False + if self.exists(): + changed = self.remove() + return changed + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the policy") + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + query = "?expandSubcollections=true" + resp = self.client.api.get(uri + query) + 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) + + rules = self._get_rule_names(response['rulesReference']) + result = SimpleParameters(params=response) + result.update(dict(rules=rules)) + return result + + def update_on_device(self): + params = self.changes.api_params() + if params: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{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) + + self._upsert_policy_rules_on_device() + + def create_on_device(self): + params = self.want.api_params() + payload = dict( + name=self.want.name, + partition=self.want.partition, + **params + ) + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=payload) + 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) + + self._upsert_policy_rules_on_device() + + return True + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ComplexManager(BaseManager): + def __init__(self, *args, **kwargs): + super(ComplexManager, self).__init__(**kwargs) + self.want = ComplexParameters(params=self.module.params) + self.have = ComplexParameters() + self.changes = ComplexChanges() + + def _set_changed_options(self): + changed = {} + for key in ComplexParameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = ComplexChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = ComplexParameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + changed[k] = change + if changed: + self.changes = ComplexChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ["present", "draft"]: + changed = self.present() + elif state == "absent": + changed = self.absent() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def should_update(self): + result = self._update_changed_options() + drafted = self.draft_status_changed() + if any(x is True for x in [result, drafted]): + return True + return False + + def draft_status_changed(self): + if self.draft_exists() and self.want.state == 'draft': + drafted = False + elif not self.draft_exists() and self.want.state == 'present': + drafted = False + else: + drafted = True + return drafted + + def present(self): + if self.draft_exists() or self.policy_exists(): + return self.update() + else: + return self.create() + + def absent(self): + changed = False + if self.draft_exists() or self.policy_exists(): + changed = self.remove() + return changed + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.draft_exists() or self.policy_exists(): + raise F5ModuleError("Failed to delete the policy") + return True + + def create(self): + self._validate_creation_parameters() + + self._set_changed_options() + if self.module.check_mode: + return True + + if not self.draft_exists(): + self._create_new_policy_draft() + + # Because we always need to modify drafts, "creating on the device" + # is actually identical to just updating. + self.update_on_device() + + if self.want.state == 'draft': + return True + else: + return self.publish() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + + if not self.draft_exists(): + self._create_existing_policy_draft() + + if self._update_changed_options(): + self.update_on_device() + + if self.want.state == 'draft': + return True + else: + return self.publish() + + def draft_exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name, sub_path='Drafts') + ) + 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 policy_exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{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_existing_policy_draft(self): + params = dict(createDraft=True) + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{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) + return True + + def _create_new_policy_draft(self): + params = self.want.api_params() + payload = dict( + name=self.want.name, + partition=self.want.partition, + subPath='Drafts', + **params + ) + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=payload) + 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) + return True + + def update_on_device(self): + params = self.changes.api_params() + if params: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name, sub_path='Drafts'), + ) + 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) + + self._upsert_policy_rules_on_device(draft=True) + + def read_current_from_device(self): + if self.draft_exists(): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name, sub_path='Drafts'), + ) + else: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + query = "?expandSubcollections=true" + resp = self.client.api.get(uri + query) + 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) + + rules = self._get_rule_names(response['rulesReference']) + result = ComplexParameters(params=response) + result.update(dict(rules=rules)) + return result + + def publish(self): + params = dict( + name=fq_name(self.want.partition, + self.want.name, + sub_path='Drafts' + ), + command="publish" + + ) + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/".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) + return True + + def remove_policy_draft_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name, sub_path='Drafts'), + ) + response = self.client.api.delete(uri) + + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def remove_policy_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def remove_from_device(self): + if self.draft_exists(): + self.remove_policy_draft_from_device() + if self.policy_exists(): + self.remove_policy_from_device() + return True + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def rules(self): + if self.want.rules != self.have.rules: + return self.want.rules + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def exec_module(self): + if self.version_is_less_than_12(): + manager = self.get_manager('simple') + else: + manager = self.get_manager('complex') + return manager.exec_module() + + def get_manager(self, type): + if type == 'simple': + return SimpleManager(**self.kwargs) + elif type == 'complex': + return ComplexManager(**self.kwargs) + + def version_is_less_than_12(self): + version = tmos_version(self.client) + if Version(version) < Version('12.1.0'): + return True + else: + return False + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True + ), + description=dict(), + rules=dict( + type='list', + elements='str', + ), + strategy=dict( + choices=['first', 'all', 'best'] + ), + state=dict( + default='present', + choices=['absent', 'present', 'draft'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_policy_rule.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_policy_rule.py new file mode 100644 index 00000000..56dedf2b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_policy_rule.py @@ -0,0 +1,2725 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: bigip_policy_rule +short_description: Manage LTM policy rules on a BIG-IP +description: + - This module manages LTM policy rules on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - The name of the rule. + type: str + required: True + policy: + description: + - The name of the policy you want to associate this rule with. + type: str + required: True + replace_with: + description: + - Specifies if the C(conditions)/C(actions) given by the user should overwrite what exists on the device. + - The option is useful when a subset of C(conditions)/C(actions) needs to be removed. This option is similar to the + replace-all-with flag available in TMSH commands. + - Using this option is not idempotent. + type: bool + default: no + rule_order: + description: + - Specifies a number that indicates the order of this rule relative to other rules in the policy. + - When not set, the device sets the parameter to 0. + - If there are rules with the same rule order number, the device uses rule names + to determine how the rules are ordered. + - The lower the number, the lower the rule is in the general order, with the lowest number C(0) being the + topmost one. + - Valid range of values is between C(0) and C(4294967295) inclusive. + type: int + version_added: "1.10.0" + description: + description: + - Description of the policy rule. + type: str + actions: + description: + - The actions you want the policy rule to perform. + - The available attributes vary by the action, however, each action requires + you specify a C(type). + - These conditions can be specified in any order. Despite the fact they are in a list, + the order in the list does not matter to the BIG-IP. + type: list + elements: dict + suboptions: + type: + description: + - The action type. This value controls which of the following options are required. + - When C(type) is C(forward), the system associates a given C(pool), or C(virtual), + or C(node) with this rule. + - When C(type) is C(enable), the system associates a given C(asm_policy) with + this rule. + - When C(type) is C(ignore), the system removes all existing actions from this + rule. + - When C(type) is C(redirect), the system redirects an HTTP request to a different URL. + - When C(type) is C(reset), the system resets the connection upon C(event). + - When C(type) is C(persist), the system associates C(cookie_insert) and C(cookie_expiry) with this rule. + - When C(type) is C(set_variable), the system sets a variable based on the evaluated Tcl C(expression) based on C(event). + - When C(type) is C(remove), the system removes C(http_set_cookie), C(http_referer), C(http_header) or C(http_cookie) with this rule. + - When C(type) is C(insert), the system inserts C(http_set_cookie), C(http_referer), C(http_header) or C(http_cookie) with this rule. + - When C(type) is C(replace), the system replaces C(http_connect), C(http_referer), C(http_header), C(http_uri) or C(http_host) with this rule. + - When C(type) is C(disable), the system disables C(disable_target) with this rule. + type: str + required: true + choices: + - forward + - enable + - ignore + - redirect + - reset + - persist + - set_variable + - remove + - insert + - replace + - disable + pool: + description: + - Pool to which you want to forward traffic. + - This parameter is only valid with the C(forward) type. + type: str + virtual: + description: + - Virtual server to which you want to forward traffic. + - This parameter is only valid with the C(forward) type. + type: str + node: + description: + - Node to which you want to forward traffic. + - This parameter is only valid with the C(forward) type. + type: str + version_added: "1.2.0" + disable_target: + description: + - Target you want to disable. + - This parameter is only valid with the C(disable) type. + type: str + version_added: "1.8.0" + choices: + - server_ssl + - persist + - asm + asm_policy: + description: + - ASM policy to enable. + - This parameter is only valid with the C(enable) type. + type: str + location: + description: + - The new URL for which a redirect response is sent. + - A Tcl command substitution can be used for this field. + type: str + event: + description: + - Events on which actions, such as reset and forward, can be triggered. + - With the C(set_variable) action, it is used for specifying + an action event, such as request or response. + - "Valid event choices for C(forward) action type are: client_accepted, proxy_request + request, ssl_client_hello and ssl_client_server_hello_send." + - "Valid event choices for C(reset) acton type are: client_accepted, proxy_connect + proxy_request, proxy_response, request, response, server_connected, ssl_client_hello, + ssl_client_server_hello_send, ssl_server_handshake, ssl_server_hello, websocket_request, + websocket_response." + - "Valid event choices for C(disable) acton type are: client_accepted, proxy_connect + proxy_request, proxy_response, request, server_connected." + type: str + expression: + description: + - A Tcl expression used with the C(set_variable) action. + type: str + variable_name: + description: + - Variable name used with the C(set_variable) action. + type: str + cookie_insert: + description: + - Cookie name on which you want to persist. + - This parameter is only valid with the C(persist) type. + type: str + version_added: "1.1.0" + cookie_expiry: + description: + - Optional argument, specifying the time for which the session is persisted. + - This parameter is only valid with the C(persist) type. + type: int + version_added: "1.1.0" + http_header: + description: + - HTTP Header that you want to remove or insert. + - This parameter is only valid with the C(remove), C(insert) and C(replace) type. + type: dict + suboptions: + event: + description: + - Type of event when the C(http_header) is removed, replaced, or inserted. + - The C(request) and C(response) events are only choices with C(remove) and C(insert) type. + - All of events are valid with C(replace) type action. + type: str + required: True + choices: + - request + - response + - proxy_connect + - proxy_request + - proxy_response + name: + description: + - The name of C(http_header). + type: str + required: True + value: + description: + - The value of C(http_header). + - Mandatory parameter when configured with C(insert) or C(replace) type. + type: str + version_added: "1.8.0" + http_referer: + description: + - HTTP Referer header you want to remove, replace, or insert. + - This parameter is only valid with the C(remove), C(insert) and C(replace) type. + type: dict + suboptions: + event: + description: + - Type of event when the c(http_referer) is removed, replaced, or inserted. + required: True + type: str + choices: + - request + - proxy_connect + - proxy_request + value: + description: + - The value of C(http_referer). + - This is a mandatory parameter when configured with C(insert) type action. + - This parameter is ignored for the C(remove) type. + - This parameter is optional for the C(replace) type. + type: str + version_added: "1.8.0" + http_set_cookie: + description: + - HTTP Set-Cookie header you want to remove or insert. + - This parameter is only valid with the C(remove) or c(insert) type. + type: dict + suboptions: + name: + description: + - The name of C(http_set_cookie). + type: str + required: True + value: + description: + - The value of C(http_set_cookie). + - This is a mandatory parameter when configured with C(insert) type action. + type: str + version_added: "1.8.0" + http_cookie: + description: + - HTTP Cookie header you want to remove or insert. + - This parameter is only valid with the C(remove) and C(insert) type. + type: dict + suboptions: + event: + description: + - Type of event when the C(http_cookie) is removed or inserted. + type: str + required: True + choices: + - request + - proxy_connect + - proxy_request + name: + description: + - The name of C(http_cookie). + type: str + required: True + value: + description: + - The value of C(http_cookie). + - This is a mandatory parameter when configured with C(insert) type action. + type: str + version_added: "1.8.0" + http_connect: + description: + - HTTP Connect header you want to replace. + - This parameter is only valid with the C(replace) type. + type: dict + suboptions: + event: + description: + - Type of event when the C(http_connect) header is replaced. + required: True + type: str + choices: + - client_accepted + - proxy_connect + - proxy_request + - proxy_response + - request + - server_connected + - ssl_client_hello + value: + description: + - The value of C(http_connect). + type: str + required: True + port: + description: + - The port number. + - If a port number is not provided, the value is set to 0 by default. + - Be explicit when defining rules, so the system does not override port values. + type: int + version_added: "1.8.0" + http_host: + description: + - HTTP Host header you want to replace. + - This parameter is only valid with the C(replace) type. + type: dict + suboptions: + event: + description: + - Type of event when the C(http_host) is replaced. + type: str + required: True + choices: + - request + - proxy_connect + - proxy_request + value: + description: + - The value of C(http_host). + type: str + required: True + version_added: "1.8.0" + http_uri: + description: + - Replaces HTTP URI, path, or string. + - This parameter is only valid with the C(replace) type. + type: dict + suboptions: + event: + description: + - Type of event when the C(http_uri) is replaced. + type: str + required: True + choices: + - request + - proxy_connect + - proxy_request + type: + description: + - Specifies the part of the C(http_uri) to be replaced. + type: str + required: True + choices: + - path + - query_string + - full_string + value: + description: + - The value of C(http_uri). + type: str + required: True + version_added: "1.8.0" + conditions: + description: + - A list of attributes that describe the condition. + - See suboptions for details on how to construct each list entry. + - The ordering of this list is important, the module ensures the order is + kept when modifying the task. + - The suboption options below are not required for all condition types, + read the description for more details. + - These conditions can be specified in any order. Despite the fact they are in a list, + the order in the list does not matter to the BIG-IP. + type: list + elements: dict + suboptions: + type: + description: + - The condition type. This value controls which of the following options are required. + - "When C(type) is C(http_uri), the valid choices are: C(path_begins_with_any), C(path_contains) or + C(path_is_any)." + - "When C(type) is C(http_host), the valid choices are: C(host_is_any), C(host_is_not_any), + C(host_begins_with_any), C(host_begins_not_with_any), C(host_ends_with_any) or C(host_ends_not_with_any)" + - "When C(type) is C(http_header), the C(header_name) parameter is mandatory and the valid choice is: + C(header_is_any)." + - "When C(type) is C(http_method), the valid choices are: C(method_matches_with_any)." + - When C(type) is C(all_traffic), the system removes all existing conditions from + this rule. + type: str + required: True + choices: + - http_uri + - all_traffic + - http_host + - http_header + - http_method + - ssl_extension + - tcp + path_begins_with_any: + description: + - A list of strings of characters the HTTP URI should start with. + - This parameter is only valid with the C(http_uri) type. + type: list + elements: str + path_contains: + description: + - A list of strings of characters the HTTP URI should contain. + - This parameter is only valid with the C(http_uri) type. + type: list + elements: str + version_added: "1.8.0" + path_is_any: + description: + - A list of strings of characters the HTTP URI should match. + - This parameter is only valid with the C(http_uri) type. + type: list + elements: str + version_added: "1.8.0" + host_is_any: + description: + - A list of strings of characters the HTTP Host should match. + - This parameter is only valid with the C(http_host) type. + type: list + elements: str + host_is_not_any: + description: + - A list of strings of characters the HTTP Host should not match. + - This parameter is only valid with the C(http_host) type. + type: list + elements: str + host_begins_with_any: + description: + - A list of strings of characters the HTTP Host should start with. + - This parameter is only valid with the C(http_host) type. + type: list + elements: str + host_begins_not_with_any: + description: + - A list of strings of characters the HTTP Host should not start with. + - This parameter is only valid with the C(http_host) type. + type: list + elements: str + version_added: "1.22.0" + host_ends_not_with_any: + description: + - A list of strings of characters the HTTP Host should not end with. + - This parameter is only valid with the C(http_host) type. + type: list + elements: str + version_added: "1.22.0" + host_ends_with_any: + description: + - A list of strings of characters the HTTP Host should end with. + - This parameter is only valid with the C(http_host) type. + type: list + elements: str + version_added: "1.8.0" + header_is_any: + description: + - A list of strings of characters the HTTP Header value should match. + - This parameter is only valid with the C(http_header) type. + type: list + elements: str + version_added: "1.8.0" + header_name: + description: + - A name of C(http_header). + - This parameter is only valid with the C(http_header) type. + type: str + version_added: "1.8.0" + method_matches_with_any: + description: + - A list of strings of characters the HTTP Method value should match. + - This parameter is only valid with the C(http_method) type. + type: list + elements: str + version_added: "1.10.0" + server_name_is_any: + description: + - A list of strings of characters the SSL Extension should match. + - This parameter is only valid with the C(ssl_extension) type. + type: list + elements: str + address_matches_with_any: + description: + - A list of IP Subnet address strings the IP address should match. + - This parameter is only valid with the C(tcp) type. + type: list + elements: str + version_added: "1.8.0" + address_matches_with_datagroup: + description: + - A list of internal datagroup strings the IP address should match. + - This parameter is only valid with the C(tcp) type. + type: list + elements: str + version_added: "1.8.0" + address_matches_with_external_datagroup: + description: + - A list of external datagroup strings the IP address should match. + - This parameter is only valid with the C(tcp) type. + type: list + elements: str + version_added: "1.10.0" + event: + description: + - Events on which conditions type match rules can be triggered. + - Supported only for C(http_header), C(http_method), C(ssl_extension) and C(tcp). + - "Valid choices for C(http_header) condition types are: C(proxy_connect), + C(proxy_request), C(proxy_response), C(request) and C(response)." + - "Valid choices for C(http_method) condition types are: C(proxy_connect), + C(proxy_request), C(proxy_response), C(request) and C(response)." + - "Valid choices for C(tcp) condition types are: C(request), C(client_accepted), + C(proxy_connect), C(proxy_request), C(proxy_response), C(ssl_client_hello), and + C(ssl_client_server_hello_send)." + - "Valid choices for C(ssl_extension) are: C(ssl_client_hello), and C(ssl_client_server_hello_send)." + type: str + state: + description: + - When C(present), ensures the key is uploaded to the device. When + C(absent), ensures the key is removed from the device. If the key + is currently in use, the module will not be able to remove the key. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +requirements: + - BIG-IP >= v12.1.0 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create policies + bigip_policy: + name: Policy-Foo + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add a rule to the new policy + bigip_policy_rule: + policy: Policy-Foo + name: rule3 + conditions: + - type: http_uri + path_begins_with_any: + - /ABC + actions: + - type: forward + pool: pool-svrs + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add multiple rules to the new policy + bigip_policy_rule: + policy: Policy-Foo + name: "{{ item.name }}" + conditions: "{{ item.conditions }}" + actions: "{{ item.actions }}" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + loop: + - name: rule1 + actions: + - type: forward + pool: pool-svrs + conditions: + - type: http_uri + path_begins_with_any: + - /euro + - name: rule2 + actions: + - type: forward + pool: pool-svrs + conditions: + - type: http_uri + path_begins_with_any: + - /HomePage/ + - name: rule3 + actions: + - type: set_variable + variable_name: user-agent + expression: tcl:[HTTP::header User-Agent] + event: request + conditions: + - type: http_uri + path_begins_with_any: + - /HomePage/ + +- name: Remove all rules and conditions from the rule + bigip_policy_rule: + policy: Policy-Foo + name: rule1 + conditions: + - type: all_traffic + actions: + - type: ignore + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +actions: + description: The new list of actions applied to the rule. + returned: changed + type: complex + contains: + type: + description: The action type. + returned: changed + type: str + sample: forward + pool: + description: Pool for forwarding to. + returned: changed + type: str + sample: foo-pool + sample: hash/dictionary of values +conditions: + description: The new list of conditions applied to the rule. + returned: changed + type: complex + contains: + type: + description: The condition type. + returned: changed + type: str + sample: http_uri + path_begins_with_any: + description: List of strings the URI begins with. + returned: changed + type: list + sample: [foo, bar] + sample: hash/dictionary of values +description: + description: The new description of the rule. + returned: changed + type: str + sample: My rule +rule_order: + description: Specifies a number that indicates the order of this rule relative to other rules in the policy. + returned: changed + type: int + sample: 10 +''' + +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import compare_complex_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'actionsReference': 'actions', + 'conditionsReference': 'conditions', + 'ordinal': 'rule_order', + } + api_attributes = [ + 'description', + 'actions', + 'conditions', + 'ordinal', + ] + + updatables = [ + 'actions', + 'conditions', + 'description', + 'rule_order', + ] + + returnables = [ + 'description', + 'action', + 'conditions', + 'rule_order', + ] + + @property + def name(self): + return self._values.get('name', None) + + @property + def description(self): + return self._values.get('description', None) + + @property + def policy(self): + if self._values['policy'] is None: + return None + return self._values['policy'] + + +class ApiParameters(Parameters): + def _remove_internal_keywords(self, resource): + items = [ + 'kind', 'generation', 'selfLink', 'poolReference', 'offset', 'datagroupReference' + ] + for item in items: + try: + del resource[item] + except KeyError: + pass + + @property + def actions(self): + result = [] + if self._values['actions'] is None or 'items' not in self._values['actions']: + return [dict(type='ignore')] + for item in self._values['actions']['items']: + action = dict() + self._remove_internal_keywords(item) + if 'forward' in item: + action.update(item) + action['type'] = 'forward' + del action['forward'] + elif 'enable' in item: + action.update(item) + action['type'] = 'enable' + del action['enable'] + elif 'disable' in item: + action.update(item) + action['type'] = 'disable' + del action['disable'] + elif 'redirect' in item: + action.update(item) + action['type'] = 'redirect' + del action['redirect'] + elif 'setVariable' in item: + action.update(item) + action['type'] = 'set_variable' + del action['fullPath'] + del action['code'] + del action['expirySecs'] + del action['length'] + del action['port'] + del action['status'] + del action['vlanId'] + del action['timeout'] + elif 'shutdown' in item: + action.update(item) + action['type'] = 'reset' + del action['shutdown'] + elif 'persist' in item: + action.update(item) + action['type'] = 'persist' + del action['persist'] + elif 'remove' in item: + action.update(item) + action['type'] = 'remove' + action.pop('fullPath', None) + action.pop('code', None) + action.pop('expirySecs', None) + action.pop('length', None) + action.pop('port', None) + action.pop('status', None) + action.pop('vlanId', None) + action.pop('timeout', None) + action.pop('offset', None) + del action['remove'] + elif 'insert' in item: + action.update(item) + action['type'] = 'insert' + action.pop('fullPath', None) + action.pop('code', None) + action.pop('expirySecs', None) + action.pop('length', None) + action.pop('port', None) + action.pop('status', None) + action.pop('vlanId', None) + action.pop('timeout', None) + action.pop('offset', None) + del action['insert'] + elif 'replace' in item: + action.update(item) + action['type'] = 'replace' + action.pop('fullPath', None) + action.pop('code', None) + action.pop('expirySecs', None) + action.pop('length', None) + action.pop('status', None) + action.pop('vlanId', None) + action.pop('timeout', None) + action.pop('offset', None) + if 'httpConnect' not in action: + action.pop('port', None) + del action['replace'] + result.append(action) + result = sorted(result, key=lambda x: x['name']) + return result + + @property + def conditions(self): + result = [] + if self._values['conditions'] is None or 'items' not in self._values['conditions']: + return [dict(type='all_traffic')] + for item in self._values['conditions']['items']: + action = dict() + self._remove_internal_keywords(item) + if 'httpUri' in item: + action.update(item) + action['type'] = 'http_uri' + del action['httpUri'] + + # Converts to common stringiness + # + # The tuple set "issubset" check that happens in the Difference + # engine does not recognize that a u'foo' and 'foo' are equal "enough" + # to consider them a subset. Therefore, we cast everything here to + # whatever the common stringiness is. + if 'values' in action: + action['values'] = [str(x) for x in action['values']] + elif 'httpHost' in item: + action.update(item) + action['type'] = 'http_host' + if 'values' in action: + action['values'] = [str(x) for x in action['values']] + del action['httpHost'] + elif 'httpMethod' in item: + action.update(item) + action['type'] = 'http_method' + if 'values' in action: + action['values'] = [str(x) for x in action['values']] + del action['httpMethod'] + elif 'httpHeader' in item: + action.update(item) + action['type'] = 'http_header' + if 'values' in action: + action['values'] = [str(x) for x in action['values']] + del action['httpHeader'] + elif 'sslExtension' in item: + action.update(item) + action['type'] = 'ssl_extension' + if 'values' in action: + action['values'] = [str(x) for x in action['values']] + del action['sslExtension'] + elif 'tcp' in item: + action.update(item) + action['type'] = 'tcp' + if 'values' in action: + action['values'] = [str(x) for x in action['values']] + result.append(action) + # Names contains the index in which the rule is at. + result = sorted(result, key=lambda x: x['name']) + return result + + +class ModuleParameters(Parameters): + @property + def rule_order(self): + if self._values['rule_order'] is None: + return None + if 0 < self._values['rule_order'] > 4294967295: + raise F5ModuleError( + "Specified number is out of valid range, correct range is between 0 and 4294967295." + ) + return self._values['rule_order'] + + @property + def actions(self): + result = [] + if self._values['actions'] is None: + return None + for idx, item in enumerate(self._values['actions']): + action = dict() + if 'name' in item: + action['name'] = str(item['name']) + else: + action['name'] = str(idx) + if item['type'] == 'forward': + self._handle_forward_action(action, item) + elif item['type'] == 'set_variable': + self._handle_set_variable_action(action, item) + elif item['type'] == 'enable': + self._handle_enable_action(action, item) + if item['type'] == 'disable': + self._handle_disable_action(action, item) + elif item['type'] == 'ignore': + return [dict(type='ignore')] + elif item['type'] == 'redirect': + self._handle_redirect_action(action, item) + elif item['type'] == 'reset': + self._handle_reset_action(action, item) + del action['shutdown'] + elif item['type'] == 'persist': + self._handle_persist_action(action, item) + elif item['type'] == 'remove': + self._handle_remove_action(action, item) + elif item['type'] == 'insert': + self._handle_insert_action(action, item) + elif item['type'] == 'replace': + self._handle_replace_action(action, item) + result.append(action) + result = sorted(result, key=lambda x: x['name']) + return result + + @property + def conditions(self): + result = [] + if self._values['conditions'] is None: + return None + for idx, item in enumerate(self._values['conditions']): + action = dict() + if 'name' in item: + action['name'] = str(item['name']) + else: + action['name'] = str(idx) + if item['type'] == 'http_uri': + self._handle_http_uri_condition(action, item) + elif item['type'] == 'http_method': + self._handle_http_method_condition(action, item) + elif item['type'] == 'http_host': + self._handle_http_host_condition(action, item) + elif item['type'] == 'http_header': + self._handle_http_header_condition(action, item) + elif item['type'] == 'ssl_extension': + self._handle_ssl_extension_condition(action, item) + elif item['type'] == 'tcp': + self._handle_tcp_condition(action, item) + elif item['type'] == 'all_traffic': + return [dict(type='all_traffic')] + result.append(action) + result = sorted(result, key=lambda x: x['name']) + return result + + def _handle_http_host_condition(self, action, item): + options = [ + 'host_begins_with_any', 'host_begins_not_with_any', 'host_ends_with_any', + 'host_ends_not_with_any', 'host_is_any', 'host_is_not_any' + ] + action['type'] = 'http_host' + + if not any(x for x in options if x in item): + raise F5ModuleError( + "A 'host_begins_with_any', 'host_begins_not_with_any', 'host_ends_with_any', 'host_ends_not_with_any'," + "'host_is_any', or 'host_is_not_any' must be specified when the 'http_uri' type is used." + ) + + if 'host_begins_with_any' in item and item['host_begins_with_any'] is not None: + if isinstance(item['host_begins_with_any'], list): + values = item['host_begins_with_any'] + else: + values = [item['host_begins_with_any']] + action.update(dict( + host=True, + startsWith=True, + values=values + )) + elif 'host_begins_not_with_any' in item and item['host_begins_not_with_any'] is not None: + if isinstance(item['host_begins_not_with_any'], list): + values = item['host_begins_not_with_any'] + else: + values = [item['host_begins_not_with_any']] + action.update({ + 'host': True, + 'startsWith': True, + 'not': True, + 'values': values + }) + elif 'host_ends_not_with_any' in item and item['host_ends_not_with_any'] is not None: + if isinstance(item['host_ends_not_with_any'], list): + values = item['host_ends_not_with_any'] + else: + values = [item['host_ends_not_with_any']] + action.update({ + 'host': True, + 'endsWith': True, + 'not': True, + 'values': values + }) + elif 'host_ends_with_any' in item and item['host_ends_with_any'] is not None: + if isinstance(item['host_ends_with_any'], list): + values = item['host_ends_with_any'] + else: + values = [item['host_ends_with_any']] + action.update(dict( + host=True, + endsWith=True, + values=values + )) + elif 'host_is_any' in item and item['host_is_any'] is not None: + if isinstance(item['host_is_any'], list): + values = item['host_is_any'] + else: + values = [item['host_is_any']] + action.update(dict( + equals=True, + host=True, + values=values + )) + elif 'host_is_not_any' in item and item['host_is_not_any'] is not None: + if isinstance(item['host_is_not_any'], list): + values = item['host_is_not_any'] + else: + values = [item['host_is_not_any']] + action.update({ + 'equals': True, + 'host': True, + 'not': True, + 'values': values + }) + + def _handle_http_method_condition(self, action, item): + options = ['method_matches_with_any'] + action['type'] = 'http_method' + event_map = dict( + proxy_connect='proxyConnect', + proxy_request='proxyRequest', + proxy_response='proxyResponse', + request='request', + response='response', + ) + + if not any(x for x in options if x in item): + raise F5ModuleError( + "A 'method_matches_with_any' must be specified when the 'http_method' type is used." + ) + + if 'event' in item and item['event'] is not None: + event = event_map.get(item['event'], None) + if event: + action[event] = True + + if 'method_matches_with_any' in item and item['method_matches_with_any'] is not None: + if isinstance(item['method_matches_with_any'], list): + values = item['method_matches_with_any'] + else: + values = [item['method_matches_with_any']] + action.update(dict( + startsWith=True, + values=values + )) + + def _handle_http_uri_condition(self, action, item): + action['type'] = 'http_uri' + options = ['path_begins_with_any', 'path_contains', 'path_is_any'] + + if all(k not in item for k in options): + raise F5ModuleError( + "A 'path_begins_with_any', 'path_contains' or 'path_is_any' must be specified " + "when the 'http_uri' type is used." + ) + + if 'path_begins_with_any' in item and item['path_begins_with_any'] is not None: + if isinstance(item['path_begins_with_any'], list): + values = item['path_begins_with_any'] + else: + values = [item['path_begins_with_any']] + action.update(dict( + path=True, + startsWith=True, + values=values + )) + elif 'path_contains' in item and item['path_contains'] is not None: + if isinstance(item['path_contains'], list): + values = item['path_contains'] + else: + values = [item['path_contains']] + action.update(dict( + path=True, + contains=True, + values=values + )) + elif 'path_is_any' in item and item['path_is_any'] is not None: + if isinstance(item['path_is_any'], list): + values = item['path_is_any'] + else: + values = [item['path_is_any']] + action.update(dict( + path=True, + equals=True, + values=values + )) + + def _handle_tcp_condition(self, action, item): + options = [ + 'address_matches_with_any', 'address_matches_with_datagroup', 'address_matches_with_external_datagroup' + ] + event_map = dict( + client_accepted='clientAccepted', + proxy_connect='proxyConnect', + proxy_request='proxyRequest', + proxy_response='proxyResponse', + request='request', + ssl_client_hello='sslClientHello', + ssl_client_server_hello_send='sslClientServerhelloSend' + ) + action['type'] = 'tcp' + if all(k not in item for k in options): + raise F5ModuleError( + "A 'address_matches_with_any','address_matches_with_datagroup' or" + "'address_matches_with_external_datagroup' must be specified when the 'tcp' type is used." + ) + if 'address_matches_with_any' in item and item['address_matches_with_any'] is not None: + if isinstance(item['address_matches_with_any'], list): + values = item['address_matches_with_any'] + else: + values = [item['address_matches_with_any']] + action.update(dict( + address=True, + matches=True, + values=values + )) + if 'address_matches_with_datagroup' in item and item['address_matches_with_datagroup'] is not None: + if isinstance(item['address_matches_with_datagroup'], list): + values = item['address_matches_with_datagroup'] + else: + values = [item['address_matches_with_datagroup']] + for value in values: + action.update(dict( + address=True, + matches=True, + datagroup=fq_name(self.partition, value) + ) + ) + if 'address_matches_with_external_datagroup' in item and \ + item['address_matches_with_external_datagroup'] is not None: + if isinstance(item['address_matches_with_external_datagroup'], list): + values = item['address_matches_with_external_datagroup'] + else: + values = [item['address_matches_with_external_datagroup']] + for value in values: + action.update(dict( + address=True, + matches=True, + datagroup=fq_name(self.partition, value) + ) + ) + if 'event' in item and item['event'] is not None: + event = event_map.get(item['event'], None) + if event: + action[event] = True + + def _handle_ssl_extension_condition(self, action, item): + action['type'] = 'ssl_extension' + if 'server_name_is_any' in item: + if isinstance(item['server_name_is_any'], list): + values = item['server_name_is_any'] + else: + values = [item['server_name_is_any']] + action.update(dict( + equals=True, + serverName=True, + values=values + )) + if 'event' not in item: + raise F5ModuleError( + "An 'event' must be specified when the 'ssl_extension' condition is used." + ) + elif 'ssl_client_hello' in item['event']: + action.update(dict( + sslClientHello=True + )) + elif 'ssl_server_hello' in item['event']: + action.update(dict( + sslServerHello=True + )) + + def _handle_http_header_condition(self, action, item): + action['type'] = 'http_header' + options = ['header_is_any'] + event_map = dict( + proxy_connect='proxyConnect', + proxy_request='proxyRequest', + proxy_response='proxyResponse', + request='request', + response='response', + ) + if 'header_name' not in item: + raise F5ModuleError( + "An 'header_name' must be specified when the 'http_header' condition is used." + ) + if not any(x for x in options if x in item): + raise F5ModuleError( + "A 'header_is_any' must be specified when the 'http_header' type is used." + ) + if 'event' in item and item['event'] is not None: + event = event_map.get(item['event'], None) + if event: + action[event] = True + + if 'header_is_any' in item: + if isinstance(item['header_is_any'], list): + values = item['header_is_any'] + else: + values = [item['header_is_any']] + + action.update(dict( + equals=True, + tmName=item['header_name'], + values=values + )) + + def _handle_forward_action(self, action, item): + """Handle the nuances of the forwarding type + + Right now there is only a single type of forwarding that can be done. As that + functionality expands, so-to will the behavior of this, and other, methods. + Therefore, do not be surprised that the logic here is so rigid. It's deliberate. + + :param action: + :param item: + :return: + """ + + event_map = dict( + client_accepted='clientAccepted', + proxy_request='proxyRequest', + request='request', + ssl_client_hello='sslClientHello', + ssl_client_server_hello_send='sslClientServerhelloSend' + ) + + action['type'] = 'forward' + options = ['pool', 'virtual', 'node'] + if not any(x for x in options if x in item): + raise F5ModuleError( + "A 'pool' or 'virtual' or 'node' must be specified when the 'forward' type is used." + ) + if item.get('pool', None): + action['pool'] = fq_name(self.partition, item['pool']) + elif item.get('virtual', None): + action['virtual'] = fq_name(self.partition, item['virtual']) + elif item.get('node', None): + action['node'] = item['node'] + + if 'event' in item and item['event'] is not None: + event = event_map.get(item['event'], None) + if event: + action[event] = True + + def _handle_set_variable_action(self, action, item): + """Handle the nuances of the set_variable type + + :param action: + :param item: + :return: + """ + if 'expression' not in item and 'variable_name' not in item: + raise F5ModuleError( + "A 'variable_name' and 'expression' must be specified when the 'set_variable' type is used." + ) + + if 'event' in item and item['event'] is not None: + action[item['event']] = True + else: + action['request'] = True + action.update(dict( + type='set_variable', + expression=item['expression'], + tmName=item['variable_name'], + setVariable=True, + tcl=True + )) + + def _handle_enable_action(self, action, item): + """Handle the nuances of the enable type + + :param action: + :param item: + :return: + """ + action['type'] = 'enable' + if 'asm_policy' not in item: + raise F5ModuleError( + "An 'asm_policy' must be specified when the 'enable' type is used." + ) + action.update(dict( + policy=fq_name(self.partition, item['asm_policy']), + asm=True + )) + + def _handle_disable_action(self, action, item): + """Handle the nuances of the disable type + + :param action: + :param item: + :return: + """ + + target_map = dict( + server_ssl='serverSsl', + persist='persist', + asm='asm' + ) + event_map = dict( + client_accepted='clientAccepted', + proxy_connect='proxyConnect', + proxy_request='proxyRequest', + proxy_response='proxyResponse', + request='request', + server_connected='serverConnected', + ) + + action['type'] = 'disable' + if 'disable_target' not in item: + raise F5ModuleError( + "An 'disable_target' must be specified when the 'enable' type is used." + ) + if 'event' in item and item['event'] is not None: + event = event_map.get(item['event'], None) + if event: + action[event] = True + + action[target_map[item['disable_target']]] = True + + def _handle_redirect_action(self, action, item): + """Handle the nuances of the redirect type + + :param action: + :param item: + :return: + """ + action['type'] = 'redirect' + if 'location' not in item: + raise F5ModuleError( + "A 'location' must be specified when the 'redirect' type is used." + ) + action.update( + location=item['location'], + httpReply=True, + ) + + def _handle_reset_action(self, action, item): + """Handle the nuances of the reset type + + :param action: + :param item: + :return: + """ + event_map = dict( + client_accepted='clientAccepted', + proxy_connect='proxyConnect', + proxy_request='proxyRequest', + proxy_response='proxyResponse', + request='request', + response='response', + server_connected='serverConnected', + ssl_client_hello='sslClientHello', + ssl_client_server_hello_send='sslClientServerhelloSend', + ssl_server_handshake='sslServerHandshake', + ssl_server_hello='sslServerHello', + websocket_request='wsRequest', + websocket_response='wsResponse' + ) + + action['type'] = 'reset' + if 'event' not in item: + raise F5ModuleError( + "An 'event' must be specified when the 'reset' type is used." + ) + event = event_map.get(item['event'], None) + if not event: + raise F5ModuleError( + "Invalid event type specified for reset action: {0}," + "check module documentation for valid event types.".format(item['event']) + ) + + action[event] = True + action.update({ + 'connection': True, + 'shutdown': True + }) + + def _handle_persist_action(self, action, item): + """Handle the nuances of the persist type + + :param action: + :param item: + :return: + """ + action['type'] = 'persist' + if 'cookie_insert' not in item: + raise F5ModuleError( + "A 'cookie_insert' must be specified when the 'persist' type is used." + ) + elif 'cookie_expiry' in item: + action.update( + cookieInsert=True, + tmName=item['cookie_insert'], + expiry=str(item['cookie_expiry']) + ) + else: + action.update( + cookieInsert=True, + tmName=item['cookie_insert'] + ) + + def _handle_remove_action(self, action, item): + """Handle the nuances of the remove type + + :param action: + :param item: + :return: + """ + + action['type'] = 'remove' + options = ['http_header', 'http_referer', 'http_set_cookie', 'http_cookie'] + if not any(x for x in options if x in item): + raise F5ModuleError( + "A 'http_header', 'http_referer', 'http_set_cookie' or 'http_cookie' must be specified when " + "the 'remove' type is used." + ) + if 'http_header' in item and item['http_header']: + if item['http_header']['event'] == 'request': + action.update( + httpHeader=True, + tmName=item['http_header']['name'], + request=True + ) + elif item['http_header']['event'] == 'response': + action.update( + httpHeader=True, + tmName=item['http_header']['name'], + response=True + ) + else: + action.update( + httpHeader=True, + tmName=item['http_header']['name'] + ) + if 'http_referer' in item and item['http_referer']: + if item['http_referer']['event'] == 'request': + action.update( + httpReferer=True, + request=True + ) + if item['http_referer']['event'] == 'proxy_connect': + action.update( + httpReferer=True, + proxyConnect=True + ) + if item['http_referer']['event'] == 'proxy_request': + action.update( + httpReferer=True, + proxyRequest=True + ) + if 'http_cookie' in item and item['http_cookie']: + if item['http_cookie']['event'] == 'request': + action.update( + httpCookie=True, + tmName=item['http_cookie']['name'], + request=True + ) + elif item['http_cookie']['event'] == 'proxy_connect': + action.update( + httpCookie=True, + tmName=item['http_cookie']['name'], + proxyConnect=True + ) + elif item['http_cookie']['event'] == 'proxy_request': + action.update( + httpCookie=True, + tmName=item['http_cookie']['name'], + proxyRequest=True + ) + else: + action.update( + httpCookie=True, + tmName=item['http_cookie']['name'] + ) + if 'http_set_cookie' in item and item['http_set_cookie']: + action.update( + httpSetCookie=True, + tmName=item['http_set_cookie']['name'], + response=True + ) + + def _handle_insert_action(self, action, item): + """Handle the nuances of the insert type + + :param action: + :param item: + :return: + """ + + action['type'] = 'insert' + options = ['http_header', 'http_referer', 'http_set_cookie', 'http_cookie'] + if not any(x for x in options if x in item): + raise F5ModuleError( + "A 'http_header', 'http_referer', 'http_set_cookie' or 'http_cookie' must be specified when " + "the 'insert' type is used." + ) + + if 'http_header' in item and item['http_header']: + if item['http_header']['value'] is None: + raise F5ModuleError( + "The http_header value key is required when action is of type 'insert'." + ) + if item['http_header']['event'] == 'request': + action.update( + httpHeader=True, + tmName=item['http_header']['name'], + value=item['http_header']['value'], + request=True + ) + elif item['http_header']['event'] == 'response': + action.update( + httpHeader=True, + tmName=item['http_header']['name'], + value=item['http_header']['value'], + response=True + ) + else: + action.update( + httpHeader=True, + tmName=item['http_header']['name'], + value=item['http_header']['value'] + ) + if 'http_referer' in item and item['http_referer']: + if item['http_referer']['value'] is None: + raise F5ModuleError( + "The http_referer value key is required when action is of type 'insert'." + ) + if item['http_referer']['event'] == 'request': + action.update( + httpReferer=True, + value=item['http_referer']['value'], + request=True + ) + if item['http_referer']['event'] == 'proxy_connect': + action.update( + httpReferer=True, + value=item['http_referer']['value'], + proxyConnect=True + ) + if item['http_referer']['event'] == 'proxy_request': + action.update( + httpReferer=True, + value=item['http_referer']['value'], + proxyRequest=True + ) + if 'http_cookie' in item and item['http_cookie']: + if item['http_cookie']['value'] is None: + raise F5ModuleError( + "The http_cookie value key is required when action is of type 'insert'." + ) + if item['http_cookie']['event'] == 'request': + action.update( + httpCookie=True, + tmName=item['http_cookie']['name'], + value=item['http_cookie']['value'], + request=True + ) + elif item['http_cookie']['event'] == 'proxy_connect': + action.update( + httpCookie=True, + tmName=item['http_cookie']['name'], + value=item['http_cookie']['value'], + proxyConnect=True + ) + elif item['http_cookie']['event'] == 'proxy_request': + action.update( + httpCookie=True, + tmName=item['http_cookie']['name'], + value=item['http_cookie']['value'], + proxyRequest=True + ) + else: + action.update( + httpCookie=True, + tmName=item['http_cookie']['name'], + value=item['http_cookie']['value'] + ) + if 'http_set_cookie' in item and item['http_set_cookie']: + if item['http_set_cookie']['value'] is None: + raise F5ModuleError( + "The http_set_cookie value key is required when action is of type 'insert'." + ) + action.update( + httpSetCookie=True, + tmName=item['http_set_cookie']['name'], + value=item['http_set_cookie']['value'], + response=True + ) + + def _handle_replace_action(self, action, item): + """Handle the nuances of the replace type + + :param action: + :param item: + :return: + """ + + action['type'] = 'replace' + options = ['http_header', 'http_referer', 'http_host', 'http_connect', 'http_uri'] + if not any(x for x in options if x in item): + raise F5ModuleError( + "A 'http_header', 'http_referer', 'http_host', 'http_connect' or 'http_uri' must be specified when " + "the 'replace' type is used." + ) + event_map = dict( + client_accepted='clientAccepted', + proxy_connect='proxyConnect', + proxy_request='proxyRequest', + proxy_response='proxyResponse', + request='request', + response='response', + server_connected='serverConnected', + ssl_client_hello='sslClientHello' + ) + type_map = dict( + path='path', + query_string='queryString', + full_string='value' + ) + if 'http_header' in item and item['http_header']: + if item['http_header']['value'] is None: + raise F5ModuleError( + "The http_header value key is required when action is of type 'replace'." + ) + if item['http_header']['event'] is not None: + action.update({ + 'httpHeader': True, + 'tmName': item['http_header']['name'], + 'value': item['http_header']['value'], + event_map[item['http_header']['event']]: True + }) + else: + action.update({ + 'httpHeader': True, + 'tmName': item['http_header']['name'], + 'value': item['http_header']['value'] + }) + if 'http_referer' in item and item['http_referer']: + if item['http_referer']['value'] is not None: + action.update({ + 'httpReferer': True, + 'value': item['http_referer']['value'], + event_map[item['http_referer']['event']]: True + }) + else: + action.update({ + 'httpReferer': True, + event_map[item['http_referer']['event']]: True + }) + if 'http_connect' in item and item['http_connect']: + if item['http_connect']['port'] is None: + action.update({ + 'httpConnect': True, + 'host': item['http_connect']['value'], + 'port': 0, + event_map[item['http_connect']['event']]: True + }) + else: + action.update({ + 'httpConnect': True, + 'host': item['http_connect']['value'], + 'port': item['http_connect']['port'], + event_map[item['http_connect']['event']]: True + }) + if 'http_uri' in item and item['http_uri']: + if item['http_uri']['event'] is not None: + action.update({ + 'httpUri': True, + type_map[item['http_uri']['type']]: item['http_uri']['value'], + event_map[item['http_uri']['event']]: True + }) + else: + action.update({ + 'httpUri': True, + type_map[item['http_uri']['type']]: item['http_uri']['value'], + }) + if 'http_host' in item and item['http_host']: + if item['http_host']['event'] is not None: + action.update({ + 'httpHost': True, + 'value': item['http_host']['value'], + event_map[item['http_host']['event']]: True + }) + else: + action.update({ + 'httpHost': True, + 'value': item['http_host']['value'], + }) + + +class Changes(Parameters): + def to_return(self): + result = {} + for returnable in self.returnables: + try: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ReportableChanges(Changes): + event_map = dict( + clientAccepted='client_accepted', + proxyConnect='proxy_connect', + proxyRequest='proxy_request', + proxyResponse='proxy_response', + request='request', + response='response', + serverConnected='server_connected', + sslClientHello='ssl_client_hello' + ) + uri_type_map = dict( + path='path', + queryString='query_string', + value='full_string' + ) + + returnables = [ + 'description', 'actions', 'conditions' + ] + + def _map_value(self, item, t=False): + if not t: + for k in self.event_map.keys(): + if k in item: + return k + else: + for k in self.uri_type_map.keys(): + if k in item: + return k + return None + + @property + def actions(self): + result = [] + if self._values['actions'] is None: + return [dict(type='ignore')] + for item in self._values['actions']: + action = dict() + if 'forward' in item: + action.update(item) + action['type'] = 'forward' + del action['forward'] + elif 'replace' in item: + action.update(item) + action['type'] = 'replace' + if 'httpHeader' in item: + event = self._map_value(item) + if event: + http_header = dict(event=self.event_map[event], name=action['tmName'], value=action['value']) + action['http_header'] = http_header + del action[event] + else: + http_header = dict(name=action['tmName'], value=action['value']) + action['http_header'] = http_header + del action['httpHeader'] + del action['value'] + del action['tmName'] + if 'httpReferer' in item: + event = self._map_value(item) + if event: + if 'value' in item: + http_ref = dict(event=self.event_map[event], value=action['value']) + action['http_referer'] = http_ref + del action['value'] + else: + http_ref = dict(event=self.event_map[event]) + action['http_referer'] = http_ref + del action[event] + else: + if 'value' in item: + http_ref = dict(event=self.event_map[event], value=action['value']) + action['http_referer'] = http_ref + del action['value'] + del action['httpReferer'] + if 'httpConnect' in item: + event = self._map_value(item) + if event: + if 'value' in item and 'port' in item: + http_con = dict(event=self.event_map[event], value=action['value'], port=action['item']) + action['http_connect'] = http_con + del action['value'] + del action['port'] + elif 'value' in item and 'port' not in item: + http_con = dict(event=self.event_map[event], value=action['value']) + action['http_connect'] = http_con + del action['value'] + elif 'value' not in item and 'port' in item: + http_con = dict(event=self.event_map[event], port=action['port']) + action['http_connect'] = http_con + del action['port'] + else: + http_con = dict(event=self.event_map[event]) + action['http_connect'] = http_con + del action[event] + else: + if 'value' in item and 'port' in item: + http_con = dict(value=action['value'], port=action['item']) + action['http_connect'] = http_con + del action['value'] + del action['port'] + elif 'value' in item and 'port' not in item: + http_con = dict(value=action['value']) + action['http_connect'] = http_con + del action['value'] + elif 'value' not in item and 'port' in item: + http_con = dict(port=action['port']) + action['http_connect'] = http_con + del action['port'] + del action['httpConnect'] + if 'httpUri' in item: + event = self._map_value(item) + kind = self._map_value(item, True) + if event: + http_uri = dict(event=self.event_map[event], type=self.uri_type_map[kind], value=action[kind]) + action['http_uri'] = http_uri + del action[event] + else: + http_uri = dict(type=self.uri_type_map[kind], value=action[kind]) + action['http_uri'] = http_uri + del action[kind] + del action['httpUri'] + if 'httpHost' in item: + event = self._map_value(item) + if event: + http_uri = dict(event=self.event_map[event], value=action['value']) + action['http_uri'] = http_uri + del action[event] + else: + http_uri = dict(value=action['value']) + action['http_uri'] = http_uri + del action['value'] + del action['httpHost'] + elif 'insert' in item: + action.update(item) + action['type'] = 'insert' + if 'httpHeader' in item: + if 'response' in item: + http_header = dict(event='response', name=action['tmName'], value=action['value']) + action['http_header'] = http_header + del action['response'] + if 'request' in item: + http_header = dict(event='request', name=action['tmName'], value=action['value']) + action['http_header'] = http_header + del action['request'] + del action['httpHeader'] + del action['tmName'] + if 'httpReferer' in item: + if 'request' in item: + http_ref = dict(event='request', value=action['value']) + action['http_referer'] = http_ref + del action['request'] + if 'proxyConnect' in item: + http_ref = dict(event='proxy_connect', value=action['value']) + action['http_referer'] = http_ref + del action['proxyConnect'] + if 'proxyRequest' in item: + http_ref = dict(event='proxy_request', value=action['value']) + action['http_referer'] = http_ref + del action['proxyRequest'] + del action['httpReferer'] + if 'httpCookie' in item: + if 'request' in item: + http_cookie = dict(event='request', name=action['tmName'], value=action['value']) + action['http_cookie'] = http_cookie + del action['request'] + if 'proxyConnect' in item: + http_cookie = dict(event='proxy_connect', name=action['tmName'], value=action['value']) + action['http_cookie'] = http_cookie + del action['proxyConnect'] + if 'proxyRequest' in item: + http_cookie = dict(event='proxy_request', name=action['tmName'], value=action['value']) + action['http_cookie'] = http_cookie + del action['proxyRequest'] + del action['httpCookie'] + del action['tmName'] + if 'httpSetCookie' in item: + http_set_cookie = dict(name=action['tmName'], value=action['value']) + action['http_set_cookie'] = http_set_cookie + del action['response'] + del action['value'] + del action['tmName'] + del action['insert'] + del action['value'] + elif 'remove' in item: + action.update(item) + action['type'] = 'remove' + if 'httpHeader' in item: + if 'response' in item: + http_header = dict(event='response', name=action['tmName']) + action['http_header'] = http_header + del action['response'] + if 'request' in item: + http_header = dict(event='request', name=action['tmName']) + action['http_header'] = http_header + del action['request'] + del action['httpHeader'] + del action['tmName'] + if 'httpReferer' in item: + if 'request' in item: + http_ref = dict(event='request') + action['http_referer'] = http_ref + del action['request'] + if 'proxyConnect' in item: + http_ref = dict(event='proxy_connect') + action['http_referer'] = http_ref + del action['proxyConnect'] + if 'proxyRequest' in item: + http_ref = dict(event='proxy_request') + action['http_referer'] = http_ref + del action['proxyRequest'] + del action['httpReferer'] + if 'httpCookie' in item: + if 'request' in item: + http_cookie = dict(event='request', name=action['tmName']) + action['http_cookie'] = http_cookie + del action['request'] + if 'proxyConnect' in item: + http_cookie = dict(event='proxy_connect', name=action['tmName']) + action['http_cookie'] = http_cookie + del action['proxyConnect'] + if 'proxyRequest' in item: + http_cookie = dict(event='proxy_request', name=action['tmName']) + action['http_cookie'] = http_cookie + del action['proxyRequest'] + del action['httpCookie'] + del action['tmName'] + if 'httpSetCookie' in item: + action['http_set_cookie'] = dict(name=action['tmName']) + del action['response'] + del action['tmName'] + del action['remove'] + elif 'set_variable' in item: + action.update(item) + action['type'] = 'set_variable' + del action['set_variable'] + elif 'enable' in item: + action.update(item) + action['type'] = 'enable' + del action['enable'] + elif 'disable' in item: + action.update(item) + action['type'] = 'disable' + if 'serverSsl' in action and action['serverSsl']: + action['disable_target'] = 'server_ssl' + del action['serverSsl'] + if 'persist' in action and action['persist']: + action['disable_target'] = 'persist' + del action['persist'] + if 'asm' in action and action['asm']: + action['disable_target'] = 'asm' + del action['asm'] + del action['enable'] + elif 'redirect' in item: + action.update(item) + action['type'] = 'redirect' + del action['redirect'] + del action['httpReply'] + elif 'reset' in item: + action.update(item) + action['type'] = 'reset' + del action['connection'] + del action['shutdown'] + elif 'persist' in item: + action.update(item) + action['type'] = 'persist' + action['cookie_insert'] = action['tmName'] + if 'expiry' in item: + action['cookie_expiry'] = int(action['expiry']) + del action['expiry'] + del action['tmName'] + del action['persist'] + del action['cookieInsert'] + result.append(action) + result = sorted(result, key=lambda x: x['name']) + return result + + @property + def conditions(self): + result = [] + if self._values['conditions'] is None: + return [dict(type='all_traffic')] + for item in self._values['conditions']: + action = dict() + if 'httpUri' in item: + action.update(item) + action['type'] = 'http_uri' + del action['httpUri'] + elif 'httpMethod' in item: + action.update(item) + action['type'] = 'http_method' + del action['httpMethod'] + elif 'httpHost' in item: + action.update(item) + action['type'] = 'http_host' + del action['httpHost'] + elif 'httpHeader' in item: + action.update(item) + action['type'] = 'http_header' + action['header_name'] = action['tmName'] + del action['httpHeader'] + del action['tmName'] + elif 'tcp' in item: + action.update(item) + action['type'] = 'tcp' + del action['tcp'] + elif 'sslExtension' in item: + action.update(item) + action['type'] = 'ssl_extension' + del action['sslExtension'] + result.append(action) + # Names contains the index in which the rule is at. + result = sorted(result, key=lambda x: x['name']) + return result + + +class UsableChanges(Changes): + @property + def actions(self): + if self._values['actions'] is None: + return None + result = [] + for action in self._values['actions']: + if 'type' not in action: + continue + if action['type'] == 'forward': + action['forward'] = True + del action['type'] + elif action['type'] == 'enable': + action['enable'] = True + del action['type'] + elif action['type'] == 'disable': + action['disable'] = True + del action['type'] + elif action['type'] == 'set_variable': + action['setVariable'] = True + action['tcl'] = True + del action['type'] + elif action['type'] == 'ignore': + result = [] + break + elif action['type'] == 'redirect': + action['httpReply'] = True + action['redirect'] = True + del action['type'] + elif action['type'] == 'reset': + action['shutdown'] = True + action['connection'] = True + del action['type'] + elif action['type'] == 'persist': + action['persist'] = True + del action['type'] + elif action['type'] == 'remove': + action['remove'] = True + del action['type'] + elif action['type'] == 'insert': + action['insert'] = True + del action['type'] + elif action['type'] == 'replace': + action['replace'] = True + del action['type'] + result.append(action) + return result + + @property + def conditions(self): + if self._values['conditions'] is None: + return None + result = [] + for condition in self._values['conditions']: + if 'type' not in condition: + continue + if condition['type'] == 'http_uri': + condition['httpUri'] = True + del condition['type'] + elif condition['type'] == 'http_method': + condition['httpMethod'] = True + del condition['type'] + elif condition['type'] == 'http_host': + condition['httpHost'] = True + del condition['type'] + elif condition['type'] == 'http_header': + condition['httpHeader'] = True + elif condition['type'] == 'tcp': + condition['tcp'] = True + del condition['type'] + elif condition['type'] == 'ssl_extension': + condition['sslExtension'] = True + del condition['type'] + elif condition['type'] == 'all_traffic': + result = [] + break + result.append(condition) + return result + + +class Difference(object): + updatables = [ + 'actions', 'conditions', 'description' + ] + + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def to_tuple(self, items): + result = [] + for x in items: + tmp = [(str(k), str(v)) for k, v in iteritems(x)] + result += tmp + return result + + def _diff_complex_items(self, want, have): + if want == [] and have is None: + return None + if want is None: + return None + w = self.to_tuple(want) + h = self.to_tuple(have) + if set(w).issubset(set(h)): + return None + else: + return want + + @property + def actions(self): + if self.want.replace_with is True: + return self.want.actions + result = self._diff_complex_items(self.want.actions, self.have.actions) + actioned = self._compare_complex_actions() + if self._conditions_missing_default_rule_for_asm(result): + raise F5ModuleError( + "Valid options when using an ASM policy in a rule's 'enable' " + "action include all_traffic, http_uri, or http_host." + ) + if result is None and actioned is True: + return self.want.actions + return result + + @property + def conditions(self): + if self.want.replace_with is True: + return self.want.conditions + result = self._diff_complex_items(self.want.conditions, self.have.conditions) + return result + + def _conditions_missing_default_rule_for_asm(self, want_actions): + if want_actions is None: + actions = self.have.actions + else: + actions = want_actions + if actions is None: + return False + if any(x for x in actions if x['type'] == 'enable'): + conditions = self._diff_complex_items(self.want.conditions, self.have.conditions) + if conditions is None: + return False + if any(y for y in conditions if y['type'] not in ['all_traffic', 'http_uri', 'http_host', 'tcp']): + return True + return False + + def _compare_complex_actions(self): + types = ['insert', 'remove', 'replace'] + if self.want.actions: + want = [item for item in self.want.actions if item['type'] in types] + have = [item for item in self.have.actions if item['type'] in types] + result = compare_complex_list(want, have) + if result: + return True + return False + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + if self.draft_exists(): + redraft = True + else: + redraft = False + self._create_existing_policy_draft_on_device() + self.update_on_device() + if redraft is False: + self.publish_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + if self.draft_exists(): + redraft = True + else: + redraft = False + self._create_existing_policy_draft_on_device() + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + if redraft is False: + self.publish_on_device() + return True + + def create(self): + self.should_update() + if self.module.check_mode: + return True + if self.draft_exists(): + redraft = True + else: + redraft = False + self._create_existing_policy_draft_on_device() + self.create_on_device() + if redraft is False: + self.publish_on_device() + return True + + def exists(self): + if self.draft_exists(): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.policy, sub_path='Drafts'), + self.want.name + ) + else: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.policy), + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def draft_exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.policy, sub_path='Drafts') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def _create_existing_policy_draft_on_device(self): + params = dict(createDraft=True) + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.policy) + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def publish_on_device(self): + params = dict( + name=fq_name(self.want.partition, + self.want.policy, + sub_path='Drafts' + ), + command="publish" + + ) + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.policy, sub_path='Drafts'), + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.policy, sub_path='Drafts'), + self.want.name + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.policy, sub_path='Drafts'), + self.want.name + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + if self.draft_exists(): + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.policy, sub_path='Drafts'), + self.want.name + ) + else: + uri = "https://{0}:{1}/mgmt/tm/ltm/policy/{2}/rules/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.policy), + self.want.name + ) + query = "?expandSubcollections=true" + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + description=dict(), + actions=dict( + type='list', + elements='dict', + options=dict( + type=dict( + choices=[ + 'forward', + 'enable', + 'ignore', + 'redirect', + 'reset', + 'persist', + 'set_variable', + 'remove', + 'insert', + 'replace', + 'disable', + ], + required=True + ), + pool=dict(), + node=dict(), + asm_policy=dict(), + virtual=dict(), + location=dict(), + event=dict(), + cookie_insert=dict(), + cookie_expiry=dict(type='int'), + expression=dict(), + variable_name=dict(), + disable_target=dict( + choices=['server_ssl', 'persist', 'asm'] + ), + http_header=dict( + type='dict', + options=dict( + event=dict( + choices=[ + 'request', 'response', 'proxy_connect', + 'proxy_request', 'proxy_response' + ], + required=True + ), + name=dict(required=True), + value=dict() + ) + ), + http_referer=dict( + type='dict', + options=dict( + event=dict( + choices=['request', 'proxy_connect', 'proxy_request'], + required=True + ), + value=dict() + ) + ), + http_set_cookie=dict( + type='dict', + options=dict( + name=dict(required=True), + value=dict() + ) + ), + http_cookie=dict( + type='dict', + options=dict( + event=dict( + choices=['request', 'proxy_connect', 'proxy_request'], + required=True + + ), + name=dict(required=True), + value=dict() + ) + ), + http_connect=dict( + type='dict', + options=dict( + event=dict( + choices=[ + 'client_accepted', 'proxy_connect', 'proxy_request', + 'proxy_response', 'request', 'server_connected', 'ssl_client_hello' + ], + required=True + ), + value=dict( + required=True + ), + port=dict(type='int'), + ) + ), + http_host=dict( + type='dict', + options=dict( + event=dict( + choices=['request', 'proxy_connect', 'proxy_request'], + required=True + ), + value=dict(required=True) + ) + ), + http_uri=dict( + type='dict', + options=dict( + event=dict( + choices=['request', 'proxy_connect', 'proxy_request'], + required=True + ), + type=dict( + choices=['path', 'query_string', 'full_string'], + required=True + ), + value=dict(required=True) + ), + + ), + ), + mutually_exclusive=[ + ['pool', 'asm_policy', 'virtual', 'location', 'cookie_insert', 'node', 'http_header', + 'http_referer', 'http_set_cookie', 'http_cookie', 'http_uri', 'http_host', 'http_connect', + 'disable_target' + ] + ] + ), + conditions=dict( + type='list', + elements='dict', + options=dict( + type=dict( + choices=[ + 'http_uri', + 'http_method', + 'http_host', + 'http_header', + 'ssl_extension', + 'all_traffic', + 'tcp' + ], + required=True + ), + path_begins_with_any=dict( + type='list', + elements='str', + ), + path_contains=dict( + type='list', + elements='str', + ), + path_is_any=dict( + type='list', + elements='str', + ), + host_begins_with_any=dict( + type='list', + elements='str', + ), + host_begins_not_with_any=dict( + type='list', + elements='str', + ), + host_ends_with_any=dict( + type='list', + elements='str', + ), + host_ends_not_with_any=dict( + type='list', + elements='str', + ), + host_is_any=dict( + type='list', + elements='str', + ), + host_is_not_any=dict( + type='list', + elements='str', + ), + header_name=dict(), + header_is_any=dict( + type='list', + elements='str', + ), + method_matches_with_any=dict( + type='list', + elements='str', + ), + server_name_is_any=dict( + type='list', + elements='str', + ), + address_matches_with_any=dict( + type='list', + elements='str', + ), + address_matches_with_datagroup=dict( + type='list', + elements='str', + ), + address_matches_with_external_datagroup=dict( + type='list', + elements='str', + ), + event=dict() + ), + ), + name=dict(required=True), + policy=dict(required=True), + rule_order=dict(type='int'), + replace_with=dict( + type='bool', + default='no' + ), + state=dict( + default='present', + choices=['absent', 'present'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_pool.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_pool.py new file mode 100644 index 00000000..773aa447 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_pool.py @@ -0,0 +1,1359 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_pool +short_description: Manages F5 BIG-IP LTM pools +description: + - Manages F5 BIG-IP LTM pools via iControl REST API. +version_added: "1.0.0" +options: + description: + description: + - Specifies descriptive text that identifies the pool. + type: str + name: + description: + - Pool name + type: str + aliases: + - pool + lb_method: + description: + - Load balancing method. When creating a new pool, if this value is not + specified, the default of C(round-robin) is used. + type: str + choices: + - dynamic-ratio-member + - dynamic-ratio-node + - fastest-app-response + - fastest-node + - least-connections-member + - least-connections-node + - least-sessions + - observed-member + - observed-node + - predictive-member + - predictive-node + - ratio-least-connections-member + - ratio-least-connections-node + - ratio-member + - ratio-node + - ratio-session + - round-robin + - weighted-least-connections-member + - weighted-least-connections-node + monitor_type: + description: + - Monitor rule type when C(monitors) is specified. + - When creating a new pool, if this value is not specified, the default + of C(and_list) is used. + - When C(single), ensures all specified monitors are checked, but + additionally includes checks to make sure you only specified a single + monitor. + - When C(and_list), ensures B(all) monitors are checked. + - When C(m_of_n), ensures C(quorum) of C(monitors) are checked. C(m_of_n) + B(requires) a C(quorum) of 1 or greater be set either in the playbook, + or already exist on the device. + - Both C(single) and C(and_list) are functionally identical, as BIG-IP + considers all monitors as "a list". + type: str + aliases: + - availability_requirements_type + choices: + - and_list + - m_of_n + - single + quorum: + description: + - Monitor quorum value when C(monitor_type) is C(m_of_n). + - Quorum must be a value of 1 or greater when C(monitor_type) is C(m_of_n). + type: int + aliases: + - availability_requirements_at_least + monitors: + description: + - Monitor template name list. If the partition is not provided as part of + the monitor name, the C(partition) option is used instead. + type: list + elements: str + slow_ramp_time: + description: + - Sets the ramp-up time (in seconds) to gradually ramp up the load on + newly added or freshly detected up pool members. + type: int + reselect_tries: + description: + - Sets the number of times the system tries to contact a pool member + after a passive failure. + type: int + service_down_action: + description: + - Sets the action to take when node goes down in pool. + type: str + choices: + - none + - reset + - drop + - reselect + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), guarantees the pool exists with the provided + attributes. + - When C(absent), removes the pool from the system. + type: str + choices: + - absent + - present + default: present + metadata: + description: + - Arbitrary key/value pairs you can attach to a pool. This is useful in + situations where you might want to annotate a pool to be managed by Ansible. + - Key names are stored as strings; this includes names that are numbers. + - Values for all of the keys are stored as strings; this includes values + that are numbers. + - Data will be persisted, not ephemeral. + type: raw + priority_group_activation: + description: + - Specifies whether the system load balances traffic according to the priority + number assigned to the pool member. + - When creating a new pool, if this parameter is not specified, the default of + C(0) is used. + - To disable this setting, provide the value C(0). + - Once you enable this setting, you can specify pool member priority when you + create a new pool or on a pool member's properties screen. + - The system treats same-priority pool members as a group. + - To enable priority group activation, provide a number from C(0) to C(65535) + that represents the minimum number of members that must be available in one + priority group before the system directs traffic to members in a lower + priority group. + - When a sufficient number of members become available in the higher priority + group, the system again directs traffic to the higher priority group. + type: int + aliases: + - minimum_active_members + min_up_members: + description: + - Specifies the minimum number of pool members that must be up, + - otherwise, the system takes the action specified in the C(min-up-members-action) option. + - Use this option for gateway pools in a redundant system where a unit number is applied to the pool. + - This indicates the pool is configured only on the specified unit. + - When creating a new pool, if this parameter is not specified, the default is C(0). + type: int + min_up_members_action: + description: + - Specifies the action to take if C(min_up_members_checking) is C(enabled) and the number of active pool members + falls below the number specified in the C(min_up_members) option. + - When creating a new pool, if this parameter is not specified, the default is C(failover). + type: str + choices: + - failover + - reboot + - restart-all + min_up_members_checking: + description: + - Enables or disables the C(min_up_members) feature. + - If you enable this feature, you must also specify a value for both the C(min_up_members) and + C(min_up_members_action) options. + - When creating a new pool, if this parameter is not specified, the default is C(disabled). + type: str + choices: + - enabled + - disabled + aggregate: + description: + - List of pool definitions to be created, modified, or removed. + - When using C(aggregates), if one of the aggregate definitions is invalid, the aggregate run will fail, + indicating the error it last encountered. + - The module will B(NOT) rollback any changes it has made prior to encountering the error. + - The module also will not indicate which changes were made prior to failure. Therefore we strongly advise + you run the module in C(check) mode to ensure basic validation prior to executing this module. + type: list + elements: dict + aliases: + - pools + replace_all_with: + description: + - Removes pools not defined in the C(aggregate) parameter. + - This operation is all or none, meaning it will stop if there are some pools + that cannot be removed. + type: bool + default: no + aliases: + - purge +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create pool + bigip_pool: + state: present + name: my-pool + partition: Common + lb_method: least-connections-member + slow_ramp_time: 120 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Modify load balancer method + bigip_pool: + state: present + name: my-pool + partition: Common + lb_method: round-robin + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Set a single monitor (with enforcement) + bigip_pool: + state: present + name: my-pool + partition: Common + monitor_type: single + monitors: + - http + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Set a single monitor (without enforcement) + bigip_pool: + state: present + name: my-pool + partition: Common + monitors: + - http + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Set multiple monitors (all must succeed) + bigip_pool: + state: present + name: my-pool + partition: Common + monitor_type: and_list + monitors: + - http + - tcp + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Set multiple monitors (at least 1 must succeed) + bigip_pool: + state: present + name: my-pool + partition: Common + monitor_type: m_of_n + quorum: 1 + monitors: + - http + - tcp + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Set multiple monitors (at least 2 must succeed) + bigip_pool: + state: present + name: my-pool + partition: Common + availability_requirements_type: m_of_n + availability_requirements_at_least: 2 + monitors: + - http + - tcp + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Delete pool + bigip_pool: + state: absent + name: my-pool + partition: Common + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add metadata to pool + bigip_pool: + state: present + name: my-pool + partition: Common + metadata: + ansible: 2.4 + updated_at: 2017-12-20T17:50:46Z + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add pools Aggregate + bigip_pool: + aggregate: + - name: my-pool + partition: Common + lb_method: least-connections-member + slow_ramp_time: 120 + - name: my-pool2 + partition: Common + lb_method: least-sessions + slow_ramp_time: 120 + - name: my-pool3 + partition: Common + lb_method: round-robin + slow_ramp_time: 120 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add pools Aggregate, purge others + bigip_pool: + aggregate: + - name: my-pool + partition: Common + lb_method: least-connections-member + slow_ramp_time: 120 + - name: my-pool2 + partition: Common + lb_method: least-sessions + slow_ramp_time: 120 + - name: my-pool3 + partition: Common + lb_method: round-robin + slow_ramp_time: 120 + replace_all_with: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +monitor_type: + description: + - Changed value for the monitor_type of the pool. + returned: changed + type: str + sample: m_of_n +quorum: + description: The quorum that was set on the pool. + returned: changed + type: int + sample: 2 +monitors: + description: Monitors set on the pool. + returned: changed + type: list + sample: ['/Common/http', '/Common/gateway_icmp'] +service_down_action: + description: Service down action that is set on the pool. + returned: changed + type: str + sample: reset +description: + description: Description set on the pool. + returned: changed + type: str + sample: Pool of web servers +lb_method: + description: The load balancing method set for the pool. + returned: changed + type: str + sample: round-robin +slow_ramp_time: + description: The new value set for the slow ramp-up time. + returned: changed + type: int + sample: 500 +reselect_tries: + description: The new value set for the number of tries to contact member. + returned: changed + type: int + sample: 10 +metadata: + description: The new value of the pool. + returned: changed + type: dict + sample: {'key1': 'foo', 'key2': 'bar'} +priority_group_activation: + description: The new minimum number of members to activate the priority group. + returned: changed + type: int + sample: 10 +replace_all_with: + description: Purges all non-aggregate pools from device + returned: changed + type: bool + sample: yes +''' + +import re +from copy import deepcopy +from datetime import datetime + +from ansible.module_utils.urls import urlparse +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import ( + TransactionContextManager, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'loadBalancingMode': 'lb_method', + 'slowRampTime': 'slow_ramp_time', + 'reselectTries': 'reselect_tries', + 'serviceDownAction': 'service_down_action', + 'monitor': 'monitors', + 'minActiveMembers': 'priority_group_activation', + 'minUpMembers': 'min_up_members', + 'minUpMembersAction': 'min_up_members_action', + 'minUpMembersChecking': 'min_up_members_checking', + } + api_attributes = [ + 'description', + 'name', + 'loadBalancingMode', + 'monitor', + 'slowRampTime', + 'reselectTries', + 'serviceDownAction', + 'metadata', + 'minActiveMembers', + 'minUpMembers', + 'minUpMembersAction', + 'minUpMembersChecking', + ] + + returnables = [ + 'monitor_type', + 'quorum', + 'monitors', + 'service_down_action', + 'description', + 'lb_method', + 'slow_ramp_time', + 'reselect_tries', + 'monitor', + 'name', + 'partition', + 'metadata', + 'priority_group_activation', + 'min_up_members', + 'min_up_members_action', + 'min_up_members_checking', + ] + + updatables = [ + 'monitor_type', + 'quorum', + 'monitors', + 'service_down_action', + 'description', + 'lb_method', + 'slow_ramp_time', + 'reselect_tries', + 'metadata', + 'priority_group_activation', + 'min_up_members', + 'min_up_members_action', + 'min_up_members_checking', + ] + + @property + def lb_method(self): + lb_method = self._values['lb_method'] + if lb_method is None: + return None + + spec = ArgumentSpec() + if lb_method not in spec.lb_choice: + raise F5ModuleError('Provided lb_method is unknown') + return lb_method + + def _verify_quorum_type(self, quorum): + try: + if quorum is None: + return None + return int(quorum) + except ValueError: + raise F5ModuleError( + "The specified 'quorum' must be an integer." + ) + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.monitor_type == 'm_of_n': + monitors = ' '.join(monitors) + result = 'min %s of { %s }' % (self.quorum, monitors) + else: + result = ' and '.join(monitors).strip() + return result + + @property + def priority_group_activation(self): + if self._values['priority_group_activation'] is None: + return None + return int(self._values['priority_group_activation']) + + @property + def min_up_members(self): + if self._values['min_up_members'] is None: + return None + return int(self._values['min_up_members']) + + @property + def min_up_members_action(self): + if self._values['min_up_members_action'] is None: + return None + return self._values['min_up_members_action'] + + @property + def min_up_members_checking(self): + if self._values['min_up_members_checking'] is None: + return None + return self._values['min_up_members_checking'] + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def quorum(self): + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of' + matches = re.search(pattern, self._values['monitors']) + if matches: + quorum = matches.group('quorum') + else: + quorum = None + result = self._verify_quorum_type(quorum) + return result + + @property + def monitor_type(self): + if self._values['monitors'] is None: + return None + pattern = r'min\s+\d+\s+of' + matches = re.search(pattern, self._values['monitors']) + if matches: + return 'm_of_n' + else: + return 'and_list' + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/[\w-]+/[^\s}]+', self._values['monitors']) + return result + except Exception: + return self._values['monitors'] + + @property + def metadata(self): + if self._values['metadata'] is None: + return None + result = [] + for md in self._values['metadata']: + tmp = dict(name=str(md['name'])) + if 'value' in md: + tmp['value'] = str(md['value']) + else: + tmp['value'] = '' + result.append(tmp) + return result + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + return self._values['monitors'] + + @property + def quorum(self): + if self._values['quorum'] is None: + return None + result = self._verify_quorum_type(self._values['quorum']) + return result + + @property + def monitor_type(self): + if self._values['monitor_type'] is None: + return None + return self._values['monitor_type'] + + @property + def metadata(self): + if self._values['metadata'] is None: + return None + if self._values['metadata'] == '': + return [] + result = [] + try: + for k, v in iteritems(self._values['metadata']): + tmp = dict(name=str(k)) + if v: + tmp['value'] = str(v) + else: + tmp['value'] = '' + result.append(tmp) + except AttributeError: + raise F5ModuleError( + "The 'metadata' parameter must be a dictionary of key/value pairs." + ) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + for returnable in self.returnables: + try: + result[returnable] = getattr(self, returnable) + except Exception: + pass + result = self._filter_params(result) + return result + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + return self._values['monitors'] + + +class UsableChanges(Changes): + @property + def monitors(self): + monitor_string = self._values['monitors'] + if monitor_string is None: + return None + + if '{' in monitor_string and '}' in monitor_string: + tmp = monitor_string.strip('}').split('{') + monitor = ''.join(tmp).rstrip() + return monitor + + return monitor_string + + +class ReportableChanges(Changes): + @property + def monitors(self): + result = sorted(re.findall(r'/[\w-]+/[^\s}]+', self._values['monitors'])) + return result + + @property + def monitor_type(self): + pattern = r'min\s+\d+\s+of' + matches = re.search(pattern, self._values['monitors']) + if matches: + return 'm_of_n' + else: + return 'and_list' + + @property + def metadata(self): + result = dict() + for x in self._values['metadata']: + result[x['name']] = x['value'] + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def to_tuple(self, items): + result = [] + for x in items: + tmp = [(str(k), str(v)) for k, v in iteritems(x)] + result += tmp + return result + + def _diff_complex_items(self, want, have): + if want == [] and have is None: + return None + if want is None: + return None + w = self.to_tuple(want) + h = self.to_tuple(have) + if set(w).issubset(set(h)): + return None + else: + return want + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + def _monitors_and_quorum(self): + if self.want.monitor_type is None: + self.want.update(dict(monitor_type=self.have.monitor_type)) + if self.want.monitor_type == 'm_of_n': + if self.want.quorum is None: + self.want.update(dict(quorum=self.have.quorum)) + if self.want.quorum is None or self.want.quorum < 1: + raise F5ModuleError( + "Quorum value must be specified with monitor_type 'm_of_n'." + ) + if self.want.monitors != self.have.monitors: + return dict( + monitors=self.want.monitors + ) + elif self.want.monitor_type == 'and_list': + if self.want.quorum is not None and self.want.quorum > 0: + raise F5ModuleError( + "Quorum values have no effect when used with 'and_list'." + ) + if self.want.monitors != self.have.monitors: + return dict( + monitors=self.want.monitors + ) + elif self.want.monitor_type == 'single': + if len(self.want.monitors_list) > 1: + raise F5ModuleError( + "When using a 'monitor_type' of 'single', only one monitor may be provided." + ) + elif len(self.have.monitors_list) > 1 and len(self.want.monitors_list) == 0: + # Handle instances where there already exists many monitors, and the + # user runs the module again specifying that the monitor_type should be + # changed to 'single' + raise F5ModuleError( + "A single monitor must be specified if more than one monitor currently exists on your pool." + ) + # Update to 'and_list' here because the above checks are all that need + # to be done before we change the value back to what is expected by + # BIG-IP. + # + # Remember that 'single' is nothing more than a fancy way of saying + # "and_list plus some extra checks" + self.want.update(dict(monitor_type='and_list')) + if self.want.monitors != self.have.monitors: + return dict( + monitors=self.want.monitors + ) + + @property + def monitor_type(self): + return self._monitors_and_quorum() + + @property + def quorum(self): + return self._monitors_and_quorum() + + @property + def monitors(self): + if self.want.monitor_type is None: + self.want.update(dict(monitor_type=self.have.monitor_type)) + if not self.want.monitors_list: + self.want.monitors = self.have.monitors_list + if not self.want.monitors and self.want.monitor_type is not None: + raise F5ModuleError( + "The 'monitors' parameter cannot be empty when 'monitor_type' parameter is specified" + ) + if self.want.monitors != self.have.monitors: + return self.want.monitors + + @property + def metadata(self): + if self.want.metadata is None: + return None + elif len(self.want.metadata) == 0 and self.have.metadata is None: + return None + elif len(self.want.metadata) == 0: + return [] + elif self.have.metadata is None: + return self.want.metadata + result = self._diff_complex_items(self.want.metadata, self.have.metadata) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = None + self.have = None + self.changes = None + self.replace_all_with = None + self.purge_links = list() + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + wants = None + if self.module.params['replace_all_with']: + self.replace_all_with = True + + if self.module.params['aggregate']: + wants = self.merge_defaults_for_aggregate(self.module.params) + + result = dict() + changed = False + + if self.replace_all_with and self.purge_links: + self.purge() + changed = True + + if self.module.params['aggregate']: + result['aggregate'] = list() + for want in wants: + output = self.execute(want) + if output['changed']: + changed = output['changed'] + result['aggregate'].append(output) + else: + output = self.execute(self.module.params) + if output['changed']: + changed = output['changed'] + result.update(output) + if changed: + result['changed'] = True + send_teem(start, self.client, self.module, version) + return result + + def merge_defaults_for_aggregate(self, params): + defaults = deepcopy(params) + aggregate = defaults.pop('aggregate') + + for i, j in enumerate(aggregate): + for k, v in iteritems(defaults): + if k != 'replace_all_with': + if j.get(k, None) is None and v is not None: + aggregate[i][k] = v + + if self.replace_all_with: + self.compare_aggregate_names(aggregate) + + return aggregate + + def compare_aggregate_names(self, items): + on_device = self._read_purge_collection() + if not on_device: + return False + aggregates = [item['name'] for item in items] + collection = [item['name'] for item in on_device] + + diff = set(collection) - set(aggregates) + + if diff: + to_purge = [item['selfLink'] for item in on_device if item['name'] in diff] + self.purge_links.extend(to_purge) + + def execute(self, params=None): + self.want = ModuleParameters(params=params) + self.have = ApiParameters() + self.changes = UsableChanges() + + changed = False + result = dict() + state = params['state'] + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the Pool") + return True + + def purge(self): + if self.module.check_mode: + return True + self.purge_from_device() + return True + + def create(self): + if self.want.monitor_type is not None: + if not self.want.monitors_list: + raise F5ModuleError( + "The 'monitors' parameter cannot be empty when 'monitor_type' parameter is specified" + ) + else: + if self.want.monitor_type is None: + self.want.update(dict(monitor_type='and_list')) + + if self.want.monitor_type == 'm_of_n' and (self.want.quorum is None or self.want.quorum < 1): + raise F5ModuleError( + "Quorum value must be specified with monitor_type 'm_of_n'." + ) + elif self.want.monitor_type == 'and_list' and self.want.quorum is not None and self.want.quorum > 0: + raise F5ModuleError( + "Quorum values have no effect when used with 'and_list'." + ) + elif self.want.monitor_type == 'single' and len(self.want.monitors_list) > 1: + raise F5ModuleError( + "When using a 'monitor_type' of 'single', only one monitor may be provided" + ) + if self.want.priority_group_activation is None: + self.want.update({'priority_group_activation': 0}) + + if self.want.min_up_members is None: + self.want.update({'min_up_members': 0}) + + if self.want.min_up_members_action is None: + self.want.update({'min_up_members_action': 'failover'}) + + if self.want.min_up_members_checking is None: + self.want.update({'min_up_members_checking': 'disabled'}) + + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def _read_purge_collection(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + query = "?$select=name,selfLink" + resp = self.client.api.get(uri + query) + + 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) + if 'items' in response: + return response['items'] + return [] + + 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/ltm/pool/".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/ltm/pool/{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 exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + query = '?expandSubcollections=true' + resp = self.client.api.get(uri + query) + 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) + return ApiParameters(params=response) + + def _prepare_links(self, collection): + purge_links = list() + purge_paths = [urlparse(link).path for link in collection] + + for path in purge_paths: + link = "https://{0}:{1}{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + path + ) + purge_links.append(link) + return purge_links + + def purge_from_device(self): + links = self._prepare_links(self.purge_links) + + with TransactionContextManager(self.client) as transact: + for link in links: + resp = transact.api.delete(link) + + 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) + return True + + +class ArgumentSpec(object): + def __init__(self): + self.lb_choice = [ + 'dynamic-ratio-member', + 'dynamic-ratio-node', + 'fastest-app-response', + 'fastest-node', + 'least-connections-member', + 'least-connections-node', + 'least-sessions', + 'observed-member', + 'observed-node', + 'predictive-member', + 'predictive-node', + 'ratio-least-connections-member', + 'ratio-least-connections-node', + 'ratio-member', + 'ratio-node', + 'ratio-session', + 'round-robin', + 'weighted-least-connections-member', + 'weighted-least-connections-node' + ] + self.supports_check_mode = True + element_spec = dict( + name=dict( + aliases=['pool'] + ), + lb_method=dict( + choices=self.lb_choice + ), + monitor_type=dict( + choices=[ + 'and_list', 'm_of_n', 'single' + ], + aliases=['availability_requirements_type'] + ), + quorum=dict( + type='int', + aliases=['availability_requirements_at_least'] + ), + monitors=dict( + type='list', + elements='str', + ), + slow_ramp_time=dict( + type='int' + ), + reselect_tries=dict( + type='int' + ), + service_down_action=dict( + choices=[ + 'none', 'reset', + 'drop', 'reselect' + ] + ), + description=dict(), + metadata=dict(type='raw'), + state=dict( + default='present', + choices=['present', 'absent'] + ), + priority_group_activation=dict( + type='int', + aliases=['minimum_active_members'] + ), + min_up_members=dict( + type='int' + ), + min_up_members_action=dict( + choices=['failover', 'reboot', 'restart-all'] + ), + min_up_members_checking=dict( + choices=['enabled', 'disabled'] + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ) + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict( + type='list', + elements='dict', + options=aggregate_spec, + aliases=['pools'] + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + replace_all_with=dict( + default='no', + type='bool', + aliases=['purge'] + ) + ) + + self.mutually_exclusive = [ + ['name', 'aggregate'] + ] + self.required_one_of = [ + ['name', 'aggregate'] + ] + + self.argument_spec = {} + self.argument_spec.update(element_spec) + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + required_one_of=spec.required_one_of + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_pool_member.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_pool_member.py new file mode 100644 index 00000000..5a1d26fc --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_pool_member.py @@ -0,0 +1,1672 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# Copyright: (c) 2013, Matt Hite +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_pool_member +short_description: Manages F5 BIG-IP LTM pool members +description: + - Manages F5 BIG-IP LTM pool members via the REST API. +version_added: "1.0.0" +options: + name: + description: + - Name of the node to create or re-use when creating a new pool member. + - While this parameter is optional, we recommend specifying this parameter + at all times to mitigate anyunexpected behavior. + - If not specified, a node name is created automatically from either the specified C(address) or C(fqdn). + - The C(enabled) state is an alias of C(present). + type: str + state: + description: + - Pool member state. + type: str + choices: + - present + - absent + - enabled + - disabled + - forced_offline + default: present + pool: + description: + - Pool name. This pool must exist. + type: str + required: True + partition: + description: + - Partition to manage resources on. + type: str + default: Common + address: + description: + - IP address of the pool member. This can be either IPv4 or IPv6. When creating a + new pool member, one of either C(address) or C(fqdn) must be provided. This + parameter cannot be updated after it is set. + type: str + aliases: + - ip + - host + fqdn: + description: + - FQDN name of the pool member. This can be any name that is a valid RFC 1123 DNS + name. Therefore, the only usable characters are "A" to "Z", + "a" to "z", "0" to "9", the hyphen ("-") and the period ("."). + - FQDN names must include at least one period; delineating the host from + the domain. For example, C(host.domain). + - FQDN names must end with a letter or a number. + - When creating a new pool member, one of either C(address) or C(fqdn) must be + provided. This parameter cannot be updated after it is set. + type: str + aliases: + - hostname + port: + description: + - Pool member port. + - This value cannot be changed after it has been set. + - Parameter must be provided when using aggregates. + type: int + connection_limit: + description: + - Pool member connection limit. Setting this to C(0) disables the limit. + type: int + description: + description: + - Pool member description. + type: str + rate_limit: + description: + - Pool member rate limit (connections-per-second). Setting this to C(0) + disables the limit. + type: int + ratio: + description: + - Pool member ratio weight. Valid values range from 1 through 100. + New pool members -- unless overridden with this value -- default + to 1. + type: int + preserve_node: + description: + - When state is C(absent), the system attempts to remove the node the pool + member references. + - The node will not be removed if it is still referenced by other pool + members. If this happens, the module will not raise an error. + - Setting this to C(yes) disables this behavior. + type: bool + priority_group: + description: + - Specifies a number representing the priority group for the pool member. + - When adding a new member, the default is C(0), meaning the member has no priority. + - To specify a priority, you must activate priority group usage when you + create a new pool or when adding or removing pool members. When activated, + the system load balances traffic according to the priority group number + assigned to the pool member. + - The higher the number, the higher the priority. So a member with a priority + of 3 has higher priority than a member with a priority of 1. + type: int + fqdn_auto_populate: + description: + - Specifies whether the system automatically creates ephemeral nodes using + the IP addresses returned by the resolution of a DNS query for a node + defined by an FQDN. + - When C(yes), the system generates an ephemeral node for each IP address + returned in response to a DNS query for the FQDN of the node. Additionally, + when a DNS response indicates the IP address of an ephemeral node no longer + exists, the system deletes the ephemeral node. + - When C(no), the system resolves a DNS query for the FQDN of the node + with the single IP address associated with the FQDN. + - When creating a new pool member, the default for this parameter is C(yes). + - Once set this parameter cannot be changed afterwards. + - This parameter is ignored when C(reuse_nodes) is C(yes). + type: bool + reuse_nodes: + description: + - Reuses node definitions if requested. + type: bool + default: yes + monitors: + description: + - Specifies the health monitors the system currently uses to monitor + this resource. + type: list + elements: str + availability_requirements: + description: + - If you activate more than one health monitor, specifies the number of health + monitors that must receive successful responses in order for the link to be + considered available. + - Specifying an empty string will remove the monitors and revert to inheriting from the pool (default). + - Specifying C(none) will remove any health monitoring from the member completely. + type: dict + suboptions: + type: + description: + - Monitor rule type when C(monitors) is specified. + - When creating a new pool, if this value is not specified, the default of + C(all) will be used. + type: str + required: True + choices: + - all + - at_least + at_least: + description: + - Specifies the minimum number of active health monitors that must be successful + before the link is considered up. + - This parameter is only relevant when a C(type) of C(at_least) is used. + - This parameter will be ignored if a type of C(all) is used. + type: int + ip_encapsulation: + description: + - Specifies the IP encapsulation using either IPIP (IP encapsulation within IP, + RFC 2003) or GRE (Generic Router Encapsulation, RFC 2784) on outbound packets + (from BIG-IP system to server-pool member). + - When C(none), disables IP encapsulation. + - When C(inherit), inherits the IP encapsulation setting from the member's pool. + - When any other value, the options are None, Inherit from Pool, and Member Specific. + type: str + aggregate: + description: + - List of pool member definitions to be created, modified, or removed. + - When using C(aggregates), if one of the aggregate definitions is invalid, the aggregate run will fail, + indicating the error it last encountered. + - The module will B(NOT) rollback any changes it has made prior to encountering the error. + - The module also will not indicate what changes were made prior to failure. Therefore we strong advise + you run the module in C(check) mode to ensure basic validation prior to executing this module. + type: list + elements: dict + aliases: + - members + replace_all_with: + description: + - Removes members not defined in the C(aggregate) parameter. + - This operation is all or none, meaning it will stop if there are some pool members + that cannot be removed. + type: bool + default: no + aliases: + - purge +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add pool member + bigip_pool_member: + pool: my-pool + partition: Common + name: my-member + host: "{{ ansible_default_ipv4['address'] }}" + port: 80 + description: web server + connection_limit: 100 + rate_limit: 50 + ratio: 2 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Modify pool member ratio and description + bigip_pool_member: + pool: my-pool + partition: Common + name: my-member + host: "{{ ansible_default_ipv4['address'] }}" + port: 80 + ratio: 1 + description: nginx server + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove pool member from pool + bigip_pool_member: + state: absent + pool: my-pool + partition: Common + name: my-member + host: "{{ ansible_default_ipv4['address'] }}" + port: 80 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Force pool member offline + bigip_pool_member: + state: forced_offline + pool: my-pool + partition: Common + name: my-member + host: "{{ ansible_default_ipv4['address'] }}" + port: 80 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create members with priority groups + bigip_pool_member: + pool: my-pool + partition: Common + host: "{{ item.address }}" + name: "{{ item.name }}" + priority_group: "{{ item.priority_group }}" + port: 80 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + loop: + - address: 1.1.1.1 + name: web1 + priority_group: 4 + - address: 2.2.2.2 + name: web2 + priority_group: 3 + - address: 3.3.3.3 + name: web3 + priority_group: 2 + - address: 4.4.4.4 + name: web4 + priority_group: 1 + +- name: Add pool members aggregate + bigip_pool_member: + pool: my-pool + aggregate: + - host: 192.168.1.1 + partition: Common + port: 80 + description: web server + connection_limit: 100 + rate_limit: 50 + ratio: 2 + - host: 192.168.1.2 + partition: Common + port: 80 + description: web server + connection_limit: 100 + rate_limit: 50 + ratio: 2 + - host: 192.168.1.3 + partition: Common + port: 80 + description: web server + connection_limit: 100 + rate_limit: 50 + ratio: 2 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add pool members aggregate, remove non aggregates + bigip_pool_member: + pool: my-pool + aggregate: + - host: 192.168.1.1 + partition: Common + port: 80 + description: web server + connection_limit: 100 + rate_limit: 50 + ratio: 2 + - host: 192.168.1.2 + partition: Common + port: 80 + description: web server + connection_limit: 100 + rate_limit: 50 + ratio: 2 + - host: 192.168.1.3 + partition: Common + port: 80 + description: web server + connection_limit: 100 + rate_limit: 50 + ratio: 2 + replace_all_with: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +rate_limit: + description: The new rate limit, in connections per second, of the pool member. + returned: changed + type: int + sample: 100 +connection_limit: + description: The new connection limit of the pool member. + returned: changed + type: int + sample: 1000 +description: + description: The new description of pool member. + returned: changed + type: str + sample: My pool member +ratio: + description: The new pool member ratio weight. + returned: changed + type: int + sample: 50 +priority_group: + description: The new priority group. + returned: changed + type: int + sample: 3 +fqdn_auto_populate: + description: Whether FQDN auto population was set on the member or not. + returned: changed + type: bool + sample: True +fqdn: + description: The FQDN of the pool member. + returned: changed + type: str + sample: foo.bar.com +address: + description: The address of the pool member. + returned: changed + type: str + sample: 1.2.3.4 +monitors: + description: The new list of monitors for the resource. + returned: changed + type: list + sample: ['/Common/monitor1', '/Common/monitor2'] +replace_all_with: + description: Purges all non-aggregate pool members from device. + returned: changed + type: bool + sample: yes +''' + +import os +import re + +from copy import deepcopy +from datetime import datetime + +from ansible.module_utils.urls import urlparse +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name, flatten_boolean, is_valid_hostname +) +from ..module_utils.ipaddress import is_valid_ip, validate_ip_v6_address +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import ( + TransactionContextManager, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'rateLimit': 'rate_limit', + 'connectionLimit': 'connection_limit', + 'priorityGroup': 'priority_group', + 'monitor': 'monitors', + 'inheritProfile': 'inherit_profile', + 'profiles': 'ip_encapsulation', + } + + api_attributes = [ + 'rateLimit', + 'connectionLimit', + 'description', + 'ratio', + 'priorityGroup', + 'address', + 'fqdn', + 'session', + 'state', + 'monitor', + + # These two settings are for IP Encapsulation + 'inheritProfile', + 'profiles', + ] + + returnables = [ + 'rate_limit', + 'connection_limit', + 'description', + 'ratio', + 'priority_group', + 'fqdn_auto_populate', + 'session', + 'state', + 'fqdn', + 'address', + 'monitors', + + # IP Encapsulation related + 'inherit_profile', + 'ip_encapsulation', + ] + + updatables = [ + 'rate_limit', + 'connection_limit', + 'description', + 'ratio', + 'priority_group', + 'fqdn_auto_populate', + 'state', + 'monitors', + 'inherit_profile', + 'ip_encapsulation', + ] + + +class ModuleParameters(Parameters): + @property + def full_name(self): + delimiter = ':' + try: + if validate_ip_v6_address(self.full_name_dict['name']): + delimiter = '.' + except TypeError: + pass + return '{0}{1}{2}'.format(self.full_name_dict['name'], delimiter, self.port) + + @property + def full_name_dict(self): + if self._values['name'] is None: + if self._values['address'] and not self._values['fqdn']: + name = self._values['address'] + else: + name = self._values['fqdn'] + else: + name = self._values['name'] + return dict( + name=name, + port=self.port + ) + + @property + def node_name(self): + return self.full_name_dict['name'] + + @property + def fqdn_name(self): + return self._values['fqdn'] + + @property + def fqdn(self): + result = {} + if self.fqdn_auto_populate: + result['autopopulate'] = 'enabled' + else: + result['autopopulate'] = 'disabled' + if self._values['fqdn'] is None: + return result + if not is_valid_hostname(self._values['fqdn']): + raise F5ModuleError( + "The specified 'fqdn' value of: {0} is not a valid hostname.".format(self._values['fqdn']) + ) + result['tmName'] = self._values['fqdn'] + return result + + @property + def pool(self): + return fq_name(self.want.partition, self._values['pool']) + + @property + def port(self): + if self._values['port'] is None: + raise F5ModuleError( + "Port value must be specified." + ) + if 0 > int(self._values['port']) or int(self._values['port']) > 65535: + raise F5ModuleError( + "Valid ports must be in range 0 - 65535" + ) + return int(self._values['port']) + + @property + def address(self): + if self._values['address'] is None: + return None + elif self._values['address'] == 'any6': + return 'any6' + address = self._values['address'].split('%')[0] + if is_valid_ip(address): + return self._values['address'] + raise F5ModuleError( + "The specified 'address' value of: {0} is not a valid IP address.".format(address) + ) + + @property + def state(self): + if self._values['state'] == 'enabled': + return 'present' + return self._values['state'] + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + if len(self._values['monitors']) == 1 and self._values['monitors'][0] == '': + return 'default' + if len(self._values['monitors']) == 1 and self._values['monitors'][0] == 'none': + return '/Common/none' + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.availability_requirement_type == 'at_least': + if self.at_least > len(self.monitors_list): + raise F5ModuleError( + "The 'at_least' value must not exceed the number of 'monitors'." + ) + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + else: + result = ' and '.join(monitors).strip() + return result + + @property + def availability_requirement_type(self): + if self._values['availability_requirements'] is None: + return None + return self._values['availability_requirements']['type'] + + @property + def at_least(self): + return self._get_availability_value('at_least') + + @property + def ip_encapsulation(self): + if self._values['ip_encapsulation'] is None: + return None + if self._values['ip_encapsulation'] == 'inherit': + return 'inherit' + if self._values['ip_encapsulation'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['ip_encapsulation']) + + def _get_availability_value(self, type): + if self._values['availability_requirements'] is None: + return None + if self._values['availability_requirements'][type] is None: + return None + return int(self._values['availability_requirements'][type]) + + +class ApiParameters(Parameters): + @property + def ip_encapsulation(self): + """Returns a simple name for the tunnel. + + The API stores the data like so + + "profiles": [ + { + "name": "gre", + "partition": "Common", + "nameReference": { + "link": "https://localhost/mgmt/tm/net/tunnels/gre/~Common~gre?ver=13.1.0.7" + } + } + ] + + This method returns that data as a simple profile name. For instance, + + /Common/gre + + This allows us to do comparisons of it in the Difference class and then, + as needed, translate it back to the more complex form in the UsableChanges + class. + + Returns: + string: The simple form representation of the tunnel + """ + if self._values['ip_encapsulation'] is None and self.inherit_profile == 'yes': + return 'inherit' + if self._values['ip_encapsulation'] is None and self.inherit_profile == 'no': + return '' + if self._values['ip_encapsulation'] is None: + return None + + # There can be only one + tunnel = self._values['ip_encapsulation'][0] + + return fq_name(tunnel['partition'], tunnel['name']) + + @property + def inherit_profile(self): + return flatten_boolean(self._values['inherit_profile']) + + @property + def allow(self): + if self._values['allow'] is None: + return '' + if self._values['allow'][0] == 'All': + return 'all' + allow = self._values['allow'] + result = list(set([str(x) for x in allow])) + result = sorted(result) + return result + + @property + def rate_limit(self): + if self._values['rate_limit'] is None: + return None + if self._values['rate_limit'] == 'disabled': + return 0 + return int(self._values['rate_limit']) + + @property + def state(self): + if (self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr', 'fqdn-up', 'fqdn-down'] + and self._values['session'] in ['user-enabled', 'monitor-enabled']): + return 'present' + elif self._values['state'] in ['down', 'up', 'checking'] and self._values['session'] == 'monitor-enabled': + # monitor-enabled + checking: + # Monitor is checking to see state of pool member. For instance, + # whether it is up or down + # + # monitor-enabled + down: + # Monitor returned and determined that pool member is down. + # + # monitor-enabled + up + # Monitor returned and determined that pool member is up. + return 'present' + elif self._values['state'] in ['user-down'] and self._values['session'] in ['user-disabled']: + return 'forced_offline' + else: + return 'disabled' + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + else: + return 'all' + + @property + def monitors_list(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def monitors(self): + if self._values['monitors'] is None: + return None + if self._values['monitors'] == 'default': + return 'default' + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.availability_requirement_type == 'at_least': + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + else: + result = ' and '.join(monitors).strip() + + return result + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + The monitor string for a Require monitor looks like this. + min 1 of { /Common/gateway_icmp } + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return matches.group('least') + + @property + def fqdn_auto_populate(self): + if self._values['fqdn'] is None: + return None + if 'autopopulate' in self._values['fqdn']: + if self._values['fqdn']['autopopulate'] == 'enabled': + return True + return False + + @property + def fqdn(self): + if self._values['fqdn'] is None: + return None + if 'tmName' in self._values['fqdn']: + return self._values['fqdn']['tmName'] + + +class NodeApiParameters(Parameters): + pass + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def monitors(self): + monitor_string = self._values['monitors'] + if monitor_string is None: + return None + if '{' in monitor_string and '}' in monitor_string: + tmp = monitor_string.strip('}').split('{') + monitor = ''.join(tmp).rstrip() + return monitor + return monitor_string + + +class ReportableChanges(Changes): + @property + def ssl_cipher_suite(self): + default = ':'.join(sorted(Parameters._ciphers.split(':'))) + if self._values['ssl_cipher_suite'] == default: + return 'default' + else: + return self._values['ssl_cipher_suite'] + + @property + def fqdn_auto_populate(self): + if self._values['fqdn'] is None: + return None + if 'autopopulate' in self._values['fqdn']: + if self._values['fqdn']['autopopulate'] == 'enabled': + return True + return False + + @property + def fqdn(self): + if self._values['fqdn'] is None: + return None + if 'tmName' in self._values['fqdn']: + return self._values['fqdn']['tmName'] + + @property + def state(self): + if (self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr', 'fqdn-up'] and + self._values['session'] in ['user-enabled']): + return 'present' + elif self._values['state'] in ['down', 'up', 'checking'] and self._values['session'] == 'monitor-enabled': + return 'present' + elif self._values['state'] in ['user-down'] and self._values['session'] in ['user-disabled']: + return 'forced_offline' + else: + return 'disabled' + + @property + def monitors(self): + if self._values['monitors'] is None: + return [] + try: + result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result.sort() + return result + except Exception: + return self._values['monitors'] + + @property + def availability_requirement_type(self): + if self._values['monitors'] is None: + return None + if 'min ' in self._values['monitors']: + return 'at_least' + else: + return 'all' + + @property + def at_least(self): + """Returns the 'at least' value from the monitor string. + The monitor string for a Require monitor looks like this. + min 1 of { /Common/gateway_icmp } + This method parses out the first of the numeric values. This values represents + the "at_least" value that can be updated in the module. + Returns: + int: The at_least value if found. None otherwise. + """ + if self._values['monitors'] is None: + return None + pattern = r'min\s+(?P\d+)\s+of\s+' + matches = re.search(pattern, self._values['monitors']) + if matches is None: + return None + return int(matches.group('least')) + + @property + def availability_requirements(self): + if self._values['monitors'] is None: + return None + result = dict() + result['type'] = self.availability_requirement_type + result['at_least'] = self.at_least + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def state(self): + if self.want.state == self.have.state: + return None + if self.want.state == 'forced_offline': + return { + 'state': 'user-down', + 'session': 'user-disabled' + } + elif self.want.state == 'disabled': + return { + 'state': 'user-up', + 'session': 'user-disabled' + } + elif self.want.state in ['present', 'enabled']: + return { + 'state': 'user-up', + 'session': 'user-enabled' + } + + @property + def fqdn_auto_populate(self): + if self.want.fqdn_auto_populate is not None: + if self.want.fqdn_auto_populate != self.have.fqdn_auto_populate: + raise F5ModuleError( + "The fqdn_auto_populate cannot be changed once it has been set." + ) + + @property + def monitors(self): + if self.want.monitors is None: + return None + if self.want.monitors == 'default' and self.have.monitors == 'default': + return None + if self.want.monitors == 'default' and self.have.monitors is None: + return None + if self.want.monitors == 'default' and len(self.have.monitors) > 0: + return 'default' + # this is necessary as in v12 there is a bug where returned value has a space at the end + if self.want.monitors == '/Common/none' and self.have.monitors in ['/Common/none', '/Common/none ']: + return None + if self.have.monitors is None: + return self.want.monitors + if self.have.monitors != self.want.monitors: + return self.want.monitors + + @property + def ip_encapsulation(self): + result = cmp_str_with_none(self.want.ip_encapsulation, self.have.ip_encapsulation) + if result is None: + return None + if result == 'inherit': + return dict( + inherit_profile='enabled', + ip_encapsulation=[] + ) + elif result in ['', 'none']: + return dict( + inherit_profile='disabled', + ip_encapsulation=[] + ) + else: + return dict( + inherit_profile='disabled', + ip_encapsulation=[ + dict( + name=os.path.basename(result).strip('/'), + partition=os.path.dirname(result) + ) + ] + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = None + self.have = None + self.changes = None + self.replace_all_with = False + self.purge_links = list() + self.on_device = None + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + wants = None + if self.module.params['replace_all_with']: + self.replace_all_with = True + + if self.module.params['aggregate']: + wants = self.merge_defaults_for_aggregate(self.module.params) + + result = dict() + changed = False + if self.replace_all_with and self.purge_links: + self.purge() + changed = True + + if self.module.params['aggregate']: + result['aggregate'] = list() + for want in wants: + output = self.execute(want) + if output['changed']: + changed = output['changed'] + result['aggregate'].append(output) + else: + output = self.execute(self.module.params) + if output['changed']: + changed = output['changed'] + result.update(output) + if changed: + result['changed'] = True + send_teem(start, self.client, self.module, version) + return result + + def merge_defaults_for_aggregate(self, params): + defaults = deepcopy(params) + aggregate = defaults.pop('aggregate') + + for i, j in enumerate(aggregate): + for k, v in iteritems(defaults): + if k != 'replace_all_with': + if j.get(k, None) is None and v is not None: + aggregate[i][k] = v + + if self.replace_all_with: + self.compare_aggregate_names(aggregate) + + return aggregate + + def _filter_ephemerals(self): + on_device = self._read_purge_collection() + if not on_device: + self.on_device = [] + return + self.on_device = [member for member in on_device if member['ephemeral'] != "true"] + + def compare_fqdns(self, items): + if any('fqdn' in item for item in items): + aggregates = [item['fqdn'] for item in items if 'fqdn' in item and item['fqdn']] + collection = [member['fqdn']['tmName'] for member in self.on_device if 'tmName' in member['fqdn']] + + diff = set(collection) - set(aggregates) + + if diff: + fqdns = [ + member['selfLink'] for member in self.on_device + if 'tmName' in member['fqdn'] and member['fqdn']['tmName'] in diff + ] + self.purge_links.extend(fqdns) + return True + return False + return False + + def _join_address_port(self, item): + if 'port' not in item: + raise F5ModuleError( + "Aggregates must be provided with both address and port." + ) + delimiter = ':' + # If user provides us a name for the aggregate element, we use its name with port, this is + # how F5 BIG-IP behaves as well. + if 'name' in item and item['name']: + return '{0}{1}{2}'.format(item['name'], delimiter, item['port']) + try: + if validate_ip_v6_address(item['address']): + delimiter = '.' + except TypeError: + pass + return '{0}{1}{2}'.format(item['address'], delimiter, item['port']) + + def compare_addresses(self, items): + if any('address' in item for item in items): + aggregates = [self._join_address_port(item) for item in items if 'address' in item and item['address']] + collection = [member['name'] for member in self.on_device] + diff = set(collection) - set(aggregates) + if diff: + addresses = [item['selfLink'] for item in self.on_device if item['name'] in diff] + self.purge_links.extend(addresses) + return True + return False + return False + + def compare_aggregate_names(self, items): + self._filter_ephemerals() + if not self.on_device: + return False + fqdns = self.compare_fqdns(items) + addresses = self.compare_addresses(items) + + if self.purge_links: + if fqdns: + if not addresses: + self.purge_links.extend([item['selfLink'] for item in self.on_device if 'tmName' not in item['fqdn']]) + + def execute(self, params=None): + self.want = ModuleParameters(params=params) + self.have = ApiParameters() + self.changes = UsableChanges() + + changed = False + result = dict() + state = params['state'] + if state in ['present', 'enabled', 'disabled', 'forced_offline']: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + elif not self.want.preserve_node and self.node_exists(): + return self.remove_node_from_device() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if not self.want.preserve_node: + self.remove_node_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def purge(self): + if self.module.check_mode: + return True + if not self.pool_exist(): + raise F5ModuleError('The specified pool does not exist') + self.purge_from_device() + return True + + def create(self): + if self.want.reuse_nodes: + self._update_address_with_existing_nodes() + + if self.want.name and not any(x for x in [self.want.address, self.want.fqdn_name]): + self._set_host_by_name() + + if self.want.ip_encapsulation == '': + self.changes.update({'inherit_profile': 'enabled'}) + self.changes.update({'profiles': []}) + elif self.want.ip_encapsulation: + # Read the current list of tunnels so that IP encapsulation + # checking can take place. + tunnels_gre = self.read_current_tunnels_from_device('gre') + tunnels_ipip = self.read_current_tunnels_from_device('ipip') + tunnels = tunnels_gre + tunnels_ipip + if self.want.ip_encapsulation not in tunnels: + raise F5ModuleError( + "The specified 'ip_encapsulation' tunnel was not found on the system." + ) + self.changes.update({'inherit_profile': 'disabled'}) + + self._update_api_state_attributes() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + if not self.pool_exist(): + raise F5ModuleError('The specified pool does not exist') + + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=fq_name(self.want.partition, self.want.pool)), + transform_name(self.want.partition, self.want.full_name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def pool_exist(self): + if self.module.check_mode: + return True + if self.replace_all_with: + pool_name = transform_name(name=fq_name(self.module.params['partition'], self.module.params['pool'])) + else: + pool_name = transform_name(name=fq_name(self.want.partition, self.want.pool)) + + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + pool_name + + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def node_exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.node_name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def _set_host_by_name(self): + if is_valid_ip(self.want.name): + self.want.update({ + 'fqdn': None, + 'address': self.want.name + }) + else: + if not is_valid_hostname(self.want.name): + raise F5ModuleError( + "'name' is neither a valid IP address or FQDN name." + ) + self.want.update({ + 'fqdn': self.want.name, + 'address': None + }) + + def _update_api_state_attributes(self): + if self.want.state == 'forced_offline': + self.want.update({ + 'state': 'user-down', + 'session': 'user-disabled', + }) + elif self.want.state == 'disabled': + self.want.update({ + 'state': 'user-up', + 'session': 'user-disabled', + }) + elif self.want.state in ['present', 'enabled']: + self.want.update({ + 'state': 'user-up', + 'session': 'user-enabled', + }) + + def _update_address_with_existing_nodes(self): + try: + have = self.read_current_node_from_device(self.want.node_name) + if self.want.fqdn_auto_populate and self.want.reuse_nodes: + self.module.warn( + "'fqdn_auto_populate' is discarded in favor of the re-used node's auto-populate setting." + ) + self.want.update({ + 'fqdn_auto_populate': True if have.fqdn['autopopulate'] == 'enabled' else False + }) + if 'tmName' in have.fqdn: + self.want.update({ + 'fqdn': have.fqdn['tmName'], + 'address': 'any6' + }) + else: + self.want.update({ + 'address': have.address + }) + except Exception: + return None + + def _read_purge_collection(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=fq_name(self.module.params['partition'], self.module.params['pool'])) + ) + + query = '?$select=name,selfLink,fqdn,address,ephemeral' + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'items' in response: + return response['items'] + return [] + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.full_name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=fq_name(self.want.partition, self.want.pool)), + + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=fq_name(self.want.partition, self.want.pool)), + transform_name(self.want.partition, self.want.full_name) + + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=fq_name(self.want.partition, self.want.pool)), + transform_name(self.want.partition, self.want.full_name) + + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def remove_node_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.node_name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=fq_name(self.want.partition, self.want.pool)), + transform_name(self.want.partition, self.want.full_name) + + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + # Read the current list of tunnels so that IP encapsulation + # checking can take place. + tunnels_gre = self.read_current_tunnels_from_device('gre') + tunnels_ipip = self.read_current_tunnels_from_device('ipip') + response['tunnels'] = tunnels_gre + tunnels_ipip + + return ApiParameters(params=response) + + def read_current_node_from_device(self, node): + uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, node) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return NodeApiParameters(params=response) + raise F5ModuleError(resp.content) + + def read_current_tunnels_from_device(self, tunnel_type): + uri = "https://{0}:{1}/mgmt/tm/net/tunnels/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + tunnel_type + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + if 'items' not in response: + return [] + return [x['fullPath'] for x in response['items']] + raise F5ModuleError(resp.content) + + def _prepare_links(self, collection): + # this is to ensure no duplicates are in the provided collection + no_dupes = list(set(collection)) + links = list() + purge_paths = [urlparse(link).path for link in no_dupes] + + for path in purge_paths: + link = "https://{0}:{1}{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + path + ) + links.append(link) + return links + + def purge_from_device(self): + links = self._prepare_links(self.purge_links) + + with TransactionContextManager(self.client) as transact: + for link in links: + resp = transact.api.delete(link) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + return True + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + element_spec = dict( + address=dict(aliases=['host', 'ip']), + fqdn=dict( + aliases=['hostname'] + ), + name=dict(), + port=dict(type='int'), + connection_limit=dict(type='int'), + description=dict(), + rate_limit=dict(type='int'), + ratio=dict(type='int'), + preserve_node=dict(type='bool'), + priority_group=dict(type='int'), + state=dict( + default='present', + choices=['absent', 'present', 'enabled', 'disabled', 'forced_offline'] + ), + fqdn_auto_populate=dict(type='bool'), + reuse_nodes=dict(type='bool', default=True), + availability_requirements=dict( + type='dict', + options=dict( + type=dict( + choices=['all', 'at_least'], + required=True + ), + at_least=dict(type='int'), + ), + required_if=[ + ['type', 'at_least', ['at_least']], + ] + ), + monitors=dict( + type='list', + elements='str', + ), + ip_encapsulation=dict(), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + ) + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + self.argument_spec = dict( + aggregate=dict( + type='list', + elements='dict', + options=aggregate_spec, + aliases=['members'], + mutually_exclusive=[ + ['address', 'fqdn'] + ], + required_one_of=[ + ['address', 'fqdn'] + ], + ), + replace_all_with=dict( + type='bool', + aliases=['purge'], + default='no' + ), + pool=dict(required=True), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + ) + + self.argument_spec.update(element_spec) + self.argument_spec.update(f5_argument_spec) + + self.mutually_exclusive = [ + ['address', 'aggregate'], + ['fqdn', 'aggregate'] + ] + self.required_one_of = [ + ['address', 'fqdn', 'aggregate'], + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + required_one_of=spec.required_one_of, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_analytics.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_analytics.py new file mode 100644 index 00000000..023290fa --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_analytics.py @@ -0,0 +1,756 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_analytics +short_description: Manage HTTP analytics profiles on a BIG-IP +description: + - Manage HTTP analytics profiles on a BIG-IP device. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(analytics) profile. + type: str + description: + description: + - Description of the profile. + type: str + collect_geo: + description: + - Enables or disables the collection of the names of the countries + from where the traffic was sent. + type: bool + collect_ip: + description: + - Enables or disables the collection of client IPs statistics. + type: bool + collect_max_tps_and_throughput: + description: + - Enables or disables the collection of maximum TPS and throughput + for all collected entities. + type: bool + collect_page_load_time: + description: + - Enables or disables the collection of the page load time + statistics. + type: bool + collect_url: + description: + - Enables or disables the collection of requested URL statistics. + type: bool + collect_user_agent: + description: + - Enables or disables the collection of user agents. + type: bool + collect_user_sessions: + description: + - Enables or disables the collection of the unique user sessions. + type: bool + collected_stats_external_logging: + description: + - Enables or disables the external logging of the collected + statistics. + type: bool + collected_stats_internal_logging: + description: + - Enables or disables the internal logging of the collected + statistics. + type: bool + external_logging_publisher: + description: + - Specifies the external logging publisher used to send statistical + data to one or more destinations. + type: str + notification_by_syslog: + description: + - Enables or disables logging of the analytics alerts into the + Syslog. + type: bool + notification_by_email: + description: + - Enables or disables sending the analytics alerts by email. + type: bool + notification_email_addresses: + description: + - Specifies which email addresses receive alerts by email when + C(notification_by_email) is enabled. + type: list + elements: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a profile + bigip_profile_analytics: + name: profile1 + 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. + returned: changed + type: str + sample: Foo is bar +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_simple_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'collectGeo': 'collect_geo', + 'collectIp': 'collect_ip', + 'collectMaxTpsAndThroughput': 'collect_max_tps_and_throughput', + 'collectPageLoadTime': 'collect_page_load_time', + 'collectUrl': 'collect_url', + 'collectUserAgent': 'collect_user_agent', + 'collectUserSessions': 'collect_user_sessions', + 'collectedStatsExternalLogging': 'collected_stats_external_logging', + 'collectedStatsInternalLogging': 'collected_stats_internal_logging', + 'externalLoggingPublisher': 'external_logging_publisher', + 'notificationBySyslog': 'notification_by_syslog', + 'notificationByEmail': 'notification_by_email', + 'notificationEmailAddresses': 'notification_email_addresses' + } + + api_attributes = [ + 'description', + 'defaultsFrom', + 'collectGeo', + 'collectIp', + 'collectMaxTpsAndThroughput', + 'collectPageLoadTime', + 'collectUrl', + 'collectUserAgent', + 'collectUserSessions', + 'collectedStatsExternalLogging', + 'collectedStatsInternalLogging', + 'externalLoggingPublisher', + 'notificationBySyslog', + 'notificationByEmail', + 'notificationEmailAddresses', + ] + + returnables = [ + 'collect_geo', + 'collect_ip', + 'collect_max_tps_and_throughput', + 'collect_page_load_time', + 'collect_url', + 'collect_user_agent', + 'collect_user_sessions', + 'collected_stats_external_logging', + 'collected_stats_internal_logging', + 'description', + 'external_logging_publisher', + 'notification_by_syslog', + 'notification_by_email', + 'notification_email_addresses', + 'parent', + ] + + updatables = [ + 'collect_geo', + 'collect_ip', + 'collect_max_tps_and_throughput', + 'collect_page_load_time', + 'collect_url', + 'collect_user_agent', + 'collect_user_sessions', + 'collected_stats_external_logging', + 'collected_stats_internal_logging', + 'description', + 'external_logging_publisher', + 'notification_by_syslog', + 'notification_by_email', + 'notification_email_addresses', + 'parent', + ] + + @property + def external_logging_publisher(self): + if self._values['external_logging_publisher'] is None: + return None + if self._values['external_logging_publisher'] in ['none', '']: + return '' + result = fq_name(self.partition, self._values['external_logging_publisher']) + return result + + @property + def collect_geo(self): + return flatten_boolean(self._values['collect_geo']) + + @property + def collect_ip(self): + return flatten_boolean(self._values['collect_ip']) + + @property + def collect_max_tps_and_throughput(self): + return flatten_boolean(self._values['collect_max_tps_and_throughput']) + + @property + def collect_page_load_time(self): + return flatten_boolean(self._values['collect_page_load_time']) + + @property + def collect_url(self): + return flatten_boolean(self._values['collect_url']) + + @property + def collect_user_agent(self): + return flatten_boolean(self._values['collect_user_agent']) + + @property + def collect_user_sessions(self): + return flatten_boolean(self._values['collect_user_sessions']) + + @property + def collected_stats_external_logging(self): + return flatten_boolean(self._values['collected_stats_external_logging']) + + @property + def collected_stats_internal_logging(self): + return flatten_boolean(self._values['collected_stats_internal_logging']) + + @property + def notification_by_syslog(self): + return flatten_boolean(self._values['notification_by_syslog']) + + @property + def notification_by_email(self): + return flatten_boolean(self._values['notification_by_email']) + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def notification_email_addresses(self): + if self._values['notification_email_addresses'] is None: + return None + elif (len(self._values['notification_email_addresses']) == 1 and + self._values['notification_email_addresses'][0] in ['', 'none']): + return [] + return self._values['notification_email_addresses'] + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def collect_geo(self): + if self._values['collect_geo'] is None: + return None + elif self._values['collect_geo'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def collect_ip(self): + if self._values['collect_ip'] is None: + return None + elif self._values['collect_ip'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def collect_max_tps_and_throughput(self): + if self._values['collect_max_tps_and_throughput'] is None: + return None + elif self._values['collect_max_tps_and_throughput'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def collect_page_load_time(self): + if self._values['collect_page_load_time'] is None: + return None + elif self._values['collect_page_load_time'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def collect_url(self): + if self._values['collect_url'] is None: + return None + elif self._values['collect_url'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def collect_user_agent(self): + if self._values['collect_user_agent'] is None: + return None + elif self._values['collect_user_agent'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def collect_user_sessions(self): + if self._values['collect_user_sessions'] is None: + return None + elif self._values['collect_user_sessions'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def collected_stats_external_logging(self): + if self._values['collected_stats_external_logging'] is None: + return None + elif self._values['collected_stats_external_logging'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def collected_stats_internal_logging(self): + if self._values['collected_stats_internal_logging'] is None: + return None + elif self._values['collected_stats_internal_logging'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def notification_by_syslog(self): + if self._values['notification_by_syslog'] is None: + return None + elif self._values['notification_by_syslog'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def notification_by_email(self): + if self._values['notification_by_email'] is None: + return None + elif self._values['notification_by_email'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def collect_geo(self): + return flatten_boolean(self._values['collect_geo']) + + @property + def collect_ip(self): + return flatten_boolean(self._values['collect_ip']) + + @property + def collect_max_tps_and_throughput(self): + return flatten_boolean(self._values['collect_max_tps_and_throughput']) + + @property + def collect_page_load_time(self): + return flatten_boolean(self._values['collect_page_load_time']) + + @property + def collect_url(self): + return flatten_boolean(self._values['collect_url']) + + @property + def collect_user_agent(self): + return flatten_boolean(self._values['collect_user_agent']) + + @property + def collect_user_sessions(self): + return flatten_boolean(self._values['collect_user_sessions']) + + @property + def collected_stats_external_logging(self): + return flatten_boolean(self._values['collected_stats_external_logging']) + + @property + def collected_stats_internal_logging(self): + return flatten_boolean(self._values['collected_stats_internal_logging']) + + @property + def notification_by_syslog(self): + return flatten_boolean(self._values['notification_by_syslog']) + + @property + def notification_by_email(self): + return flatten_boolean(self._values['notification_by_email']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + if self.want.description is None: + return None + if self.have.description is None and self.want.description == '': + return None + if self.want.description != self.have.description: + return self.want.description + + @property + def notification_email_addresses(self): + return cmp_simple_list(self.want.notification_email_addresses, self.have.notification_email_addresses) + + @property + def external_logging_publisher(self): + if self.want.external_logging_publisher is None: + return None + if self.have.external_logging_publisher is None and self.want.external_logging_publisher == '': + return None + if self.want.external_logging_publisher != self.have.external_logging_publisher: + return self.want.external_logging_publisher + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/analytics/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/profile/analytics/".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, 409, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/analytics/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/analytics/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/analytics/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + description=dict(), + collect_geo=dict(type='bool'), + collect_ip=dict(type='bool'), + collect_max_tps_and_throughput=dict(type='bool'), + collect_page_load_time=dict(type='bool'), + collect_url=dict(type='bool'), + collect_user_agent=dict(type='bool'), + collect_user_sessions=dict(type='bool'), + collected_stats_external_logging=dict(type='bool'), + collected_stats_internal_logging=dict(type='bool'), + external_logging_publisher=dict(), + notification_by_syslog=dict(type='bool'), + notification_by_email=dict(type='bool'), + notification_email_addresses=dict( + type='list', + elements='str', + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_client_ssl.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_client_ssl.py new file mode 100644 index 00000000..0cdb27be --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_client_ssl.py @@ -0,0 +1,1244 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_client_ssl +short_description: Manages client SSL profiles on a BIG-IP +description: + - Manages client SSL profiles on a BIG-IP device. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. By default, this value is the C(clientssl) + parent on the C(Common) partition. + type: str + ciphers: + description: + - Specifies the list of ciphers the system supports. + - When the C(cipher_group) parameter is in use, the C(ciphers) parameter needs to be set to either C(none) or C(''). + type: str + cipher_group: + description: + - Specifies the cipher group to assign to this profile. + - When the C(ciphers) parameter is in use, the C(cipher_group) must be set to either C(none) or C(''). + - When creating a new profile with C(cipher_group), if the parent profile has C(ciphers) set by default, + the C(cipher) parameter must be set to C(none) or C('') during creation. + - The parameter only works on TMOS version 13.x and later. + type: str + version_added: "1.2.0" + cert_key_chain: + description: + - One or more certificates and keys to associate with the SSL profile. This + option is always a list. The keys in the list dictate the details of the + client/key/chain combination. Note that BIG-IPs can only have one of each + type of each certificate/key type. This means you can only have one + RSA, one DSA, and one ECDSA per profile. If you attempt to assign two + RSA, DSA, or ECDSA certificate/key combo, the device rejects it. + - This list is a complex list that specifies a number of keys. + type: list + elements: dict + suboptions: + cert: + description: + - Specifies a certificate name for use. + type: str + required: True + key: + description: + - Contains a key name. + type: str + required: True + chain: + description: + - Contains a certificate chain relevant to the certificate and key + mentioned previously. + - This key is optional. + type: str + passphrase: + description: + - Contains the passphrase of the key file, if required. + - Passphrases are encrypted on the remote BIG-IP device. Therefore, there is no way + to compare them when updating a client SSL profile. Due to this, if you specify a + passphrase, this module will always register a C(changed) event. + type: str + true_names: + description: + - When C(yes), the module will not append C(.crt) and C(.key) extensions to the given certificate and key names. + - When C(no), the module will append C(.crt) and C(.key) extensions to the given certificate and key names. + type: bool + default: no + version_added: "1.1.0" + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + options: + description: + - Options the system uses for SSL processing in the form of a list. When + creating a new profile, the list is provided by the parent profile. + - When C('') or C(none), all options for SSL processing are disabled. + type: list + elements: str + choices: + - netscape-reuse-cipher-change-bug + - microsoft-big-sslv3-buffer + - msie-sslv2-rsa-padding + - ssleay-080-client-dh-bug + - tls-d5-bug + - tls-block-padding-bug + - dont-insert-empty-fragments + - no-ssl + - no-dtls + - no-session-resumption-on-renegotiation + - no-tlsv1.1 + - no-tlsv1.2 + - no-tlsv1.3 + - single-dh-use + - ephemeral-rsa + - cipher-server-preference + - tls-rollback-bug + - no-sslv2 + - no-sslv3 + - no-tls + - no-tlsv1 + - pkcs1-check-1 + - pkcs1-check-2 + - netscape-ca-dn-bug + - netscape-demo-cipher-change-bug + - "none" + secure_renegotiation: + description: + - Specifies the method of secure renegotiations for SSL connections. When + creating a new profile, the setting is provided by the parent profile. + - When C(request), the system requests secure renegotiation of SSL + connections. + - C(require) is a default setting and when set, the system permits initial SSL + handshakes from clients, but terminates renegotiations from unpatched clients. + - With the C(require-strict) setting, the system requires strict renegotiation of SSL + connections. In this mode, the system refuses connections to insecure servers, + and terminates existing SSL connections to insecure servers. + type: str + choices: + - require + - require-strict + - request + allow_non_ssl: + description: + - Enables or disables acceptance of non-SSL connections. + - When creating a new profile, the setting is provided by the parent profile. + type: bool + server_name: + description: + - Specifies the fully qualified DNS hostname of the server used in Server Name Indication communications. + When creating a new profile, the setting is provided by the parent profile. + - The server name can also be a wildcard string containing the asterisk C(*) character. + type: str + sni_default: + description: + - Indicates the system uses this profile as the default SSL profile when there is no match to the + server name, or when the client provides no SNI extension support. + - When creating a new profile, the setting is provided by the parent profile. + - There can be only one SSL profile with this setting enabled. + type: bool + sni_require: + description: + - Requires the network peers also provide SNI support. This setting only takes effect when C(sni_default) is + set to C(true). + - When creating a new profile, the setting is provided by the parent profile. + type: bool + strict_resume: + description: + - Enables or disables the resumption of SSL sessions after an unclean shutdown. + - When creating a new profile, the setting is provided by the parent profile. + type: bool + client_certificate: + description: + - Specifies the way the system handles client certificates. + - When C(ignore), specifies the system ignores certificates from client + systems. + - When C(require), specifies the system requires a client to present a + valid certificate. + - When C(request), specifies the system requests a valid certificate from a + client but always authenticate the client. + type: str + choices: + - ignore + - require + - request + client_auth_frequency: + description: + - Specifies the frequency of client authentication for an SSL session. + - When C(once), specifies the system authenticates the client once for an + SSL session. + - When C(always), specifies the system authenticates the client once for an + SSL session and also upon reuse of that session. + type: str + choices: + - once + - always + renegotiation: + description: + - Enables or disables SSL renegotiation. + - When creating a new profile, the setting is provided by the parent profile. + type: bool + retain_certificate: + description: + - When C(yes), the client certificate is retained in SSL session. + type: bool + cert_auth_depth: + description: + - Specifies the maximum number of certificates to be traversed in a client + certificate chain. + type: int + trusted_cert_authority: + description: + - Specifies a client CA the system trusts. + type: str + advertised_cert_authority: + description: + - Specifies the CAs the system advertises to clients is being trusted + by the profile. + type: str + client_auth_crl: + description: + - Specifies the name of a file containing a list of revoked client certificates. + type: str + allow_expired_crl: + description: + - Instructs the system to use the specified CRL file even if it has expired. + type: bool + cache_size: + description: + - Specifies the number of sessions in the SSL session cache. + - The valid value range is between 0 and 4194304 inclusive. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: int + version_added: "1.0.0" + cache_timeout: + description: + - Specifies the timeout value in seconds of the SSL session cache entries. + - Acceptable values are between 0 and 86400 inclusive. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: int + version_added: "1.0.0" + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +notes: + - Requires BIG-IP software version >= 12 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create client SSL profile + bigip_profile_client_ssl: + state: present + name: my_profile + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create client SSL profile with specific ciphers + bigip_profile_client_ssl: + state: present + name: my_profile + ciphers: "!SSLv3:!SSLv2:ECDHE+AES-GCM+SHA256:ECDHE-RSA-AES128-CBC-SHA" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create client SSL profile with specific cipher group + bigip_profile_client_ssl: + state: present + name: my_profile + ciphers: "none" + cipher_group: "/Common/f5-secure" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create client SSL profile with specific SSL options + bigip_profile_client_ssl: + state: present + name: my_profile + options: + - no-sslv2 + - no-sslv3 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create client SSL profile require secure renegotiation + bigip_profile_client_ssl: + state: present + name: my_profile + secure_renegotiation: request + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create a client SSL profile with a cert/key/chain setting + bigip_profile_client_ssl: + state: present + name: my_profile + cert_key_chain: + - cert: bigip_ssl_cert1 + key: bigip_ssl_key1 + chain: bigip_ssl_cert1 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +ciphers: + description: The ciphers applied to the profile. + returned: changed + type: str + sample: "!SSLv3:!SSLv2:ECDHE+AES-GCM+SHA256:ECDHE-RSA-AES128-CBC-SHA" +cipher_group: + description: The cipher group applied to the profile. + returned: changed + type: str + sample: "/Common/f5-secure" +options: + description: The list of options for SSL processing. + returned: changed + type: list + sample: ['no-sslv2', 'no-sslv3'] +secure_renegotiation: + description: The method of secure SSL renegotiation. + returned: changed + type: str + sample: request +allow_non_ssl: + description: Acceptance of non-SSL connections. + returned: changed + type: bool + sample: yes +strict_resume: + description: Resumption of SSL sessions after an unclean shutdown. + returned: changed + type: bool + sample: yes +renegotiation: + description: Renegotiation of SSL sessions. + returned: changed + type: bool + sample: yes +cache_size: + description: Specifies the number of sessions in the SSL session cache. + returned: changed + type: int + sample: 2000 +cache_timeout: + description: Specifies the timeout value in seconds of the SSL session cache entries. + returned: changed + type: int + sample: 1800 +''' + +import os +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name, is_empty_list +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'certKeyChain': 'cert_key_chain', + 'cipherGroup': 'cipher_group', + 'defaultsFrom': 'parent', + 'allowNonSsl': 'allow_non_ssl', + 'secureRenegotiation': 'secure_renegotiation', + 'tmOptions': 'options', + 'sniDefault': 'sni_default', + 'sniRequire': 'sni_require', + 'serverName': 'server_name', + 'peerCertMode': 'client_certificate', + 'authenticate': 'client_auth_frequency', + 'retainCertificate': 'retain_certificate', + 'authenticateDepth': 'cert_auth_depth', + 'caFile': 'trusted_cert_authority', + 'clientCertCa': 'advertised_cert_authority', + 'crlFile': 'client_auth_crl', + 'allowExpiredCrl': 'allow_expired_crl', + 'strictResume': 'strict_resume', + 'cacheSize': 'cache_size', + 'cacheTimeout': 'cache_timeout', + } + + api_attributes = [ + 'ciphers', + 'cipherGroup', + 'certKeyChain', + 'defaultsFrom', + 'tmOptions', + 'secureRenegotiation', + 'allowNonSsl', + 'sniDefault', + 'sniRequire', + 'serverName', + 'peerCertMode', + 'authenticate', + 'retainCertificate', + 'authenticateDepth', + 'caFile', + 'clientCertCa', + 'crlFile', + 'allowExpiredCrl', + 'strictResume', + 'renegotiation', + 'cacheSize', + 'cacheTimeout', + ] + + returnables = [ + 'ciphers', + 'cipher_group', + 'allow_non_ssl', + 'options', + 'secure_renegotiation', + 'cert_key_chain', + 'parent', + 'sni_default', + 'sni_require', + 'server_name', + 'client_certificate', + 'client_auth_frequency', + 'retain_certificate', + 'cert_auth_depth', + 'trusted_cert_authority', + 'advertised_cert_authority', + 'client_auth_crl', + 'allow_expired_crl', + 'strict_resume', + 'renegotiation', + 'cache_size', + 'cache_timeout', + ] + + updatables = [ + 'parent', + 'ciphers', + 'cipher_group', + 'cert_key_chain', + 'allow_non_ssl', + 'options', + 'secure_renegotiation', + 'sni_default', + 'sni_require', + 'server_name', + 'client_certificate', + 'client_auth_frequency', + 'retain_certificate', + 'cert_auth_depth', + 'trusted_cert_authority', + 'advertised_cert_authority', + 'client_auth_crl', + 'allow_expired_crl', + 'strict_resume', + 'renegotiation', + 'cache_size', + 'cache_timeout', + ] + + @property + def retain_certificate(self): + return flatten_boolean(self._values['retain_certificate']) + + @property + def allow_expired_crl(self): + return flatten_boolean(self._values['allow_expired_crl']) + + +class ModuleParameters(Parameters): + def _key_filename(self, name, true_name): + if true_name: + return name + if name.endswith('.key'): + return name + else: + return name + '.key' + + def _cert_filename(self, name, true_name): + if true_name: + return name + if name.endswith('.crt'): + return name + else: + return name + '.crt' + + def _get_chain_value(self, item, true_name): + if 'chain' not in item or item['chain'] in ('none', None, 'None'): + result = 'none' + else: + result = self._cert_filename(fq_name(self.partition, item['chain']), true_name) + return result + + def _get_true_names(self, item): + if 'true_names' not in item: + return False + result = flatten_boolean(item['true_names']) + if result == 'yes': + return True + if result == 'no': + return False + + @property + def parent(self): + if self._values['parent'] is None: + return None + if self._values['parent'] == 'clientssl': + return '/Common/clientssl' + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def cert_key_chain(self): + if self._values['cert_key_chain'] is None: + return None + result = [] + for item in self._values['cert_key_chain']: + if 'key' in item and 'cert' not in item: + raise F5ModuleError( + "When providing a 'key', you must also provide a 'cert'" + ) + if 'cert' in item and 'key' not in item: + raise F5ModuleError( + "When providing a 'cert', you must also provide a 'key'" + ) + item['true_names'] = self._get_true_names(item) + key = self._key_filename(item['key'], item['true_names']) + cert = self._cert_filename(item['cert'], item['true_names']) + chain = self._get_chain_value(item, item['true_names']) + name = os.path.basename(cert) + filename, ex = os.path.splitext(name) + tmp = { + 'name': filename, + 'cert': fq_name(self.partition, cert), + 'key': fq_name(self.partition, key), + 'chain': chain + } + if 'passphrase' in item and item['passphrase'] not in ('None', None, 'none'): + tmp['passphrase'] = item['passphrase'] + result.append(tmp) + result = sorted(result, key=lambda x: x['name']) + return result + + @property + def allow_non_ssl(self): + result = flatten_boolean(self._values['allow_non_ssl']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def strict_resume(self): + result = flatten_boolean(self._values['strict_resume']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def renegotiation(self): + result = flatten_boolean(self._values['renegotiation']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def options(self): + options = self._values['options'] + if options is None: + return None + if is_empty_list(options): + return [] + return options + + @property + def sni_require(self): + require = flatten_boolean(self._values['sni_require']) + default = self.sni_default + if require is None: + return None + if default in [None, False]: + if require == 'yes': + raise F5ModuleError( + "Cannot set 'sni_require' to {0} if 'sni_default' is set as {1}".format(require, default)) + if require == 'yes': + return True + else: + return False + + @property + def trusted_cert_authority(self): + if self._values['trusted_cert_authority'] is None: + return None + if self._values['trusted_cert_authority'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['trusted_cert_authority']) + return result + + @property + def advertised_cert_authority(self): + if self._values['advertised_cert_authority'] is None: + return None + if self._values['advertised_cert_authority'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['advertised_cert_authority']) + return result + + @property + def client_auth_crl(self): + if self._values['client_auth_crl'] is None: + return None + if self._values['client_auth_crl'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['client_auth_crl']) + return result + + @property + def ciphers(self): + if self._values['ciphers'] is None: + return None + if self._values['ciphers'] in ['', 'none']: + return 'none' + if self.cipher_group and self.cipher_group != 'none': + raise F5ModuleError("The cipher parameter must be set to 'none' if cipher_group is defined.") + return self._values['ciphers'] + + @property + def cipher_group(self): + if self._values['cipher_group'] is None: + return None + if self._values['cipher_group'] in ['', 'none']: + return 'none' + if self.ciphers and self.ciphers != 'none': + raise F5ModuleError("The cipher_group parameter must be set to 'none' if cipher is defined.") + result = fq_name(self.partition, self._values['cipher_group']) + return result + + +class ApiParameters(Parameters): + @property + def cert_key_chain(self): + if self._values['cert_key_chain'] is None: + return None + result = [] + for item in self._values['cert_key_chain']: + tmp = dict( + name=item['name'], + ) + for x in ['cert', 'key', 'chain', 'passphrase', 'true_names']: + if x in item: + tmp[x] = item[x] + if 'chain' not in item: + tmp['chain'] = 'none' + result.append(tmp) + result = sorted(result, key=lambda y: y['name']) + return result + + @property + def sni_default(self): + result = self._values['sni_default'] + if result is None: + return None + if result == 'true': + return True + else: + return False + + @property + def sni_require(self): + result = self._values['sni_require'] + if result is None: + return None + if result == 'true': + return True + else: + return False + + @property + def trusted_cert_authority(self): + if self._values['trusted_cert_authority'] in [None, 'none']: + return None + return self._values['trusted_cert_authority'] + + @property + def advertised_cert_authority(self): + if self._values['advertised_cert_authority'] in [None, 'none']: + return None + return self._values['advertised_cert_authority'] + + @property + def client_auth_crl(self): + if self._values['client_auth_crl'] in [None, 'none']: + return None + return self._values['client_auth_crl'] + + @property + def cache_size(self): + if self._values['cache_size'] is None: + return None + if 0 <= self._values['cache_size'] <= 4194304: + return self._values['cache_size'] + raise F5ModuleError( + "Valid 'cache_size' must be in range 0 - 4194304 sessions." + ) + + @property + def cache_timeout(self): + if self._values['cache_timeout'] is None: + return None + if 0 <= self._values['cache_timeout'] <= 4194304: + return self._values['cache_timeout'] + raise F5ModuleError( + "Valid 'cache_timeout' must be in range 0 - 86400 seconds." + ) + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def retain_certificate(self): + if self._values['retain_certificate'] is None: + return None + elif self._values['retain_certificate'] == 'yes': + return 'true' + return 'false' + + @property + def allow_expired_crl(self): + if self._values['allow_expired_crl'] is None: + return None + elif self._values['allow_expired_crl'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def allow_non_ssl(self): + if self._values['allow_non_ssl'] is None: + return None + elif self._values['allow_non_ssl'] == 'enabled': + return 'yes' + return 'no' + + @property + def strict_resume(self): + if self._values['strict_resume'] is None: + return None + elif self._values['strict_resume'] == 'enabled': + return 'yes' + return 'no' + + @property + def retain_certificate(self): + return flatten_boolean(self._values['retain_certificate']) + + @property + def allow_expired_crl(self): + return flatten_boolean(self._values['allow_expired_crl']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def to_tuple(self, items): + result = [] + for x in items: + tmp = [(str(k), str(v)) for k, v in iteritems(x)] + result += tmp + return result + + def _diff_complex_items(self, want, have): + if want == [] and have is None: + return None + if want is None: + return None + w = self.to_tuple(want) + h = self.to_tuple(have) + if set(w).issubset(set(h)): + return None + else: + return want + + @property + def cert_key_chain(self): + result = self._diff_complex_items(self.want.cert_key_chain, self.have.cert_key_chain) + return result + + @property + def options(self): + if self.want.options is None: + return None + # starting with v14 options may return as a space delimited string in curly + # braces, eg "{ option1 option2 }", or simply "none" to indicate empty set + if self.have.options is None or self.have.options == 'none': + self.have.options = [] + if not isinstance(self.have.options, list): + if self.have.options.startswith('{'): + self.have.options = self.have.options[2:-2].split(' ') + else: + self.have.options = [self.have.options] + if not self.want.options: + # we don't want options. If we have any, indicate we should remove, else noop + return [] if self.have.options else None + if not self.have.options: + return self.want.options + if set(self.want.options) != set(self.have.options): + return self.want.options + + @property + def sni_require(self): + if self.want.sni_require is None: + return None + if self.want.sni_require is False: + if self.have.sni_default is True and self.want.sni_default is None: + raise F5ModuleError( + "Cannot set 'sni_require' to {0} if 'sni_default' is {1}".format( + self.want.sni_require, self.have.sni_default) + ) + if self.want.sni_require == self.have.sni_require: + return None + return self.want.sni_require + + @property + def trusted_cert_authority(self): + if self.want.trusted_cert_authority is None: + return None + if self.want.trusted_cert_authority == '' and self.have.trusted_cert_authority is None: + return None + if self.want.trusted_cert_authority != self.have.trusted_cert_authority: + return self.want.trusted_cert_authority + + @property + def advertised_cert_authority(self): + if self.want.advertised_cert_authority is None: + return None + if self.want.advertised_cert_authority == '' and self.have.advertised_cert_authority is None: + return None + if self.want.advertised_cert_authority != self.have.advertised_cert_authority: + return self.want.advertised_cert_authority + + @property + def client_auth_crl(self): + if self.want.client_auth_crl is None: + return None + if self.want.client_auth_crl == '' and self.have.client_auth_crl is None: + return None + if self.want.client_auth_crl != self.have.client_auth_crl: + return self.want.client_auth_crl + + @property + def ciphers(self): + if self.want.ciphers is None: + return None + if self.want.ciphers == 'none' and self.have.ciphers == 'none': + return None + if self.want.ciphers != self.have.ciphers: + return self.want.ciphers + + @property + def cipher_group(self): + if self.want.cipher_group is None: + return None + if self.want.cipher_group == 'none' and self.have.cipher_group == 'none': + return None + if self.want.cipher_group != self.have.cipher_group: + return self.want.cipher_group + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/client-ssl/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/profile/client-ssl/".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, 404]: + 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/ltm/profile/client-ssl/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/client-ssl/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/client-ssl/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + ciphers=dict(), + cipher_group=dict(), + allow_non_ssl=dict(type='bool'), + secure_renegotiation=dict( + choices=['require', 'require-strict', 'request'] + ), + options=dict( + type='list', + elements='str', + choices=[ + 'netscape-reuse-cipher-change-bug', + 'microsoft-big-sslv3-buffer', + 'msie-sslv2-rsa-padding', + 'ssleay-080-client-dh-bug', + 'tls-d5-bug', + 'tls-block-padding-bug', + 'dont-insert-empty-fragments', + 'no-ssl', + 'no-dtls', + 'no-session-resumption-on-renegotiation', + 'no-tlsv1.1', + 'no-tlsv1.2', + 'no-tlsv1.3', + 'single-dh-use', + 'ephemeral-rsa', + 'cipher-server-preference', + 'tls-rollback-bug', + 'no-sslv2', + 'no-sslv3', + 'no-tls', + 'no-tlsv1', + 'pkcs1-check-1', + 'pkcs1-check-2', + 'netscape-ca-dn-bug', + 'netscape-demo-cipher-change-bug', + 'none', + ] + ), + cert_key_chain=dict( + type='list', + elements='dict', + options=dict( + cert=dict(required=True), + key=dict(required=True), + chain=dict(), + passphrase=dict(), + true_names=dict( + type='bool', + default='no' + ), + ) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ), + sni_default=dict(type='bool'), + sni_require=dict(type='bool'), + server_name=dict(), + client_certificate=dict( + choices=['require', 'ignore', 'request'] + ), + client_auth_frequency=dict( + choices=['once', 'always'] + ), + cert_auth_depth=dict(type='int'), + retain_certificate=dict(type='bool'), + trusted_cert_authority=dict(), + advertised_cert_authority=dict(), + client_auth_crl=dict(), + allow_expired_crl=dict(type='bool'), + strict_resume=dict(type='bool'), + renegotiation=dict(type='bool'), + cache_size=dict(type='int'), + cache_timeout=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_dns.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_dns.py new file mode 100644 index 00000000..71da678d --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_dns.py @@ -0,0 +1,749 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_dns +short_description: Manage DNS profiles on a BIG-IP +description: + - Manage DNS profiles on a BIG-IP. There are many DNS profiles options, each with their + own adjustments to the standard C(dns) profile. Users of this module should be aware + that many of the configurable options have no module default. Instead, the default is + assigned by the BIG-IP system itself which, in most cases, is acceptable. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the DNS profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(dns) profile. + type: str + enable_dns_express: + description: + - Specifies whether the DNS Express engine is enabled. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + - The DNS Express engine receives zone transfers from the authoritative DNS server + for the zone. If the C(enable_zone_transfer) setting is also C(yes) on this profile, + the DNS Express engine also responds to zone transfer requests made by the nameservers + configured as zone transfer clients for the DNS Express zone. + type: bool + enable_zone_transfer: + description: + - Specifies whether the system answers zone transfer requests for a DNS zone created + on the system. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + - The C(enable_dns_express) and C(enable_zone_transfer) settings on a DNS profile + affect how the system responds to zone transfer requests. + - When the C(enable_dns_express) and C(enable_zone_transfer) settings are both C(yes), + if a zone transfer request matches a DNS Express zone, DNS Express answers the + request. + - When the C(enable_dns_express) setting is C(no) and the C(enable_zone_transfer) + setting is C(yes), the BIG-IP system processes zone transfer requests based on the + last action and answers the request from local BIND or a pool member. + type: bool + enable_dnssec: + description: + - Specifies whether the system signs responses with DNSSEC keys and replies to DNSSEC + specific queries (for example, DNSKEY query type). + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + type: bool + enable_gtm: + description: + - Specifies whether the system uses Global Traffic Manager (now BIG-IP DNS) to manage the response. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + type: bool + process_recursion_desired: + description: + - Specifies whether to process client-side DNS packets with Recursion Desired set in + the header. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + - If set to C(no), processing of the packet is subject to the unhandled-query-action + option. + type: bool + use_local_bind: + description: + - Specifies whether the system forwards non-wide IP queries to the local BIND server + on the BIG-IP system. + - For best performance, disable this setting when using a DNS cache. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + type: bool + enable_dns_firewall: + description: + - Specifies whether the DNS firewall is enabled. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + type: bool + enable_cache: + description: + - Specifies whether the system caches DNS responses. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + - When C(yes), the BIG-IP system caches DNS responses handled by the virtual + servers associated with this profile. When you enable this setting, you must + also specify a value for C(cache_name). + - When C(no), the BIG-IP system does not cache DNS responses handled by the + virtual servers associated with this profile. However, the profile retains + the association with the DNS cache in the C(cache_name) parameter. Disable + this setting when you want to debug the system. + type: bool + cache_name: + description: + - Specifies the user-created cache the system uses to cache DNS responses. + - When you select a cache for the system to use, you must also set C(enable_dns_cache) + to C(yes) + type: str + unhandled_query_action: + description: + - Specifies the action to take when a query does not match a Wide IP or a DNS Express Zone. + - When C(allow), the BIG-IP system forwards queries to a DNS server or pool member. + If a pool is not associated with a listener and the Use BIND Server on BIG-IP setting + is set to Enabled, requests are forwarded to the local BIND server. + - When C(drop), the BIG-IP system does not respond to the query. + - When C(reject), the BIG-IP system returns the query with the REFUSED return code. + - When C(hint), the BIG-IP system returns the query with a list of root name servers. + - When C(no-error), the BIG-IP system returns the query with the NOERROR return code. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + type: str + choices: + - allow + - drop + - reject + - hint + - no-error + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a DNS profile + bigip_profile_dns: + name: foo + enable_dns_express: no + enable_dnssec: no + enable_gtm: no + process_recursion_desired: no + use_local_bind: no + enable_dns_firewall: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +enable_dns_express: + description: Whether DNS Express is enabled on the resource or not. + returned: changed + type: bool + sample: yes +enable_zone_transfer: + description: Whether zone transfer are enabled on the resource or not. + returned: changed + type: bool + sample: no +enable_dnssec: + description: Whether DNSSEC is enabled on the resource or not. + returned: changed + type: bool + sample: no +enable_gtm: + description: Whether GTM is used to manage the resource or not. + returned: changed + type: bool + sample: yes +process_recursion_desired: + description: Whether client-side DNS packets are processed with Recursion Desired set. + returned: changed + type: bool + sample: yes +use_local_bind: + description: Whether non-wide IP queries are forwarded to the local BIND server or not. + returned: changed + type: bool + sample: no +enable_dns_firewall: + description: Whether DNS firewall capability is enabled or not. + returned: changed + type: bool + sample: no +enable_cache: + description: Whether DNS caching is enabled or not. + returned: changed + type: bool + sample: no +cache_name: + description: Name of the cache used by DNS. + returned: changed + type: str + sample: /Common/cache1 +unhandled_query_action: + description: What to do with unhandled queries + returned: changed + type: str + sample: allow +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'enableDnsFirewall': 'enable_dns_firewall', + 'useLocalBind': 'use_local_bind', + 'processRd': 'process_recursion_desired', + 'enableGtm': 'enable_gtm', + 'enableDnssec': 'enable_dnssec', + 'processXfr': 'enable_zone_transfer', + 'enableDnsExpress': 'enable_dns_express', + 'defaultsFrom': 'parent', + 'enableCache': 'enable_cache', + 'cache': 'cache_name', + 'unhandledQueryAction': 'unhandled_query_action', + } + + api_attributes = [ + 'enableDnsFirewall', + 'useLocalBind', + 'processRd', + 'enableGtm', + 'enableDnssec', + 'processXfr', + 'enableDnsExpress', + 'defaultsFrom', + 'cache', + 'enableCache', + 'unhandledQueryAction', + ] + + returnables = [ + 'enable_dns_firewall', + 'use_local_bind', + 'process_recursion_desired', + 'enable_gtm', + 'enable_dnssec', + 'enable_zone_transfer', + 'enable_dns_express', + 'cache_name', + 'enable_cache', + 'unhandled_query_action', + ] + + updatables = [ + 'enable_dns_firewall', + 'use_local_bind', + 'process_recursion_desired', + 'enable_gtm', + 'enable_dnssec', + 'enable_zone_transfer', + 'enable_dns_express', + 'cache_name', + 'enable_cache', + 'unhandled_query_action', + ] + + +class ApiParameters(Parameters): + @property + def enable_dns_firewall(self): + if self._values['enable_dns_firewall'] is None: + return None + if self._values['enable_dns_firewall'] == 'yes': + return True + return False + + @property + def use_local_bind(self): + if self._values['use_local_bind'] is None: + return None + if self._values['use_local_bind'] == 'yes': + return True + return False + + @property + def process_recursion_desired(self): + if self._values['process_recursion_desired'] is None: + return None + if self._values['process_recursion_desired'] == 'yes': + return True + return False + + @property + def enable_gtm(self): + if self._values['enable_gtm'] is None: + return None + if self._values['enable_gtm'] == 'yes': + return True + return False + + @property + def enable_cache(self): + if self._values['enable_cache'] is None: + return None + if self._values['enable_cache'] == 'yes': + return True + return False + + @property + def enable_dnssec(self): + if self._values['enable_dnssec'] is None: + return None + if self._values['enable_dnssec'] == 'yes': + return True + return False + + @property + def enable_zone_transfer(self): + if self._values['enable_zone_transfer'] is None: + return None + if self._values['enable_zone_transfer'] == 'yes': + return True + return False + + @property + def enable_dns_express(self): + if self._values['enable_dns_express'] is None: + return None + if self._values['enable_dns_express'] == 'yes': + return True + return False + + @property + def unhandled_query_action(self): + if self._values['unhandled_query_action'] is None: + return None + elif self._values['unhandled_query_action'] == 'noerror': + return 'no-error' + return self._values['unhandled_query_action'] + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def cache_name(self): + if self._values['cache_name'] is None: + return None + if self._values['cache_name'] == '': + return '' + result = fq_name(self.partition, self._values['cache_name']) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def enable_dns_firewall(self): + if self._values['enable_dns_firewall'] is None: + return None + if self._values['enable_dns_firewall']: + return 'yes' + return 'no' + + @property + def use_local_bind(self): + if self._values['use_local_bind'] is None: + return None + if self._values['use_local_bind']: + return 'yes' + return 'no' + + @property + def process_recursion_desired(self): + if self._values['process_recursion_desired'] is None: + return None + if self._values['process_recursion_desired']: + return 'yes' + return 'no' + + @property + def enable_gtm(self): + if self._values['enable_gtm'] is None: + return None + if self._values['enable_gtm']: + return 'yes' + return 'no' + + @property + def enable_cache(self): + if self._values['enable_cache'] is None: + return None + if self._values['enable_cache']: + return 'yes' + return 'no' + + @property + def enable_dnssec(self): + if self._values['enable_dnssec'] is None: + return None + if self._values['enable_dnssec']: + return 'yes' + return 'no' + + @property + def enable_zone_transfer(self): + if self._values['enable_zone_transfer'] is None: + return None + if self._values['enable_zone_transfer']: + return 'yes' + return 'no' + + @property + def enable_dns_express(self): + if self._values['enable_dns_express'] is None: + return None + if self._values['enable_dns_express']: + return 'yes' + return 'no' + + @property + def unhandled_query_action(self): + if self._values['unhandled_query_action'] is None: + return None + elif self._values['unhandled_query_action'] == 'no-error': + return 'noerror' + return self._values['unhandled_query_action'] + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/dns/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.changes.enable_cache is True or self.have.enable_cache is True: + if not self.have.cache_name or self.changes.cache_name == '': + raise F5ModuleError( + "To enable DNS cache, a DNS cache must be specified." + ) + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.want.enable_cache is True and not self.want.cache_name: + raise F5ModuleError( + "You must specify a 'cache_name' when creating a DNS profile that sets 'enable_cache' to 'yes'." + ) + + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/profile/dns/".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, 404]: + 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/ltm/profile/dns/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/dns/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/dns/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + enable_dns_express=dict(type='bool'), + enable_zone_transfer=dict(type='bool'), + enable_dnssec=dict(type='bool'), + enable_gtm=dict(type='bool'), + process_recursion_desired=dict(type='bool'), + use_local_bind=dict(type='bool'), + enable_dns_firewall=dict(type='bool'), + enable_cache=dict(type='bool'), + unhandled_query_action=dict( + choices=['allow', 'drop', 'reject', 'hint', 'no-error'] + ), + cache_name=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_fastl4.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_fastl4.py new file mode 100644 index 00000000..a39cce00 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_fastl4.py @@ -0,0 +1,1457 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_fastl4 +short_description: Manages Fast L4 profiles +description: + - Manages Fast L4 profiles. Using the FastL4 profile can increase virtual server + performance and throughput for supported platforms by using the embedded Packet + Velocity Acceleration (ePVA) chip to accelerate traffic. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(fastL4) profile. + type: str + idle_timeout: + description: + - Specifies the length of time a connection is idle (has no traffic) before + the connection is eligible for deletion. + - When creating a new profile, if this parameter is not specified, the remote + device will choose a default value appropriate for the profile, based on its + C(parent) profile. + - When a number is specified, indicates the number of seconds the TCP + connection can remain idle before the system deletes it. + - When C(indefinite), specifies the system does not delete TCP connections + regardless of how long they remain idle. + - When C(0), or C(immediate), specifies the system deletes connections + immediately when they become idle. + type: str + client_timeout: + description: + - Specifies a timeout for Late Binding. + - This is the time limit for the client to provide the application data required to + select a back-end server, meaning the maximum time the BIG-IP system + waits for information about the sender and the target. + - This information typically arrives at the beginning of the FIX logon packet. + - When C(0), or C(immediate), allows for no time beyond the moment of the first packet + transmission. + - When C(indefinite), disables the limit. This allows the client unlimited time + to send the sender and target information. + type: str + description: + description: + - Description of the profile. + type: str + explicit_flow_migration: + description: + - Specifies whether a qualified late-binding connection requires an explicit iRule + command to migrate down to ePVA hardware. + - When C(no), a late-binding connection migrates down to ePVA immediately after + establishing the server-side connection. + - When C(yes), this parameter stops automatic migration to ePVA, and requires + the iRule explicitly trigger ePVA processing by invoking the C(release_flow) + iRule command. This allows an iRule author to control when the connection uses the + ePVA hardware. + type: bool + ip_df_mode: + description: + - Specifies the Don't Fragment (DF) bit setting in the IP Header of the outgoing TCP packet. + - When C(pmtu), sets the outgoing IP Header DF bit based on IP pmtu setting. + - When C(preserve), sets the outgoing Packet's IP Header DF bit to be same as incoming IP + Header DF bit. + - When C(set), sets the outgoing packet's IP Header DF bit. + - When C(clear), clears the outgoing packet's IP Header DF bit. + type: str + choices: + - pmtu + - preserve + - set + - clear + ip_tos_to_client: + description: + - For IP traffic passing through the system to clients, specifies whether the system + modifies the IP type-of-service (ToS) setting in an IP packet header. + - May be a number between 0 and 255 (inclusive). When a number, specifies the IP ToS + setting that the system inserts in the IP packet header. + - When C(pass-through), specifies the IP ToS setting remains unchanged. + - When C(mimic), specifies the system sets the ToS level of outgoing packets to + the same ToS level of the most-recently received incoming packet. + type: str + ip_tos_to_server: + description: + - For IP traffic passing through the system to back-end servers, specifies whether + the system modifies the IP type-of-service (ToS) setting in an IP packet header. + - May be a number between 0 and 255 (inclusive). When a number, specifies the IP ToS + setting that the system inserts in the IP packet header. + - When C(pass-through), specifies that IP ToS setting remains unchanged. + - When C(mimic), specifies the system sets the ToS level of outgoing packets to + the same ToS level of the most-recently received incoming packet. + type: str + ip_ttl_mode: + description: + - Specifies the outgoing TCP packet's IP Header TTL mode. + - When C(proxy), sets the outgoing IP Header TTL value to 255/64 for IPv4/IPv6 respectively. + - When C(preserve), sets the outgoing IP Header TTL value to be same as the incoming + IP Header TTL value. + - When C(decrement), sets the outgoing IP Header TTL value to be one less than the + incoming TTL value. + - When C(set), sets the outgoing IP Header TTL value to a specific value(as specified + by C(ip_ttl_v4) or C(ip_ttl_v6). + type: str + choices: + - proxy + - preserve + - decrement + - set + ip_ttl_v4: + description: + - Specifies the outgoing packet's IP Header TTL value for IPv4 traffic. + - The maximum TTL value is 255. + type: int + ip_ttl_v6: + description: + - Specifies the outgoing packet's IP Header TTL value for IPv6 traffic. + - The maximum TTL value is 255. + type: int + keep_alive_interval: + description: + - Specifies the keep-alive probe interval, in seconds. + type: int + late_binding: + description: + - Enables intelligent selection of a back-end server or pool, using an + iRule to make the selection. + - "Enabling C(late_binding) on BIG-IP running TMOS 12.x branch requires software syn cookie is enabled." + type: bool + link_qos_to_client: + description: + - For IP traffic passing through the system to clients, specifies + whether the system modifies the link quality-of-service (QoS) setting + in an IP packet header. + - The link QoS value prioritizes the IP packet relative to other Layer + 2 traffic. + - You can specify a number between 0 (lowest priority) and 7 (highest priority). + - When a number, specifies the link QoS setting that the system inserts + in the IP packet header. + - When C(pass-through), specifies the link QoS setting remains unchanged. + type: str + link_qos_to_server: + description: + - For IP traffic passing through the system to back-end servers, specifies + whether the system modifies the link quality-of-service (QoS) setting + in an IP packet header. + - The link QoS value prioritizes the IP packet relative to other Layer + 2 traffic. + - You can specify a number between 0 (lowest priority) and 7 (highest priority). + - When a number, specifies the link QoS setting that the system inserts + in the IP packet header. + - When C(pass-through), specifies the link QoS setting remains unchanged. + type: str + loose_close: + description: + - When C(yes), specifies the system closes a loosely-initiated connection + when the system receives the first FIN packet from either the client or the server. + type: bool + loose_initialization: + description: + - When C(yes), specifies the system initializes a connection when it + receives any TCP packet, rather than requiring a SYN packet for connection + initiation. + type: bool + mss_override: + description: + - Specifies a maximum segment size (MSS) override for server-side connections. + - Valid range is 256 to 9162 or 0 to disable. + type: int + reassemble_fragments: + description: + - When C(yes), specifies the system reassembles IP fragments. + type: bool + receive_window_size: + description: + - Specifies the amount of data the BIG-IP system can accept without acknowledging + the server. + type: int + reset_on_timeout: + description: + - When C(yes), specifies the system sends a reset packet (RST) in addition + to deleting the connection, when a connection exceeds the idle timeout value. + type: bool + rtt_from_client: + description: + - When C(yes), specifies the system uses TCP timestamp options to measure + the round-trip time to the client. + type: bool + rtt_from_server: + description: + - When C(yes), specifies the system uses TCP timestamp options to measure + the round-trip time to the server. + type: bool + server_sack: + description: + - Specifies whether the BIG-IP system processes Selective ACK (Sack) packets + in cookie responses from the server. + type: bool + server_timestamp: + description: + - Specifies whether the BIG-IP system processes timestamp request packets in + cookie responses from the server. + type: bool + syn_cookie_mss: + description: + - Specifies a value that overrides the SYN cookie maximum segment size (MSS) + value in the SYN-ACK packet that is returned to the client. + - Valid values are 0, and values from 256 through 9162. + - Parameter only available on TMOS 13.0.0 and higher. + type: int + tcp_close_timeout: + description: + - Specifies the length of time a connection can remain idle before deletion. + type: str + tcp_generate_isn: + description: + - When C(yes), specifies the system generates initial sequence numbers + for SYN packets, according to RFC 1948. + type: bool + tcp_handshake_timeout: + description: + - Specifies the acceptable duration for a TCP handshake (the maximum + idle time between a client synchronization (SYN) and a client acknowledgment + (ACK)). If the TCP handshake takes longer than the timeout, the system + automatically closes the connection. + - When a number, specifies how long the system can try to establish a TCP + handshake before timing out. + - When C(disabled), specifies the system does not apply a timeout to a + TCP handshake. + - When C(indefinite), specifies attempting a TCP handshake never times out. + type: str + tcp_strip_sack: + description: + - When C(yes), specifies the system blocks a TCP selective ACK SackOK + option from passing to the server on an initiating SYN. + type: bool + tcp_time_wait_timeout: + description: + - Specifies the number of milliseconds a connection is in the TIME-WAIT + state before closing. + - This parameter is only available on TMOS 13.0.0 and later. + type: int + tcp_timestamp_mode: + description: + - Specifies the action the system should take on TCP timestamps. + type: str + choices: + - preserve + - rewrite + - strip + tcp_wscale_mode: + description: + - Specifies the action the system should take on TCP windows. + type: str + choices: + - preserve + - rewrite + - strip + timeout_recovery: + description: + - Specifies how to handle client-timeout errors for Late Binding. + - Timeout errors may be caused by a DoS attack or a lossy connection. + - When C(disconnect), causes the BIG-IP system to drop the connection. + - When C(fallback), reverts the connection to normal FastL4 load-balancing, + based on the client's TCP header. This causes the BIG-IP system to choose + a back-end server based only on the source address and port. + type: str + choices: + - disconnect + - fallback + hardware_syn_cookie: + description: + - Enables or disables hardware SYN cookie support when PVA10 is present on the system. + type: bool + syn_cookie_enable: + description: + - Specifies whether or not to use SYN Cookie. + type: bool + version_added: "1.11.0" + pva_acceleration: + description: + - Specifies the Packet Velocity ASIC acceleration policy. + type: str + choices: + - full + - dedicated + - partial + - none + version_added: "1.3.0" + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a fastL4 profile + bigip_profile_fastl4: + name: foo + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +client_timeout: + description: The new client timeout value of the resource. + returned: changed + type: str + sample: true +description: + description: The new description. + returned: changed + type: str + sample: My description +explicit_flow_migration: + description: The new flow migration setting. + returned: changed + type: bool + sample: yes +idle_timeout: + description: The new idle timeout setting. + returned: changed + type: str + sample: 123 +ip_df_mode: + description: The new Don't Fragment Flag (DF) setting. + returned: changed + type: str + sample: clear +ip_tos_to_client: + description: The new IP ToS to Client setting. + returned: changed + type: str + sample: 100 +ip_tos_to_server: + description: The new IP ToS to Server setting. + returned: changed + type: str + sample: 100 +ip_ttl_mode: + description: The new Time To Live (TTL) setting. + returned: changed + type: str + sample: proxy +ip_ttl_v4: + description: The new Time To Live (TTL) v4 setting. + returned: changed + type: int + sample: 200 +ip_ttl_v6: + description: The new Time To Live (TTL) v6 setting. + returned: changed + type: int + sample: 200 +keep_alive_interval: + description: The new TCP Keep Alive Interval setting. + returned: changed + type: int + sample: 100 +late_binding: + description: The new Late Binding setting. + returned: changed + type: bool + sample: yes +link_qos_to_client: + description: The new Link QoS to Client setting. + returned: changed + type: str + sample: pass-through +link_qos_to_server: + description: The new Link QoS to Server setting. + returned: changed + type: str + sample: 123 +loose_close: + description: The new Loose Close setting. + returned: changed + type: bool + sample: no +loose_initialization: + description: The new Loose Initiation setting. + returned: changed + type: bool + sample: no +mss_override: + description: The new Maximum Segment Size Override setting. + returned: changed + type: int + sample: 300 +reassemble_fragments: + description: The new Reassemble IP Fragments setting. + returned: changed + type: bool + sample: yes +receive_window_size: + description: The new Receive Window setting. + returned: changed + type: int + sample: 1024 +reset_on_timeout: + description: The new Reset on Timeout setting. + returned: changed + type: bool + sample: no +rtt_from_client: + description: The new RTT from Client setting. + returned: changed + type: bool + sample: no +rtt_from_server: + description: The new RTT from Server setting. + returned: changed + type: bool + sample: no +server_sack: + description: The new Server Sack setting. + returned: changed + type: bool + sample: yes +server_timestamp: + description: The new Server Timestamp setting. + returned: changed + type: bool + sample: yes +syn_cookie_mss: + description: The new SYN Cookie MSS setting. + returned: changed + type: int + sample: 1024 +tcp_close_timeout: + description: The new TCP Close Timeout setting. + returned: changed + type: str + sample: 100 +tcp_generate_isn: + description: The new Generate Initial Sequence Number setting. + returned: changed + type: bool + sample: no +tcp_handshake_timeout: + description: The new TCP Handshake Timeout setting. + returned: changed + type: int + sample: 5 +tcp_strip_sack: + description: The new Strip Sack OK setting. + returned: changed + type: bool + sample: no +tcp_time_wait_timeout: + description: The new TCP Time Wait Timeout setting. + returned: changed + type: int + sample: 100 +tcp_timestamp_mode: + description: The new TCP Timestamp Mode setting. + returned: changed + type: str + sample: rewrite +tcp_wscale_mode: + description: The new TCP Window Scale Mode setting. + returned: changed + type: str + sample: strip +timeout_recovery: + description: The new Timeout Recovery setting. + returned: changed + type: str + sample: fallback +hardware_syn_cookie: + description: Enables or disables hardware SYN cookie support when PVA10 is present on the system. + returned: changed + type: bool + sample: no +syn_cookie_enable: + description: Specifies whether or not to use SYN Cookie. + returned: changed + type: bool + sample: no +pva_acceleration: + description: Specifies the Packet Velocity ASIC acceleration policy. + returned: changed + type: str + sample: full +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'clientTimeout': 'client_timeout', + 'defaultsFrom': 'parent', + 'explicitFlowMigration': 'explicit_flow_migration', + 'idleTimeout': 'idle_timeout', + 'ipDfMode': 'ip_df_mode', + 'ipTosToClient': 'ip_tos_to_client', + 'ipTosToServer': 'ip_tos_to_server', + 'ipTtlMode': 'ip_ttl_mode', + 'ipTtlV4': 'ip_ttl_v4', + 'ipTtlV6': 'ip_ttl_v6', + 'keepAliveInterval': 'keep_alive_interval', + 'lateBinding': 'late_binding', + 'linkQosToClient': 'link_qos_to_client', + 'linkQosToServer': 'link_qos_to_server', + 'looseClose': 'loose_close', + 'looseInitialization': 'loose_initialization', + 'mssOverride': 'mss_override', + 'reassembleFragments': 'reassemble_fragments', + 'receiveWindowSize': 'receive_window_size', + 'resetOnTimeout': 'reset_on_timeout', + 'rttFromClient': 'rtt_from_client', + 'rttFromServer': 'rtt_from_server', + 'serverSack': 'server_sack', + 'serverTimestamp': 'server_timestamp', + 'synCookieMss': 'syn_cookie_mss', + 'tcpCloseTimeout': 'tcp_close_timeout', + 'tcpGenerateIsn': 'tcp_generate_isn', + 'tcpHandshakeTimeout': 'tcp_handshake_timeout', + 'tcpStripSack': 'tcp_strip_sack', + 'tcpTimeWaitTimeout': 'tcp_time_wait_timeout', + 'tcpTimestampMode': 'tcp_timestamp_mode', + 'tcpWscaleMode': 'tcp_wscale_mode', + 'timeoutRecovery': 'timeout_recovery', + 'hardwareSynCookie': 'hardware_syn_cookie', + 'synCookieEnable': 'syn_cookie_enable', + 'pvaAcceleration': 'pva_acceleration', + } + + api_attributes = [ + 'clientTimeout', + 'defaultsFrom', + 'description', + 'explicitFlowMigration', + 'idleTimeout', + 'ipDfMode', + 'ipTosToClient', + 'ipTosToServer', + 'ipTtlMode', + 'ipTtlV4', + 'ipTtlV6', + 'keepAliveInterval', + 'lateBinding', + 'linkQosToClient', + 'linkQosToServer', + 'looseClose', + 'looseInitialization', + 'mssOverride', + 'reassembleFragments', + 'receiveWindowSize', + 'resetOnTimeout', + 'rttFromClient', + 'rttFromServer', + 'serverSack', + 'serverTimestamp', + 'synCookieMss', + 'tcpCloseTimeout', + 'tcpGenerateIsn', + 'tcpHandshakeTimeout', + 'tcpStripSack', + 'tcpTimeWaitTimeout', + 'tcpTimestampMode', + 'tcpWscaleMode', + 'timeoutRecovery', + 'hardwareSynCookie', + 'synCookieEnable', + 'pvaAcceleration', + ] + + returnables = [ + 'client_timeout', + 'description', + 'explicit_flow_migration', + 'idle_timeout', + 'ip_df_mode', + 'ip_tos_to_client', + 'ip_tos_to_server', + 'ip_ttl_mode', + 'ip_ttl_v4', + 'ip_ttl_v6', + 'keep_alive_interval', + 'late_binding', + 'link_qos_to_client', + 'link_qos_to_server', + 'loose_close', + 'loose_initialization', + 'mss_override', + 'parent', + 'reassemble_fragments', + 'receive_window_size', + 'reset_on_timeout', + 'rtt_from_client', + 'rtt_from_server', + 'server_sack', + 'server_timestamp', + 'syn_cookie_mss', + 'tcp_close_timeout', + 'tcp_generate_isn', + 'tcp_handshake_timeout', + 'tcp_strip_sack', + 'tcp_time_wait_timeout', + 'tcp_timestamp_mode', + 'tcp_wscale_mode', + 'timeout_recovery', + 'hardware_syn_cookie', + 'pva_acceleration', + ] + + updatables = [ + 'client_timeout', + 'description', + 'explicit_flow_migration', + 'idle_timeout', + 'ip_df_mode', + 'ip_tos_to_client', + 'ip_tos_to_server', + 'ip_ttl_mode', + 'ip_ttl_v4', + 'ip_ttl_v6', + 'keep_alive_interval', + 'late_binding', + 'link_qos_to_client', + 'link_qos_to_server', + 'loose_close', + 'loose_initialization', + 'mss_override', + 'parent', + 'reassemble_fragments', + 'receive_window_size', + 'reset_on_timeout', + 'rtt_from_client', + 'rtt_from_server', + 'server_sack', + 'server_timestamp', + 'syn_cookie_mss', + 'tcp_close_timeout', + 'tcp_generate_isn', + 'tcp_handshake_timeout', + 'tcp_strip_sack', + 'tcp_time_wait_timeout', + 'tcp_timestamp_mode', + 'tcp_wscale_mode', + 'timeout_recovery', + 'hardware_syn_cookie', + 'syn_cookie_enable', + 'pva_acceleration', + ] + + @property + def explicit_flow_migration(self): + result = flatten_boolean(self._values['explicit_flow_migration']) + return result + + @property + def late_binding(self): + return flatten_boolean(self._values['late_binding']) + + @property + def loose_close(self): + return flatten_boolean(self._values['loose_close']) + + @property + def loose_initialization(self): + return flatten_boolean(self._values['loose_initialization']) + + @property + def reassemble_fragments(self): + return flatten_boolean(self._values['reassemble_fragments']) + + @property + def reset_on_timeout(self): + return flatten_boolean(self._values['reset_on_timeout']) + + @property + def rtt_from_client(self): + return flatten_boolean(self._values['rtt_from_client']) + + @property + def rtt_from_server(self): + return flatten_boolean(self._values['rtt_from_server']) + + @property + def server_sack(self): + return flatten_boolean(self._values['server_sack']) + + @property + def server_timestamp(self): + return flatten_boolean(self._values['server_timestamp']) + + @property + def tcp_generate_isn(self): + return flatten_boolean(self._values['tcp_generate_isn']) + + @property + def tcp_strip_sack(self): + return flatten_boolean(self._values['tcp_strip_sack']) + + @property + def tcp_close_timeout(self): + if self._values['tcp_close_timeout'] is None: + return None + try: + return int(self._values['tcp_close_timeout']) + except ValueError: + return self._values['tcp_close_timeout'] + + @property + def tcp_handshake_timeout(self): + if self._values['tcp_handshake_timeout'] is None: + return None + try: + return int(self._values['tcp_handshake_timeout']) + except ValueError: + if self._values['tcp_handshake_timeout'] in ['disabled', 'immediate']: + return 0 + return self._values['tcp_handshake_timeout'] + + @property + def client_timeout(self): + if self._values['client_timeout'] is None: + return None + try: + return int(self._values['client_timeout']) + except ValueError: + if self._values['client_timeout'] == 'immediate': + return 0 + if self._values['client_timeout'] == 'indefinite': + return 4294967295 + return self._values['client_timeout'] + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def idle_timeout(self): + if self._values['idle_timeout'] is None: + return None + try: + return int(self._values['idle_timeout']) + except ValueError: + return self._values['idle_timeout'] + + @property + def ip_tos_to_client(self): + return self.transform_ip_tos('ip_tos_to_client') + + @property + def ip_tos_to_server(self): + return self.transform_ip_tos('ip_tos_to_server') + + @property + def keep_alive_interval(self): + if self._values['keep_alive_interval'] is None: + return None + try: + return int(self._values['keep_alive_interval']) + except ValueError: + return self._values['keep_alive_interval'] + + @property + def link_qos_to_client(self): + return self.transform_link_qos('link_qos_to_client') + + @property + def link_qos_to_server(self): + return self.transform_link_qos('link_qos_to_server') + + def transform_ip_tos(self, key): + if self._values[key] is None: + return None + try: + result = int(self._values[key]) + return result + except ValueError: + return self._values[key] + + def transform_link_qos(self, key): + if self._values[key] is None: + return None + if self._values[key] == 'pass-through': + return 'pass-through' + if 0 <= int(self._values[key]) <= 7: + return int(self._values[key]) + + +class ModuleParameters(Parameters): + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def idle_timeout(self): + if self._values['idle_timeout'] is None: + return None + try: + result = int(self._values['idle_timeout']) + if result == 0: + return 'immediate' + return result + except ValueError: + return self._values['idle_timeout'] + + @property + def ip_tos_to_client(self): + return self.transform_ip_tos('ip_tos_to_client') + + @property + def ip_tos_to_server(self): + return self.transform_ip_tos('ip_tos_to_server') + + @property + def ip_ttl_v4(self): + if self._values['ip_ttl_v4'] is None: + return None + if 0 <= self._values['ip_ttl_v4'] <= 255: + return int(self._values['ip_ttl_v4']) + raise F5ModuleError( + 'ip_ttl_v4 must be between 0 and 255' + ) + + @property + def ip_ttl_v6(self): + if self._values['ip_ttl_v6'] is None: + return None + if 0 <= self._values['ip_ttl_v6'] <= 255: + return int(self._values['ip_ttl_v6']) + raise F5ModuleError( + 'ip_ttl_v6 must be between 0 and 255' + ) + + @property + def keep_alive_interval(self): + if self._values['keep_alive_interval'] is None: + return None + result = int(self._values['keep_alive_interval']) + if result == 0: + return 'disabled' + return result + + @property + def link_qos_to_client(self): + result = self.transform_link_qos('link_qos_to_client') + if result == -1: + raise F5ModuleError( + 'link_qos_to_client must be between 0 and 7' + ) + return result + + @property + def link_qos_to_server(self): + result = self.transform_link_qos('link_qos_to_server') + if result == -1: + raise F5ModuleError( + 'link_qos_to_server must be between 0 and 7' + ) + return result + + def transform_link_qos(self, key): + if self._values[key] is None: + return None + if self._values[key] == 'pass-through': + return 'pass-through' + if 0 <= int(self._values[key]) <= 7: + return int(self._values[key]) + else: + return -1 + + def transform_ip_tos(self, key): + if self._values[key] is None: + return None + try: + result = int(self._values[key]) + return result + except ValueError: + if self._values[key] == 'mimic': + return 65534 + return self._values[key] + + @property + def hardware_syn_cookie(self): + result = flatten_boolean(self._values['hardware_syn_cookie']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def syn_cookie_enable(self): + result = flatten_boolean(self._values['syn_cookie_enable']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def explicit_flow_migration(self): + if self._values['explicit_flow_migration'] == 'yes': + return 'enabled' + elif self._values['explicit_flow_migration'] == 'no': + return 'disabled' + + @property + def late_binding(self): + if self._values['late_binding'] == 'yes': + return 'enabled' + elif self._values['late_binding'] == 'no': + return 'disabled' + + @property + def loose_close(self): + if self._values['loose_close'] == 'yes': + return 'enabled' + elif self._values['loose_close'] == 'no': + return 'disabled' + + @property + def loose_initialization(self): + if self._values['loose_initialization'] == 'yes': + return 'enabled' + elif self._values['loose_initialization'] == 'no': + return 'disabled' + + @property + def reassemble_fragments(self): + if self._values['reassemble_fragments'] == 'yes': + return 'enabled' + elif self._values['reassemble_fragments'] == 'no': + return 'disabled' + + @property + def reset_on_timeout(self): + if self._values['reset_on_timeout'] == 'yes': + return 'enabled' + elif self._values['reset_on_timeout'] == 'no': + return 'disabled' + + @property + def rtt_from_client(self): + if self._values['rtt_from_client'] == 'yes': + return 'enabled' + elif self._values['rtt_from_client'] == 'no': + return 'disabled' + + @property + def rtt_from_server(self): + if self._values['rtt_from_server'] == 'yes': + return 'enabled' + elif self._values['rtt_from_server'] == 'no': + return 'disabled' + + @property + def server_sack(self): + if self._values['server_sack'] == 'yes': + return 'enabled' + elif self._values['server_sack'] == 'no': + return 'disabled' + + @property + def server_timestamp(self): + if self._values['server_timestamp'] == 'yes': + return 'enabled' + elif self._values['server_timestamp'] == 'no': + return 'disabled' + + @property + def tcp_generate_isn(self): + if self._values['tcp_generate_isn'] == 'yes': + return 'enabled' + elif self._values['tcp_generate_isn'] == 'no': + return 'disabled' + + @property + def tcp_strip_sack(self): + if self._values['tcp_strip_sack'] == 'yes': + return 'enabled' + elif self._values['tcp_strip_sack'] == 'no': + return 'disabled' + + +class ReportableChanges(Changes): + @property + def explicit_flow_migration(self): + result = flatten_boolean(self._values['explicit_flow_migration']) + return result + + @property + def late_binding(self): + result = flatten_boolean(self._values['late_binding']) + return result + + @property + def loose_close(self): + result = flatten_boolean(self._values['loose_close']) + return result + + @property + def loose_initialization(self): + result = flatten_boolean(self._values['loose_initialization']) + return result + + @property + def reassemble_fragments(self): + result = flatten_boolean(self._values['reassemble_fragments']) + return result + + @property + def reset_on_timeout(self): + result = flatten_boolean(self._values['reset_on_timeout']) + return result + + @property + def rtt_from_client(self): + result = flatten_boolean(self._values['rtt_from_client']) + return result + + @property + def rtt_from_server(self): + result = flatten_boolean(self._values['rtt_from_server']) + return result + + @property + def server_sack(self): + result = flatten_boolean(self._values['server_sack']) + return result + + @property + def server_timestamp(self): + result = flatten_boolean(self._values['server_timestamp']) + return result + + @property + def tcp_generate_isn(self): + result = flatten_boolean(self._values['tcp_generate_isn']) + return result + + @property + def tcp_strip_sack(self): + result = flatten_boolean(self._values['tcp_strip_sack']) + return result + + @property + def ip_tos_to_client(self): + return self.report_ip_tos('ip_tos_to_client') + + @property + def ip_tos_to_server(self): + return self.report_ip_tos('ip_tos_to_server') + + @property + def keep_alive_interval(self): + if self._values['keep_alive_interval'] is None: + return None + if self._values['keep_alive_interval'] == 'disabled': + return 0 + return self._values['keep_alive_interval'] + + @property + def client_timeout(self): + if self._values['client_timeout'] is None: + return None + try: + return int(self._values['client_timeout']) + except ValueError: + if self._values['client_timeout'] == 0: + return 'immediate' + if self._values['client_timeout'] == 4294967295: + return 'indefinite' + return self._values['client_timeout'] + + def report_ip_tos(self, key): + if self._values[key] is None: + return None + if self._values[key] == 65534: + return 'mimic' + try: + return int(self._values[key]) + except ValueError: + return self._values[key] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + if self.want.description is None: + return None + if self.have.description is None and self.want.description == '': + return None + if self.want.description != self.have.description: + return self.want.description + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/profile/fastl4/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + idle_timeout=dict(), + client_timeout=dict(), + description=dict(), + explicit_flow_migration=dict(type='bool'), + ip_df_mode=dict( + choices=['pmtu', 'preserve', 'set', 'clear'] + ), + ip_tos_to_client=dict(), + ip_tos_to_server=dict(), + ip_ttl_v4=dict(type='int'), + ip_ttl_v6=dict(type='int'), + ip_ttl_mode=dict( + choices=['proxy', 'set', 'preserve', 'decrement'] + ), + keep_alive_interval=dict(type='int'), + late_binding=dict(type='bool'), + link_qos_to_client=dict(), + link_qos_to_server=dict(), + loose_close=dict(type='bool'), + loose_initialization=dict(type='bool'), + mss_override=dict(type='int'), + reassemble_fragments=dict(type='bool'), + receive_window_size=dict(type='int'), + reset_on_timeout=dict(type='bool'), + rtt_from_client=dict(type='bool'), + rtt_from_server=dict(type='bool'), + server_sack=dict(type='bool'), + server_timestamp=dict(type='bool'), + syn_cookie_mss=dict(type='int'), + tcp_close_timeout=dict(), + tcp_generate_isn=dict(type='bool'), + tcp_handshake_timeout=dict(), + tcp_strip_sack=dict(type='bool'), + tcp_time_wait_timeout=dict(type='int'), + tcp_timestamp_mode=dict( + choices=['preserve', 'rewrite', 'strip'] + ), + tcp_wscale_mode=dict( + choices=['preserve', 'rewrite', 'strip'] + ), + timeout_recovery=dict( + choices=['fallback', 'disconnect'] + ), + hardware_syn_cookie=dict(type='bool'), + syn_cookie_enable=dict(type='bool'), + pva_acceleration=dict( + choices=['full', 'dedicated', 'partial', 'none'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_ftp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_ftp.py new file mode 100644 index 00000000..0439c793 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_ftp.py @@ -0,0 +1,653 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_ftp +short_description: Manages FTP profiles +description: + - Manages FTP profiles on the BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + allow_ftps: + description: + - Allows explicit FTPS negotiation. + type: bool + description: + description: + - Description of the profile. + type: str + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(ftp) profile. + type: str + inherit_parent_profile: + description: + - Enables the FTP data channel to inherit the TCP profile used by the control channel. + - "When C(no), the data channel uses FastL4 (BigProto) only." + type: bool + log_profile: + description: + - Configures the ALG log profile that controls logging. + type: str + log_publisher: + description: + - Configures the log publisher that handles events logging for this profile. + type: str + translate_extended: + description: + - Translates RFC 2428 extended requests C(EPSV) and C(EPRT) to C(PASV) and C(PORT) + when communicating with IPv4 servers. + - This option can only be used if the system is licensed for the BIG-IP Application Security Manager (ASM). + type: bool + port: + description: + - Specifies a service for the data channel port used for this FTP profile. + - Valid range of values is between C(0) and C(65535) inclusive. + type: int + security: + description: + - Enables secure FTP traffic for the BIG-IP Application Security Manager. + - This option can only be used if the system is licensed for the BIG-IP ASM. + type: bool + state: + description: + - When C(state) is C(present), ensures the ftp profile exists. + - When C(state) is C(absent), ensures the ftp profile is removed. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an ftp profile + bigip_profile_ftp: + name: foo + parent: /Common/barfoo + port: 2221 + allow_ftps: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify an ftp profile + bigip_profile_ftp: + name: foo + log_profile: /Common/alg_log + log_publisher: /Common/foo_publisher + security: yes + description: my description + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove an ftp profile + bigip_profile_ftp: + name: foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +allow_ftps: + description: Allow explicit FTPS negotiation. + returned: changed + type: bool + sample: yes +description: + description: Description of the profile. + returned: changed + type: str + sample: Foo is bar +parent: + description: Specifies the profile from which this profile inherits settings. + returned: changed + type: str + sample: /Common/ftp +inherit_parent_profile: + description: Enables the FTP data channel to inherit the TCP profile used by the control channel. + returned: changed + type: bool + sample: no +log_profile: + description: The ALG log profile that controls logging. + returned: changed + type: str + sample: /Common/foo_log_profile +log_publisher: + description: The name of the log publisher that handles events logging for this profile. + returned: changed + type: list + sample: /Common/publisher_1 +translate_extended: + description: Translates RFC 2428 extended requests when communicating with IPv4 servers. + returned: changed + type: bool + sample: yes +port: + description: Specifies a service for the data channel port used for this FTP profile. + returned: changed + type: int + sample: 20 +security: + description: Enables secure FTP traffic for the BIG-IP Application Security Manager. + returned: changed + type: bool + sample: no +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'allowFtps': 'allow_ftps', + 'defaultsFrom': 'parent', + 'inheritParentProfile': 'inherit_parent_profile', + 'logProfile': 'log_profile', + 'logPublisher': 'log_publisher', + 'translateExtended': 'translate_extended', + } + + api_attributes = [ + 'allowFtps', + 'description', + 'inheritParentProfile', + 'logProfile', + 'logPublisher', + 'port', + 'security', + 'translateExtended', + ] + + returnables = [ + 'allow_ftps', + 'description', + 'inherit_parent_profile', + 'log_profile', + 'log_publisher', + 'parent', + 'port', + 'security', + 'translate_extended', + ] + + updatables = [ + 'allow_ftps', + 'description', + 'inherit_parent_profile', + 'log_profile', + 'log_publisher', + 'parent', + 'port', + 'security', + 'translate_extended', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def allow_ftps(self): + result = flatten_boolean(self._values['allow_ftps']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def inherit_parent_profile(self): + result = flatten_boolean(self._values['inherit_parent_profile']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def log_profile(self): + if self._values['log_profile'] is None: + return None + if self._values['log_profile'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['log_profile']) + return result + + @property + def log_publisher(self): + if self._values['log_publisher'] is None: + return None + if self._values['log_publisher'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['log_publisher']) + return result + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def port(self): + if self._values['port'] is None: + return None + if 0 <= self._values['port'] <= 65535: + return self._values['port'] + raise F5ModuleError( + "Valid 'port' must be in range 0 - 65535." + ) + + @property + def security(self): + result = flatten_boolean(self._values['security']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def translate_extended(self): + result = flatten_boolean(self._values['translate_extended']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + + @property + def allow_ftps(self): + result = flatten_boolean(self._values['allow_ftps']) + return result + + @property + def inherit_parent_profile(self): + result = flatten_boolean(self._values['inherit_parent_profile']) + return result + + @property + def security(self): + result = flatten_boolean(self._values['security']) + return result + + @property + def translate_extended(self): + result = flatten_boolean(self._values['translate_extended']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def log_profile(self): + if self.want.log_profile is None: + return None + if self.want.log_profile == '' and self.have.log_profile in [None, 'none']: + return None + if self.want.log_profile == '': + if self.have.log_publisher not in [None, 'none'] and self.want.log_publisher is None: + raise F5ModuleError( + "The log_profile cannot be removed if log_publisher is defined on device." + ) + if self.want.log_profile != '': + if self.want.log_publisher is None and self.have.log_publisher in [None, 'none']: + raise F5ModuleError( + "The log_profile cannot be specified without an existing valid log_publisher." + ) + if self.want.log_profile != self.have.log_profile: + return self.want.log_profile + + @property + def log_publisher(self): + if self.want.log_publisher is None: + return None + if self.want.log_publisher == '' and self.have.log_publisher in [None, 'none']: + return None + if self.want.log_publisher == '': + if self.want.log_profile is None and self.have.log_profile not in [None, 'none']: + raise F5ModuleError( + "The log_publisher cannot be removed if log_profile is defined on device." + ) + if self.want.log_publisher != self.have.log_publisher: + return self.want.log_publisher + + @property + def description(self): + if self.want.description is None: + return None + if self.have.description in [None, 'none'] and self.want.description == '': + return None + if self.want.description != self.have.description: + return self.want.description + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/ftp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/profile/ftp/".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/ftp/{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/ltm/profile/ftp/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/ftp/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + allow_ftps=dict(type='bool'), + description=dict(), + parent=dict(), + inherit_parent_profile=dict(type='bool'), + log_profile=dict(), + log_publisher=dict(), + translate_extended=dict(type='bool'), + port=dict(type='int'), + security=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http.py new file mode 100644 index 00000000..c9673a17 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http.py @@ -0,0 +1,1797 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_http +short_description: Manage HTTP profiles on a BIG-IP +description: + - Manage HTTP profiles on a BIG-IP device. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(http) profile. + type: str + description: + description: + - Description of the profile. + type: str + proxy_type: + description: + - Specifies the proxy mode for the profile. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: str + choices: + - reverse + - transparent + - explicit + dns_resolver: + description: + - Specifies the name of a configured DNS resolver, this option is mandatory when C(proxy_type) + is set to C(explicit). + - Format of the name can be either be prepended by partition (C(/Common/foo)), or specified + just as an object name (C(foo)). + - To remove the entry, you can set a value of C(none) or C(''), however the profile C(proxy_type) + must not be set as C(explicit). + type: str + insert_xforwarded_for: + description: + - Specifies the system inserts an X-Forwarded-For header in an HTTP request + with the client IP address, to use with connection pooling. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + redirect_rewrite: + description: + - Specifies whether the system rewrites the URIs that are part of HTTP + redirect (3XX) responses. + - When set to C(none), the system will not rewrite the URI in any + HTTP redirect responses. + - When set to C(all), the system rewrites the URI in all HTTP redirect responses. + - When set to C(matching), the system rewrites the URI in any + HTTP redirect responses that match the request URI. + - When set to C(nodes), if the URI contains a node IP address instead of a host name, + the system changes it to the virtual server address. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: str + choices: + - none + - all + - matching + - nodes + encrypt_cookies: + description: + - Cookie names for the system to encrypt. + - To remove the entry completely, set a value of C(none) or C(''). + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: list + elements: str + encrypt_cookie_secret: + description: + - Passphrase for cookie encryption. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: str + update_password: + description: + - C(always) will update passwords if the C(encrypt_cookie_secret) is specified. + - C(on_create) will only set the password for newly created profiles. + type: str + choices: + - always + - on_create + default: always + header_erase: + description: + - The name of a header in an HTTP request, which the system removes from request. + - To remove the entry completely, set a value of C(none) or C(''). + - The format of the header must be in C(KEY:VALUE) format, otherwise an error occurs. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: str + header_insert: + description: + - A string the system inserts as a header in an HTTP request. + - To remove the entry completely, set a value of C(none) or C(''). + - The format of the header must be in C(KEY:VALUE) format, otherwise an error occurs. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: str + server_agent_name: + description: + - Specifies the string used as the server name in traffic generated by BIG-IP. + - To remove the entry completely, set a value of C(none) or C(''). + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: str + include_subdomains: + description: + - When set to C(yes), applies the HSTS policy to the HSTS host and its sub-domains. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + maximum_age: + description: + - Specifies the maximum length of time, in seconds, that HSTS functionality + requests clients only use HTTPS to connect to the current host and + any sub-domains of the current host's domain name. + - The accepted value range is C(0 - 4294967295) seconds. A value of C(0) seconds + re-enables plaintext HTTP access, while specifying C(indefinite) sets it to the maximum value. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: str + hsts_mode: + description: + - When set to C(yes), enables the HSTS settings. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: bool + hsts_preload: + description: + - When set to C(yes), adds the HSTS host and its subdomains to the browser's + HSTS preload list of sites that are considered HTTPS only. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: bool + version_added: "1.22.0" + accept_xff: + description: + - Enables or disables trusting the client IP address, and statistics from the client IP address, + based on the request's XFF (X-forwarded-for) headers, if they exist. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: bool + xff_alternative_names: + description: + - Specifies alternative XFF headers instead of the default X-forwarded-for header. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: list + elements: str + fallback_host: + description: + - Specifies an HTTP fallback host. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: str + fallback_status_codes: + description: + - Specifies one or more HTTP error codes from server responses that should trigger + a redirection to the fallback host. + - The accepted valid error codes are as defined by RFC2616. + - The codes can be specified as individual items or as valid ranges, for example C(400-417) or C(500-505). + - Mixing response code range across error types is invalid, for example defining C(400-505) will raise an error. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: list + elements: str + oneconnect_transformations: + description: + - Enables the system to perform HTTP header transformations for keeping server-side + connections open. This feature requires a OneConnect profile. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: bool + request_chunking: + description: + - Specifies how to handle chunked and unchunked requests. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: str + choices: + - rechunk + - selective + - preserve + - sustain + - unchunk + response_chunking: + description: + - Specifies how to handle chunked and unchunked responses. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: str + choices: + - rechunk + - selective + - preserve + - sustain + - unchunk + enforcement: + description: + - Specifies protocol enforcement settings for the HTTP profile. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + suboptions: + truncated_redirects: + description: + - Specifies what happens if a truncated redirect is seen from a server. + - If C(yes), the redirect is forwarded to the client, otherwise the malformed HTTP + is silently ignored. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: bool + excess_client_headers: + description: + - Specifies the behavior when too many client headers are received. + - If set to C(pass-through), it switches to pass-through mode, when C(reject), the connection is rejected. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: str + choices: + - reject + - pass-through + excess_server_headers: + description: + - Specifies the behavior when too many server headers are received. + - If set to C(pass-through), it switches to pass-through mode, when C(reject) the connection is rejected. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: str + choices: + - reject + - pass-through + oversize_client_headers: + description: + - Specifies the behavior when too-large client headers are received. + - If set to C(pass-through),it switches to pass-through mode, when C(reject) the connection is rejected. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: str + choices: + - reject + - pass-through + oversize_server_headers: + description: + - Specifies the behavior when too-large server headers are received. + - If set to C(pass-through), it switches to pass-through mode, when C(reject) the connection is rejected. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: str + choices: + - reject + - pass-through + pipeline: + description: + - Enables HTTP/1.1 pipelining, allowing clients to make requests even when prior requests have not received + a response. + - In order for this to succeed, destination servers must include support for pipelining. + - If set to C(pass-through), pipelined data causes the BIG-IP to immediately switch to pass-through mode + and disable the HTTP filter. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: str + choices: + - allow + - reject + - pass-through + unknown_method: + description: + - Specifies whether to allow, reject or switch to pass-through mode when an unknown HTTP method is parsed. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: str + choices: + - allow + - reject + - pass-through + max_header_count: + description: + - Specifies the maximum number of headers allowed in HTTP request/response. + - The valid value range is between 16 and 4096 inclusive. + - When set to C(default), the value is C(64). + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: str + max_header_size: + description: + - Specifies the maximum header size specified in bytes. + - The valid value range is between 0 and 4294967295 inclusive. + - When set to C(default), the value is C(32768) bytes + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: str + max_requests: + description: + - Specifies the number of requests the system accepts on a per-connection basis. + - The valid value range is between 0 and 4294967295 inclusive. + - When set to C(default), the value is C(0), which means the system + will not limit the number of requests per connection. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: str + known_methods: + description: + - Specifies which HTTP methods count as being known, removing RFC-defined methods from this list + will cause the HTTP filter to not recognize them. + - "The default list provided with the system include: C(CONNECT), C(DELETE), C(GET), + C(HEAD), C(LOCK), C(OPTIONS), C(POST), C(PROPFIND), C(PUT), C(TRACE) ,C(UNLOCK). The list can be appended by + by specifying the C(default) keyword as one of the list elements." + - The C(default) keyword can also be used to restore the default C(known_methods) on the system. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: list + elements: str + type: dict + sflow: + description: + - Specifies sFlow settings for the HTTP profile. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + suboptions: + poll_interval: + description: + - Specifies the maximum interval in seconds between two pollings. + - The valid value range is between 0 and 4294967295 seconds inclusive. + - For this setting to take effect the C(poll_interval_global) parameter must be set to C(no). + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: int + poll_interval_global: + description: + - Specifies whether the global HTTP poll-interval setting overrides the object-level C(poll-interval) setting. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: bool + sampling_rate: + description: + - Specifies the ratio of packets observed to the samples generated. For example, a sampling rate of C(2000) + specifies 1 sample will be randomly generated for every 2000 packets observed. + - The valid value range is between 0 and 4294967295 packets inclusive. + - For this setting to take effect the C(sampling_rate_global) parameter must be set to C(no). + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: int + sampling_rate_global: + description: + - Specifies whether the global HTTP sampling-rate setting overrides the object-level sampling-rate setting. + - When creating a new profile, if this parameter is not specified, the default is provided + by the parent profile. + type: bool + type: dict + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create HTTP profile + bigip_profile_http: + name: my_profile + insert_xforwarded_for: yes + redirect_rewrite: all + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove HTTP profile + bigip_profile_http: + name: my_profile + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add HTTP profile for transparent proxy + bigip_profile_http: + name: my_profile + proxy_type: transparent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: Specifies the profile from which this profile inherits settings. + returned: changed + type: str + sample: /Common/http +description: + description: Description of the profile. + returned: changed + type: str + sample: My profile +proxy_type: + description: Specify proxy mode of the profile. + returned: changed + type: str + sample: explicit +hsts_mode: + description: Enables the HSTS settings. + returned: changed + type: bool + sample: no +hsts_preload: + description: Enables the HSTS preload. + returned: changed + type: bool + sample: no +maximum_age: + description: The maximum length of time, in seconds, that HSTS functionality requests that clients only use HTTPS. + returned: changed + type: str + sample: indefinite +include_subdomains: + description: Applies the HSTS policy to the HSTS host and its sub-domains. + returned: changed + type: bool + sample: yes +server_agent_name: + description: The string used as the server name in traffic generated by BIG-IP. + returned: changed + type: str + sample: foobar +header_erase: + description: The name of a header in an HTTP request, which the system removes from request. + returned: changed + type: str + sample: FOO:BAR +header_insert: + description: The string the system inserts as a header in an HTTP request. + returned: changed + type: str + sample: FOO:BAR +insert_xforwarded_for: + description: Insert X-Forwarded-For-Header. + returned: changed + type: bool + sample: yes +redirect_rewrite: + description: Rewrite URI that are part of 3xx responses. + returned: changed + type: str + sample: all +encrypt_cookies: + description: Cookie names to encrypt. + returned: changed + type: list + sample: ['MyCookie1', 'MyCookie2'] +dns_resolver: + description: Configured dns resolver. + returned: changed + type: str + sample: '/Common/FooBar' +accept_xff: + description: Enables or disables trusting the client IP address and statistics from the client IP address. + returned: changed + type: bool + sample: yes +xff_alternative_names: + description: Specifies alternative XFF headers instead of the default X-forwarded-for header. + returned: changed + type: list + sample: ['FooBar', 'client1'] +fallback_host: + description: Specifies an HTTP fallback host. + returned: changed + type: str + sample: 'foobar.com' +fallback_status_codes: + description: HTTP error codes from server responses that should trigger a redirection to the fallback host. + returned: changed + type: list + sample: ['400-404', '500', '501'] +oneconnect_transformations: + description: Enables or disables HTTP header transformations. + returned: changed + type: bool + sample: no +request_chunking: + description: Specifies how to handle chunked and unchunked requests. + returned: changed + type: str + sample: rechunk +response_chunking: + description: Specifies how to handle chunked and unchunked responses. + returned: changed + type: str + sample: rechunk +enforcement: + description: Specifies protocol enforcement settings for the HTTP profile. + type: complex + returned: changed + contains: + truncated_redirects: + description: Specifies what happens if a truncated redirect is seen from a server. + returned: changed + type: bool + sample: yes + excess_server_headers: + description: Specifies the behavior when too many server headers are received. + returned: changed + type: str + sample: pass-through + oversize_client_headers: + description: Specifies the behavior when too-large client headers are received. + returned: changed + type: str + sample: reject + oversize_server_headers: + description: Specifies the behavior when too-large server headers are received. + returned: changed + type: str + sample: reject + pipeline: + description: Allows, rejects. or switches to pass-through mode when dealing with pipelined data. + returned: changed + type: str + sample: allow + unknown_method: + description: Allows, rejects. or switches to pass-through mode when an unknown HTTP method is parsed. + returned: changed + type: str + sample: allow + max_header_count: + description: The maximum number of headers allowed in HTTP request/response. + returned: changed + type: str + sample: 4096 + max_header_size: + description: The maximum header size specified in bytes. + returned: changed + type: str + sample: default + max_requests: + description: The number of requests the system accepts on a per-connection basis. + returned: changed + type: str + sample: default + known_methods: + description: The list of known HTTP methods. + returned: changed + type: list + sample: ['default', 'FOO', 'BAR'] + sample: hash/dictionary of values +sflow: + description: Specifies sFlow settings for the HTTP profile. + type: complex + returned: changed + contains: + poll_interval: + description: Specifies the maximum interval in seconds between two pollings. + returned: changed + type: int + sample: 30 + poll_interval_global: + description: Enables/Disables overriding HTTP poll-interval setting. + returned: changed + type: bool + sample: yes + sampling_rate: + description: Specifies the ratio of packets observed to the samples generated. + returned: changed + type: int + sample: 2000 + sampling_rate_global: + description: Enables/Disables overriding HTTP sampling-rate setting. + returned: changed + type: bool + sample: yes + sample: hash/dictionary of values +''' + +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_simple_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'insertXforwardedFor': 'insert_xforwarded_for', + 'redirectRewrite': 'redirect_rewrite', + 'encryptCookies': 'encrypt_cookies', + 'encryptCookieSecret': 'encrypt_cookie_secret', + 'proxyType': 'proxy_type', + 'explicitProxy': 'explicit_proxy', + 'headerErase': 'header_erase', + 'headerInsert': 'header_insert', + 'serverAgentName': 'server_agent_name', + 'includeSubdomains': 'include_subdomains', + 'maximumAge': 'maximum_age', + 'mode': 'hsts_mode', + 'acceptXff': 'accept_xff', + 'xffAlternativeNames': 'xff_alternative_names', + 'fallbackHost': 'fallback_host', + 'fallbackStatusCodes': 'fallback_status_codes', + 'oneconnectTransformations': 'oneconnect_transformations', + 'requestChunking': 'request_chunking', + 'responseChunking': 'response_chunking', + } + + api_attributes = [ + 'insertXforwardedFor', + 'description', + 'defaultsFrom', + 'redirectRewrite', + 'encryptCookies', + 'encryptCookieSecret', + 'proxyType', + 'explicitProxy', + 'headerErase', + 'headerInsert', + 'hsts', + 'serverAgentName', + 'acceptXff', + 'xffAlternativeNames', + 'fallbackHost', + 'fallbackStatusCodes', + 'oneconnectTransformations', + 'requestChunking', + 'responseChunking', + 'enforcement', + 'sflow', + ] + + returnables = [ + 'parent', + 'description', + 'insert_xforwarded_for', + 'redirect_rewrite', + 'encrypt_cookies', + 'proxy_type', + 'explicit_proxy', + 'dns_resolver', + 'hsts_mode', + 'hsts_preload', + 'maximum_age', + 'include_subdomains', + 'server_agent_name', + 'header_erase', + 'header_insert', + 'accept_xff', + 'xff_alternative_names', + 'fallback_host', + 'fallback_status_codes', + 'oneconnect_transformations', + 'request_chunking', + 'response_chunking', + 'truncated_redirects', + 'excess_client_headers', + 'excess_server_headers', + 'oversize_client_headers', + 'oversize_server_headers', + 'pipeline', + 'unknown_method', + 'max_header_count', + 'max_header_size', + 'max_requests', + 'known_methods', + 'poll_interval', + 'poll_interval_global', + 'sampling_rate', + 'sampling_rate_global', + ] + + updatables = [ + 'description', + 'insert_xforwarded_for', + 'redirect_rewrite', + 'encrypt_cookies', + 'encrypt_cookie_secret', + 'proxy_type', + 'dns_resolver', + 'hsts_mode', + 'hsts_preload', + 'maximum_age', + 'include_subdomains', + 'server_agent_name', + 'header_erase', + 'header_insert', + 'accept_xff', + 'xff_alternative_names', + 'fallback_host', + 'fallback_status_codes', + 'oneconnect_transformations', + 'request_chunking', + 'response_chunking', + 'truncated_redirects', + 'excess_client_headers', + 'excess_server_headers', + 'oversize_client_headers', + 'oversize_server_headers', + 'pipeline', + 'unknown_method', + 'max_header_count', + 'max_header_size', + 'max_requests', + 'known_methods', + 'poll_interval', + 'poll_interval_global', + 'sampling_rate', + 'sampling_rate_global', + 'parent', + ] + + +class ApiParameters(Parameters): + @property + def poll_interval(self): + return self._values['sflow']['pollInterval'] + + @property + def poll_interval_global(self): + return self._values['sflow']['pollIntervalGlobal'] + + @property + def sampling_rate(self): + return self._values['sflow']['samplingRate'] + + @property + def sampling_rate_global(self): + return self._values['sflow']['samplingRateGlobal'] + + @property + def truncated_redirects(self): + return self._values['enforcement']['truncatedRedirects'] + + @property + def excess_client_headers(self): + return self._values['enforcement']['excessClientHeaders'] + + @property + def excess_server_headers(self): + return self._values['enforcement']['excessServerHeaders'] + + @property + def oversize_client_headers(self): + return self._values['enforcement']['oversizeClientHeaders'] + + @property + def oversize_server_headers(self): + return self._values['enforcement']['oversizeServerHeaders'] + + @property + def pipeline(self): + return self._values['enforcement']['pipeline'] + + @property + def unknown_method(self): + return self._values['enforcement']['unknownMethod'] + + @property + def max_header_count(self): + return self._values['enforcement']['maxHeaderCount'] + + @property + def max_header_size(self): + return self._values['enforcement']['maxHeaderSize'] + + @property + def max_requests(self): + return self._values['enforcement']['maxRequests'] + + @property + def known_methods(self): + return self._values['enforcement'].get('knownMethods', None) + + @property + def dns_resolver(self): + if self._values['explicit_proxy'] is None: + return None + if 'dnsResolver' in self._values['explicit_proxy']: + return self._values['explicit_proxy']['dnsResolver'] + + @property + def dns_resolver_address(self): + if self._values['explicit_proxy'] is None: + return None + if 'dnsResolverReference' in self._values['explicit_proxy']: + return self._values['explicit_proxy']['dnsResolverReference'] + + @property + def include_subdomains(self): + if self._values['hsts'] is None: + return None + return self._values['hsts']['includeSubdomains'] + + @property + def hsts_mode(self): + if self._values['hsts'] is None: + return None + return self._values['hsts']['mode'] + + @property + def hsts_preload(self): + if self._values['hsts'] is None: + return None + return self._values['hsts']['preload'] + + @property + def maximum_age(self): + if self._values['hsts'] is None: + return None + return self._values['hsts']['maximumAge'] + + +class ModuleParameters(Parameters): + @property + def accept_xff(self): + result = flatten_boolean(self._values['accept_xff']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def fallback_status_codes(self): + if self._values['fallback_status_codes'] is None: + return None + + p1 = r'(?!([4][0-1][0-7]))\d{3}' + p2 = r'(?!(50[0-5]))\d{3}' + + for code in self._values['fallback_status_codes']: + match_4xx = re.search(p1, code) + if match_4xx: + match_5xx = re.search(p2, code) + if match_5xx: + raise F5ModuleError( + 'Invalid HTTP error code or error code range specified.' + ) + return self._values['fallback_status_codes'] + + @property + def oneconnect_transformations(self): + result = flatten_boolean(self._values['oneconnect_transformations']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def proxy_type(self): + if self._values['proxy_type'] is None: + return None + if self._values['proxy_type'] == 'explicit': + if self.dns_resolver is None or self.dns_resolver == '': + raise F5ModuleError( + 'A proxy type cannot be set to {0} without providing DNS resolver.'.format( + self._values['proxy_type'] + ) + ) + return self._values['proxy_type'] + + @property + def dns_resolver(self): + if self._values['dns_resolver'] is None: + return None + if self._values['dns_resolver'] == '' or self._values['dns_resolver'] == 'none': + return '' + result = fq_name(self.partition, self._values['dns_resolver']) + return result + + @property + def dns_resolver_address(self): + resolver = self.dns_resolver + if resolver is None: + return None + tmp = resolver.split('/') + link = dict(link='https://localhost/mgmt/tm/net/dns-resolver/~{0}~{1}'.format(tmp[1], tmp[2])) + return link + + @property + def insert_xforwarded_for(self): + result = flatten_boolean(self._values['insert_xforwarded_for']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def encrypt_cookies(self): + if self._values['encrypt_cookies'] is None: + return None + if self._values['encrypt_cookies'] == [''] or self._values['encrypt_cookies'] == ['none']: + return list() + return self._values['encrypt_cookies'] + + @property + def explicit_proxy(self): + if self.dns_resolver is None: + return None + result = dict( + dnsResolver=self.dns_resolver, + dnsResolverReference=self.dns_resolver_address + ) + return result + + @property + def include_subdomains(self): + result = flatten_boolean(self._values['include_subdomains']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def maximum_age(self): + if self._values['maximum_age'] is None: + return None + if self._values['maximum_age'] == 'indefinite': + return 4294967295 + if 0 <= int(self._values['maximum_age']) <= 4294967295: + return int(self._values['maximum_age']) + raise F5ModuleError( + "Valid 'maximum_age' must be in range 0 - 4294967295, or 'indefinite'." + ) + + @property + def hsts_mode(self): + result = flatten_boolean(self._values['hsts_mode']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def hsts_preload(self): + result = flatten_boolean(self._values['hsts_preload']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def header_erase(self): + header_erase = self._values['header_erase'] + if header_erase is None: + return None + if header_erase in ['none', '']: + return self._values['header_erase'] + return header_erase + + @property + def header_insert(self): + header_insert = self._values['header_insert'] + if header_insert is None: + return None + if header_insert in ['none', '']: + return self._values['header_insert'] + return header_insert + + @property + def excess_client_headers(self): + if self._values['enforcement'] is None: + return None + return self._values['enforcement']['excess_client_headers'] + + @property + def excess_server_headers(self): + if self._values['enforcement'] is None: + return None + return self._values['enforcement']['excess_server_headers'] + + @property + def oversize_client_headers(self): + if self._values['enforcement'] is None: + return None + return self._values['enforcement']['oversize_client_headers'] + + @property + def oversize_server_headers(self): + if self._values['enforcement'] is None: + return None + return self._values['enforcement']['oversize_server_headers'] + + @property + def pipeline(self): + if self._values['enforcement'] is None: + return None + return self._values['enforcement']['pipeline'] + + @property + def unknown_method(self): + if self._values['enforcement'] is None: + return None + return self._values['enforcement']['unknown_method'] + + @property + def truncated_redirects(self): + if self._values['enforcement'] is None: + return None + result = flatten_boolean(self._values['enforcement']['truncated_redirects']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def max_header_count(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['max_header_count'] is None: + return None + if self._values['enforcement']['max_header_count'] == 'default': + return 64 + if 16 <= int(self._values['enforcement']['max_header_count']) <= 4096: + return int(self._values['enforcement']['max_header_count']) + raise F5ModuleError( + "Valid 'max_header_count' must be in range 16 - 4096, or 'default'." + ) + + @property + def max_header_size(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['max_header_size'] is None: + return None + if self._values['enforcement']['max_header_size'] == 'default': + return 32768 + if 0 <= int(self._values['enforcement']['max_header_size']) <= 4294967295: + return int(self._values['enforcement']['max_header_size']) + raise F5ModuleError( + "Valid 'max_header_size' must be in range 0 - 4294967295, or 'default'." + ) + + @property + def max_requests(self): + if self._values['enforcement'] is None: + return None + if self._values['enforcement']['max_requests'] is None: + return None + if self._values['enforcement']['max_requests'] == 'default': + return 0 + if 0 <= int(self._values['enforcement']['max_requests']) <= 4294967295: + return int(self._values['enforcement']['max_requests']) + raise F5ModuleError( + "Valid 'max_requests' must be in range 0 - 4294967295, or 'default'." + ) + + @property + def known_methods(self): + if self._values['enforcement'] is None: + return None + defaults = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'LOCK', 'OPTIONS', 'POST', 'PROPFIND', 'PUT', 'TRACE', 'UNLOCK'] + known = self._values['enforcement']['known_methods'] + if known is None: + return None + if len(known) == 1: + if known[0] == 'default': + return defaults + if known[0] == '': + return [] + if 'default' in known: + to_return = [method for method in known if method != 'default'] + to_return.extend(defaults) + return to_return + result = list(known) + return result + + @property + def poll_interval(self): + if self._values['sflow'] is None: + return None + if self._values['sflow']['poll_interval'] is None: + return None + if 0 <= self._values['sflow']['poll_interval'] <= 4294967295: + return self._values['sflow']['poll_interval'] + raise F5ModuleError( + "Valid 'poll_interval' must be in range 0 - 4294967295 seconds." + ) + + @property + def sampling_rate(self): + if self._values['sflow'] is None: + return None + if self._values['sflow']['sampling_rate'] is None: + return None + if 0 <= self._values['sflow']['sampling_rate'] <= 4294967295: + return self._values['sflow']['sampling_rate'] + raise F5ModuleError( + "Valid 'sampling_rate' must be in range 0 - 4294967295 packets." + ) + + @property + def poll_interval_global(self): + if self._values['sflow'] is None: + return None + result = flatten_boolean(self._values['sflow']['poll_interval_global']) + return result + + @property + def sampling_rate_global(self): + if self._values['sflow'] is None: + return None + result = flatten_boolean(self._values['sflow']['sampling_rate_global']) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def explicit_proxy(self): + result = dict() + if self._values['dns_resolver'] is not None: + result['dnsResolver'] = self._values['dns_resolver'] + if self._values['dns_resolver_address'] is not None: + result['dnsResolverReference'] = self._values['dns_resolver_address'] + if not result: + return None + return result + + @property + def hsts(self): + result = dict() + if self._values['hsts_mode'] is not None: + result['mode'] = self._values['hsts_mode'] + if self._values['maximum_age'] is not None: + result['maximumAge'] = self._values['maximum_age'] + if self._values['include_subdomains'] is not None: + result['includeSubdomains'] = self._values['include_subdomains'] + if self._values['hsts_preload'] is not None: + result['preload'] = self._values['hsts_preload'] + if not result: + return None + return result + + @property + def enforcement(self): + to_filter = dict( + excessClientHeaders=self._values['excess_client_headers'], + excessServerHeaders=self._values['excess_server_headers'], + knownMethods=self._values['known_methods'], + maxHeaderCount=self._values['max_header_count'], + maxHeaderSize=self._values['max_header_size'], + maxRequests=self._values['max_requests'], + oversizeClientHeaders=self._values['oversize_client_headers'], + oversizeServerHeaders=self._values['oversize_server_headers'], + pipeline=self._values['pipeline'], + truncatedRedirects=self._values['truncated_redirects'], + unknownMethod=self._values['unknown_method'] + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def sflow(self): + to_filter = dict( + pollInterval=self._values['poll_interval'], + pollIntervalGlobal=self._values['poll_interval_global'], + samplingRate=self._values['sampling_rate'], + samplingRateGlobal=self._values['sampling_rate_global'], + ) + result = self._filter_params(to_filter) + if result: + return result + + +class ReportableChanges(Changes): + returnables = [ + 'parent', + 'description', + 'insert_xforwarded_for', + 'redirect_rewrite', + 'encrypt_cookies', + 'proxy_type', + 'explicit_proxy', + 'dns_resolver', + 'hsts_mode', + 'hsts_preload', + 'maximum_age', + 'include_subdomains', + 'server_agent_name', + 'header_erase', + 'header_insert', + 'accept_xff', + 'xff_alternative_names', + 'fallback_host', + 'fallback_status_codes', + 'oneconnect_transformations', + 'request_chunking', + 'response_chunking', + 'enforcement', + 'sflow' + ] + + @property + def insert_xforwarded_for(self): + if self._values['insert_xforwarded_for'] is None: + return None + elif self._values['insert_xforwarded_for'] == 'enabled': + return 'yes' + return 'no' + + @property + def hsts_mode(self): + if self._values['hsts_mode'] is None: + return None + elif self._values['hsts_mode'] == 'enabled': + return 'yes' + return 'no' + + @property + def hsts_preload(self): + if self._values['hsts_preload'] is None: + return None + elif self._values['hsts_preload'] == 'enabled': + return 'yes' + return 'no' + + @property + def include_subdomains(self): + if self._values['include_subdomains'] is None: + return None + elif self._values['include_subdomains'] == 'enabled': + return 'yes' + return 'no' + + @property + def maximum_age(self): + if self._values['maximum_age'] is None: + return None + if self._values['maximum_age'] == 4294967295: + return 'indefinite' + return int(self._values['maximum_age']) + + @property + def truncated_redirects(self): + result = flatten_boolean(self._values['truncated_redirects']) + return result + + @property + def max_header_count(self): + if self._values['max_header_count'] is None: + return None + if self._values['max_header_count'] == 64: + return 'default' + return str(self._values['max_header_count']) + + @property + def max_header_size(self): + if self._values['max_header_size'] is None: + return None + if self._values['max_header_size'] == 32768: + return 'default' + return str(self._values['max_header_size']) + + @property + def max_requests(self): + if self._values['max_requests'] is None: + return None + if self._values['max_requests'] == 0: + return 'default' + return str(self._values['max_requests']) + + @property + def known_methods(self): + defaults = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'LOCK', 'OPTIONS', 'POST', 'PROPFIND', 'PUT', 'TRACE', 'UNLOCK'] + known = self._values['known_methods'] + if known is None: + return None + if not known: + return [''] + if set(known) == set(defaults): + return ['default'] + if set(known).issuperset(set(defaults)): + result = [item for item in known if item not in defaults] + result.append('default') + return result + return known + + @property + def enforcement(self): + to_filter = dict( + excess_client_headers=self._values['excess_client_headers'], + excess_server_headers=self._values['excess_server_headers'], + known_methods=self.known_methods, + max_header_count=self.max_header_count, + max_header_size=self.max_header_size, + max_requests=self.max_requests, + oversize_client_headers=self._values['oversize_client_headers'], + oversize_server_headers=self._values['oversize_server_headers'], + pipeline=self._values['pipeline'], + truncated_redirects=self.truncated_redirects, + unknown_method=self._values['unknown_method'] + ) + result = self._filter_params(to_filter) + if result: + return result + + @property + def accept_xff(self): + result = flatten_boolean(self._values['accept_xff']) + return result + + @property + def oneconnect_transformations(self): + result = flatten_boolean(self._values['oneconnect_transformations']) + return result + + @property + def sflow(self): + to_filter = dict( + poll_interval=self._values['poll_interval'], + poll_interval_global=self._values['poll_interval_global'], + sampling_rate=self._values['sampling_rate'], + sampling_rate_global=self._values['sampling_rate_global'], + ) + result = self._filter_params(to_filter) + if result: + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def dns_resolver(self): + if self.want.dns_resolver is None: + return None + if self.want.dns_resolver == '': + if self.have.dns_resolver is None or self.have.dns_resolver == 'none': + return None + elif self.have.proxy_type == 'explicit' and self.want.proxy_type is None: + raise F5ModuleError( + "DNS resolver cannot be empty or 'none' if an existing profile proxy type is set to {0}.".format( + self.have.proxy_type + ) + ) + elif self.have.dns_resolver is not None: + return self.want.dns_resolver + if self.have.dns_resolver is None: + return self.want.dns_resolver + + @property + def header_erase(self): + if self.want.header_erase is None: + return None + if self.want.header_erase in ['none', '']: + if self.have.header_erase in [None, 'none']: + return None + if self.want.header_erase != self.have.header_erase: + return self.want.header_erase + + @property + def header_insert(self): + if self.want.header_insert is None: + return None + if self.want.header_insert in ['none', '']: + if self.have.header_insert in [None, 'none']: + return None + if self.want.header_insert != self.have.header_insert: + return self.want.header_insert + + @property + def server_agent_name(self): + if self.want.server_agent_name is None: + return None + if self.want.server_agent_name in ['none', '']: + if self.have.server_agent_name in [None, 'none']: + return None + if self.want.server_agent_name != self.have.server_agent_name: + return self.want.server_agent_name + + @property + def encrypt_cookies(self): + if self.want.encrypt_cookies is None: + return None + if self.have.encrypt_cookies in [None, []]: + if not self.want.encrypt_cookies: + return None + else: + return self.want.encrypt_cookies + if set(self.want.encrypt_cookies) != set(self.have.encrypt_cookies): + return self.want.encrypt_cookies + + @property + def encrypt_cookie_secret(self): + if self.want.encrypt_cookie_secret != self.have.encrypt_cookie_secret: + if self.want.update_password == 'always': + result = self.want.encrypt_cookie_secret + return result + + @property + def xff_alternative_names(self): + result = cmp_simple_list(self.want.xff_alternative_names, self.have.xff_alternative_names) + return result + + @property + def fallback_status_codes(self): + result = cmp_simple_list(self.want.fallback_status_codes, self.have.fallback_status_codes) + return result + + @property + def known_methods(self): + result = cmp_simple_list(self.want.known_methods, self.have.known_methods) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/profile/http/".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, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.chunk = ['rechunk', 'selective', 'preserve', 'sustain', 'unchunk'] + self.choices = ['pass-through', 'reject'] + self.select = ['allow', 'pass-through', 'reject'] + argument_spec = dict( + name=dict(required=True), + parent=dict(), + description=dict(), + accept_xff=dict(type='bool'), + xff_alternative_names=dict( + type='list', + elements='str', + ), + fallback_host=dict(), + fallback_status_codes=dict( + type='list', + elements='str', + ), + oneconnect_transformations=dict(type='bool'), + request_chunking=dict(choices=self.chunk), + response_chunking=dict(choices=self.chunk), + proxy_type=dict( + choices=[ + 'reverse', + 'transparent', + 'explicit' + ] + ), + dns_resolver=dict(), + insert_xforwarded_for=dict(type='bool'), + redirect_rewrite=dict( + choices=[ + 'none', + 'all', + 'matching', + 'nodes' + ] + ), + encrypt_cookies=dict( + type='list', + elements='str', + ), + encrypt_cookie_secret=dict(no_log=True), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + header_erase=dict(), + header_insert=dict(), + server_agent_name=dict(), + hsts_mode=dict(type='bool'), + hsts_preload=dict(type='bool'), + maximum_age=dict(), + include_subdomains=dict(type='bool'), + enforcement=dict( + type='dict', + options=dict( + truncated_redirects=dict(type='bool'), + excess_client_headers=dict(choices=self.choices), + excess_server_headers=dict(choices=self.choices), + oversize_client_headers=dict(choices=self.choices), + oversize_server_headers=dict(choices=self.choices), + pipeline=dict(choices=self.select), + unknown_method=dict(choices=self.select), + max_header_count=dict(), + max_header_size=dict(), + max_requests=dict(), + known_methods=dict( + type='list', + elements='str', + ), + ) + ), + sflow=dict( + type='dict', + options=dict( + poll_interval=dict(type='int'), + poll_interval_global=dict(type='bool'), + sampling_rate=dict(type='int'), + sampling_rate_global=dict(type='bool'), + ) + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http2.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http2.py new file mode 100644 index 00000000..37daa2a6 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http2.py @@ -0,0 +1,671 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_http2 +short_description: Manage HTTP2 profiles on a BIG-IP +description: + - Manage HTTP2 profiles on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(http2) profile. + type: str + description: + description: + - Description of the profile. + type: str + streams: + description: + - Specifies the number of outstanding concurrent requests allowed on a single HTTP/2 connection. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + - The valid value range is C(1 - 256). + type: int + idle_timeout: + description: + - Specifies the number of seconds an HTTP/2 connection is idly left open before being shut down. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: int + insert_header: + description: + - Specifies whether an HTTP header indicating the use of HTTP/2 should be inserted into the request + that goes to the server. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: bool + insert_header_name: + description: + - Specifies the name of the HTTP header controlled by C(insert_header) parameter. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: str + enforce_tls_requirements: + description: + - Specifies whether the system requires TLS for communications between specified senders and recipients. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: bool + activation_modes: + description: + - Specifies what will cause an incoming connection to be handled as a HTTP/2 connection. + - The C(alpn) and C(always) are mutually exclusive. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: list + elements: str + choices: + - alpn + - always + frame_size: + description: + - Specifies the size of data frames, in bytes, that HTTP/2 sends to the client. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + - The valid value range in bytes is C(1024 - 16384). + type: int + write_size: + description: + - Specifies the total size of combined data frames, in bytes, that HTTP/2 sends in a single write. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + - The valid value range in bytes is C(2048 - 32768). + type: int + receive_window: + description: + - Specifies the way the HTTP/2 profile performs flow control. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + - The valid value range in kilobytes is C(16 - 128). + type: int + header_table_size: + description: + - Specifies the size of the header table, in bytes. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + - The valid value range in bytes is C(0 - 65535). + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create HTTP2 profile + bigip_profile_http2: + name: my_profile + insert_header: yes + insert_header_name: FOO + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove HTTP profile + bigip_profile_http2: + name: my_profile + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add HTTP profile set activation modes + bigip_profile_http: + name: my_profile + activation_modes: + - always + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: Description of the profile. + returned: changed + type: str + sample: My profile +insert_header_name: + description: Specifies the name of the HTTP2 header. + returned: changed + type: str + sample: X-HTTP2 +streams: + description: The number of outstanding concurrent requests allowed on a single HTTP/2 connection. + returned: changed + type: int + sample: 30 +enforce_tls_requirements: + description: Specifies whether the system requires TLS for communications. + returned: changed + type: bool + sample: yes +frame_size: + description: The size of the data frames. + returned: changed + type: int + sample: 30 +activation_modes: + description: Specifies HTTP/2 connection handling modes. + returned: changed + type: list + sample: ['always'] +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name, is_empty_list +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'activationModes': 'activation_modes', + 'concurrentStreamsPerConnection': 'streams', + 'connectionIdleTimeout': 'idle_timeout', + 'defaultsFrom': 'parent', + 'enforceTlsRequirements': 'enforce_tls_requirements', + 'frameSize': 'frame_size', + 'headerTableSize': 'header_table_size', + 'insertHeader': 'insert_header', + 'insertHeaderName': 'insert_header_name', + 'receiveWindow': 'receive_window', + 'writeSize': 'write_size', + } + + api_attributes = [ + 'activationModes', + 'concurrentStreamsPerConnection', + 'connectionIdleTimeout', + 'description', + 'defaultsFrom', + 'enforceTlsRequirements', + 'frameSize', + 'headerTableSize', + 'insertHeader', + 'insertHeaderName', + 'receiveWindow', + 'writeSize', + ] + + returnables = [ + 'activation_modes', + 'streams', + 'description', + 'idle_timeout', + 'parent', + 'enforce_tls_requirements', + 'frame_size', + 'header_table_size', + 'insert_header', + 'insert_header_name', + 'receive_window', + 'write_size', + ] + + updatables = [ + 'activation_modes', + 'streams', + 'description', + 'idle_timeout', + 'parent', + 'enforce_tls_requirements', + 'frame_size', + 'header_table_size', + 'insert_header', + 'insert_header_name', + 'receive_window', + 'write_size', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def streams(self): + streams = self._values['streams'] + if streams is None: + return None + if streams < 1 or streams > 256: + raise F5ModuleError( + "Streams value must be between 1 and 256" + ) + return self._values['streams'] + + @property + def receive_window(self): + window = self._values['receive_window'] + if window is None: + return None + if window < 16 or window > 128: + raise F5ModuleError( + "Receive Window value must be between 16 and 128" + ) + return self._values['receive_window'] + + @property + def header_table_size(self): + header = self._values['header_table_size'] + if header is None: + return None + if header < 0 or header > 65535: + raise F5ModuleError( + "Header Table Size value must be between 0 and 65535" + ) + return self._values['header_table_size'] + + @property + def write_size(self): + write = self._values['write_size'] + if write is None: + return None + if write < 2048 or write > 32768: + raise F5ModuleError( + "Write Size value must be between 2048 and 32768" + ) + return self._values['write_size'] + + @property + def frame_size(self): + frame = self._values['frame_size'] + if frame is None: + return None + if frame < 1024 or frame > 16384: + raise F5ModuleError( + "Write Size value must be between 1024 and 16384" + ) + return self._values['frame_size'] + + @property + def enforce_tls_requirements(self): + result = flatten_boolean(self._values['enforce_tls_requirements']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def insert_header(self): + result = flatten_boolean(self._values['insert_header']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def activation_modes(self): + value = self._values['activation_modes'] + if value is None: + return None + if is_empty_list(value): + raise F5ModuleError( + "Activation Modes cannot be empty, please provide a value" + ) + return value + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def insert_header(self): + if self._values['insert_header'] is None: + return None + elif self._values['insert_header'] == 'enabled': + return 'yes' + return 'no' + + @property + def enforce_tls_requirements(self): + if self._values['enforce_tls_requirements'] is None: + return None + elif self._values['enforce_tls_requirements'] == 'enabled': + return 'yes' + return 'no' + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + if self.want.description is None: + return None + if self.want.description == '': + if self.have.description is None or self.have.description == "none": + return None + if self.want.description != self.have.description: + return self.want.description + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http2/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/profile/http2/".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, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http2/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http2/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http2/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + activation_modes=dict( + type='list', + elements='str', + choices=[ + 'alpn', 'always' + ], + mutually_exclusive=[['always', 'alpn']], + ), + description=dict(), + enforce_tls_requirements=dict(type='bool'), + streams=dict(type='int'), + idle_timeout=dict(type='int'), + frame_size=dict(type='int'), + header_table_size=dict(type='int'), + insert_header=dict(type='bool'), + insert_header_name=dict(), + receive_window=dict(type='int'), + write_size=dict(type='int'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http_compression.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http_compression.py new file mode 100644 index 00000000..a8829cd6 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_http_compression.py @@ -0,0 +1,551 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_http_compression +short_description: Manage HTTP compression profiles on a BIG-IP +description: + - Manage HTTP compression profiles on a BIG-IP device. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the compression profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(httpcompression) profile. + type: str + description: + description: + - Description of the HTTP compression profile. + type: str + buffer_size: + description: + - Maximum number of compressed bytes the system buffers before inserting + a Content-Length header (which specifies the compressed size) into the response. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + type: int + gzip_level: + description: + - Specifies the degree to which the system compresses the content. + - Higher compression levels cause the compression process to be slower. + - Valid values are between 1 (least compression and fastest) to 9 (most + compression and slowest). + type: int + choices: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + gzip_memory_level: + description: + - Number of kilobytes of memory the system uses for internal compression + buffers when compressing a server response. + type: int + choices: + - 1 + - 2 + - 4 + - 8 + - 16 + - 32 + - 64 + - 128 + - 256 + gzip_window_size: + description: + - Number of kilobytes in the window size the system uses when compressing + a server response. + type: int + choices: + - 1 + - 2 + - 4 + - 8 + - 16 + - 32 + - 64 + - 128 + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present + content_type_include: + description: Specifies the list of the content types that are allowed. + type: list + elements: str + version_added: "1.15.0" +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create an HTTP compression profile + bigip_profile_http_compression: + name: profile1 + description: Custom HTTP Compression Profile + buffer_size: 131072 + gzip_level: 6 + gzip_memory_level: 16k + gzip_window_size: 64k + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the resource. + returned: changed + type: str + sample: My custom profile +buffer_size: + description: The new buffer size of the profile. + returned: changed + type: int + sample: 4096 +gzip_memory_level: + description: The new GZIP memory level of the profile, in KB. + returned: changed + type: int + sample: 16 +gzip_level: + description: The new GZIP level of the profile. Smaller is less compression. + returned: changed + type: int + sample: 2 +gzip_window_size: + description: The new GZIP window size of the profile, in KB. + returned: changed + type: int + sample: 64 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'bufferSize': 'buffer_size', + 'defaultsFrom': 'parent', + 'gzipMemoryLevel': 'gzip_memory_level', + 'gzipLevel': 'gzip_level', + 'gzipWindowSize': 'gzip_window_size', + 'contentTypeInclude': 'content_type_include', + } + + api_attributes = [ + 'description', + 'bufferSize', + 'defaultsFrom', + 'gzipMemoryLevel', + 'gzipLevel', + 'gzipWindowSize', + 'contentTypeInclude', + ] + + returnables = [ + 'description', + 'buffer_size', + 'gzip_memory_level', + 'gzip_level', + 'gzip_window_size', + 'content_type_include', + ] + + updatables = [ + 'description', + 'buffer_size', + 'gzip_memory_level', + 'gzip_level', + 'gzip_window_size', + 'parent', + 'content_type_include' + ] + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + @property + def gzip_memory_level(self): + if self._values['gzip_memory_level'] is None: + return None + return self._values['gzip_memory_level'] / 1024 + + @property + def gzip_window_size(self): + if self._values['gzip_window_size'] is None: + return None + return self._values['gzip_window_size'] / 1024 + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def gzip_memory_level(self): + if self._values['gzip_memory_level'] is None: + return None + return self._values['gzip_memory_level'] * 1024 + + @property + def gzip_window_size(self): + if self._values['gzip_window_size'] is None: + return None + return self._values['gzip_window_size'] * 1024 + + +class ReportableChanges(Changes): + @property + def gzip_memory_level(self): + if self._values['gzip_memory_level'] is None: + return None + return self._values['gzip_memory_level'] / 1024 + + @property + def gzip_window_size(self): + if self._values['gzip_window_size'] is None: + return None + return self._values['gzip_window_size'] / 1024 + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http-compression/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/profile/http-compression/".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, 404]: + 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/ltm/profile/http-compression/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http-compression/{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 + raise F5ModuleError(resp.content) + + def read_current_from_device(self): # lgtm [py/similar-function] + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http-compression/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + buffer_size=dict(type='int'), + description=dict(), + gzip_level=dict( + type='int', + choices=[1, 2, 3, 4, 5, 6, 7, 8, 9] + ), + gzip_memory_level=dict( + type='int', + choices=[1, 2, 4, 8, 16, 32, 64, 128, 256] + ), + gzip_window_size=dict( + type='int', + choices=[1, 2, 4, 8, 16, 32, 64, 128] + ), + state=dict( + default='present', + choices=['present', 'absent'] + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + content_type_include=dict( + elements='str', + type='list' + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_oneconnect.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_oneconnect.py new file mode 100644 index 00000000..0c10b37b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_oneconnect.py @@ -0,0 +1,612 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_oneconnect +short_description: Manage OneConnect profiles on a BIG-IP +description: + - Manage OneConnect profiles on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the OneConnect profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(oneconnect) profile. + type: str + source_mask: + description: + - Specifies a value the system applies to the source address to determine + its eligibility for reuse. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + - The system applies the value of this setting to the server-side source address to + determine its eligibility for reuse. + - A mask of C(0) causes the system to share reused connections across all source + addresses. A host mask of C(32) causes the system to share only those reused + connections originating from the same source address. + - When you are using a SNAT or SNAT pool, the server-side source address is + translated first and then the OneConnect mask is applied to the translated address. + type: str + description: + description: + - Description of the profile. + type: str + maximum_size: + description: + - Specifies the maximum number of connections the system holds in the + connection reuse pool. + - If the pool is already full, a server-side connection closes after the + response is completed. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: int + maximum_age: + description: + - Specifies the maximum number of seconds allowed for a connection in the connection + reuse pool. + - For any connection with an age higher than this value, the system removes that + connection from the re-use pool. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: int + maximum_reuse: + description: + - Specifies the maximum number of times that a server-side connection can be reused. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: int + idle_timeout_override: + description: + - Specifies the number of seconds a connection is idle before the connection + flow is eligible for deletion. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + - You may specify a number of seconds for the timeout override. + - When C(disabled), specifies there is no timeout override for the connection. + - When C(indefinite), specifies a connection may be idle with no timeout + override. + type: str + limit_type: + description: + - When C(none), simultaneous in-flight requests and responses over TCP connections + to a pool member are counted toward the limit. This is the historical behavior. + - When C(idle), idle connections will be dropped as the TCP connection limit is + reached. For short intervals, during the overlap of the idle connection being + dropped and the new connection being established, the TCP connection limit may + be exceeded. + - When C(strict), the TCP connection limit is honored with no exceptions. This means + that idle connections will prevent new TCP connections from being made until + they expire, even if they could otherwise be reused. + - C(strict) is not a recommended configuration except in very special cases with + short expiration timeouts. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + type: str + choices: + - none + - idle + - strict + share_pools: + description: + - Indicates connections may be shared not only within a virtual server, but + also among similar virtual servers. + - When C(yes), all virtual servers that use the same OneConnect and other internal + network profiles can share connections. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a OneConnect profile + bigip_profile_oneconnect: + name: foo + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +source_mask: + description: Value the system applies to the source address to determine its eligibility for reuse. + returned: changed + type: str + sample: 255.255.255.255 +description: + description: Description of the profile. + returned: changed + type: str + sample: My profile +maximum_size: + description: Maximum number of connections the system holds in the connection reuse pool. + returned: changed + type: int + sample: 3000 +maximum_age: + description: Maximum number of seconds allowed for a connection in the connection reuse pool. + returned: changed + type: int + sample: 2000 +maximum_reuse: + description: Maximum number of times a server-side connection can be reused. + returned: changed + type: int + sample: 1000 +idle_timeout_override: + description: The new idle timeout override. + returned: changed + type: str + sample: disabled +limit_type: + description: New limit type of the profile. + returned: changed + type: str + sample: idle +share_pools: + description: Share connections among similar virtual servers. + returned: changed + type: bool + sample: yes +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'sourceMask': 'source_mask', + 'maxSize': 'maximum_size', + 'maxReuse': 'maximum_reuse', + 'maxAge': 'maximum_age', + 'defaultsFrom': 'parent', + 'limitType': 'limit_type', + 'idleTimeoutOverride': 'idle_timeout_override', + 'sharePools': 'share_pools', + } + + api_attributes = [ + 'sourceMask', + 'maxSize', + 'defaultsFrom', + 'description', + 'limitType', + 'idleTimeoutOverride', + 'maxAge', + 'maxReuse', + 'sharePools', + ] + + returnables = [ + 'description', + 'source_mask', + 'maximum_size', + 'maximum_age', + 'maximum_reuse', + 'limit_type', + 'idle_timeout_override', + 'share_pools', + 'parent', + ] + + updatables = [ + 'description', + 'source_mask', + 'maximum_size', + 'maximum_age', + 'maximum_reuse', + 'limit_type', + 'idle_timeout_override', + 'share_pools', + 'parent', + ] + + +class ApiParameters(Parameters): + @property + def source_mask(self): + if self._values['source_mask'] is None: + return None + elif self._values['source_mask'] == 'any': + return 0 + return self._values['source_mask'] + + @property + def idle_timeout_override(self): + if self._values['idle_timeout_override'] is None: + return None + try: + return int(self._values['idle_timeout_override']) + except ValueError: + return self._values['idle_timeout_override'] + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def idle_timeout_override(self): + if self._values['idle_timeout_override'] is None: + return None + try: + return int(self._values['idle_timeout_override']) + except ValueError: + return self._values['idle_timeout_override'] + + @property + def source_mask(self): + if self._values['source_mask'] is None: + return None + elif self._values['source_mask'] == 'any': + return 0 + try: + int(self._values['source_mask']) + raise F5ModuleError( + "'source_mask' must not be in CIDR format." + ) + except ValueError: + pass + + if is_valid_ip(self._values['source_mask']): + return self._values['source_mask'] + + @property + def share_pools(self): + if self._values['share_pools'] is None: + return None + elif self._values['share_pools'] is True: + return 'enabled' + return 'disabled' + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def idle_timeout_override(self): + try: + return int(self._values['idle_timeout_override']) + except ValueError: + return self._values['idle_timeout_override'] + + @property + def share_pools(self): + if self._values['idle_timeout_override'] is None: + return None + elif self._values['idle_timeout_override'] == 'enabled': + return 'yes' + elif self._values['idle_timeout_override'] == 'disabled': + return 'no' + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/profile/one-connect/".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, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + parent=dict(), + source_mask=dict(), + maximum_size=dict(type='int'), + maximum_reuse=dict(type='int'), + maximum_age=dict(type='int'), + limit_type=dict( + choices=['none', 'idle', 'strict'] + ), + idle_timeout_override=dict(), + share_pools=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_cookie.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_cookie.py new file mode 100644 index 00000000..a26c774c --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_cookie.py @@ -0,0 +1,964 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_persistence_cookie +short_description: Manage cookie persistence profiles on BIG-IP +description: + - Manage cookie persistence profiles on BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + description: + description: + - Description of the profile. + type: str + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(cookie) profile. + type: str + default: cookie + cookie_method: + description: + - Specifies the type of cookie processing the system uses. + - When C(hash), specifies the server provides the cookie, which the + system then maps consistently to a specific node. This persistence type + requires a C(cookie_name) value. + - When C(insert), specifies the system inserts server information, + in the form of a cookie, into the header of the server response. + - When C(passive), specifies the server provides the cookie, formatted + with the correct server information and timeout. This persistence type + requires a C(cookie_name) value. + - When C(rewrite), specifies the system intercepts the BIGipCookie + header, sent from the server, and overwrites the name and value of that + cookie. + type: str + choices: + - hash + - insert + - passive + - rewrite + cookie_name: + description: + - Specifies a unique name for the cookie. + type: str + http_only: + description: + - Specifies whether the httponly attribute should be enabled or + disabled for the inserted cookies. + type: bool + match_across_services: + description: + - When C(yes), specifies all persistent connections from a client IP address that go + to the same virtual IP address also go to the same node. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + match_across_virtuals: + description: + - When C(yes), specifies all persistent connections from the same client IP address + go to the same node. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + match_across_pools: + description: + - When C(yes), specifies the system can use any pool that contains this persistence + record. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + cookie_encryption: + description: + - Specifies the way in which the cookie encryption format is used. + - When C(disabled), generates the cookie format unencrypted. + - When C(preferred), generates an encrypted cookie, but accepts both encrypted and unencrypted formats. + - When C(required), cookie format must be encrypted. + type: str + choices: + - disabled + - preferred + - required + override_connection_limit: + description: + - When C(yes), specifies the system allows you to specify that pool member connection + limits will be overridden for persisted clients. + - Per-virtual connection limits remain hard limits and are not overridden. + type: bool + encrypt_cookie_pool_name: + description: + - Specifies whether the pool-name in the inserted BIG-IP default cookie should be encrypted. + type: bool + always_send: + description: + - Sends the cookie persistence entry on every reply, even if the + entry has previously been supplied to the client. + type: bool + secure: + description: + - Specifies whether the secure attribute should be enabled or + disabled for the inserted cookies. + type: bool + encryption_passphrase: + description: + - Specifies a passphrase to be used for cookie encryption. + type: str + update_password: + description: + - C(always) will allow updating passphrases if the user chooses to do so. + C(on_create) will only set the passphrase for newly created profiles. + type: str + choices: + - always + - on_create + default: always + expiration: + description: + - Specifies the expiration time of the cookie. By default the system generates and uses a session cookie. + This cookie expires when the user session expires (when the browser is closed). + suboptions: + days: + description: + - Cookie expiration time in days. The value must be in range from C(0) to C(24855) days. + type: int + hours: + description: + - Cookie expiration time in hours. The value must be in the range from C(0) to C(23) hours. + type: int + minutes: + description: + - Cookie expiration time in minutes. The value must be in the range from C(0) to C(59) minutes. + type: int + seconds: + description: + - Cookie expiration time in seconds. The value must be in the range from C(0) to C(59) seconds. + type: int + default: 0 + type: dict + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a persistence cookie profile + bigip_profile_persistence_cookie: + name: foo + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +- name: Create a persistence cookie profile with expiration time + bigip_profile_persistence_cookie: + name: foo + expiration: + days: 7 + hours: 12 + minutes: 30 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +cookie_name: + description: The new Cookie Name value. + returned: changed + type: str + sample: cookie1 +cookie_method: + description: The new Cookie Method. + returned: changed + type: str + sample: insert +parent: + description: The parent profile. + returned: changed + type: str + sample: /Common/cookie +cookie_encryption: + description: The new Cookie Encryption type. + returned: changed + type: str + sample: preferred +match_across_pools: + description: The new Match Across Pools value. + returned: changed + type: bool + sample: yes +match_across_services: + description: The new Match Across Services value. + returned: changed + type: bool + sample: no +match_across_virtuals: + description: The new Match Across Virtuals value. + returned: changed + type: bool + sample: yes +override_connection_limit: + description: The new Override Connection Limit value. + returned: changed + type: bool + sample: no +encrypt_cookie_pool_name: + description: The new Encrypt Cookie Pool Name value. + returned: changed + type: bool + sample: yes +always_send: + description: The new Always Send value. + returned: changed + type: bool + sample: no +http_only: + description: The new HTTP Only value. + returned: changed + type: bool + sample: yes +description: + description: The new description. + returned: changed + type: str + sample: My description +secure: + description: The new Secure Cookie value. + returned: changed + type: bool + sample: no +expiration: + description: The expiration time of the cookie. + returned: changed + type: complex + contains: + days: + description: Cookie expiration time in days. + returned: changed + type: int + sample: 125 + hours: + description: Cookie expiration time in hours. + returned: changed + type: int + sample: 22 + minutes: + description: Cookie expiration time in minutes. + returned: changed + type: int + sample: 58 + seconds: + description: Cookie expiration time in seconds. + returned: changed + type: int + sample: 20 + sample: hash/dictionary of values +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'cookieName': 'cookie_name', + 'method': 'cookie_method', + 'defaultsFrom': 'parent', + 'cookieEncryption': 'cookie_encryption', + 'matchAcrossPools': 'match_across_pools', + 'matchAcrossServices': 'match_across_services', + 'matchAcrossVirtuals': 'match_across_virtuals', + 'overrideConnectionLimit': 'override_connection_limit', + 'encryptCookiePoolname': 'encrypt_cookie_pool_name', + 'alwaysSend': 'always_send', + 'httponly': 'http_only', + 'cookieEncryptionPassphrase': 'encryption_passphrase', + } + + api_attributes = [ + 'description', + 'cookieName', + 'defaultsFrom', + 'cookieEncryption', + 'matchAcrossPools', + 'matchAcrossServices', + 'matchAcrossVirtuals', + 'overrideConnectionLimit', + 'encryptCookiePoolname', + 'alwaysSend', + 'httponly', + 'secure', + 'cookieEncryptionPassphrase', + 'method', + 'expiration' + ] + + returnables = [ + 'cookie_name', + 'cookie_method', + 'parent', + 'cookie_encryption', + 'match_across_pools', + 'match_across_services', + 'match_across_virtuals', + 'override_connection_limit', + 'encrypt_cookie_pool_name', + 'always_send', + 'http_only', + 'encryption_passphrase', + 'description', + 'secure', + 'expiration', + ] + + updatables = [ + 'cookie_name', + 'cookie_method', + 'parent', + 'cookie_encryption', + 'match_across_pools', + 'match_across_services', + 'match_across_virtuals', + 'override_connection_limit', + 'encrypt_cookie_pool_name', + 'always_send', + 'http_only', + 'encryption_passphrase', + 'description', + 'secure', + 'expiration', + ] + + @property + def encrypt_cookie_pool_name(self): + return flatten_boolean(self._values['encrypt_cookie_pool_name']) + + @property + def always_send(self): + return flatten_boolean(self._values['always_send']) + + @property + def match_across_pools(self): + return flatten_boolean(self._values['match_across_pools']) + + @property + def match_across_services(self): + return flatten_boolean(self._values['match_across_services']) + + @property + def match_across_virtuals(self): + return flatten_boolean(self._values['match_across_virtuals']) + + @property + def http_only(self): + return flatten_boolean(self._values['http_only']) + + @property + def secure(self): + return flatten_boolean(self._values['secure']) + + @property + def override_connection_limit(self): + return flatten_boolean(self._values['override_connection_limit']) + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + @property + def expiration(self): + if self._values['expiration'] is None: + return None + + days = self.days + hours = self.hours + minutes = self.minutes + seconds = self.seconds + + if days is not None: + if hours is None: + raise F5ModuleError( + "Incorrect format, 'hours' parameter is missing value." + ) + if minutes is None: + raise F5ModuleError( + "Incorrect format, 'minutes' parameter is missing value." + ) + + expiry_time = '{0}:{1}:{2}:{3}'.format(days, hours, minutes, seconds) + return expiry_time + + if hours is not None: + if minutes is None: + raise F5ModuleError( + "Incorrect format, 'minutes' parameter is missing value." + ) + + expiry_time = '{0}:{1}:{2}'.format(hours, minutes, seconds) + return expiry_time + + if minutes is not None: + expiry_time = '{0}:{1}'.format(minutes, seconds) + return expiry_time + + return str(seconds) + + @property + def days(self): + days = self._values['expiration']['days'] + if days is None: + return None + if days < 0 or days >= 24856: + raise F5ModuleError( + 'The provided value is invalid, the correct value range is: 0 - 24855 days.' + ) + return days + + @property + def hours(self): + hours = self._values['expiration']['hours'] + if hours is None: + return None + if hours < 0 or hours > 23: + raise F5ModuleError( + 'The provided value is invalid, the correct value range is: 0 - 23 hours.' + ) + return hours + + @property + def minutes(self): + minutes = self._values['expiration']['minutes'] + if minutes is None: + return None + if minutes < 0 or minutes > 59: + raise F5ModuleError( + 'The provided value is invalid, the correct value range is: 0 - 59 minutes.' + ) + return minutes + + @property + def seconds(self): + seconds = self._values['expiration']['seconds'] + if seconds < 0 or seconds > 59: + raise F5ModuleError( + 'The provided value is invalid, the correct value range is: 0 - 59 seconds.' + ) + return seconds + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def encrypt_cookie_pool_name(self): + if self._values['encrypt_cookie_pool_name'] is None: + return None + elif self._values['encrypt_cookie_pool_name'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def always_send(self): + if self._values['always_send'] is None: + return None + elif self._values['always_send'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def match_across_pools(self): + if self._values['match_across_pools'] is None: + return None + elif self._values['match_across_pools'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def match_across_services(self): + if self._values['match_across_services'] is None: + return None + elif self._values['match_across_services'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def match_across_virtuals(self): + if self._values['match_across_virtuals'] is None: + return None + elif self._values['match_across_virtuals'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def http_only(self): + if self._values['http_only'] is None: + return None + elif self._values['http_only'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def secure(self): + if self._values['secure'] is None: + return None + elif self._values['secure'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def override_connection_limit(self): + if self._values['override_connection_limit'] is None: + return None + elif self._values['override_connection_limit'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def encrypt_cookie_pool_name(self): + return flatten_boolean(self._values['encrypt_cookie_pool_name']) + + @property + def always_send(self): + return flatten_boolean(self._values['always_send']) + + @property + def match_across_pools(self): + return flatten_boolean(self._values['match_across_pools']) + + @property + def match_across_services(self): + return flatten_boolean(self._values['match_across_services']) + + @property + def match_across_virtuals(self): + return flatten_boolean(self._values['match_across_virtuals']) + + @property + def http_only(self): + return flatten_boolean(self._values['http_only']) + + @property + def secure(self): + return flatten_boolean(self._values['secure']) + + @property + def override_connection_limit(self): + return flatten_boolean(self._values['override_connection_limit']) + + @property + def encryption_passphrase(self): + return None + + @property + def expiration(self): + expire = self._values['expiration'] + result = dict() + + if expire is None: + return None + tmp = expire.split(':') + + if len(tmp) == 1: + result['seconds'] = int(tmp[0]) + if len(tmp) == 2: + result['minutes'] = int(tmp[0]) + result['seconds'] = int(tmp[1]) + if len(tmp) == 3: + result['hours'] = int(tmp[0]) + result['minutes'] = int(tmp[1]) + result['seconds'] = int(tmp[2]) + if len(tmp) == 4: + result['days'] = int(tmp[0]) + result['hours'] = int(tmp[1]) + result['minutes'] = int(tmp[2]) + result['seconds'] = int(tmp[3]) + + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/persistence/cookie/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + + if self.want.update_password == 'always': + self.want.update({'encryption_passphrase': self.want.encryption_passphrase}) + else: + if self.want.encryption_passphrase: + del self.want._values['encryption_passphrase'] + + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/persistence/cookie/".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, 404]: + 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/ltm/persistence/cookie/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/persistence/cookie/{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 + raise F5ModuleError(resp.content) + + def read_current_from_device(self): # lgtm [py/similar-function] + uri = "https://{0}:{1}/mgmt/tm/ltm/persistence/cookie/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(default='cookie'), + cookie_name=dict(), + cookie_method=dict( + choices=[ + 'hash', 'insert', 'passive', 'rewrite' + ] + ), + description=dict(), + secure=dict(type='bool'), + http_only=dict(type='bool'), + cookie_encryption=dict( + choices=[ + 'disabled', 'preferred', 'required' + ] + ), + always_send=dict(type='bool'), + match_across_services=dict(type='bool'), + match_across_virtuals=dict(type='bool'), + match_across_pools=dict(type='bool'), + override_connection_limit=dict(type='bool'), + encrypt_cookie_pool_name=dict(type='bool'), + encryption_passphrase=dict(no_log=True), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + expiration=dict( + type='dict', + options=dict( + days=dict( + type='int' + ), + hours=dict( + type='int' + ), + minutes=dict( + type='int' + ), + seconds=dict( + type='int', + default=0 + ) + ) + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_src_addr.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_src_addr.py new file mode 100644 index 00000000..bd17905d --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_src_addr.py @@ -0,0 +1,630 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_persistence_src_addr +short_description: Manage source address persistence profiles +description: + - Manages source address persistence profiles on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(source_addr) profile. + type: str + match_across_services: + description: + - When C(yes), specifies all persistent connections from a client IP address that go + to the same virtual IP address also go to the same node. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + match_across_virtuals: + description: + - When C(yes), specifies all persistent connections from the same client IP address + go to the same node. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + match_across_pools: + description: + - When C(yes), specifies the system can use any pool that contains this persistence + record. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + mirror: + description: + - When C(yes), specifies that if the active unit goes into the standby mode, the system + mirrors any persistence records to its peer. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + mask: + description: + - Specifies a value the system applies as the prefix length. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: str + hash_algorithm: + description: + - Specifies the algorithm the system uses for hash persistence load balancing. The hash + result is the input for the algorithm. + - When C(default), specifies the system uses the index of pool members to obtain the + hash result for the input to the algorithm. + - When C(carp), specifies the system uses the Cache Array Routing Protocol (CARP) + to obtain the hash result for the input to the algorithm. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: str + choices: + - default + - carp + entry_timeout: + description: + - Specifies the duration of the persistence entries. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + - To specify an indefinite timeout, use the value C(indefinite). + - If specifying a numeric timeout, the value must be between C(1) and C(4294967295). + type: str + override_connection_limit: + description: + - When C(yes), specifies the system allows you to specify that pool member connection + limits will be overridden for persisted clients. + - Per-virtual connection limits remain hard limits and are not overridden. + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a profile + bigip_profile_persistence_src_addr: + name: foo + state: present + hash_algorithm: carp + match_across_services: yes + match_across_virtuals: yes + mirror: yes + mask: 255.255.255.255 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: The parent profile. + returned: changed + type: str + sample: /Common/cookie +hash_algorithm: + description: The algorithm used for hash persistence. + returned: changed + type: str + sample: default +match_across_pools: + description: The new Match Across Pools value. + returned: changed + type: bool + sample: yes +match_across_services: + description: The new Match Across Services value. + returned: changed + type: bool + sample: no +match_across_virtuals: + description: The new Match Across Virtuals value. + returned: changed + type: bool + sample: yes +override_connection_limit: + description: The new Override Connection Limit value. + returned: changed + type: bool + sample: no +entry_timeout: + description: The duration of the persistence entries. + returned: changed + type: str + sample: 180 +mirror: + description: The new Mirror value. + returned: changed + type: bool + sample: yes +mask: + description: The persist mask value. + returned: changed + type: str + sample: 255.255.255.255 +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'hashAlgorithm': 'hash_algorithm', + 'matchAcrossPools': 'match_across_pools', + 'matchAcrossServices': 'match_across_services', + 'matchAcrossVirtuals': 'match_across_virtuals', + 'overrideConnectionLimit': 'override_connection_limit', + + # This timeout name needs to be overridden because 'timeout' is a connection + # parameter and we don't want that to be the value that is always set here. + 'timeout': 'entry_timeout' + } + + api_attributes = [ + 'defaultsFrom', + 'hashAlgorithm', + 'matchAcrossPools', + 'matchAcrossServices', + 'matchAcrossVirtuals', + 'overrideConnectionLimit', + 'timeout', + 'mirror', + 'mask', + ] + + returnables = [ + 'parent', + 'hash_algorithm', + 'match_across_pools', + 'match_across_services', + 'match_across_virtuals', + 'override_connection_limit', + 'entry_timeout', + 'mirror', + 'mask', + ] + + updatables = [ + 'hash_algorithm', + 'match_across_pools', + 'match_across_services', + 'match_across_virtuals', + 'override_connection_limit', + 'entry_timeout', + 'parent', + 'mirror', + 'mask', + ] + + @property + def entry_timeout(self): + if self._values['entry_timeout'] in [None, 'indefinite']: + return self._values['entry_timeout'] + timeout = int(self._values['entry_timeout']) + if 1 > timeout > 4294967295: + raise F5ModuleError( + "'timeout' value must be between 1 and 4294967295, or the value 'indefinite'." + ) + return timeout + + @property + def match_across_pools(self): + return flatten_boolean(self._values['match_across_pools']) + + @property + def match_across_services(self): + return flatten_boolean(self._values['match_across_services']) + + @property + def match_across_virtuals(self): + return flatten_boolean(self._values['match_across_virtuals']) + + @property + def override_connection_limit(self): + return flatten_boolean(self._values['override_connection_limit']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def mirror(self): + if self._values['mirror'] is None: + return None + result = flatten_boolean(self._values['mirror']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def mask(self): + if self._values['mask'] is None: + return None + if is_valid_ip(self._values['mask']): + return self._values['mask'] + else: + raise F5ModuleError( + "The provided 'mask' is not a valid IP address" + ) + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def match_across_pools(self): + if self._values['match_across_pools'] is None: + return None + elif self._values['match_across_pools'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def match_across_services(self): + if self._values['match_across_services'] is None: + return None + elif self._values['match_across_services'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def match_across_virtuals(self): + if self._values['match_across_virtuals'] is None: + return None + elif self._values['match_across_virtuals'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def override_connection_limit(self): + if self._values['override_connection_limit'] is None: + return None + elif self._values['override_connection_limit'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def match_across_pools(self): + return flatten_boolean(self._values['match_across_pools']) + + @property + def match_across_services(self): + return flatten_boolean(self._values['match_across_services']) + + @property + def match_across_virtuals(self): + return flatten_boolean(self._values['match_across_virtuals']) + + @property + def override_connection_limit(self): + return flatten_boolean(self._values['override_connection_limit']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/persistence/source-addr/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/persistence/source-addr/".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, 404]: + 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/ltm/persistence/source-addr/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/persistence/source-addr/{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 + raise F5ModuleError(resp.content) + + def read_current_from_device(self): # lgtm [py/similar-function] + uri = "https://{0}:{1}/mgmt/tm/ltm/persistence/source-addr/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + match_across_services=dict(type='bool'), + match_across_virtuals=dict(type='bool'), + match_across_pools=dict(type='bool'), + mirror=dict(type='bool'), + mask=dict(), + hash_algorithm=dict(choices=['default', 'carp']), + entry_timeout=dict(), + override_connection_limit=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_universal.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_universal.py new file mode 100644 index 00000000..7a2e5f6b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_persistence_universal.py @@ -0,0 +1,600 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_persistence_universal +short_description: Manage universal persistence profiles +description: + - Manages universal persistence profiles on the BIG-IP system. +version_added: "1.1.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(universal) profile. + type: str + app_service: + description: + - The iApp service to be associated with this profile. When no service is + specified, the default is None. + type: str + match_across_services: + description: + - When C(yes), specifies all persistent connections from a client IP address that go + to the same virtual IP address also go to the same node. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + match_across_virtuals: + description: + - When C(yes), specifies all persistent connections from the same client IP address + go to the same node. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + match_across_pools: + description: + - When C(yes), specifies the system can use any pool that contains this persistence + record. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + mirror: + description: + - When C(yes), specifies if the active unit goes into the standby mode, the system + mirrors any persistence records to its peer. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + rule: + description: + - Specifies the iRule used to select a persistence entry. + - When creating a new profile, if this parameter is not specified, the + default is C(None), which disables this setting. + type: str + timeout: + description: + - Specifies the duration of the persistence entries. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + - To specify an indefinite timeout, use the value C(indefinite). + - If specifying a numeric timeout, the value must be between C(1) and C(4294967295). + type: str + override_connection_limit: + description: + - When C(yes), specifies the system allows you to specify that pool member connection + limits will be overridden for persisted clients. + - Per-virtual connection limits remain hard limits and are not overridden. + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Nitin Khanna (@nitinthewiz) +''' + +EXAMPLES = r''' +- name: Create a profile + bigip_profile_persistence_universal: + name: foo + state: present + match_across_services: yes + match_across_virtuals: yes + mirror: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: The parent profile. + returned: changed + type: str + sample: /Common/cookie +app_service: + description: The iApp service associated with this profile + returned: changed + type: str + sample: /Common/good_service.app/good_service +match_across_pools: + description: The new Match Across Pools value. + returned: changed + type: bool + sample: yes +match_across_services: + description: The new Match Across Services value. + returned: changed + type: bool + sample: no +match_across_virtuals: + description: The new Match Across Virtuals value. + returned: changed + type: bool + sample: yes +override_connection_limit: + description: The new Override Connection Limit value. + returned: changed + type: bool + sample: no +timeout: + description: The duration of the persistence entries. + returned: changed + type: str + sample: 180 +mirror: + description: The new Mirror value. + returned: changed + type: bool + sample: yes +rule: + description: The iRule used to select persistence entry. + returned: changed + type: str + sample: /Common/_sys_https_redirect +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'appService': 'app_service', + 'defaultsFrom': 'parent', + 'matchAcrossPools': 'match_across_pools', + 'matchAcrossServices': 'match_across_services', + 'matchAcrossVirtuals': 'match_across_virtuals', + 'overrideConnectionLimit': 'override_connection_limit', + } + + api_attributes = [ + 'appService', + 'defaultsFrom', + 'matchAcrossPools', + 'matchAcrossServices', + 'matchAcrossVirtuals', + 'overrideConnectionLimit', + 'timeout', + 'mirror', + 'rule', + ] + + returnables = [ + 'app_service', + 'parent', + 'match_across_pools', + 'match_across_services', + 'match_across_virtuals', + 'override_connection_limit', + 'timeout', + 'mirror', + 'rule', + ] + + updatables = [ + 'app_service', + 'match_across_pools', + 'match_across_services', + 'match_across_virtuals', + 'override_connection_limit', + 'timeout', + 'parent', + 'mirror', + 'rule', + ] + + @property + def timeout(self): + if self._values['timeout'] in [None, 'indefinite']: + return self._values['timeout'] + timeout = int(self._values['timeout']) + if 1 > timeout > 4294967295: + raise F5ModuleError( + "'timeout' value must be between 1 and 4294967295, or the value 'indefinite'." + ) + return timeout + + @property + def match_across_pools(self): + return flatten_boolean(self._values['match_across_pools']) + + @property + def match_across_services(self): + return flatten_boolean(self._values['match_across_services']) + + @property + def match_across_virtuals(self): + return flatten_boolean(self._values['match_across_virtuals']) + + @property + def override_connection_limit(self): + return flatten_boolean(self._values['override_connection_limit']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def mirror(self): + result = flatten_boolean(self._values['mirror']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def match_across_pools(self): + if self._values['match_across_pools'] is None: + return None + elif self._values['match_across_pools'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def match_across_services(self): + if self._values['match_across_services'] is None: + return None + elif self._values['match_across_services'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def match_across_virtuals(self): + if self._values['match_across_virtuals'] is None: + return None + elif self._values['match_across_virtuals'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def override_connection_limit(self): + if self._values['override_connection_limit'] is None: + return None + elif self._values['override_connection_limit'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def match_across_pools(self): + return flatten_boolean(self._values['match_across_pools']) + + @property + def match_across_services(self): + return flatten_boolean(self._values['match_across_services']) + + @property + def match_across_virtuals(self): + return flatten_boolean(self._values['match_across_virtuals']) + + @property + def override_connection_limit(self): + return flatten_boolean(self._values['override_connection_limit']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/persistence/universal/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/persistence/universal/".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, 404]: + 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/ltm/persistence/universal/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/persistence/universal/{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 + raise F5ModuleError(resp.content) + + def read_current_from_device(self): # lgtm [py/similar-function] + uri = "https://{0}:{1}/mgmt/tm/ltm/persistence/universal/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + app_service=dict(), + parent=dict(), + match_across_services=dict(type='bool'), + match_across_virtuals=dict(type='bool'), + match_across_pools=dict(type='bool'), + mirror=dict(type='bool'), + rule=dict(), + timeout=dict(), + override_connection_limit=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_server_ssl.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_server_ssl.py new file mode 100644 index 00000000..15389ee5 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_server_ssl.py @@ -0,0 +1,853 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_server_ssl +short_description: Manages server SSL profiles on a BIG-IP +description: + - Manages server SSL profiles on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - The parent template of this monitor template. Once this value has + been set, it cannot be changed. + type: str + default: /Common/serverssl + ciphers: + description: + - Specifies the list of ciphers the system supports. When creating a new + profile, the default cipher list is provided by the parent profile. + - When the C(cipher_group) parameter is in use, the C(ciphers) parameter needs to be set to either C(none) or C(''). + type: str + cipher_group: + description: + - Specifies the cipher group to assign to this profile. + - When the C(ciphers) parameter is in use, the C(cipher_group) must be set to either C(none) or C(''). + - When creating a new profile with C(cipher_group), if the parent profile has C(ciphers) set by default, + the C(cipher) parameter must be set to C(none) or C('') during creation. + - The parameter only works on TMOS version 13.x and later. + type: str + version_added: "1.12.0" + renegotiation: + description: + - Enables or disables SSL renegotiation. + - When creating a new profile, the setting is provided by the parent profile. + type: bool + secure_renegotiation: + description: + - Specifies the method of secure renegotiations for SSL connections. When + creating a new profile, the setting is provided by the parent profile. + - When C(request) is set, the system requests secure renegotiation of SSL + connections. + - C(require) is a default setting and when set, the system permits initial SSL + handshakes from clients but terminates renegotiations from unpatched clients. + - With the C(require-strict) setting, the system requires strict renegotiation of SSL + connections. In this mode the system refuses connections to insecure servers, + and terminates existing SSL connections to insecure servers. + type: str + choices: + - require + - require-strict + - request + server_name: + description: + - Specifies the fully qualified DNS hostname of the server used in Server Name + Indication communications. When creating a new profile, the setting is provided + by the parent profile. + type: str + sni_default: + description: + - Indicates the system uses this profile as the default SSL profile when there + is no match to the server name, or when the client provides no SNI extension support. + - When creating a new profile, the setting is provided by the parent profile. + - There can be only one SSL profile with this setting enabled. + type: bool + sni_require: + description: + - Requires the network peers also provide SNI support. This setting only takes + effect when C(sni_default) is C(yes). + - When creating a new profile, the setting is provided by the parent profile. + type: bool + server_certificate: + description: + - Specifies the way the system handles server certificates. + - When C(ignore), specifies the system ignores certificates from server systems. + - When C(require), specifies the system requires a server to present a valid + certificate. + type: str + choices: + - ignore + - require + certificate: + description: + - Specifies the name of the certificate the system uses for server-side SSL + processing. + type: str + key: + description: + - Specifies the file name of the SSL key. + type: str + chain: + description: + - Specifies the certificates-key chain to associate with the SSL profile. + type: str + authenticate_name: + description: + - Specifies a Common Name (CN) that is embedded in a server certificate. + The system authenticates a server based on the specified CN. + type: str + ca_file: + description: + - Specifies a server CA the system trusts. + The default is (None). + type: str + options: + description: + - Options the system uses for SSL processing in the form of a list. When + creating a new profile, the list is provided by the parent profile. + - When C('') or C(none), all options for SSL processing are disabled. + type: list + elements: str + choices: + - dont-insert-empty-fragments + - no-ssl + - no-dtls + - no-session-resumption-on-renegotiation + - no-tlsv1.1 + - no-tlsv1.2 + - no-tlsv1.3 + - single-dh-use + - tls-rollback-bug + - no-sslv3 + - no-tls + - no-tlsv1 + - "none" + passphrase: + description: + - Specifies a passphrase used to encrypt the key. + type: str + update_password: + description: + - C(always) allows users to update passwords if they choose to do so. + C(on_create) only sets the password for newly created profiles. + type: str + choices: + - always + - on_create + default: always + ocsp_profile: + description: + - Specifies the name of the OCSP profile for purpose of validating the status + of server certificate. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a new server SSL profile + bigip_profile_server_ssl: + name: foo + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create server SSL profile with specific cipher group + bigip_profile_server_ssl: + state: present + name: foo_group + ciphers: "none" + cipher_group: "/Common/f5-secure" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +ciphers: + description: The ciphers applied to the profile. + returned: changed + type: str + sample: "!SSLv3:!SSLv2:ECDHE+AES-GCM+SHA256:ECDHE-RSA-AES128-CBC-SHA" +cipher_group: + description: The cipher group applied to the profile. + returned: changed + type: str + sample: "/Common/f5-secure" +secure_renegotiation: + description: The method of secure SSL renegotiation. + returned: changed + type: str + sample: request +renegotiation: + description: Renegotiation of SSL sessions. + returned: changed + type: bool + sample: yes +options: + description: The list of options for SSL processing. + returned: changed + type: list + sample: ['no-ssl', 'no-sslv3'] +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name, is_empty_list +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'cert': 'certificate', + 'cipherGroup': 'cipher_group', + 'ocsp': 'ocsp_profile', + 'defaultsFrom': 'parent', + 'secureRenegotiation': 'secure_renegotiation', + 'sniDefault': 'sni_default', + 'sniRequire': 'sni_require', + 'serverName': 'server_name', + 'peerCertMode': 'server_certificate', + 'caFile': 'ca_file', + 'authenticateName': 'authenticate_name', + 'tmOptions': 'options', + } + + api_attributes = [ + 'cert', + 'chain', + 'ciphers', + 'cipherGroup', + 'defaultsFrom', + 'key', + 'ocsp', + 'secureRenegotiation', + 'sniDefault', + 'sniRequire', + 'serverName', + 'peerCertMode', + 'renegotiation', + 'caFile', + 'authenticateName', + 'tmOptions', + ] + + returnables = [ + 'certificate', + 'chain', + 'ciphers', + 'cipher_group', + 'key', + 'ocsp_profile', + 'secure_renegotiation', + 'parent', + 'sni_default', + 'sni_require', + 'server_name', + 'server_certificate', + 'renegotiation', + 'ca_file', + 'authenticate_name', + 'options', + ] + + updatables = [ + 'certificate', + 'chain', + 'ciphers', + 'cipher_group', + 'key', + 'ocsp_profile', + 'secure_renegotiation', + 'sni_default', + 'sni_require', + 'server_name', + 'server_certificate', + 'renegotiation', + 'parent', + 'ca_file', + 'authenticate_name', + 'options', + ] + + @property + def sni_default(self): + return flatten_boolean(self._values['sni_default']) + + @property + def certificate(self): + if self._values['certificate'] is None: + return None + if self._values['certificate'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['certificate']) + return result + + @property + def key(self): + if self._values['key'] is None: + return None + if self._values['key'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['key']) + return result + + @property + def chain(self): + if self._values['chain'] is None: + return None + if self._values['chain'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['chain']) + return result + + @property + def ocsp_profile(self): + if self._values['ocsp_profile'] is None: + return None + if self._values['ocsp_profile'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['ocsp_profile']) + return result + + @property + def ca_file(self): + if self._values['ca_file'] is None: + return None + if self._values['ca_file'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['ca_file']) + return result + + +class ApiParameters(Parameters): + @property + def sni_require(self): + return flatten_boolean(self._values['sni_require']) + + @property + def server_name(self): + if self._values['server_name'] in [None, 'none']: + return None + return self._values['server_name'] + + +class ModuleParameters(Parameters): + @property + def server_name(self): + if self._values['server_name'] is None: + return None + if self._values['server_name'] in ['', 'none']: + return '' + return self._values['server_name'] + + @property + def parent(self): + if self._values['parent'] is None: + return None + if self._values['parent'] == 'serverssl': + return '/Common/serverssl' + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def renegotiation(self): + result = flatten_boolean(self._values['renegotiation']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def sni_require(self): + require = flatten_boolean(self._values['sni_require']) + default = self.sni_default + if require is None: + return None + if default in [None, 'no']: + if require == 'yes': + raise F5ModuleError( + "Cannot set 'sni_require' to {0} if 'sni_default' is set as {1}".format(require, default)) + return require + + @property + def ciphers(self): + if self._values['ciphers'] is None: + return None + if self._values['ciphers'] in ['', 'none']: + return 'none' + if self.cipher_group and self.cipher_group != 'none': + raise F5ModuleError("The cipher parameter must be set to 'none' if cipher_group is defined.") + return self._values['ciphers'] + + @property + def cipher_group(self): + if self._values['cipher_group'] is None: + return None + if self._values['cipher_group'] in ['', 'none']: + return 'none' + if self.ciphers and self.ciphers != 'none': + raise F5ModuleError("The cipher_group parameter must be set to 'none' if cipher is defined.") + result = fq_name(self.partition, self._values['cipher_group']) + return result + + @property + def options(self): + options = self._values['options'] + if options is None: + return None + if is_empty_list(options): + return [] + return options + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def sni_default(self): + if self._values['sni_default'] is None: + return None + elif self._values['sni_default'] == 'yes': + return 'true' + else: + return 'false' + + @property + def sni_require(self): + if self._values['sni_require'] is None: + return None + elif self._values['sni_require'] == 'yes': + return 'true' + else: + return 'false' + + +class ReportableChanges(Changes): + @property + def sni_default(self): + result = flatten_boolean(self._values['sni_default']) + return result + + @property + def sni_require(self): + result = flatten_boolean(self._values['sni_require']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def sni_require(self): + if self.want.sni_require is None: + return None + if self.want.sni_require == 'no': + if self.have.sni_default == 'yes' and self.want.sni_default is None: + raise F5ModuleError( + "Cannot set 'sni_require' to {0} if 'sni_default' is {1}".format( + self.want.sni_require, self.have.sni_default + ) + ) + if self.want.sni_require != self.have.sni_require: + return self.want.sni_require + + @property + def server_name(self): + if self.want.server_name is None: + return None + if self.want.server_name == '' and self.have.server_name is None: + return None + if self.want.server_name != self.have.server_name: + return self.want.server_name + + @property + def cipher_group(self): + if self.want.cipher_group is None: + return None + if self.want.cipher_group == 'none' and self.have.cipher_group == 'none': + return None + if self.want.cipher_group != self.have.cipher_group: + return self.want.cipher_group + + @property + def options(self): + if self.want.options is None: + return None + # starting with v14 options may return as a space delimited string in curly + # braces, eg "{ option1 option2 }", or simply "none" to indicate empty set + if self.have.options is None or self.have.options == 'none': + self.have.options = [] + if not isinstance(self.have.options, list): + if self.have.options.startswith('{'): + self.have.options = self.have.options[2:-2].split(' ') + else: + self.have.options = [self.have.options] + if not self.want.options: + # we don't want options. If we have any, indicate we should remove, else noop + return [] if self.have.options else None + if not self.have.options: + return self.want.options + if set(self.want.options) != set(self.have.options): + return self.want.options + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/server-ssl/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + + if self.want.update_password == 'always': + self.want.update({'passphrase': self.want.passphrase}) + else: + if self.want.passphrase: + del self.want._values['passphrase'] + + if not self.should_update(): + return False + + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/profile/server-ssl/".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, 404]: + 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/ltm/profile/server-ssl/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/server-ssl/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/server-ssl/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + certificate=dict(), + chain=dict(), + key=dict(), + passphrase=dict(no_log=True), + parent=dict(default='/Common/serverssl'), + ciphers=dict(), + cipher_group=dict(), + authenticate_name=dict(), + ca_file=dict(), + options=dict( + type='list', + elements='str', + choices=[ + 'dont-insert-empty-fragments', + 'no-ssl', + 'no-dtls', + 'no-session-resumption-on-renegotiation', + 'no-tlsv1.1', + 'no-tlsv1.2', + 'no-tlsv1.3', + 'single-dh-use', + 'tls-rollback-bug', + 'no-sslv3', + 'no-tls', + 'no-tlsv1', + 'none', + ] + ), + secure_renegotiation=dict( + choices=['require', 'require-strict', 'request'] + ), + server_certificate=dict( + choices=['ignore', 'require'] + ), + state=dict( + default='present', + choices=['present', 'absent'] + ), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + sni_default=dict(type='bool'), + sni_require=dict(type='bool'), + server_name=dict(), + ocsp_profile=dict(), + renegotiation=dict(type='bool'), + 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.required_together = [ + ['certificate', 'key'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_together=spec.required_together, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_sip.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_sip.py new file mode 100644 index 00000000..cf4f9c5c --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_sip.py @@ -0,0 +1,786 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_sip +short_description: Manage SIP profiles on a BIG-IP +description: + - Manage SIP profiles on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the SIP profile to manage. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(sip) profile. + type: str + community: + description: + - When the C(dialog_aware) is C(yes) and the configuration requires multiple SIP virtual server-profile pairings, + this string value indicates whether the pair belongs to the same SIP proxy functional group. + type: str + description: + description: + - Description of the profile. + - To remove the entry completely, set a value of C(''). + type: str + dialog_aware: + description: + - When C(yes), the system gathers SIP dialog information and automatically forwards SIP messages belonging to + the known SIP dialog. + type: bool + enable_sip_firewall: + description: + - Specifies whether the Advanced Firewall Manager (AFM) policy is enabled. + - When C(yes), the SIP Security settings configured in the DoS Profile in AFM apply to the virtual servers that + use this profile. + type: bool + insert_record_route_header: + description: + - When C(yes), inserts a Record-Route SIP header, which indicates the next hop for the following SIP request + messages. + type: bool + insert_via_header: + description: + - When C(yes), inserts a Via header in the forwarded SIP request. + - Via headers indicate the path taken through proxy devices and transports used. The response message uses this + routing information. + type: bool + user_via_header: + description: + - When C(insert_via_header) is C(yes), specifies the Via value the system inserts as the top Via header in a + SIP REQUEST message. + - "The valid value must include SIP protocol and sent_by settings, for example: C(SIP/2.0/UDP 10.10.10.10:5060)." + - To remove the entry completely, set a value of C(''). + type: str + log_profile: + description: + - Specifies the logging settings the publisher uses to send log messages. + - The format of the name can be either be prepended by partition (C(/Common/foo)), or specified + just as an object name (C(foo)). + - To remove the entry. set a value of C(''), however the profile C(log_publisher) + must also be set as C(''). + type: str + log_publisher: + description: + - Specifies the publisher defined to log messages. + - Format of the name can be either be prepended by partition (C(/Common/foo)), or specified + just as an object name (C(foo)). + - To remove the entry. set a value of C(''), however the profile C(log_profile) + must also be set as C(''). + type: str + secure_via_header: + description: + - When checked (enabled), inserts a secure Via header in the forwarded SIP request. + - A secure Via header indicates where the message originated. + - This parameter causes the inserted Via header to specify Transport Layer Security. For this option to take + effect, C(insert_via_header) must be set to (yes). + type: bool + security: + description: + - "When C(yes). enables the use of enhanced Horizontal Security Layer (HSL) security checking." + type: bool + terminate_on_bye: + description: + - When C(yes), closes a connection when a BYE transaction finishes. + - A BYE transaction is a message an application sends to another application when it is ready to close the + connection between the two. + type: bool + max_size: + description: + - Specifies the maximum SIP message size that the BIG-IP system accepts. + - The accepted value range is C(0 - 4294967295) bytes. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a SIP profile + bigip_profile_sip: + name: foo + parent: sip + log_profile: alg_log + log_publisher: foo-publisher + description: this is a new profile + security: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Update SIP profile + bigip_profile_sip: + name: foo + insert_record_route_header: yes + enable_sip_firewall: yes + insert_via_header: yes + user_via_header: "SIP/2.0/UDP 10.10.10.10:5060" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Delete a SIP profile + bigip_profile_sip: + name: foo + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: Description of the profile. + returned: changed + type: str + sample: "custom description" +community: + description: Indicates whether the pair belongs to the same SIP proxy functional group. + returned: changed + type: str + sample: foo_community +parent: + description: Specifies the profile from which this profile inherits settings. + returned: changed + type: str + sample: /Common/sip +dialog_aware: + description: Specifies if the system gathers SIP dialog information. + returned: changed + type: bool + sample: no +enable_sip_firewall: + description: Specifies whether the Advanced Firewall Manager policy is enabled. + returned: changed + type: bool + sample: yes +insert_record_route_header: + description: Specifies if the system will insert a Record-Route SIP header. + returned: changed + type: bool + sample: yes +insert_via_header: + description: Specifies if the system will insert a Via header in the forwarded SIP request. + returned: changed + type: bool + sample: yes +user_via_header: + description: The value the system inserts as the top Via header in a SIP REQUEST message. + returned: changed + type: str + sample: "SIP/2.0/UDP 10.10.10.10:5060" +log_profile: + description: The logging settings the publisher uses to send log messages. + returned: changed + type: str + sample: "/Common/alg_profile" +log_publisher: + description: The publisher defined to log messages. + returned: changed + type: str + sample: "/Common/foo_publisher" +secure_via_header: + description: Specifies if the system will insert a secure Via header in the forwarded SIP request. + returned: changed + type: bool + sample: no +security: + description: Enables the use of enhanced Horizontal Security Layer security checking. + returned: changed + type: bool + sample: yes +terminate_on_bye: + description: Specifies if the system will close a connection when a BYE transaction finishes. + returned: changed + type: bool + sample: no +max_size: + description: Specifies if the system will close a connection when a BYE transaction finishes. + returned: changed + type: bool + sample: no +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultsFrom': 'parent', + 'dialogAware': 'dialog_aware', + 'enableSipFirewall': 'enable_sip_firewall', + 'insertRecordRouteHeader': 'insert_record_route_header', + 'insertViaHeader': 'insert_via_header', + 'userViaHeader': 'user_via_header', + 'logProfile': 'log_profile', + 'logPublisher': 'log_publisher', + 'secureViaHeader': 'secure_via_header', + 'terminateOnBye': 'terminate_on_bye', + 'maxSize': 'max_size', + } + + api_attributes = [ + 'community', + 'description', + 'defaultsFrom', + 'dialogAware', + 'enableSipFirewall', + 'insertRecordRouteHeader', + 'insertViaHeader', + 'logProfile', + 'logPublisher', + 'secureViaHeader', + 'security', + 'terminateOnBye', + 'userViaHeader', + 'maxSize', + + ] + + returnables = [ + 'description', + 'community', + 'parent', + 'dialog_aware', + 'enable_sip_firewall', + 'insert_record_route_header', + 'insert_via_header', + 'user_via_header', + 'log_profile', + 'log_publisher', + 'secure_via_header', + 'security', + 'terminate_on_bye', + 'max_size', + ] + + updatables = [ + 'description', + 'community', + 'parent', + 'dialog_aware', + 'enable_sip_firewall', + 'insert_record_route_header', + 'insert_via_header', + 'user_via_header', + 'log_profile', + 'log_publisher', + 'secure_via_header', + 'security', + 'terminate_on_bye', + 'max_size', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def security(self): + if self._values['security'] is None: + return None + result = flatten_boolean(self._values['security']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def dialog_aware(self): + if self._values['dialog_aware'] is None: + return None + result = flatten_boolean(self._values['dialog_aware']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def enable_sip_firewall(self): + if self._values['enable_sip_firewall'] is None: + return None + result = flatten_boolean(self._values['enable_sip_firewall']) + return result + + @property + def insert_via_header(self): + if self._values['insert_via_header'] is None: + return None + result = flatten_boolean(self._values['insert_via_header']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def secure_via_header(self): + if self._values['secure_via_header'] is None: + return None + result = flatten_boolean(self._values['secure_via_header']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def terminate_on_bye(self): + if self._values['terminate_on_bye'] is None: + return None + result = flatten_boolean(self._values['terminate_on_bye']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def insert_record_route_header(self): + if self._values['insert_record_route_header'] is None: + return None + result = flatten_boolean(self._values['insert_record_route_header']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def max_size(self): + if self._values['max_size'] is None: + return None + if 0 <= self._values['max_size'] <= 4294967295: + return self._values['max_size'] + raise F5ModuleError( + "Valid 'max_size' must be in range 0 - 4294967295 bytes." + ) + + @property + def log_profile(self): + if self._values['log_profile'] in [None, '']: + return self._values['log_profile'] + result = fq_name(self.partition, self._values['log_profile']) + return result + + @property + def log_publisher(self): + if self._values['log_publisher'] in [None, '']: + return self._values['log_publisher'] + result = fq_name(self.partition, self._values['log_publisher']) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def security(self): + result = flatten_boolean(self._values['security']) + return result + + @property + def dialog_aware(self): + result = flatten_boolean(self._values['dialog_aware']) + return result + + @property + def enable_sip_firewall(self): + result = flatten_boolean(self._values['enable_sip_firewall']) + return result + + @property + def insert_via_header(self): + result = flatten_boolean(self._values['insert_via_header']) + return result + + @property + def terminate_on_bye(self): + result = flatten_boolean(self._values['terminate_on_bye']) + return result + + @property + def insert_record_route_header(self): + result = flatten_boolean(self._values['insert_record_route_header']) + return result + + @property + def secure_via_header(self): + result = flatten_boolean(self._values['secure_via_header']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + if self.want.description is None: + return None + if self.want.description == '': + if self.have.description in [None, "none"]: + return None + if self.want.description != self.have.description: + return self.want.description + + @property + def user_via_header(self): + if self.want.user_via_header is None: + return None + if self.want.user_via_header == '': + if self.have.user_via_header in [None, "none"]: + return None + if self.want.user_via_header != self.have.user_via_header: + return self.want.user_via_header + + @property + def log_profile(self): + if self.want.log_profile is None: + return None + if self.want.log_profile == '': + if self.have.log_profile in [None, "none"]: + return None + if self.want.log_profile != self.have.log_profile: + return self.want.log_profile + + @property + def log_publisher(self): + if self.want.log_publisher is None: + return None + if self.want.log_publisher == '': + if self.have.log_publisher in [None, "none"]: + return None + if self.want.log_publisher != self.have.log_publisher: + return self.want.log_publisher + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/sip/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/profile/sip/".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/sip/{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/ltm/profile/sip/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/sip/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + security=dict(type='bool'), + description=dict(), + community=dict(), + dialog_aware=dict(type='bool'), + enable_sip_firewall=dict(type='bool'), + insert_via_header=dict(type='bool'), + user_via_header=dict(), + secure_via_header=dict(type='bool'), + terminate_on_bye=dict(type='bool'), + max_size=dict(type='int'), + log_profile=dict(), + log_publisher=dict(), + insert_record_route_header=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_tcp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_tcp.py new file mode 100644 index 00000000..6bc6597f --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_tcp.py @@ -0,0 +1,808 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_tcp +short_description: Manage TCP profiles on a BIG-IP +description: + - Manage TCP profiles on a BIG-IP system. There are many TCP profiles, each with their + own adjustments to the standard C(tcp) profile. Users of this module should be aware + that many of the available options have no module default. Instead, the default is + assigned by the BIG-IP system itself which, in most cases, is acceptable. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(tcp) profile. + type: str + idle_timeout: + description: + - Specifies the length of time a connection is idle (has no traffic) before + the connection is eligible for deletion. + - When creating a new profile, if this parameter is not specified, the remote + device will choose a default value appropriate for the profile, based on its + C(parent) profile. + - When a number is specified, indicates the number of seconds the TCP + connection can remain idle before the system deletes it. + - When C(0), or C(indefinite), specifies the system does not delete TCP connections + regardless of how long they remain idle. + type: str + keep_alive_interval: + description: + - Specifies how frequently the system sends data over an idle TCP connection, + to determine whether the connection is still valid. + - When creating a new profile, if this parameter is not specified, the remote + device will choose a default value appropriate for the profile, based on its + C(parent) profile. + - When C(0), or C(indefinite), specifies that the system does not send keep-alive communication. + type: str + version_added: "1.22.0" + time_wait_recycle: + description: + - Specifies connections in a TIME-WAIT state are reused if a SYN packet (indicating a request + for a new connection) is received. + - When C(no), connections in a TIME-WAIT state remain unused for a specified length of time. + - When creating a new profile, if this parameter is not specified, the default + is provided by the parent profile. + type: bool + nagle: + description: + - When C(enabled) the system applies Nagle's algorithm to reduce the number of short segments on the network. + - When C(auto), the use of Nagle's algorithm is decided based on network conditions. + - For interactive protocols such as Telnet, rlogin, or SSH, F5 recommends disabling this setting on + high-latency networks, to improve application responsiveness. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: str + choices: + - auto + - enabled + - disabled + early_retransmit: + description: + - When C(yes), the system uses early fast retransmits to reduce the recovery time for connections that are + receive-buffer or user-data limited. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: bool + proxy_options: + description: + - When C(yes), the system advertises an option, such as a time-stamp, to the server only if it was negotiated + with the client. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: bool + initial_congestion_window_size: + description: + - Specifies the initial congestion window size for connections to this destination. The actual window size is + this value multiplied by the MSS for the same connection. + - When set to C(0), the system uses the values specified in RFC2414. + - The valid value range is 0 - 16 inclusive. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: int + initial_receive_window_size: + description: + - Specifies the initial receive window size for connections to this destination. The actual window size is + this value multiplied by the MSS for the same connection. + - When set to C(0), the system uses the Slow Start value. + - The valid value range is 0 - 16 inclusive. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: int + syn_rto_base: + description: + - Specifies the initial RTO C(Retransmission TimeOut) base multiplier for SYN retransmission, in C(milliseconds). + - This value is modified by the exponential backoff table to select the interval for subsequent retransmissions. + - The valid value range is 0 - 5000 inclusive. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: int + delayed_acks: + description: + - When C(yes), the system sends fewer than one ACK segment per data segment received. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: bool + ip_tos_to_client: + description: + - Specifies the L3 Type of Service level the system inserts in TCP packets destined for clients. + - When C(pass-through), the IP ToS setting remains unchanged. + - When C(mimic), the system sets the ToS level of outgoing packets to the same ToS level of the most-recently + received incoming packet. + - When set as a number, the number indicates the IP ToS setting the system inserts in the IP packet header. + Valid number range is 0 - 255 inclusive. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: str + time_wait_timeout: + description: + - Specifies the number of milliseconds a connection is in the TIME-WAIT state before closing. + - When C(immediate), the system closes the connection immediately after the connection enters the TIME-WAIT state. + - When C(indefinite) or C(0), the system does not close TCP connections regardless of how long they remain in the + TIME-WAIT state. + - The valid number range is from 0 to 600000 milliseconds. + - When creating a new profile, if this parameter is not specified, the default is provided by the parent profile. + type: str + version_added: "1.3.0" + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a TCP profile + bigip_profile_tcp: + name: foo + parent: f5-tcp-progressive + time_wait_recycle: no + idle_timeout: 300 + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: The new parent of the resource. + returned: changed + type: str + sample: f5-tcp-optimized +idle_timeout: + description: The new idle timeout of the resource. + returned: changed + type: int + sample: 100 +time_wait_recycle: + description: Reuse connections in TIME-WAIT state. + returned: changed + type: bool + sample: yes +nagle: + description: Specifies the use of Nagle's algorithm. + returned: changed + type: str + sample: auto +early_retransmit: + description: Specifies the use of early fast retransmits. + returned: changed + type: bool + sample: yes +proxy_options: + description: Specifies if the system advertises negotiated options to the server. + returned: changed + type: bool + sample: no +initial_congestion_window_size: + description: Specifies the initial congestion window size for connections to this destination. + returned: changed + type: int + sample: 5 +initial_receive_window_size: + description: Specifies the initial receive window size for connections to this destination. + returned: changed + type: int + sample: 10 +syn_rto_base: + description: Specifies the initial Retransmission TimeOut base multiplier for SYN retransmission. + returned: changed + type: int + sample: 2000 +delayed_acks: + description: Specifies if the system sends fewer than one ACK segment per data segment received. + returned: changed + type: bool + sample: yes +ip_tos_to_client: + description: Specifies the L3 Type of Service level that the system inserts in TCP packets destined for clients. + returned: changed + type: str + sample: mimic +time_wait_timeout: + description: Specifies the number of milliseconds that a connection is in the TIME-WAIT state before closing. + returned: changed + type: str + sample: immediate +keep_alive_interval: + description: Specifies how frequently the system sends data over an idle TCP connection. + returned: changed + type: str + sample: indefinite +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'idleTimeout': 'idle_timeout', + 'defaultsFrom': 'parent', + 'timeWaitRecycle': 'time_wait_recycle', + 'earlyRetransmit': 'early_retransmit', + 'proxyOptions': 'proxy_options', + 'initCwnd': 'initial_congestion_window_size', + 'initRwnd': 'initial_receive_window_size', + 'synRtoBase': 'syn_rto_base', + 'delayedAcks': 'delayed_acks', + 'ipTosToClient': 'ip_tos_to_client', + 'timeWaitTimeout': 'time_wait_timeout', + 'keepAliveInterval': 'keep_alive_interval', + } + + api_attributes = [ + 'idleTimeout', + 'defaultsFrom', + 'timeWaitRecycle', + 'nagle', + 'earlyRetransmit', + 'proxyOptions', + 'initCwnd', + 'initRwnd', + 'synRtoBase', + 'delayedAcks', + 'ipTosToClient', + 'timeWaitTimeout', + 'keepAliveInterval', + ] + + returnables = [ + 'idle_timeout', + 'parent', + 'time_wait_recycle', + 'nagle', + 'early_retransmit', + 'proxy_options', + 'initial_congestion_window_size', + 'initial_receive_window_size', + 'syn_rto_base', + 'delayed_acks', + 'ip_tos_to_client', + 'time_wait_timeout', + 'keep_alive_interval', + ] + + updatables = [ + 'idle_timeout', + 'parent', + 'time_wait_recycle', + 'nagle', + 'early_retransmit', + 'proxy_options', + 'initial_congestion_window_size', + 'initial_receive_window_size', + 'syn_rto_base', + 'delayed_acks', + 'ip_tos_to_client', + 'time_wait_timeout', + 'keep_alive_interval', + ] + + +class ApiParameters(Parameters): + @property + def ip_tos_to_client(self): + if self._values['ip_tos_to_client'] is None: + return None + if self._values['ip_tos_to_client'] in ['pass-through', 'mimic']: + return self._values['ip_tos_to_client'] + return int(self._values['ip_tos_to_client']) + + @property + def time_wait_timeout(self): + if self._values['time_wait_timeout'] is None: + return None + if self._values['time_wait_timeout'] == '0': + return 'immediate' + return self._values['time_wait_timeout'] + + +class ModuleParameters(Parameters): + @property + def time_wait_timeout(self): + if self._values['time_wait_timeout'] is None: + return None + if self._values['time_wait_timeout'] == '0': + return 'immediate' + if self._values['time_wait_timeout'] in ['indefinite', 'immediate']: + return self._values['time_wait_timeout'] + if 0 <= int(self._values['time_wait_timeout']) <= 600000: + return self._values['time_wait_timeout'] + raise F5ModuleError( + "Valid 'time_wait_timeout' must be in range 0 - 600000 milliseconds or 'immediate', 'indefinite'." + ) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def idle_timeout(self): + if self._values['idle_timeout'] is None: + return None + if self._values['idle_timeout'] == 'indefinite': + return 4294967295 + return int(self._values['idle_timeout']) + + @property + def keep_alive_interval(self): + if self._values['keep_alive_interval'] is None: + return None + if self._values['keep_alive_interval'] == 'indefinite': + return 0 + if 0 <= int(self._values['keep_alive_interval']) <= 4294967295: + return int(self._values['keep_alive_interval']) + raise F5ModuleError( + "Valid 'keep_alive_interval' must be in range 0 - 4294967295 or 'indefinite'." + ) + + @property + def time_wait_recycle(self): + result = flatten_boolean(self._values['time_wait_recycle']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def early_retransmit(self): + result = flatten_boolean(self._values['early_retransmit']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def proxy_options(self): + result = flatten_boolean(self._values['proxy_options']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def initial_congestion_window_size(self): + if self._values['initial_congestion_window_size'] is None: + return None + if 0 <= self._values['initial_congestion_window_size'] <= 16: + return self._values['initial_congestion_window_size'] + raise F5ModuleError( + "Valid 'initial_congestion_window_size' must be in range 0 - 16 MSS unit." + ) + + @property + def initial_receive_window_size(self): + if self._values['initial_receive_window_size'] is None: + return None + if 0 <= self._values['initial_receive_window_size'] <= 16: + return self._values['initial_receive_window_size'] + raise F5ModuleError( + "Valid 'initial_receive_window_size' must be in range 0 - 16 MSS unit." + ) + + @property + def syn_rto_base(self): + if self._values['syn_rto_base'] is None: + return None + if 0 <= self._values['syn_rto_base'] <= 5000: + return self._values['syn_rto_base'] + raise F5ModuleError( + "Valid 'syn_rto_base' must be in range 0 - 5000 milliseconds." + ) + + @property + def delayed_acks(self): + result = flatten_boolean(self._values['delayed_acks']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def ip_tos_to_client(self): + if self._values['ip_tos_to_client'] is None: + return None + if self._values['ip_tos_to_client'] in ['pass-through', 'mimic']: + return self._values['ip_tos_to_client'] + if 0 <= int(self._values['ip_tos_to_client']) <= 255: + return int(self._values['ip_tos_to_client']) + raise F5ModuleError( + "Valid 'ip_tos_to_client' must be in range 0 - 255 or 'pass-through', 'mimic'." + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def idle_timeout(self): + if self._values['idle_timeout'] is None: + return None + if 0 <= self._values['idle_timeout'] <= 4294967295: + return self._values['idle_timeout'] + raise F5ModuleError( + "Valid 'idle_timeout' must be in range 1 - 4294967295, or 'indefinite'." + ) + + @property + def time_wait_timeout(self): + if self._values['time_wait_timeout'] is None: + return None + if self._values['time_wait_timeout'] == 'immediate': + return '0' + return self._values['time_wait_timeout'] + + +class ReportableChanges(Changes): + @property + def idle_timeout(self): + if self._values['idle_timeout'] is None: + return None + if self._values['idle_timeout'] == 4294967295: + return 'indefinite' + return int(self._values['idle_timeout']) + + @property + def keep_alive_interval(self): + if self._values['keep_alive_interval'] is None: + return None + if self._values['keep_alive_interval'] == 0: + return 'indefinite' + return str(self._values['keep_alive_interval']) + + @property + def time_wait_recycle(self): + if self._values['time_wait_recycle'] is None: + return None + elif self._values['time_wait_recycle'] == 'enabled': + return 'yes' + return 'no' + + @property + def early_retransmit(self): + result = flatten_boolean(self._values['early_retransmit']) + return result + + @property + def proxy_options(self): + result = flatten_boolean(self._values['proxy_options']) + return result + + @property + def time_wait_timeout(self): + if self._values['time_wait_timeout'] is None: + return None + if self._values['time_wait_timeout'] == '0': + return 'immediate' + return self._values['time_wait_timeout'] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def ip_tos_to_client(self): + if self.want.ip_tos_to_client is None: + return None + if self.want.ip_tos_to_client in ['pass-through', 'mimic']: + if isinstance(self.have.ip_tos_to_client, int): + return self.want.ip_tos_to_client + if self.have.ip_tos_to_client in ['pass-through', 'mimic']: + if isinstance(self.want.ip_tos_to_client, int): + return self.want.ip_tos_to_client + if self.want.ip_tos_to_client != self.have.ip_tos_to_client: + return self.want.ip_tos_to_client + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/tcp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.parent is None: + self.want.update({'parent': fq_name(self.want.partition, 'tcp')}) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/profile/tcp/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/tcp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/tcp/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/tcp/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + idle_timeout=dict(), + state=dict( + default='present', + choices=['present', 'absent'] + ), + time_wait_recycle=dict(type='bool'), + nagle=dict( + choices=['enabled', 'disabled', 'auto'] + ), + early_retransmit=dict(type='bool'), + proxy_options=dict(type='bool'), + initial_congestion_window_size=dict(type='int'), + initial_receive_window_size=dict(type='int'), + syn_rto_base=dict(type='int'), + delayed_acks=dict(type='bool'), + ip_tos_to_client=dict(), + time_wait_timeout=dict(), + keep_alive_interval=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_udp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_udp.py new file mode 100644 index 00000000..9adb264d --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_profile_udp.py @@ -0,0 +1,463 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_profile_udp +short_description: Manage UDP profiles on a BIG-IP +description: + - Manage UDP profiles on a BIG-IP system. There are many UDP profiles, each with their + own adjustments to the standard C(udp) profile. Users of this module should be aware + that many of the available options have no module default. Instead, the default is + assigned by the BIG-IP system itself which, in most cases, is acceptable. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the profile. + type: str + required: True + parent: + description: + - Specifies the profile from which this profile inherits settings. + - When creating a new profile, if this parameter is not specified, the default + is the system-supplied C(udp) profile. + type: str + idle_timeout: + description: + - Specifies the length of time a connection is idle (has no traffic) before + the connection is eligible for deletion. + - When creating a new profile, if this parameter is not specified, the remote + device will choose a default value appropriate for the profile, based on its + C(parent) profile. + - When a number is specified, indicates the number of seconds the UDP + connection can remain idle before the system deletes it. + - When C(indefinite), specifies UDP connections can remain idle + indefinitely. + - When C(0) or C(immediate), specifies you do not want the UDP connection to + remain idle, and it is therefore immediately eligible for deletion. + type: str + datagram_load_balancing: + description: + - When C(yes), specifies the system load balances UDP traffic + packet-by-packet. + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the profile exists. + - When C(absent), ensures the profile is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a TCP profile + bigip_profile_tcp: + name: foo + parent: udp + idle_timeout: 300 + datagram_load_balancing: no + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +parent: + description: The new parent of the resource. + returned: changed + type: str + sample: udp +idle_timeout: + description: The new idle timeout of the resource. + returned: changed + type: int + sample: 100 +datagram_load_balancing: + description: The new datagram load balancing setting of the resource. + returned: changed + type: bool + sample: True +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'datagramLoadBalancing': 'datagram_load_balancing', + 'idleTimeout': 'idle_timeout', + 'defaultsFrom': 'parent', + } + + api_attributes = [ + 'datagramLoadBalancing', + 'idleTimeout', + 'defaultsFrom', + ] + + returnables = [ + 'datagram_load_balancing', + 'idle_timeout', + 'parent', + ] + + updatables = [ + 'datagram_load_balancing', + 'idle_timeout', + 'parent', + ] + + @property + def idle_timeout(self): + if self._values['idle_timeout'] is None: + return None + if self._values['idle_timeout'] == 'indefinite': + return self._values['idle_timeout'] + if self._values['idle_timeout'] in ['0', 'immediate']: + return 'immediate' + return int(self._values['idle_timeout']) + + +class ApiParameters(Parameters): + @property + def datagram_load_balancing(self): + if self._values['datagram_load_balancing'] is None: + return None + if self._values['datagram_load_balancing'] == 'enabled': + return True + return False + + +class ModuleParameters(Parameters): + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def datagram_load_balancing(self): + if self._values['datagram_load_balancing'] is None: + return None + if self._values['datagram_load_balancing']: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def datagram_load_balancing(self): + if self._values['datagram_load_balancing'] is None: + return None + if self._values['datagram_load_balancing'] == 'enabled': + return True + return False + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/udp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/ltm/profile/udp/".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, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/udp/{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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/udp/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/udp/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + parent=dict(), + idle_timeout=dict(), + datagram_load_balancing=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_provision.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_provision.py new file mode 100644 index 00000000..4e83b174 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_provision.py @@ -0,0 +1,1153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_provision +short_description: Manage BIG-IP module provisioning +description: + - Manage BIG-IP module provisioning. This module will only provision at the + standard levels of Dedicated, Nominal, and Minimum. +version_added: "1.0.0" +options: + module: + description: + - The module to provision in BIG-IP. + type: str + required: True + choices: + - am + - afm + - apm + - asm + - avr + - cgnat + - fps + - gtm + - ilx + - lc + - ltm + - mgmt + - pem + - sam + - sslo + - swg + - urldb + - vcmp + aliases: + - name + level: + description: + - Sets the provisioning level for the requested modules. Changing the + level for one module may require modifying the level of another module. + For example, changing one module to C(dedicated) requires setting all + others to C(none). Setting the level of a module to C(none) means + the module is not activated. + - Use a C(state) of B(absent) to set c(level) to none and de-provision the module. + - This parameter is not relevant to C(cgnat - pre tmos 15.0) or C(mgmt) and will not be + applied to the C(cgnat - pre tmos 15.0) or C(mgmt) module. + type: str + choices: + - dedicated + - nominal + - minimum + default: nominal + memory: + description: + - Sets additional memory for the management module. This is in addition to + minimum allocated RAM of 1264MB. + - The accepted value range is C(0 - 8192). Maximum value is restricted by + the available RAM in the system. + - Specifying C(large) reserves an additional 500MB for the mgmt module. + - Specifying C(medium) reserves an additional 200MB for the mgmt module. + - Specifying C(small) reserves no additional RAM for the mgmt module. + - Use C(large) for configurations containing more than 2000 objects, or + more specifically, for any configuration that exceeds 1000 objects + per 2 GB of installed memory. Changing the Management C(mgmt) size + after initial provisioning causes a reprovision operation. + type: str + state: + description: + - The state of the provisioned module on the system. When C(present), + guarantees the specified module is provisioned at the requested + level, provided there are sufficient resources on the device (such + as physical RAM) to support the module. + - When C(absent), de-provision the module. + - C(absent), is not a relevent option for the C(mgmt) module, as it can not be de-provisioned. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Greg Crosby (@crosbygw) +''' + +EXAMPLES = r''' +- name: Provision PEM at "nominal" level + bigip_provision: + module: pem + level: nominal + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost + +- name: Provision a dedicated SWG. This will unprovision every other module + bigip_provision: + module: swg + level: dedicated + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost + +- name: Provision mgmt with medium amount of memory. + bigip_provision: + module: mgmt + memory: medium + provider: + server: lb.mydomain.com + password: secret + user: admin + delegate_to: localhost +''' + +RETURN = r''' +level: + description: The new provisioning level of the module. + returned: changed + type: str + sample: minimum +memory: + description: The new provisioned amount of memory for mgmt module. + returned: changed + type: str + sample: large +''' + +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, +) +from ..module_utils.icontrol import ( + TransactionContextManager, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'value': 'memory', + } + + api_attributes = [ + 'level', + 'value', + ] + + returnables = [ + 'level', + 'memory', + ] + + updatables = [ + 'level', + 'cgnat', + 'memory', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + + def _validate_memory_limit(self, limit): + if self._values['memory'] == 'small': + return '0' + if self._values['memory'] == 'medium': + return '200' + if self._values['memory'] == 'large': + return '500' + if 0 <= int(limit) <= 8192: + return str(limit) + raise F5ModuleError( + "Valid 'memory' must be in range 0 - 8192, 'small', 'medium', or 'large'." + ) + + @property + def level(self): + if self._values['level'] is None: + return None + if self._values['module'] == 'mgmt': + return None + if self.state == 'absent': + return 'none' + return str(self._values['level']) + + @property + def memory(self): + if self._values['memory'] is None: + return None + if self._values['module'] != 'mgmt': + return None + return int(self._validate_memory_limit(self._values['memory'])) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + except Exception: + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def memory(self): + if self._values['memory'] is None: + return None + if self._values['memory'] == '0': + return 'small' + if self._values['memory'] == '200': + return 'medium' + if self._values['memory'] == '500': + return 'large' + return str(self._values['memory']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def cgnat(self): + if self.want.module == 'cgnat': + if self.want.state == 'absent' and self.have.enabled is True: + return True + if self.want.state == 'present' and self.have.disabled is True: + return True + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def version_is_greater_or_equal_15(self): + version = tmos_version(self.client) + if Version(version) >= Version('15.0.0'): + return True + else: + return False + + def present(self): + if self.exists(): + return False + return self.update() + + def exists(self): + if self.want.module == 'cgnat' and not self.version_is_greater_or_equal_15(): + uri = "https://{0}:{1}/mgmt/tm/sys/feature-module/cgnat/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + if 'disabled' in response and response['disabled'] is True: + return False + elif 'enabled' in response and response['enabled'] is True: + return True + elif self.want.module == 'mgmt': + uri = "https://{0}:{1}/mgmt/tm/sys/db/provision.extramb/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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 + if str(response['value']) != 0 and self.want.memory == 0: + return False + if str(response['value']) == 0 and self.want.memory == 0: + return True + if str(response['value']) == self.want.memory: + return True + return False + try: + for x in range(0, 5): + uri = "https://{0}:{1}/mgmt/tm/sys/provision/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.module + ) + 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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if str(response['level']) != 'none' and self.want.level == 'none': + return True + if str(response['level']) == 'none' and self.want.level == 'none': + return False + if str(response['level']) == self.want.level: + return True + return False + except Exception as ex: + if 'not registered' in str(ex): + return False + time.sleep(1) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + result = self.update_on_device() + if self.want.module == 'cgnat' and not self.version_is_greater_or_equal_15(): + return result + self._wait_for_module_provisioning() + self._wait_for_rest_api_available() + + if self.want.module == 'vcmp': + self._wait_for_reboot() + self._wait_for_module_provisioning() + self._wait_for_rest_api_available() + + if self.want.module == 'asm': + self._wait_for_asm_ready() + if self.want.module == 'afm': + self._wait_for_afm_ready() + if self.want.module == 'cgnat': + self._wait_for_cgnat_ready() + if self.want.module == 'mgmt': + self._wait_for_mgmt_ready() + return True + + def should_reboot(self): + for x in range(0, 24): + try: + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 'provision.action' + ) + 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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if response['value'] == 'reboot': + return True + elif response['value'] == 'none': + time.sleep(5) + except Exception: + time.sleep(5) + return False + + def reboot_device(self): + nops = 0 + last_reboot = self._get_last_reboot() + + try: + params = dict( + command="run", + utilCmdArgs='-c "/sbin/reboot"' + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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) + if 'commandResult' in response: + return str(response['commandResult']) + except Exception: + pass + + # Sleep a little to let rebooting take effect + time.sleep(20) + + while nops < 3: + try: + self.client.reconnect() + next_reboot = self._get_last_reboot() + if next_reboot is None: + nops = 0 + if next_reboot == last_reboot: + nops = 0 + else: + nops += 1 + except Exception: + # This can be caused by restjavad restarting. + pass + time.sleep(10) + return None + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + if self.want.module == 'cgnat' and not self.version_is_greater_or_equal_15(): + if self.changes.cgnat: + return self.provision_cgnat_on_device() + return False + elif self.want.level == 'dedicated' and self.want.module != 'mgmt': + self.provision_dedicated_on_device() + else: + self.provision_non_dedicated_on_device() + + def provision_cgnat_on_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/feature-module/cgnat/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + params = dict(enabled=True) + 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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return True + + def provision_dedicated_on_device(self): + params = self.want.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/provision/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + resources = [x['name'] for x in response['items'] if x['name'] != self.want.module] + + with TransactionContextManager(self.client) as transact: + for resource in resources: + target = uri + resource + resp = transact.api.patch(target, json=dict(level='none')) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + target = uri + self.want.module + resp = transact.api.patch(target, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def provision_non_dedicated_on_device(self): + params = self.want.api_params() + if self.want.module == 'mgmt': + uri = "https://{0}:{1}/mgmt/tm/sys/db/provision.extramb/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + else: + uri = "https://{0}:{1}/mgmt/tm/sys/provision/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.module + ) + 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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + if self.want.module == 'cgnat' and not self.version_is_greater_or_equal_15(): + uri = "https://{0}:{1}/mgmt/tm/sys/feature-module/cgnat/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + elif self.want.module == 'mgmt': + uri = "https://{0}:{1}/mgmt/tm/sys/db/provision.extramb/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + else: + uri = "https://{0}:{1}/mgmt/tm/sys/provision/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.module + ) + 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'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return ApiParameters(params=response) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + if self.want.module == 'cgnat' and not self.version_is_greater_or_equal_15(): + return self.deprovision_cgnat_on_device() + + self.remove_from_device() + self._wait_for_module_provisioning() + self._wait_for_rest_api_available() + # For vCMP, because it has to reboot, we also wait for mcpd to become available + # before "moving on", or else the REST API would not be available and subsequent + # Tasks would fail. + if self.want.module == 'vcmp': + self._wait_for_reboot() + self._wait_for_module_provisioning() + self._wait_for_rest_api_available() + + if self.should_reboot(): + self.save_on_device() + self.reboot_device() + self._wait_for_module_provisioning() + self._wait_for_rest_api_available() + + if self.exists(): + raise F5ModuleError("Failed to de-provision the module") + return True + + def save_on_device(self): + command = 'tmsh save sys config' + params = dict( + command="run", + utilCmdArgs='-c "{0}"'.format(command) + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/provision/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.module + ) + resp = self.client.api.patch(uri, json=dict(level='none')) + 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 deprovision_cgnat_on_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/feature-module/cgnat/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + params = dict(disabled=True) + 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) + return True + + def _wait_for_module_provisioning(self): + # To prevent things from running forever, the hack is to check + # for mprov's status twice. If mprov is finished, then in most + # cases (not ASM) the provisioning is probably ready. + nops = 0 + + # Sleep a little to let provisioning settle and begin properly + time.sleep(5) + + while nops < 3: + try: + if not self._is_mprov_running_on_device(): + nops += 1 + else: + nops = 0 + except Exception: + # This can be caused by restjavad restarting. + try: + self.client.reconnect() + except Exception: + pass + time.sleep(5) + + def _is_mprov_running_on_device(self): + # /usr/libexec/qemu-kvm is added here to prevent vcmp provisioning + # from never allowing the mprov provisioning to succeed. + # + # It turns out that the 'mprov' string is found when enabling vcmp. The + # qemu-kvm command that is run includes it. + # + # For example, + # /usr/libexec/qemu-kvm -rt-usecs 880 ... -mem-path /dev/mprov/vcmp -f5-tracing ... + # + try: + command = "ps aux | grep \'[m]prov\' | grep -v /usr/libexec/qemu-kvm" + params = dict( + command="run", + utilCmdArgs='-c "{0}"'.format(command) + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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) + + if 'commandResult' in response: + return True + except Exception: + pass + return False + + def _wait_for_rest_api_available(self): + nops = 0 + time.sleep(5) + + while nops < 3: + try: + if self._is_rest_available(): + nops += 1 + else: + nops = 0 + time.sleep(5) + except Exception: + # This can be caused by restjavad restarting. + try: + self.client.reconnect() + except Exception: + pass + + def _is_rest_available(self): + try: + uri = "https://{0}:{1}/mgmt/tm/sys/available".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + 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'] in [400, 403, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if resp.status in [200, 201]: + return True + except Exception: + pass + return False + + def _wait_for_asm_ready(self): + """Waits specifically for ASM + + On older versions, ASM can take longer to actually start up than + all the previous checks take. This check here is specifically waiting for + the Policies API to stop raising errors + :return: + """ + nops = 0 + restarted_asm = False + while nops < 3: + try: + uri = "https://{0}:{1}/mgmt/tm/asm/policies/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if len(response['items']) >= 0: + nops += 1 + else: + nops = 0 + except Exception: + if not restarted_asm: + self._restart_asm() + restarted_asm = True + time.sleep(5) + + def _wait_for_afm_ready(self): + """Waits specifically for AFM + + AFM can take longer to actually start up than all the previous checks take. + This check here is specifically waiting for the Security API to stop raising + errors. + :return: + """ + nops = 0 + while nops < 3: + try: + uri = "https://{0}:{1}/mgmt/tm/security/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if len(response['items']) >= 0: + nops += 1 + else: + nops = 0 + except Exception: + pass + time.sleep(5) + + def _wait_for_cgnat_ready(self): + """Waits specifically for CGNAT + + Starting in TMOS 15.0 cgnat can take longer to actually start up than all the previous checks take. + This check here is specifically waiting for a cgnat API to stop raising + errors. + :return: + """ + nops = 0 + while nops < 3: + try: + uri = "https://{0}:{1}/mgmt/tm/ltm/lsn-pool".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if len(response['items']) >= 0: + nops += 1 + else: + nops = 0 + except Exception: + pass + time.sleep(5) + + def _wait_for_mgmt_ready(self): + """Waits specifically for MGMT + + Modifying memory reserve for mgmt can take longer to actually start up than all the previous checks take. + This check here is specifically waiting for a MGMT API to stop raising + errors. + :return: + """ + nops = 0 + while nops < 3: + try: + uri = "https://{0}:{1}/mgmt/tm".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if len(response['items']) >= 0: + nops += 1 + else: + nops = 0 + except Exception: + pass + time.sleep(5) + + def _restart_asm(self): + try: + params = dict( + command="run", + utilCmdArgs='-c "bigstart restart asm"' + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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) + time.sleep(60) + return True + except Exception: + pass + return None + + def _get_last_reboot(self): + try: + params = dict( + command="run", + utilCmdArgs='-c "/usr/bin/last reboot | head -1"' + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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) + + if 'commandResult' in response: + return str(response['commandResult']) + except Exception: + pass + return None + + def _wait_for_reboot(self): + nops = 0 + + last_reboot = self._get_last_reboot() + + # Sleep a little to let provisioning settle and begin properly + time.sleep(5) + + while nops < 6: + try: + self.client.reconnect() + next_reboot = self._get_last_reboot() + if next_reboot is None: + nops = 0 + if next_reboot == last_reboot: + nops = 0 + else: + nops += 1 + except Exception: + # This can be caused by restjavad restarting. + pass + time.sleep(10) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + module=dict( + required=True, + choices=[ + 'afm', 'am', 'apm', 'asm', 'avr', 'cgnat', + 'fps', 'gtm', 'ilx', 'lc', 'ltm', 'mgmt', + 'pem', 'sam', 'sslo', 'swg', 'urldb', 'vcmp' + ], + aliases=['name'] + ), + level=dict( + default='nominal', + choices=['nominal', 'dedicated', 'minimum'] + ), + memory=dict(), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_qkview.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_qkview.py new file mode 100644 index 00000000..422b9d11 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_qkview.py @@ -0,0 +1,623 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_qkview +short_description: Manage QKviews on the device +description: + - Manages creating and downloading QKviews from a BIG-IP. The qkview utility automatically + collects configuration and diagnostic information from BIG-IP systems, and combines + the data into a QKView file. F5 Support may request you send or upload this + QKview to assist in troubleshooting. +version_added: "1.0.0" +options: + filename: + description: + - Name of the QKview file to create on the remote BIG-IP. + type: str + default: "localhost.localdomain.qkview" + dest: + description: + - Destination on your local filesystem where you want to save the QKview. + type: path + asm_request_log: + description: + - When C(true), includes ASM request log data. When C(False), + excludes ASM request log data. + type: bool + default: no + max_file_size: + description: + - Maximum file size of the QKview file, in bytes. By default, no max + file size is specified. + type: int + default: 0 + complete_information: + description: + - Include complete (all applicable) information in the QKview. + type: bool + default: no + exclude_core: + description: + - Exclude core files from the QKview. + type: bool + default: no + exclude: + description: + - Exclude various file from the QKview. + type: list + elements: str + choices: + - all + - audit + - secure + - bash_history + force: + description: + - If C(no), the file will only be transferred if the destination does not + exist. + type: bool + default: yes + only_create_file: + description: + - If C(yes), the file is created on the device and not downloaded. The file will not be deleted by the + module from the device. + type: bool + default: no + version_added: "1.20.0" +notes: + - This module does not include the "max time" or "restrict to blade" options. + - If you are using this module with either Ansible Tower or Ansible AWX, you + should be aware of how these Ansible products execute jobs in restricted + environments. More information can be found here + https://clouddocs.f5.com/products/orchestration/ansible/devel/usage/module-usage-with-tower.html + - Some longer running tasks might cause the REST interface on BIG-IP to time out, to avoid this adjust the timers as + per this KB article https://support.f5.com/csp/article/K94602685 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Fetch a qkview from the remote device + bigip_qkview: + asm_request_log: yes + exclude: + - audit + - secure + dest: /tmp/localhost.localdomain.qkview + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import os +import re +import socket +import ssl +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import ( + download_file, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_attributes = [ + 'asm_request_log', + 'complete_information', + 'exclude', + 'exclude_core', + 'filename_cmd', + 'max_file_size', + ] + + returnables = ['stdout', 'stdout_lines', 'warnings'] + + @property + def exclude(self): + if self._values['exclude'] is None: + return None + exclude = ' '.join(self._values['exclude']) + return "--exclude='{0}'".format(exclude) + + @property + def exclude_raw(self): + return self._values['exclude'] + + @property + def exclude_core(self): + if self._values['exclude']: + return '-C' + else: + return None + + @property + def complete_information(self): + if self._values['complete_information']: + return '-c' + return None + + @property + def max_file_size(self): + if self._values['max_file_size'] in [None]: + return None + return '-s {0}'.format(self._values['max_file_size']) + + @property + def asm_request_log(self): + if self._values['asm_request_log']: + return '-o asm-request-log' + return None + + @property + def filename(self): + pattern = r'^[\w\.]+$' + filename = os.path.basename(self._values['filename']) + if re.match(pattern, filename): + return filename + else: + raise F5ModuleError( + "The provided filename must contain word characters only." + ) + + @property + def filename_cmd(self): + return '-f {0}'.format(self.filename) + + 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 + + def api_params(self): + result = {} + for api_attribute in self.api_attributes: + if self.api_map is not None and api_attribute in self.api_map: + result[api_attribute] = getattr(self, self.api_map[api_attribute]) + else: + result[api_attribute] = getattr(self, api_attribute) + result = self._filter_params(result) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def exec_module(self): + if self.is_version_less_than_14(): + manager = self.get_manager('madm') + else: + manager = self.get_manager('bulk') + return manager.exec_module() + + def get_manager(self, type): + if type == 'madm': + return MadmLocationManager(**self.kwargs) + elif type == 'bulk': + return BulkLocationManager(**self.kwargs) + + def is_version_less_than_14(self): + version = tmos_version(self.client) + if Version(version) < Version('14.0.0'): + return True + else: + return False + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = Parameters(params=self.module.params) + self.changes = Parameters() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Parameters(params=changed) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + self.present() + + result.update(**self.changes.to_return()) + result.update(dict(changed=False)) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if not self.want.only_create_file: + if os.path.exists(self.want.dest) and not self.want.force: + raise F5ModuleError( + "The specified 'dest' file already exists." + ) + if not os.path.exists(os.path.dirname(self.want.dest)): + raise F5ModuleError( + "The directory of your 'dest' file does not exist." + ) + if self.want.exclude: + choices = ['all', 'audit', 'secure', 'bash_history'] + if not all(x in choices for x in self.want.exclude_raw): + raise F5ModuleError( + "The specified excludes must be in the following list: " + "{0}".format(','.join(choices)) + ) + self.execute() + + def exists(self): + params = dict( + command='run', + utilCmdArgs=self.remote_dir + ) + uri = "https://{0}:{1}/mgmt/tm/util/unix-ls".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + + try: + if self.want.filename in response['commandResult']: + return True + except KeyError: + return False + + def execute(self): + response = self.execute_on_device() + if not response: + raise F5ModuleError( + "Failed to create qkview on device." + ) + + if not self.want.only_create_file: + result = self._move_qkview_to_download() + if not result: + raise F5ModuleError( + "Failed to move the file to a downloadable location" + ) + + self._download_file() + if not os.path.exists(self.want.dest): + raise F5ModuleError( + "Failed to save the qkview to local disk" + ) + + self._delete_qkview() + result = self.exists() + if result: + raise F5ModuleError( + "Failed to remove the remote qkview" + ) + + def _delete_qkview(self): + tpath_name = '{0}/{1}'.format(self.remote_dir, self.want.filename) + params = dict( + command='run', + utilCmdArgs=tpath_name + ) + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + + def execute_on_device(self): + self._upsert_temporary_cli_script_on_device() + task_id = self._create_async_task_on_device() + self._exec_async_task_on_device(task_id) + self._wait_for_async_task_to_finish_on_device(task_id) + self._remove_temporary_cli_script_from_device() + return True + + def _upsert_temporary_cli_script_on_device(self): + args = { + "name": "__ansible_mkqkview", + "apiAnonymous": """ + proc script::run {} { + set cmd [lreplace $tmsh::argv 0 0]; eval "exec $cmd 2> /dev/null" + } + """ + } + result = self._create_temporary_cli_script_on_device(args) + if result: + return True + return self._update_temporary_cli_script_on_device(args) + + def _create_temporary_cli_script_on_device(self, args): + uri = "https://{0}:{1}/mgmt/tm/cli/script".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + if 'code' in response and response['code'] in [404, 409]: + return False + except ValueError: + pass + if resp.status in [404, 409]: + return False + return True + + def _update_temporary_cli_script_on_device(self, args): + uri = "https://{0}:{1}/mgmt/tm/cli/script/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', '__ansible_mkqkview') + ) + resp = self.client.api.put(uri, json=args) + try: + resp.json() + return True + except ValueError: + raise F5ModuleError( + "Failed to update temporary cli script on device." + ) + + def _create_async_task_on_device(self): + """Creates an async cli script task in the REST API + + Returns: + int: The ID of the task staged for running. + + :return: + """ + command = ' '.join(self.want.api_params().values()) + args = { + "command": "run", + "name": "__ansible_mkqkview", + "utilCmdArgs": "/usr/bin/qkview {0}".format(command) + } + uri = "https://{0}:{1}/mgmt/tm/task/cli/script".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + return response['_taskId'] + except ValueError: + raise F5ModuleError( + "Failed to create the async task on the device." + ) + + def _exec_async_task_on_device(self, task_id): + args = {"_taskState": "VALIDATING"} + uri = "https://{0}:{1}/mgmt/tm/task/cli/script/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + task_id + ) + resp = self.client.api.put(uri, json=args) + try: + resp.json() + return True + except ValueError: + raise F5ModuleError( + "Failed to execute the async task on the device" + ) + + def _wait_for_async_task_to_finish_on_device(self, task_id): + uri = "https://{0}:{1}/mgmt/tm/task/cli/script/{2}/result".format( + self.client.provider['server'], + self.client.provider['server_port'], + task_id + ) + while True: + try: + resp = self.client.api.get(uri, timeout=10) + except (socket.timeout, ssl.SSLError): + continue + try: + response = resp.json() + except ValueError: + # It is possible that the API call can return invalid JSON. + # This invalid JSON appears to be just empty strings. + continue + if response['_taskState'] == 'FAILED': + raise F5ModuleError( + "qkview creation task failed unexpectedly." + ) + if response['_taskState'] == 'COMPLETED': + return True + time.sleep(3) + + def _remove_temporary_cli_script_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/task/cli/script/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', '__ansible_mkqkview') + ) + try: + self.client.api.delete(uri) + return True + except ValueError: + raise F5ModuleError( + "Failed to remove the temporary cli script from the device." + ) + + def _move_qkview_to_download(self): + uri = "https://{0}:{1}/mgmt/tm/util/unix-mv/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + args = dict( + command='run', + utilCmdArgs='/var/tmp/{0} {1}/{0}'.format(self.want.filename, self.remote_dir) + ) + self.client.api.post(uri, json=args) + return True + + +class BulkLocationManager(BaseManager): + def __init__(self, *args, **kwargs): + super(BulkLocationManager, self).__init__(**kwargs) + self.remote_dir = '/var/config/rest/bulk' + + def _download_file(self): + uri = "https://{0}:{1}/mgmt/shared/file-transfer/bulk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.filename + ) + download_file(self.client, uri, self.want.dest) + if os.path.exists(self.want.dest): + return True + return False + + +class MadmLocationManager(BaseManager): + def __init__(self, *args, **kwargs): + super(MadmLocationManager, self).__init__(**kwargs) + self.remote_dir = '/var/config/rest/madm' + + def _download_file(self): + uri = "https://{0}:{1}/mgmt/shared/file-transfer/madm/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.filename + ) + download_file(self.client, uri, self.want.dest) + if os.path.exists(self.want.dest): + return True + return False + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + filename=dict( + default='localhost.localdomain.qkview' + ), + asm_request_log=dict( + type='bool', + default='no', + ), + max_file_size=dict( + type='int', + ), + complete_information=dict( + default='no', + type='bool' + ), + exclude_core=dict( + default="no", + type='bool' + ), + force=dict( + default=True, + type='bool' + ), + exclude=dict( + type='list', + elements='str', + choices=[ + 'all', 'audit', 'secure', 'bash_history' + ] + ), + only_create_file=dict( + default='no', + type='bool' + ), + dest=dict( + type='path' + ) + ) + self.required_if = [ + ['only_create_file', 'no', ['dest']] + ] + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_role.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_role.py new file mode 100644 index 00000000..9dacd108 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_role.py @@ -0,0 +1,553 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_remote_role +short_description: Manage remote roles on a BIG-IP +description: + - Manages remote roles on a BIG-IP system. Remote roles are used in situations where + user authentication is handled off-box. Local access control to the BIG-IP + is controlled by the defined remote role, and authentication (and by + extension, assignment to the role) is handled off-box. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the remote role. + type: str + required: True + line_order: + description: + - Specifies the order of the line in the file C(/config/bigip/auth/remoterole). + - The LDAP and Active Directory servers read this file line by line. + - The order of the information is important; therefore, F5 recommends + you set the first line at 1000. This allows you to insert + lines before the first line in the future. + - When creating a new remote role, this parameter is required. + type: int + attribute_string: + description: + - Specifies the user account attributes saved in the group, in the format + C(cn=, ou=, dc=). + - When creating a new remote role, this parameter is required. + type: str + remote_access: + description: + - Enables or disables remote access for the specified group of remotely + authenticated users. + - When creating a new remote role, if this parameter is not specified, the default + is C(yes). + type: bool + assigned_role: + description: + - Specifies the authorization (level of access) for the account. + - When creating a new remote role, if this parameter is not provided, the + default is C(none). + - The C(partition_access) parameter controls which partitions the account can + access. + - The role you choose may affect the partitions that one is allowed to specify. + Specifically, roles such as C(administrator), C(auditor) and C(resource-administrator) + require a C(partition_access) of C(all). + - A set of pre-existing roles ship with the system. They are C(none), C(guest), + C(operator), C(application-editor), C(manager), C(certificate-manager), + C(irule-manager), C(user-manager), C(resource-administrator), C(auditor), + C(administrator), and C(firewall-manager). + type: str + partition_access: + description: + - Specifies the accessible partitions for the account. + - This parameter supports the reserved names C(all) and C(Common), as well as + specific partitions a user may access. + - Users who have access to a partition can operate on objects in that partition, + as determined by the permissions conferred by the user's C(assigned_role). + - When creating a new remote role, if this parameter is not specified, the default + is C(all). + type: str + terminal_access: + description: + - Specifies terminal-based accessibility for remote accounts not already + explicitly assigned a user role. + - Common values for this include C(tmsh) and C(none), but you can also + specify custom values. + - When creating a new remote role, if this parameter is not specified, the default + is C(none). + type: str + state: + description: + - When C(present), guarantees the remote role exists. + - When C(absent), removes the remote role from the system. + type: str + choices: + - absent + - present + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a remote role + bigip_remote_role: + name: ldap_group + line_order: 1 + attribute_string: memberOf=cn=ldap_group,cn=ldap.group,ou=ldap + remote_access: yes + assigned_role: administrator + partition_access: all + terminal_access: none + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +attribute_string: + description: The new attribute string of the resource. + returned: changed + type: str + sample: "memberOf=cn=ldap_group,cn=ldap.group,ou=ldap" +terminal_access: + description: The terminal setting of the remote role. + returned: changed + type: str + sample: tmsh +line_order: + description: Order of the remote role for LDAP and Active Directory servers. + returned: changed + type: int + sample: 1000 +assigned_role: + description: System role this remote role is associated with. + returned: changed + type: str + sample: administrator +partition_access: + description: Partition the role has access to. + returned: changed + type: str + sample: all +remote_access: + description: Whether remote access is allowed or not. + returned: changed + type: bool + sample: no +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'attribute': 'attribute_string', + 'console': 'terminal_access', + 'lineOrder': 'line_order', + 'role': 'assigned_role', + 'userPartition': 'partition_access', + 'deny': 'remote_access' + } + + api_attributes = [ + 'attribute', + 'console', + 'lineOrder', + 'role', + 'deny', + 'userPartition', + ] + + returnables = [ + 'attribute_string', + 'terminal_access', + 'line_order', + 'assigned_role', + 'partition_access', + 'remote_access', + ] + + updatables = [ + 'attribute_string', + 'terminal_access', + 'line_order', + 'assigned_role', + 'partition_access', + 'remote_access', + ] + + role_map = { + 'application-editor': 'applicationeditor', + 'none': 'noaccess', + 'certificate-manager': 'certificatemanager', + 'irule-manager': 'irulemanager', + 'user-manager': 'usermanager', + 'resource-administrator': 'resourceadmin', + 'firewall-manager': 'firewallmanager' + } + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def partition(self): + return 'Common' + + @property + def assigned_role(self): + if self._values['assigned_role'] is None: + return None + return self.role_map.get(self._values['assigned_role'], self._values['assigned_role']) + + @property + def terminal_access(self): + if self._values['terminal_access'] in [None, 'tmsh']: + return self._values['terminal_access'] + elif self._values['terminal_access'] == 'none': + return 'disable' + return self._values['terminal_access'] + + @property + def partition_access(self): + if self._values['partition_access'] is None: + return None + if self._values['partition_access'] == 'all': + return 'All' + return self._values['partition_access'] + + @property + def remote_access(self): + result = flatten_boolean(self._values['remote_access']) + if result == 'yes': + return 'disabled' + elif result == 'no': + return 'enabled' + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def assigned_role(self): + if self._values['assigned_role'] is None: + return None + rmap = dict((v, k) for k, v in iteritems(self.role_map)) + return rmap.get(self._values['assigned_role'], self._values['assigned_role']) + + @property + def terminal_access(self): + if self._values['terminal_access'] in [None, 'tmsh']: + return self._values['terminal_access'] + elif self._values['terminal_access'] == 'disabled': + return 'none' + return self._values['terminal_access'] + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/auth/remote-role/role-info/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.partition_access is None: + self.want.update({'partition_access': 'all'}) + if self.want.remote_access is None: + self.want.update({'remote_access': True}) + if self.want.assigned_role is None: + self.want.update({'assigned_role': 'none'}) + if self.want.terminal_access is None: + self.want.update({'terminal_access': 'none'}) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + uri = "https://{0}:{1}/mgmt/tm/auth/remote-role/role-info/".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/auth/remote-role/role-info/{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: + if 'Once configured [All] partition, remote user group cannot' in response['message']: + raise F5ModuleError( + "The specified 'attribute_string' is already used in the 'all' partition." + ) + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/remote-role/role-info/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/remote-role/role-info/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + line_order=dict(type='int'), + attribute_string=dict(), + remote_access=dict(type='bool'), + assigned_role=dict(), + partition_access=dict(), + terminal_access=dict(), + state=dict( + default='present', + choices=['present', 'absent'] + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_syslog.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_syslog.py new file mode 100644 index 00000000..752d6f0e --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_syslog.py @@ -0,0 +1,458 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_remote_syslog +short_description: Manipulate remote syslog settings on a BIG-IP +description: + - Manipulate remote syslog settings on a BIG-IP system. +version_added: "1.0.0" +options: + remote_host: + description: + - Specifies the IP address or hostname for the remote system, to + which the system sends log messages. + type: str + required: True + name: + description: + - Specifies the name of the syslog object. + - This option is required when multiple C(remote_host)s with the same IP + or hostname are present on the device. + - If C(name) is not provided, C(remote_host) is used by default. + type: str + remote_port: + description: + - Specifies the port the system uses to send messages to the + remote logging server. + - When creating a remote syslog, if this parameter is not specified, the + default value is C(514). + type: str + local_ip: + description: + - Specifies the local IP address of the system that is logging. To + provide no local IP, specify the value C(none). + - When creating a remote syslog, if this parameter is not specified, the + default value is C(none). + type: str + state: + description: + - When C(present), guarantees the remote syslog exists with the provided + attributes. + - When C(absent), removes the remote syslog from the system. + type: str + choices: + - absent + - present + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add a remote syslog server to log to + bigip_remote_syslog: + remote_host: 10.10.10.10 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Add a remote syslog server on a non-standard port to log to + bigip_remote_syslog: + remote_host: 10.10.10.10 + remote_port: 1234 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +remote_port: + description: New remote port of the remote syslog server. + returned: changed + type: int + sample: 514 +local_ip: + description: The new local IP of the remote syslog server. + returned: changed + type: str + sample: 10.10.10.10 +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, fq_name, is_valid_hostname +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'remotePort': 'remote_port', + 'localIp': 'local_ip', + 'host': 'remote_host', + } + + updatables = [ + 'remote_port', + 'local_ip', + 'remote_host', + 'name', + ] + + returnables = [ + 'remote_port', + 'local_ip', + 'remote_host', + 'name', + 'remoteServers', + ] + + api_attributes = [ + 'remotePort', + 'localIp', + 'host', + 'name', + 'remoteServers', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def remote_host(self): + if is_valid_ip(self._values['remote_host']): + return self._values['remote_host'] + elif is_valid_hostname(self._values['remote_host']): + return str(self._values['remote_host']) + raise F5ModuleError( + "The provided 'remote_host' is not a valid IP or hostname" + ) + + @property + def remote_port(self): + if self._values['remote_port'] in [None, 'none']: + return None + if self._values['remote_port'] == 0: + raise F5ModuleError( + "The 'remote_port' value must between 1 and 65535" + ) + return int(self._values['remote_port']) + + @property + def local_ip(self): + if self._values['local_ip'] in [None, 'none']: + return None + if is_valid_ip(self._values['local_ip']): + return self._values['local_ip'] + else: + raise F5ModuleError( + "The provided 'local_ip' is not a valid IP address" + ) + + @property + def name(self): + if self._values['remote_host'] is None: + return None + if self._values['name'] is None: + return None + name = fq_name(self.partition, self._values['name']) + return name + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + change = getattr(self, returnable) + if isinstance(change, dict): + result.update(change) + else: + result[returnable] = change + result = self._filter_params(result) + except Exception: + pass + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def remote_port(self): + if self._values['remote_port'] is None: + return None + return int(self._values['remote_port']) + + @property + def remoteServers(self): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + self._local_ip = None + self._remote_port = None + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.pop('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + # A list of all the syslogs queried from the API when reading current info + # from the device. This is used when updating the API as the value that needs + # to be updated is a list of syslogs and PATCHing a list would override any + # default settings. + self.syslogs = dict() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def format_syslogs(self, syslogs): + result = None + for x in syslogs: + syslog = ApiParameters(params=x) + self.syslogs[syslog.name] = x + + if syslog.name == self.want.name: + result = syslog + elif syslog.remote_host == self.want.remote_host: + result = syslog + + if not result: + return ApiParameters() + return result + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + return self.update() + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.format_syslogs(self.read_current_from_device()) + if not self.should_update() and self.want.state != 'absent': + return False + if self.module.check_mode: + return True + + if self.want.name is None: + self.want.update({'name': self.want.remote_host}) + + syslogs = [v for k, v in iteritems(self.syslogs)] + dupes = [x for x in syslogs if x['host'] == self.want.remote_host] + if len(dupes) > 1: + raise F5ModuleError( + "Multiple occurrences of hostname: {0} detected, please specify 'name' parameter". format( + self.want.remote_host + ) + ) + + # A absent syslog does not appear in the list of existing syslogs + if self.want.state == 'absent': + if self.want.name not in self.syslogs: + return False + + # At this point we know the existing syslog is not absent, so we need + # to change it in some way. + # + # First, if we see that the syslog is in the current list of syslogs, + # we are going to update it + changes = dict(self.changes.api_params()) + if self.want.name in self.syslogs: + self.syslogs[self.want.name].update(changes) + else: + # else, we are going to add it to the list of syslogs + self.syslogs[self.want.name] = changes + + # Since the name attribute is not a parameter tracked in the Parameter + # classes, we will add the name to the list of attributes so that when + # we update the API, it creates the correct vector + self.syslogs[self.want.name].update({'name': self.want.name}) + + # Finally, the absent state forces us to remove the syslog from the + # list. + if self.want.state == 'absent': + del self.syslogs[self.want.name] + + # All of the syslogs must be re-assembled into a list of dictionaries + # so that when we PATCH the API endpoint, the syslogs list is filled + # correctly. + # + # There are **not** individual API endpoints for the individual syslogs. + # Instead, the endpoint includes a list of syslogs that is part of the + # system config + result = [v for k, v in iteritems(self.syslogs)] + + self.changes = Changes(params=dict(remoteServers=result)) + self.changes.update(self.want._values) + self.update_on_device() + return True + + def update_on_device(self): + params = self.changes.api_params() + params = dict( + remoteServers=params.get('remoteServers') + ) + uri = "https://{0}:{1}/mgmt/tm/sys/syslog/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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'] in [400, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/syslog/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = response.get('remoteServers', []) + return result + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + remote_host=dict( + required=True + ), + remote_port=dict(), + local_ip=dict(), + name=dict(), + state=dict( + default='present', + choices=['absent', 'present'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_user.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_user.py new file mode 100644 index 00000000..23fdd9ad --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_remote_user.py @@ -0,0 +1,383 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_remote_user +short_description: Manages default settings for remote user accounts on a BIG-IP +description: + - Manages the default settings for remote user accounts on a BIG-IP system. +version_added: "1.0.0" +options: + default_role: + description: + - Specifies the default role for all remote user accounts. + - The default system value is C(no-access). + type: str + choices: + - acceleration-policy-editor + - admin + - application-editor + - auditor + - certificate-manager + - firewall-manager + - fraud-protection-manager + - guest + - irule-manager + - manager + - no-access + - operator + - resource-admin + - user-manager + - web-application-security-administrator + - web-application-security-editor + default_partition: + description: + - Specifies the default partition for all remote user accounts. + - The default system value is C(all) for all partitions. + type: str + console_access: + description: + - Enables or disables the default console access for all remote user accounts. + - The default system value is C(disabled). + type: bool + description: + description: + - User-defined description. + type: str +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Modify default partition and console access + bigip_remote_user: + default_partition: Common + console_access: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Modify default role, partition and console access + bigip_remote_user: + default_partition: Common + default_role: manager + console_access: yes + description: "Changed new settings" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Revert to default settings + bigip_remote_user: + default_partition: all + default_role: "no-access" + console_access: no + description: "" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +default_role: + description: The default role for all remote user accounts. + returned: changed + type: str + sample: auditor +default_partition: + description: The default partition for all remote user accounts. + returned: changed + type: str + sample: Common +console_access: + description: The default console access for all remote user accounts. + returned: changed + type: bool + sample: no +description: + description: The user-defined description. + returned: changed + type: str + sample: Foo is bar +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultPartition': 'default_partition', + 'defaultRole': 'default_role', + 'remoteConsoleAccess': 'console_access', + } + + api_attributes = [ + 'defaultPartition', + 'defaultRole', + 'description', + 'remoteConsoleAccess', + + ] + + returnables = [ + 'default_partition', + 'default_role', + 'console_access', + 'description', + ] + + updatables = [ + 'default_partition', + 'default_role', + 'console_access', + 'description', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def console_access(self): + result = flatten_boolean(self._values['console_access']) + if result == 'yes': + return 'tmsh' + if result == 'no': + return 'disabled' + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def console_access(self): + if self._values['console_access'] is None: + return None + if self._values['console_access'] == 'tmsh': + return 'yes' + if self._values['console_access'] == 'disabled': + return 'no' + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + result = cmp_str_with_none(self.want.description, self.have.description) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.update() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/auth/remote-user/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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 read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/remote-user/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.choices = [ + 'acceleration-policy-editor', + 'admin', + 'application-editor', + 'auditor', + 'certificate-manager', + 'firewall-manager', + 'fraud-protection-manager', + 'guest', + 'irule-manager', + 'manager', + 'no-access', + 'operator', + 'resource-admin', + 'user-manager', + 'web-application-security-administrator', + 'web-application-security-editor' + ] + argument_spec = dict( + default_role=dict( + choices=self.choices + ), + default_partition=dict(), + console_access=dict(type='bool'), + description=dict() + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_one_of = [ + ['default_role', 'default_partition'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_routedomain.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_routedomain.py new file mode 100644 index 00000000..eeb2b5ae --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_routedomain.py @@ -0,0 +1,741 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2016, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_routedomain +short_description: Manage route domains on a BIG-IP +description: + - Manage route domains on a BIG-IP system. A route domain is a BIG-IP + configuration object that isolates network traffic for a particular + application on the network. +version_added: "1.0.0" +options: + name: + description: + - The name of the route domain. + type: str + bwc_policy: + description: + - The bandwidth controller for the route domain. + type: str + connection_limit: + description: + - The maximum number of concurrent connections allowed for the + route domain. Setting this to C(0) turns off connection limits. + type: int + description: + description: + - Specifies descriptive text that identifies the route domain. + type: str + flow_eviction_policy: + description: + - The eviction policy to use with this route domain. Apply an eviction + policy to provide customized responses to flow overflows and slow + flows on the route domain. + type: str + id: + description: + - The unique identifying integer representing the route domain. + - This field is required when creating a new route domain. + - In version 2.5, this value is no longer used to reference a route domain when + making modifications to it (for instance during update and delete operations). + Instead, the C(name) parameter is used. In version 2.6, the C(name) value will + become a required parameter. + type: int + parent: + description: + - Specifies the route domain the system searches when it cannot + find a route in the configured domain. + type: str + partition: + description: + - Partition on which you want to create the route domain. Partitions cannot be updated + once they are created. + type: str + default: Common + routing_protocol: + description: + - Dynamic routing protocols for the system to use in the route domain. + type: list + elements: str + choices: + - none + - BFD + - BGP + - IS-IS + - OSPFv2 + - OSPFv3 + - PIM + - RIP + - RIPng + service_policy: + description: + - Service policy to associate with the route domain. + type: str + state: + description: + - Whether the route domain should exist or not. + type: str + choices: + - present + - absent + default: present + strict: + description: + - Specifies whether the system enforces cross-routing restrictions or not. + type: bool + vlans: + description: + - VLANs for the system to use in the route domain. + type: list + elements: str + fw_enforced_policy: + description: + - Specifies an AFM policy to be attached to route domain. + - To remove attached AFM policy use C("") or C(none) as values. + type: str +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a route domain + bigip_routedomain: + name: foo + id: 1234 + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Set VLANs on the route domain + bigip_routedomain: + name: bar + state: present + vlans: + - net1 + - foo + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +id: + description: The ID of the route domain that was changed. + returned: changed + type: int + sample: 2 +description: + description: The description of the route domain. + returned: changed + type: str + sample: route domain foo +strict: + description: The new strict isolation setting. + returned: changed + type: str + sample: enabled +parent: + description: The new parent route domain. + returned: changed + type: int + sample: 0 +vlans: + description: List of new VLANs to which the route domain is applied. + returned: changed + type: list + sample: ['/Common/http-tunnel', '/Common/socks-tunnel'] +routing_protocol: + description: List of routing protocols applied to the route domain. + returned: changed + type: list + sample: ['bfd', 'bgp'] +bwc_policy: + description: The new bandwidth controller. + returned: changed + type: str + sample: /Common/foo +connection_limit: + description: The new connection limit for the route domain. + returned: changed + type: int + sample: 100 +flow_eviction_policy: + description: The new eviction policy to use with this route domain. + returned: changed + type: str + sample: /Common/default-eviction-policy +service_policy: + description: The new service policy to use with this route domain. + returned: changed + type: str + sample: /Common-my-service-policy +fw_enforced_policy: + description: Specifies the AFM policy to be attached to route domain. + returned: changed + type: str + sample: /Common/afm-blocking-policy +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_simple_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'connectionLimit': 'connection_limit', + 'servicePolicy': 'service_policy', + 'bwcPolicy': 'bwc_policy', + 'flowEvictionPolicy': 'flow_eviction_policy', + 'routingProtocol': 'routing_protocol', + 'fwEnforcedPolicy': 'fw_enforced_policy', + 'fwEnforcedPolicyReference': 'fw_policy_link', + } + + api_attributes = [ + 'connectionLimit', + 'description', + 'strict', + 'parent', + 'servicePolicy', + 'bwcPolicy', + 'flowEvictionPolicy', + 'routingProtocol', + 'vlans', + 'id', + 'fwEnforcedPolicy', + 'fwEnforcedPolicyReference', + ] + + returnables = [ + 'description', + 'strict', + 'parent', + 'service_policy', + 'bwc_policy', + 'flow_eviction_policy', + 'routing_protocol', + 'vlans', + 'connection_limit', + 'id', + ] + + updatables = [ + 'description', + 'strict', + 'parent', + 'service_policy', + 'bwc_policy', + 'flow_eviction_policy', + 'routing_protocol', + 'vlans', + 'connection_limit', + 'id', + 'fw_enforced_policy', + 'fw_policy_link', + ] + + @property + def connection_limit(self): + if self._values['connection_limit'] is None: + return None + return int(self._values['connection_limit']) + + @property + def id(self): + if self._values['id'] is None: + return None + return int(self._values['id']) + + +class ApiParameters(Parameters): + @property + def strict(self): + if self._values['strict'] is None: + return None + if self._values['strict'] == 'enabled': + return True + return False + + @property + def domains(self): + domains = self.read_domains_from_device() + result = [x['fullPath'] for x in domains['items']] + return result + + def read_domains_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/route-domain/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + return response + + +class ModuleParameters(Parameters): + @property + def bwc_policy(self): + if self._values['bwc_policy'] is None: + return None + return fq_name(self.partition, self._values['bwc_policy']) + + @property + def flow_eviction_policy(self): + if self._values['flow_eviction_policy'] is None: + return None + return fq_name(self.partition, self._values['flow_eviction_policy']) + + @property + def service_policy(self): + if self._values['service_policy'] is None: + return None + return fq_name(self.partition, self._values['service_policy']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def vlans(self): + if self._values['vlans'] is None: + return None + if len(self._values['vlans']) == 1 and self._values['vlans'][0] == '': + return '' + return [fq_name(self.partition, x) for x in self._values['vlans']] + + @property + def name(self): + if self._values['name'] is None: + return str(self.id) + return self._values['name'] + + @property + def routing_protocol(self): + if self._values['routing_protocol'] is None: + return None + if len(self._values['routing_protocol']) == 1 and self._values['routing_protocol'][0] in ['', 'none']: + return '' + return self._values['routing_protocol'] + + @property + def fw_enforced_policy(self): + policy = self._values['fw_enforced_policy'] + if policy is None: + return None + if policy.lower() in ['none', '']: + return '' + name = self._values['fw_enforced_policy'] + return fq_name(self.partition, name) + + @property + def fw_policy_link(self): + policy = self.fw_enforced_policy + if not policy: + return None + tmp = policy.split('/') + link = dict(link='https://localhost/mgmt/tm/security/firewall/policy/~{0}~{1}'.format(tmp[1], tmp[2])) + return link + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def strict(self): + if self._values['strict'] is None: + return None + if self._values['strict']: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def strict(self): + if self._values['strict'] is None: + return None + if self._values['strict'] == 'enabled': + return 'yes' + return 'no' + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def routing_protocol(self): + return cmp_simple_list(self.want.routing_protocol, self.have.routing_protocol) + + @property + def vlans(self): + return cmp_simple_list(self.want.vlans, self.have.vlans) + + @property + def fw_policy_link(self): + if self.want.fw_enforced_policy is None: + return None + if self.want.fw_enforced_policy == '' and self.have.fw_enforced_policy is None: + return None + if self.want.fw_enforced_policy == self.have.fw_enforced_policy: + return None + if self.want.fw_policy_link != self.have.fw_policy_link: + return self.want.fw_policy_link + + @property + def fw_enforced_policy(self): + if self.want.fw_enforced_policy is None: + return None + if self.want.fw_enforced_policy == '' and self.have.fw_enforced_policy is None: + return None + if self.want.fw_enforced_policy != self.have.fw_enforced_policy: + return self.want.fw_enforced_policy + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params, client=self.client) + self.have = ApiParameters(client=self.client) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.want.parent and self.want.parent not in self.have.domains: + raise F5ModuleError( + "The parent route domain was not found." + ) + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.id is None: + raise F5ModuleError( + "The 'id' parameter is required when creating new route domains." + ) + if self.want.parent and self.want.parent not in self.have.domains: + raise F5ModuleError( + "The parent route domain was not found." + ) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/route-domain/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/net/route-domain/".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if self.want.fw_enforced_policy: + payload = dict( + fwEnforcedPolicy=self.want.fw_enforced_policy, + fwEnforcedPolicyReference=self.want.fw_policy_link + ) + uri = "https://{0}:{1}/mgmt/tm/net/route-domain/{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=payload) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/net/route-domain/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/route-domain/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/route-domain/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response, client=self.client) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(), + id=dict(type='int'), + description=dict(), + strict=dict(type='bool'), + parent=dict(), + vlans=dict( + type='list', + elements='str', + ), + routing_protocol=dict( + type='list', + elements='str', + choices=['BFD', 'BGP', 'IS-IS', 'OSPFv2', 'OSPFv3', 'PIM', 'RIP', 'RIPng', 'none'] + ), + bwc_policy=dict(), + connection_limit=dict(type='int'), + flow_eviction_policy=dict(), + service_policy=dict(), + fw_enforced_policy=dict(), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_one_of = [ + ['name', 'id'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_one_of=spec.required_one_of + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_selfip.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_selfip.py new file mode 100644 index 00000000..2ab13a1d --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_selfip.py @@ -0,0 +1,919 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2016, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_selfip +short_description: Manage Self-IPs on a BIG-IP system +description: + - Manage Self-IP addresses on a BIG-IP system. +version_added: "1.0.0" +options: + address: + description: + - The IP addresses for the new self IP. This value is ignored upon update + as addresses themselves cannot be changed after they are created. + - This value is required when creating new self IPs. + type: str + allow_service: + description: + - Configure port lockdown for the self IP. By default, the self IP has a + "default deny" policy. This can be changed to allow TCP and UDP ports, + as well as specific protocols. This list should contain C(protocol):C(port) + values. + type: list + elements: str + name: + description: + - The name of the self IP to create. + - If this parameter is not specified, it defaults to the value supplied + in the C(address) parameter. + type: str + required: True + description: + description: + - Description of the traffic selector. + type: str + netmask: + description: + - The netmask for the self IP. When creating a new self IP, this value + is required. + type: str + state: + description: + - When C(present), guarantees the self IP exists with the provided + attributes. + - When C(absent), removes the self IP from the system. + type: str + choices: + - absent + - present + default: present + traffic_group: + description: + - The traffic group for the self IP addresses in an active-active, + redundant load balancer configuration. When creating a new self IP, if + this value is not specified, the default is C(/Common/traffic-group-local-only). + type: str + vlan: + description: + - The VLAN for the new self IPs. When creating a new self + IP, this value is required. + type: str + route_domain: + description: + - The route domain id of the system. When creating a new self IP, if + this value is not specified, the default value is C(0). + - This value cannot be changed after it is set. + type: int + fw_enforced_policy: + description: + - Specifies an AFM policy to attach to Self IP. + type: str + version_added: "1.1.0" + partition: + description: + - Device partition to manage resources on. You can set different partitions + for self IPs, but the address used may not match any other address used + by a self IP. Thus, self IPs are not isolated by partitions as + other resources on a BIG-IP are. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create Self IP + bigip_selfip: + address: 10.10.10.10 + name: self1 + netmask: 255.255.255.0 + vlan: vlan1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create Self IP with a Route Domain + bigip_selfip: + name: self1 + address: 10.10.10.10 + netmask: 255.255.255.0 + vlan: vlan1 + route_domain: 10 + allow_service: default + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Delete Self IP + bigip_selfip: + name: self1 + state: absent + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Allow management web UI to be accessed on this Self IP + bigip_selfip: + name: self1 + state: absent + allow_service: + - tcp:443 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Allow HTTPS and SSH access to this Self IP + bigip_selfip: + name: self1 + state: absent + allow_service: + - tcp:443 + - tcp:22 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Allow all services access to this Self IP + bigip_selfip: + name: self1 + state: absent + allow_service: + - all + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Allow only GRE and IGMP protocols access to this Self IP + bigip_selfip: + name: self1 + state: absent + allow_service: + - gre:0 + - igmp:0 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Allow all TCP, but no other protocols access to this Self IP + bigip_selfip: + name: self1 + state: absent + allow_service: + - tcp:0 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +allow_service: + description: Services that are allowed via this self IP. + returned: changed + type: list + sample: ['igmp:0','tcp:22','udp:53'] +address: + description: The address for the self IP. + returned: changed + type: str + sample: 192.0.2.10 +name: + description: The name of the self IP. + returned: created + type: str + sample: self1 +netmask: + description: The netmask of the self IP. + returned: changed + type: str + sample: 255.255.255.0 +traffic_group: + description: The traffic group of which the self IP is a member. + returned: changed + type: str + sample: traffic-group-local-only +vlan: + description: The VLAN set on the self IP. + returned: changed + type: str + sample: vlan1 +fw_enforced_policy: + description: Specifies an AFM policy to be attached to the self IP. + returned: changed + type: str + sample: /Common/afm-blocking-policy +''' + +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ipaddress import ( + ip_network, ip_interface, ip_address +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import ( + is_valid_ip, ipv6_netmask_to_cidr +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'trafficGroup': 'traffic_group', + 'allowService': 'allow_service', + 'fwEnforcedPolicy': 'fw_enforced_policy', + 'fwEnforcedPolicyReference': 'fw_policy_link', + } + + api_attributes = [ + 'trafficGroup', + 'allowService', + 'vlan', + 'address', + 'description', + 'fwEnforcedPolicy', + 'fwEnforcedPolicyReference', + ] + + updatables = [ + 'traffic_group', + 'allow_service', + 'vlan', + 'netmask', + 'address', + 'description', + 'fw_enforced_policy', + 'fw_policy_link', + ] + + returnables = [ + 'traffic_group', + 'allow_service', + 'vlan', + 'route_domain', + 'netmask', + 'address', + 'description', + ] + + @property + def vlan(self): + if self._values['vlan'] is None: + return None + return fq_name(self.partition, self._values['vlan']) + + +class ModuleParameters(Parameters): + @property + def address(self): + address = "{0}%{1}/{2}".format( + self.ip, self.route_domain, self.netmask + ) + return address + + @property + def ip(self): + if self._values['address'] is None: + return None + if is_valid_ip(self._values['address']): + return self._values['address'] + else: + raise F5ModuleError( + 'The provided address is not a valid IP address' + ) + + @property + def traffic_group(self): + if self._values['traffic_group'] is None: + return None + return fq_name(self.partition, self._values['traffic_group']) + + @property + def route_domain(self): + if self._values['route_domain'] is None: + return None + result = int(self._values['route_domain']) + return result + + @property + def netmask(self): + if self._values['netmask'] is None: + return None + result = -1 + try: + result = int(self._values['netmask']) + if 0 < result < 256: + pass + except ValueError: + if is_valid_ip(self._values['netmask']): + addr = ip_address(u'{0}'.format(str(self._values['netmask']))) + if addr.version == 4: + ip = ip_network(u'0.0.0.0/%s' % str(self._values['netmask'])) + result = ip.prefixlen + else: + result = ipv6_netmask_to_cidr(self._values['netmask']) + if result < 0: + raise F5ModuleError( + 'The provided netmask {0} is neither in IP or CIDR format'.format(result) + ) + return result + + @property + def allow_service(self): + """Verifies that a supplied service string has correct format + + The string format for port lockdown is PROTOCOL:PORT. This method + will verify that the provided input matches the allowed protocols + and the port ranges before submitting to BIG-IP. + + The only allowed exceptions to this rule are the following values + + * all + * default + * none + + These are special cases that are handled differently in the API. + "all" is set as a string, "default" is set as a one item list, and + "none" removes the key entirely from the REST API. + + :raises F5ModuleError: + """ + if self._values['allow_service'] is None: + return None + result = [] + allowed_protocols = [ + 'eigrp', 'egp', 'gre', 'icmp', 'igmp', 'igp', 'ipip', + 'l2tp', 'ospf', 'pim', 'tcp', 'udp' + ] + special_protocols = [ + 'all', 'none', 'default' + ] + for svc in self._values['allow_service']: + if svc in special_protocols: + result = [svc] + break + elif svc in allowed_protocols: + full_service = '{0}:0'.format(svc) + result.append(full_service) + else: + tmp = svc.split(':') + if tmp[0] not in allowed_protocols: + raise F5ModuleError( + "The provided protocol '%s' is invalid" % (tmp[0]) + ) + try: + port = int(tmp[1]) + except Exception: + raise F5ModuleError( + "The provided port '%s' is not a number" % (tmp[1]) + ) + + if port < 0 or port > 65535: + raise F5ModuleError( + "The provided port '{0}' must be between 0 and 65535".format(port) + ) + else: + result.append(svc) + result = sorted(list(set(result))) + return result + + @property + def fw_enforced_policy(self): + if self._values['fw_enforced_policy'] is None: + return None + if self._values['fw_enforced_policy'] in ['none', '']: + return None + name = self._values['fw_enforced_policy'] + return fq_name(self.partition, name) + + @property + def fw_policy_link(self): + policy = self.fw_enforced_policy + if policy is None: + return None + tmp = policy.split('/') + link = dict(link='https://localhost/mgmt/tm/security/firewall/policy/~{0}~{1}'.format(tmp[1], tmp[2])) + return link + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + +class ApiParameters(Parameters): + @property + def allow_service(self): + if self._values['allow_service'] is None: + return None + if self._values['allow_service'] == 'all': + self._values['allow_service'] = ['all'] + return sorted(self._values['allow_service']) + + @property + def destination_ip(self): + if self._values['address'] is None: + return None + try: + pattern = r'(?P%[0-9]+)' + addr = re.sub(pattern, '', self._values['address']) + ip = ip_interface(u'{0}'.format(addr)) + return ip.with_prefixlen + except ValueError: + raise F5ModuleError( + "The provided destination is not an IP address" + ) + + @property + def netmask(self): + ip = ip_interface(self.destination_ip) + return int(ip.network.prefixlen) + + @property + def ip(self): + result = ip_interface(self.destination_ip) + return str(result.ip) + + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def allow_service(self): + if self._values['allow_service'] is None: + return None + if self._values['allow_service'] == ['all']: + return 'all' + return sorted(self._values['allow_service']) + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def address(self): + return None + + @property + def allow_service(self): + """Returns services formatted for consumption by f5-sdk update + + The BIG-IP endpoint for services takes different values depending on + what you want the "allowed services" to be. It can be any of the + following + + - a list containing "protocol:port" values + - the string "all" + - a null value, or None + + This is a convenience function to massage the values the user has + supplied so that they are formatted in such a way that BIG-IP will + accept them and apply the specified policy. + """ + if self.want.allow_service is None: + return None + result = self.want.allow_service + if result[0] == 'none' and self.have.allow_service is None: + return None + elif self.have.allow_service is None: + return result + elif result[0] == 'all' and self.have.allow_service[0] != 'all': + return ['all'] + elif result[0] == 'none': + return [] + elif set(self.want.allow_service) != set(self.have.allow_service): + return result + + @property + def netmask(self): + if self.want.netmask is None: + return None + ip = self.have.ip + if is_valid_ip(ip): + if self.want.route_domain is not None: + want = "{0}%{1}/{2}".format(ip, self.want.route_domain, self.want.netmask) + have = "{0}%{1}/{2}".format(ip, self.want.route_domain, self.have.netmask) + elif self.have.route_domain is not None: + want = "{0}%{1}/{2}".format(ip, self.have.route_domain, self.want.netmask) + have = "{0}%{1}/{2}".format(ip, self.have.route_domain, self.have.netmask) + else: + want = "{0}/{1}".format(ip, self.want.netmask) + have = "{0}/{1}".format(ip, self.have.netmask) + if want != have: + return want + else: + raise F5ModuleError( + 'The provided address/netmask value "{0}" was invalid'.format(self.have.ip) + ) + + @property + def traffic_group(self): + if self.want.traffic_group != self.have.traffic_group: + return self.want.traffic_group + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + @property + def fw_policy_link(self): + if self.want.fw_enforced_policy is None: + return None + if self.want.fw_enforced_policy == self.have.fw_enforced_policy: + return None + if self.want.fw_policy_link != self.have.fw_policy_link: + return self.want.fw_policy_link + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = ApiParameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if k in ['netmask']: + changed['address'] = change + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + changed = self.update() + else: + changed = self.create() + return changed + + def absent(self): + changed = False + if self.exists(): + changed = self.remove() + return changed + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the Self IP") + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def create(self): + if self.want.address is None or self.want.netmask is None: + raise F5ModuleError( + 'An address and a netmask must be specified' + ) + if self.want.vlan is None: + raise F5ModuleError( + 'A VLAN name must be specified' + ) + if self.want.route_domain is None: + rd = self.read_partition_default_route_domain_from_device() + self.want.update({'route_domain': rd}) + + if self.want.traffic_group is None: + self.want.update({'traffic_group': '/Common/traffic-group-local-only'}) + if self.want.route_domain is None: + self.want.update({'route_domain': 0}) + if self.want.allow_service: + if 'all' in self.want.allow_service: + self.want.update(dict(allow_service=['all'])) + elif 'none' in self.want.allow_service: + self.want.update(dict(allow_service=[])) + elif 'default' in self.want.allow_service: + self.want.update(dict(allow_service=['default'])) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + if self.exists(): + return True + else: + raise F5ModuleError("Failed to create the Self IP") + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/self/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/net/self/".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) + + if self.want.fw_enforced_policy: + payload = dict( + fwEnforcedPolicy=self.want.fw_enforced_policy, + fwEnforcedPolicyReference=self.want.fw_policy_link + ) + uri = "https://{0}:{1}/mgmt/tm/net/self/{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=payload) + + 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) + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/net/self/{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/net/self/{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/net/self/{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) + return ApiParameters(params=response) + + def read_partition_default_route_domain_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/partition/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.partition + ) + 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) + return int(response['defaultRouteDomain']) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + address=dict(), + allow_service=dict( + elements='str', + type='list', + ), + name=dict(required=True), + netmask=dict(), + traffic_group=dict(), + vlan=dict(), + route_domain=dict(type='int'), + description=dict(), + fw_enforced_policy=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_service_policy.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_service_policy.py new file mode 100644 index 00000000..432e880c --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_service_policy.py @@ -0,0 +1,444 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_service_policy +short_description: Manages service policies on a BIG-IP. +description: + - Service policies allow you to configure timers and port misuse rules + (if enabled) on a per rule or per context basis. +version_added: "1.0.0" +options: + name: + description: + - Name of the service policy. + required: True + type: str + description: + description: + - Description of the service policy. + type: str + timer_policy: + description: + - The timer policy to attach to the service policy. + type: str + port_misuse_policy: + description: + - The port misuse policy to attach to the service policy. + - Requires C(afm) (Advanced Firewall Manager) be provisioned to use. If C(afm) is not provisioned, this parameter + is ignored. + type: str + state: + description: + - Whether the resource should exist or not. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a service policy + bigip_service_policy: + name: foo + timer_policy: timer1 + port_misuse_policy: misuse1 + timer_policy_enabled: yes + port_misuse_policy_enabled: yes + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +timer_policy: + description: The new timer policy attached to the resource. + returned: changed + type: str + sample: /Common/timer1 +port_misuse_policy: + description: The new port misuse policy attached to the resource. + returned: changed + type: str + sample: /Common/misuse1 +description: + description: New description of the resource. + returned: changed + type: str + sample: My service policy description +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + module_provisioned, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'portMisusePolicy': 'port_misuse_policy', + 'timerPolicy': 'timer_policy', + } + + api_attributes = [ + 'description', + 'timerPolicy', + 'portMisusePolicy', + ] + + returnables = [ + 'description', + 'timer_policy', + 'port_misuse_policy', + ] + + updatables = [ + 'description', + 'timer_policy', + 'port_misuse_policy', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def timer_policy(self): + if self._values['timer_policy'] is None: + return None + if self._values['timer_policy'] == '': + return '' + return fq_name(self.partition, self._values['timer_policy']) + + @property + def port_misuse_policy(self): + if self._values['port_misuse_policy'] is None: + return None + if self._values['port_misuse_policy'] == '': + return '' + return fq_name(self.partition, self._values['port_misuse_policy']) + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.want.port_misuse_policy: + if not module_provisioned(self.client, 'afm'): + raise F5ModuleError( + "To configure a 'port_misuse_policy', you must have AFM provisioned." + ) + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.want.port_misuse_policy: + if not module_provisioned(self.client, 'afm'): + raise F5ModuleError( + "To configure a 'port_misuse_policy', you must have AFM provisioned." + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/service-policy/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/net/service-policy/".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/net/service-policy/{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/net/service-policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/service-policy/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True, + ), + description=dict(), + timer_policy=dict(), + port_misuse_policy=dict(), + state=dict( + default='present', + choices=['absent', 'present'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_smtp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_smtp.py new file mode 100644 index 00000000..5ff8ad3e --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_smtp.py @@ -0,0 +1,569 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_smtp +short_description: Manages SMTP settings on the BIG-IP +description: + - Allows configuring of the BIG-IP to send mail via an SMTP server by + configuring the parameters of an SMTP server. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the SMTP server configuration. + type: str + required: True + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + smtp_server: + description: + - SMTP server host name in the format of a fully qualified domain name. + - This value is required when creating a new SMTP configuration. + type: str + smtp_server_port: + description: + - Specifies the SMTP port number. + - When creating a new SMTP configuration, the default is C(25) when + C(encryption) is C(none) or C(tls). The default is C(465) when C(ssl) is selected. + type: int + local_host_name: + description: + - Hostname used in SMTP headers in the format of a fully qualified + domain name. This setting does not refer to the hostname of the BIG-IP system. + type: str + from_address: + description: + - Email address from which the email is being sent. This is the "Reply-to" + address the recipient sees. + type: str + encryption: + description: + - Specifies whether the SMTP server requires an encrypted connection in + order to send mail. + type: str + choices: + - none + - ssl + - tls + authentication: + description: + - Credentials can be set on an SMTP server's configuration even if that + authentication is not used (for example, staging configs or emergency changes). + This parameter acts as a switch to make the specified C(smtp_server_username) + and C(smtp_server_password) parameters active or not. + - When C(yes), the authentication parameters are active. + - When C(no), the authentication parameters are inactive. + type: bool + smtp_server_username: + description: + - User name the SMTP server requires when validating a user. + type: str + smtp_server_password: + description: + - Password the SMTP server requires when validating a user. + type: str + state: + description: + - When C(present), ensures the SMTP configuration exists. + - When C(absent), ensures the SMTP configuration does not exist. + type: str + choices: + - present + - absent + default: present + update_password: + description: + - Passwords are stored encrypted, so the module cannot know if the supplied + C(smtp_server_password) is the same or different than the existing password. + This parameter controls the updating of the C(smtp_server_password) + credential. + - When C(always), the system always updates the password. + - When C(on_create), the system only sets the password for newly created SMTP server + configurations. + type: str + choices: + - always + - on_create + default: always +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a base SMTP server configuration + bigip_smtp: + name: my-smtp + smtp_server: 1.1.1.1 + smtp_server_username: mail-admin + smtp_server_password: mail-secret + local_host_name: smtp.mydomain.com + from_address: no-reply@mydomain.com + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +smtp_server: + description: The new C(smtp_server) value of the SMTP configuration. + returned: changed + type: str + sample: mail.mydomain.com +smtp_server_port: + description: The new C(smtp_server_port) value of the SMTP configuration. + returned: changed + type: int + sample: 25 +local_host_name: + description: The new C(local_host_name) value of the SMTP configuration. + returned: changed + type: str + sample: smtp.mydomain.com +from_address: + description: The new C(from_address) value of the SMTP configuration. + returned: changed + type: str + sample: no-reply@mydomain.com +encryption: + description: The new C(encryption) value of the SMTP configuration. + returned: changed + type: str + sample: tls +authentication: + description: Whether the authentication parameters are active or not. + returned: changed + type: bool + sample: yes +''' +from datetime import datetime +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, is_valid_hostname +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'username': 'smtp_server_username', + 'passwordEncrypted': 'smtp_server_password', + 'localHostName': 'local_host_name', + 'smtpServerHostName': 'smtp_server', + 'smtpServerPort': 'smtp_server_port', + 'encryptedConnection': 'encryption', + 'authenticationEnabled': 'authentication_enabled', + 'authenticationDisabled': 'authentication_disabled', + 'fromAddress': 'from_address', + } + + api_attributes = [ + 'username', + 'passwordEncrypted', + 'localHostName', + 'smtpServerHostName', + 'smtpServerPort', + 'encryptedConnection', + 'authenticationEnabled', + 'authenticationDisabled', + 'fromAddress', + ] + + returnables = [ + 'smtp_server_username', + 'smtp_server_password', + 'local_host_name', + 'smtp_server', + 'smtp_server_port', + 'encryption', + 'authentication', + 'from_address', + ] + + updatables = [ + 'smtp_server_username', + 'smtp_server_password', + 'local_host_name', + 'smtp_server', + 'smtp_server_port', + 'encryption', + 'authentication', + 'from_address', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def local_host_name(self): + if self._values['local_host_name'] is None: + return None + if is_valid_ip(self._values['local_host_name']): + return self._values['local_host_name'] + elif is_valid_hostname(self._values['local_host_name']): + # else fallback to checking reasonably well formatted hostnames + return str(self._values['local_host_name']) + raise F5ModuleError( + "The provided 'local_host_name' value {0} is not a valid IP or hostname".format( + str(self._values['local_host_name']) + ) + ) + + @property + def authentication_enabled(self): + if self._values['authentication'] is None: + return None + if self._values['authentication']: + return True + + @property + def authentication_disabled(self): + if self._values['authentication'] is None: + return None + if not self._values['authentication']: + return True + + @property + def smtp_server_port(self): + if self._values['smtp_server_port'] is None: + return None + return int(self._values['smtp_server_port']) + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def smtp_server_password(self): + return None + + @property + def smtp_server_username(self): + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def smtp_server_password(self): + if self.want.update_password == 'on_create': + return None + return self.want.smtp_server_password + + @property + def authentication(self): + if self.want.authentication_enabled: + if self.want.authentication_enabled != self.have.authentication_enabled: + return dict( + authentication_enabled=self.want.authentication_enabled + ) + if self.want.authentication_disabled: + if self.want.authentication_disabled != self.have.authentication_disabled: + return dict( + authentication_disable=self.want.authentication_disabled + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/smtp-server/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.want.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/smtp-server/".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.want.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/smtp-server/{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/smtp-server/{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/smtp-server/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + smtp_server=dict(), + smtp_server_port=dict(type='int'), + smtp_server_username=dict(no_log=True), + smtp_server_password=dict(no_log=True), + local_host_name=dict(), + encryption=dict(choices=['none', 'ssl', 'tls']), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + from_address=dict(), + authentication=dict(type='bool'), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snat_pool.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snat_pool.py new file mode 100644 index 00000000..c3660b71 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snat_pool.py @@ -0,0 +1,528 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2016, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_snat_pool +short_description: Manage SNAT pools on a BIG-IP +description: + - Manage SNAT pools on a BIG-IP system. +version_added: "1.0.0" +options: + members: + description: + - List of members to put in the SNAT pool. When C(state) is C(present), + this parameter is required, otherwise it is optional. + - The members can be either IP addresses or names of the SNAT translation objects. + type: list + elements: str + aliases: + - member + description: + description: + - An optional description of the SNAT pool. + type: str + name: + description: + - The name of the SNAT pool. + type: str + required: True + state: + description: + - Whether the SNAT pool should exist or not. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +notes: + - When the C(bigip_snat_pool) object is removed, it also removes any associated C(bigip_snat_translation) objects. + - This is a BIG-IP behavior not module behavior, and it only occurs when the C(bigip_snat_translation) objects + are also not referenced by another C(bigip_snat_pool). +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add the SNAT pool 'my-snat-pool' + bigip_snat_pool: + name: my-snat-pool + state: present + members: + - 10.10.10.10 + - 20.20.20.20 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Change the SNAT pool's members to a single member + bigip_snat_pool: + name: my-snat-pool + state: present + member: 30.30.30.30 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove the SNAT pool 'my-snat-pool' + bigip_snat_pool: + name: johnd + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add the SNAT pool 'my-snat-pool' with a description + bigip_snat_pool: + name: my-snat-pool + state: present + members: + - 10.10.10.10 + - 20.20.20.20 + description: A SNAT pool description + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +members: + description: + - List of members that are part of the SNAT pool. + returned: changed and success + type: list + sample: "['10.10.10.10']" +''' + +import re +import os +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.ipaddress import ( + is_valid_ip, compress_address +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = {} + + updatables = [ + 'members', + 'description', + ] + + returnables = [ + 'members', + 'description', + ] + + api_attributes = [ + 'members', + 'description', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + def _clear_member_prefix(self, member): + result = os.path.basename(member) + return result + + def _format_member_address(self, member): + if len(member.split('%')) > 1: + address, rd = member.split('%') + if is_valid_ip(address): + result = '/{0}/{1}%{2}'.format(self.partition, compress_address(address), rd) + return result + else: + if is_valid_ip(member): + address = '/{0}/{1}'.format(self.partition, member) + return address + else: + # names must start with alphabetic character, and can contain hyphens and underscores and numbers + # no special characters are allowed + pattern = re.compile(r'(?!-)[A-Z-].*(? 1: + address, rd = self._values['address'].split('%') + if is_valid_ip(address): + result = '{0}%{1}'.format(compress_address(address), rd) + return result + else: + if is_valid_ip(self._values['address']): + return self._values['address'] + raise F5ModuleError( + "The provided address: {0} is not a valid IP address".format(self._values['address']) + ) + + @property + def arp(self): + result = flatten_boolean(self._values['arp']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def connection_limit(self): + if self._values['connection_limit'] is None: + return None + return int(self._validate_conn_limit(self._values['connection_limit'])) + + @property + def description(self): + if self._values['description'] is None: + return None + if self._values['description'] in ['', 'none']: + return '' + return self._values['description'] + + @property + def disabled(self): + if self._values['state'] == 'disabled': + return True + + @property + def enabled(self): + if self._values['state'] in ['enabled', 'present']: + return True + + @property + def ip_idle_timeout(self): + return self._validate_timeout_limit(self._values['ip_idle_timeout']) + + @property + def state(self): + if self.enabled is True and self._values['state'] != 'present': + return 'enabled' + elif self.disabled is True: + return 'disabled' + else: + return self._values['state'] + + @property + def tcp_idle_timeout(self): + return self._validate_timeout_limit(self._values['tcp_idle_timeout']) + + @property + def traffic_group(self): + if self._values['traffic_group'] is None: + return None + return fq_name(self.partition, self._values['traffic_group']) + + @property + def udp_idle_timeout(self): + return self._validate_timeout_limit(self._values['udp_idle_timeout']) + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + def _change_limit_value(self, value): + if value == 4294967295: + return 'indefinite' + else: + return value + + @property + def arp(self): + return flatten_boolean(self._values['arp']) + + @property + def ip_idle_timeout(self): + if self._values['ip_idle_timeout'] is None: + return None + return self._change_limit_value(self._values['ip_idle_timeout']) + + @property + def tcp_idle_timeout(self): + if self._values['tcp_idle_timeout'] is None: + return None + return self._change_limit_value(self._values['tcp_idle_timeout']) + + @property + def udp_idle_timeout(self): + if self._values['udp_idle_timeout'] is None: + return None + return self._change_limit_value(self._values['udp_idle_timeout']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ['present', 'enabled', 'disabled']: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + + if self.module._diff and self.have: + result['diff'] = self.make_diff() + + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _grab_attr(self, item): + result = dict() + updatables = Parameters.updatables + for k in updatables: + if getattr(item, k) is not None: + result[k] = getattr(item, k) + return result + + def make_diff(self): + result = dict(before=self._grab_attr(self.have), after=self._grab_attr(self.want)) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + changed = False + if self.exists(): + changed = self.remove() + return changed + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + if not self.exists(): + raise F5ModuleError("Failed to create the SNAT pool") + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the SNAT pool") + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/snat-translation/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/ltm/snat-translation/".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return True + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/snat-translation/{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/ltm/snat-translation/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/snat-translation/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + address=dict( + aliases=['ip'] + ), + arp=dict( + type='bool' + ), + connection_limit=dict( + type='int' + ), + description=dict(), + ip_idle_timeout=dict(), + name=dict(required=True), + partition=dict( + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['absent', 'present', 'enabled', 'disabled'] + ), + tcp_idle_timeout=dict(), + traffic_group=dict(), + udp_idle_timeout=dict() + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_if = [ + ['state', 'present', ['address', 'name']], + ['state', 'enabled', ['address', 'name']], + ['state', 'disabled', ['address', 'name']], + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp.py new file mode 100644 index 00000000..0fe6075b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp.py @@ -0,0 +1,415 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_snmp +short_description: Manipulate general SNMP settings on a BIG-IP +description: + - Manipulate general SNMP settings on a BIG-IP system. +version_added: "1.0.0" +options: + allowed_addresses: + description: + - Configures the IP addresses of the SNMP clients from which the snmpd + daemon accepts requests. + - This value can be hostnames, IP addresses, or IP networks. + - You may specify a single list item of C(default) to set the value back + to the system default of C(127.0.0.0/8). + - You can remove all allowed addresses by either providing the word C(none), or + by providing the empty string C(""). + type: raw + contact: + description: + - Specifies the name of the person who administers the SNMP + service for this system. + type: str + agent_status_traps: + description: + - When C(enabled), ensures the system sends a trap whenever the + SNMP agent starts running or stops running. This is usually enabled + by default on a BIG-IP. + type: str + choices: + - enabled + - disabled + agent_authentication_traps: + description: + - When C(enabled), ensures the system sends authentication warning + traps to the trap destinations. This is usually disabled by default on + a BIG-IP. + type: str + choices: + - enabled + - disabled + device_warning_traps: + description: + - When C(enabled), ensures the system sends device warning traps + to the trap destinations. This is usually enabled by default on a + BIG-IP. + type: str + choices: + - enabled + - disabled + location: + description: + - Specifies the description of this system's physical location. + type: str +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Set snmp contact + bigip_snmp: + contact: Joe User + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set snmp location + bigip_snmp: + location: US West 1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +agent_status_traps: + description: Value of the agent status traps. + returned: changed + type: str + sample: enabled +agent_authentication_traps: + description: Value of the authentication status traps. + returned: changed + type: str + sample: enabled +device_warning_traps: + description: Value of the warning status traps. + returned: changed + type: str + sample: enabled +contact: + description: The new value for the person who administers SNMP on the device. + returned: changed + type: str + sample: Joe User +location: + description: The new value for the system's physical location. + returned: changed + type: str + sample: US West 1a +allowed_addresses: + description: The new allowed addresses for SNMP client connections. + returned: changed + type: list + sample: ['127.0.0.0/8', 'foo.bar.com', '10.10.10.10'] +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types + +from ipaddress import ip_network + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, is_valid_hostname +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'agentTrap': 'agent_status_traps', + 'authTrap': 'agent_authentication_traps', + 'bigipTraps': 'device_warning_traps', + 'sysLocation': 'location', + 'sysContact': 'contact', + 'allowedAddresses': 'allowed_addresses', + } + + updatables = [ + 'agent_status_traps', + 'agent_authentication_traps', + 'device_warning_traps', + 'location', + 'contact', + 'allowed_addresses', + ] + + returnables = [ + 'agent_status_traps', + 'agent_authentication_traps', + 'device_warning_traps', + 'location', 'contact', + 'allowed_addresses', + ] + + api_attributes = [ + 'agentTrap', + 'authTrap', + 'bigipTraps', + 'sysLocation', + 'sysContact', + 'allowedAddresses', + ] + + +class ApiParameters(Parameters): + @property + def allowed_addresses(self): + if self._values['allowed_addresses'] is None: + return None + result = list(set(self._values['allowed_addresses'])) + result.sort() + return result + + +class ModuleParameters(Parameters): + @property + def allowed_addresses(self): + if self._values['allowed_addresses'] is None: + return None + result = [] + addresses = self._values['allowed_addresses'] + if isinstance(addresses, string_types): + if addresses in ['', 'none']: + return [] + else: + addresses = [addresses] + if len(addresses) == 1 and addresses[0] in ['default', '']: + result = ['127.0.0.0/8'] + return result + for address in addresses: + try: + # Check for valid IPv4 or IPv6 entries + ip_network(u'%s' % str(address)) + result.append(address) + except ValueError: + # else fallback to checking reasonably well formatted hostnames + if is_valid_hostname(address): + result.append(str(address)) + continue + raise F5ModuleError( + "The provided 'allowed_address' value {0} is not a valid IP or hostname".format(address) + ) + result = list(set(result)) + result.sort() + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def allowed_addresses(self): + if self.want.allowed_addresses is None: + return None + if self.have.allowed_addresses is None: + if self.want.allowed_addresses: + return self.want.allowed_addresses + return None + want = set(self.want.allowed_addresses) + have = set(self.have.allowed_addresses) + if want != have: + result = list(want) + result.sort() + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = ApiParameters() + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.update() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + return ApiParameters(params=response) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.choices = ['enabled', 'disabled'] + argument_spec = dict( + contact=dict(), + agent_status_traps=dict( + choices=self.choices + ), + agent_authentication_traps=dict( + choices=self.choices + ), + device_warning_traps=dict( + choices=self.choices + ), + location=dict(), + allowed_addresses=dict(type='raw') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp_community.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp_community.py new file mode 100644 index 00000000..601cfac1 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp_community.py @@ -0,0 +1,924 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_snmp_community +short_description: Manages SNMP communities on a BIG-IP. +description: + - Assists in managing Simple Network Management Protocol (SNMP) communities on a BIG-IP system. Different SNMP versions are supported + by this module. Note the different parameters offered by this module, as different + parameters work for different versions of SNMP. This is important if you + are mixing versions C(v2c) and C(3). +version_added: "1.0.0" +options: + state: + description: + - When C(present), ensures the address list and entries exists. + - When C(absent), ensures the address list is removed. + type: str + choices: + - present + - absent + default: present + version: + description: + - Specifies to which SNMP version the trap destination applies. + type: str + choices: + - v1 + - v2c + - v3 + default: v2c + name: + description: + - Name that identifies the SNMP community. + - When C(version) is C(v1) or C(v2c), this parameter is required. + - The name C(public) is a reserved name on the BIG-IP. This module handles that name differently + than others. Functionally, you should not see a difference. + type: str + community: + description: + - Specifies the community string (password) for access to the MIB. + - This parameter is only relevant when C(version) is C(v1) or C(v2c). If C(version) is + something else, this parameter is ignored. + type: str + source: + description: + - Specifies the source address for access to the MIB. + - This parameter can accept a value of C(all). + - If this parameter is not specified, the value is C(all). + - This parameter is only relevant when C(version) is C(v1) or C(v2c). If C(version) is + something else, this parameter is ignored. + - If C(source) is set to C(all), it is not possible to specify an C(oid). This will + raise an error. + - You should provide this parameter when C(state) is C(absent), so the correct community + is removed. To remove the C(public) SNMP community that comes with a BIG-IP, this parameter + should be C(default). + type: str + port: + description: + - Specifies the port for the trap destination. + - This parameter is only relevant when C(version) is C(v1) or C(v2c). If C(version) is + something else, this parameter is ignored. + type: int + oid: + description: + - Specifies the object identifier (OID) for the record. + - When C(version) is C(v3), this parameter is required. + - When C(version) is either C(v1) or C(v2c), if this value is specified, then C(source) + must not be set to C(all). + type: str + access: + description: + - Specifies the user's access level to the MIB. + - When creating a new community, if this parameter is not specified, the default is C(ro). + - When C(ro), specifies the user can view the MIB, but cannot modify the MIB. + - When C(rw), specifies the user can view and modify the MIB. + type: str + choices: + - ro + - rw + - read-only + - read-write + ip_version: + description: + - Specifies whether the record applies to IPv4 or IPv6 addresses. + - When creating a new community, if this value is not specified, the default is C(4). + - This parameter is only relevant when C(version) is C(v1) or C(v2c). If C(version) is + something else, this parameter is ignored. + type: str + choices: + - '4' + - '6' + snmp_username: + description: + - Specifies the name of the user for whom you want to grant access to the SNMP v3 MIB. + - This parameter is only relevant when C(version) is C(v3). If C(version) is something + else, this parameter is ignored. + - When creating a new SNMP C(v3) community, this parameter is required. + - This parameter cannot be changed once it has been set. + type: str + snmp_auth_protocol: + description: + - Specifies the authentication method for the user. + - When C(md5), specifies the system uses the MD5 algorithm to authenticate the user. + - When C(sha), specifies the secure hash algorithm (SHA) to authenticate the user. + - When C(none), specifies the user does not require authentication. + - When creating a new SNMP C(v3) community, if this parameter is not specified, the default + is C(sha). + type: str + choices: + - md5 + - sha + - none + snmp_auth_password: + description: + - Specifies the password for the user. + - When creating a new SNMP C(v3) community, this parameter is required. + - This value must be at least 8 characters long. + type: str + snmp_privacy_protocol: + description: + - Specifies the encryption protocol. + - When C(aes), specifies the system encrypts the user information using AES + (Advanced Encryption Standard). + - When C(des), specifies the system encrypts the user information using DES + (Data Encryption Standard). + - When C(none), specifies the system does not encrypt the user information. + - When creating a new SNMP C(v3) community, if this parameter is not specified, the + default is C(aes). + type: str + choices: + - aes + - des + - none + snmp_privacy_password: + description: + - Specifies the password for the user. + - When creating a new SNMP C(v3) community, this parameter is required. + - This value must be at least 8 characters long. + type: str + update_password: + description: + - C(always) allows users to update passwords. + C(on_create) only sets the password for newly created resources. + type: str + choices: + - always + - on_create + default: always + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an SMNP v2c read-only community + bigip_snmp_community: + name: foo + version: v2c + source: all + oid: .1 + access: ro + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create an SMNP v3 read-write community + bigip_snmp_community: + name: foo + version: v3 + snmp_username: foo + snmp_auth_protocol: sha + snmp_auth_password: secret + snmp_privacy_protocol: aes + snmp_privacy_password: secret + oid: .1 + access: rw + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove the default 'public' SNMP community + bigip_snmp_community: + name: public + source: default + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +community: + description: The new community value. + returned: changed + type: str + sample: community1 +oid: + description: The new OID value. + returned: changed + type: str + sample: .1 +ip_version: + description: The new IP version value. + returned: changed + type: str + sample: .1 +snmp_auth_protocol: + description: The new SNMP auth protocol. + returned: changed + type: str + sample: sha +snmp_privacy_protocol: + description: The new SNMP privacy protocol. + returned: changed + type: str + sample: aes +access: + description: The new access level for the MIB. + returned: changed + type: str + sample: ro +source: + description: The new source address to access the MIB. + returned: changed + type: str + sample: 1.1.1.1 +snmp_username: + description: The new SNMP username. + returned: changed + type: str + sample: user1 +snmp_auth_password: + description: The new password of the given snmp_username. + returned: changed + type: str + sample: secret1 +snmp_privacy_password: + description: The new password of the given snmp_username. + returned: changed + type: str + sample: secret2 +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'communityName': 'community', + 'oidSubset': 'oid', + 'ipv6': 'ip_version', + 'authProtocol': 'snmp_auth_protocol', + 'privacyProtocol': 'snmp_privacy_protocol', + 'username': 'snmp_username', + 'securityLevel': 'security_level', + 'authPassword': 'snmp_auth_password', + 'privacyPassword': 'snmp_privacy_password', + } + + api_attributes = [ + 'source', + 'oidSubset', + 'ipv6', + 'communityName', + 'access', + 'authPassword', + 'authProtocol', + 'username', + 'securityLevel', + 'privacyProtocol', + 'privacyPassword', + ] + + returnables = [ + 'community', + 'oid', + 'ip_version', + 'snmp_auth_protocol', + 'snmp_privacy_protocol', + 'access', + 'source', + 'snmp_username', + 'snmp_auth_password', + 'snmp_privacy_password', + ] + + updatables = [ + 'community', + 'oid', + 'ip_version', + 'snmp_auth_protocol', + 'snmp_privacy_protocol', + 'access', + 'source', + 'snmp_auth_password', + 'snmp_privacy_password', + 'security_level', + 'snmp_username', + ] + + @property + def port(self): + if self._values['port'] is None: + return None + return int(self._values['port']) + + +class ApiParameters(Parameters): + @property + def ip_version(self): + if self._values['ip_version'] is None: + return None + if self._values['ip_version'] == 'enabled': + return 6 + return 4 + + @property + def source(self): + if self._values['source'] is None: + return 'all' + return self._values['source'] + + +class ModuleParameters(Parameters): + @property + def ip_version(self): + if self._values['ip_version'] is None: + return None + return int(self._values['ip_version']) + + @property + def source(self): + if self._values['source'] is None: + return None + if self._values['source'] == '': + return 'all' + return self._values['source'] + + @property + def access(self): + if self._values['access'] is None: + return None + elif self._values['access'] in ['ro', 'read-only']: + return 'ro' + elif self._values['access'] in ['rw', 'read-write']: + return 'rw' + else: + raise F5ModuleError( + "Unknown access format specified: '{0}'.".format(self._values['access']) + ) + + @property + def snmp_auth_password(self): + if self._values['snmp_auth_password'] is None: + return None + if len(self._values['snmp_auth_password']) < 8: + raise F5ModuleError( + "snmp_auth_password must be at least 8 characters long." + ) + return self._values['snmp_auth_password'] + + @property + def snmp_privacy_password(self): + if self._values['snmp_privacy_password'] is None: + return None + if len(self._values['snmp_privacy_password']) < 8: + raise F5ModuleError( + "snmp_privacy_password must be at least 8 characters long." + ) + return self._values['snmp_privacy_password'] + + @property + def name(self): + if self._values['name'] == 'public': + return 'comm-public' + return self._values['name'] + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def ip_version(self): + if self._values['ip_version'] is None: + return None + elif self._values['ip_version'] == 4: + return 'disabled' + return 'enabled' + + @property + def source(self): + if self._values['source'] is None: + return None + if self._values['source'] == 'all': + return '' + return self._values['source'] + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def _check_source_and_oid(self): + if self.have.oid is not None: + if self.want.source == 'all' and self.want.oid != '': + raise F5ModuleError( + "When specifying an 'all' source for a resource with an existing OID, " + "you must specify a new, empty, OID." + ) + if self.want.source == 'all' and self.want.oid != '': + raise F5ModuleError( + "When specifying an 'all' source for a resource, you may not specify an OID." + ) + + @property + def source(self): + self._check_source_and_oid() + if self.want.source != self.have.source: + return self.want.source + + @property + def oid(self): + self._check_source_and_oid() + if self.want.oid != self.have.oid: + return self.want.oid + + @property + def snmp_privacy_password(self): + if self.want.update_password == 'always' and self.want.snmp_privacy_password is not None: + return self.want.snmp_privacy_password + + @property + def snmp_auth_password(self): + if self.want.update_password == 'always' and self.want.snmp_auth_password is not None: + return self.want.snmp_auth_password + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + + def exec_module(self): + if self.version_is_less_than_3(): + manager = self.get_manager('v1') + else: + manager = self.get_manager('v2') + return manager.exec_module() + + def get_manager(self, type): + if type == 'v1': + return V1Manager(**self.kwargs) + elif type == 'v2': + return V2Manager(**self.kwargs) + + def version_is_less_than_3(self): + version = self.module.params.get('version') + if version == 'v3': + return False + else: + return True + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + +class V1Manager(BaseManager): + """Handles SNMP v1 and v2c + + """ + def create(self): + if self.want.ip_version is None: + self.want.update({'ip_version': 4}) + if self.want.access is None: + self.want.update({'access': 'ro'}) + self._set_changed_options() + if self.want.oid is not None and self.want.source == 'all': + raise F5ModuleError( + "When specify an oid, source may not be set to 'all'." + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/communities/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/snmp/communities/".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/snmp/communities/{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/snmp/communities/{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/snmp/communities/{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) + return ApiParameters(params=response) + + +class V2Manager(BaseManager): + """Handles SNMP v3 + + SNMP v3 has (almost) a completely separate set of variables than v2c or v1. + The functionality is placed in this separate class to handle these differences. + + """ + def create(self): + if self.want.access is None: + self.want.update({'access': 'ro'}) + if self.want.snmp_auth_protocol is None: + self.want.update({'snmp_auth_protocol': 'sha'}) + if self.want.snmp_privacy_protocol is None: + self.want.update({'snmp_privacy_protocol': 'aes'}) + + self._set_changed_options() + if self.want.snmp_username is None: + raise F5ModuleError( + "snmp_username must be specified when creating a new v3 community." + ) + if self.want.snmp_auth_password is None: + raise F5ModuleError( + "snmp_auth_password must be specified when creating a new v3 community." + ) + if self.want.snmp_privacy_password is None: + raise F5ModuleError( + "snmp_privacy_password must be specified when creating a new v3 community." + ) + if self.want.oid is None: + raise F5ModuleError( + "oid must be specified when creating a new v3 community." + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/users/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.snmp_username) + ) + 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.snmp_username + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/users/".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/snmp/users/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.snmp_username) + ) + 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/snmp/users/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.snmp_username) + ) + 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/snmp/users/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.snmp_username) + ) + 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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + version=dict( + default='v2c', + choices=['v1', 'v2c', 'v3'] + ), + name=dict(), + community=dict(), + source=dict(), + port=dict(type='int'), + oid=dict(), + access=dict( + choices=['ro', 'rw', 'read-only', 'read-write'] + ), + ip_version=dict( + choices=['4', '6'] + ), + snmp_username=dict(), + snmp_auth_protocol=dict( + choices=['md5', 'sha', 'none'] + ), + snmp_auth_password=dict(no_log=True), + snmp_privacy_protocol=dict( + choices=['aes', 'des', 'none'] + ), + snmp_privacy_password=dict(no_log=True), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + state=dict(default='present', choices=['absent', 'present']), + 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.required_if = [ + ['version', 'v1', ['name']], + ['version', 'v2', ['name']], + ['version', 'v3', ['snmp_username']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp_trap.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp_trap.py new file mode 100644 index 00000000..7b7048a9 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_snmp_trap.py @@ -0,0 +1,829 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_snmp_trap +short_description: Manipulate SNMP trap information on a BIG-IP +description: + - Manipulate SNMP trap information on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Name of the SNMP configuration endpoint. + type: str + required: True + snmp_version: + description: + - Specifies to which Simple Network Management Protocol (SNMP) version + the trap destination applies. + type: str + choices: + - '1' + - '2c' + - '3' + community: + description: + - Specifies the community name for the trap destination. + type: str + destination: + description: + - Specifies the address for the trap destination. This can be either an + IP address or a hostname. + type: str + port: + description: + - Specifies the port for the trap destination. + type: str + network: + description: + - Specifies the name of the trap network. This option is not supported in + versions of BIG-IP prior to 12.1.0, and is simply ignored on those versions. + - The value C(default) was removed in BIG-IP version 13.1.0. Specifying this + value when configuring a BIG-IP causes the module to stop and report + an error. In this case, choose one of the other options, such as + C(management). + type: str + choices: + - other + - management + - default + security_name: + description: + - Specifies the security name to used for v3 snmp trap + - Required for the C(snmp_version) matches C(v3). + type: str + version_added: "1.16.0" + security_level: + description: + - Specifies the port for the trap destination. + - Required for the C(snmp_version) matches C(v3). + type: str + choices: + - auth-no-privacy + - auth-privacy + version_added: "1.16.0" + auth_protocol: + description: + - Specifies the Authentication protocol to be used for snmp v3 traps + - Required for the C(security_level) + type: str + choices: + - sha + - md5 + version_added: "1.16.0" + auth_password: + description: + - Specifies the Authentication protocol password to be used for snmp v3 traps + - Required for the C(snmp_version) matches C(v3) and for the C(security_level) + type: str + version_added: "1.16.0" + privacy_protocol: + description: + - Specifies the Privacy protocol to be used for snmp v3 traps + - Required for the C(security_level) matches c(auth-privacy) + type: str + choices: + - aes + - des + version_added: "1.16.0" + privacy_password: + description: + - Specifies the Privacy protocol password to be used for snmp v3 traps + - Required for the C(security_level) matches c(auth-privacy) + type: str + version_added: "1.16.0" + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource does not exist. + type: str + choices: + - present + - absent + default: present + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +notes: + - This module only supports version v1 and v2c of SNMP. + - The C(network) option is not supported on versions of BIG-IP prior to 12.1.0 because + the platform did not support that option until 12.1.0. If used on versions + prior to 12.1.0, it is simply be ignored. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create snmp v1 trap + bigip_snmp_trap: + community: general + destination: 1.2.3.4 + name: my-trap1 + network: management + port: 9000 + snmp_version: 1 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create snmp v2 trap + bigip_snmp_trap: + community: general + destination: 5.6.7.8 + name: my-trap2 + network: default + port: 7000 + snmp_version: 2c + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Create snmp v3 trap + bigip_snmp_trap: + community: general + destination: 5.6.7.9 + name: my-trap3 + network: management + port: 7001 + snmp_version: 3 + auth_protocol: 'sha' + auth_password: 'test12345' + security_name: "testsec2" + security_level: "auth-no-privacy" + provider: + server: lb.mydomain.com + user: admin + password: secret + state: absent + delegate_to: localhost + +- name: Create snmp v3 trap-2 + bigip_snmp_trap: + community: general + destination: 5.6.7.10 + name: my-trap4 + network: management + port: 7002 + snmp_version: 3 + auth_protocol: 'sha' + auth_password: 'test123456' + security_name: "testsec3" + security_level: "auth-privacy" + privacy_protocol: "des" + privacy_password: 'test@12345' + provider: + server: lb.mydomain.com + user: admin + password: secret + state: absent + delegate_to: localhost +''' + +RETURN = r''' +snmp_version: + description: The new C(snmp_version) configured on the remote device. + returned: changed and success + type: str + sample: 2c +community: + description: The new C(community) name for the trap destination. + returned: changed and success + type: list + sample: secret +destination: + description: The new address for the trap destination in either IP or hostname form. + returned: changed and success + type: str + sample: 1.2.3.4 +port: + description: The new C(port) of the trap destination. + returned: changed and success + type: str + sample: 900 +network: + description: The new name of the network the SNMP trap is on. + returned: changed and success + type: str + sample: management +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'version': 'snmp_version', + 'community': 'community', + 'host': 'destination', + 'securityName': 'security_name', + 'authProtocol': 'auth_protocol', + 'authPassword': 'auth_password', + 'securityLevel': 'security_level', + 'privacyProtocol': 'privacy_protocol', + 'privacyPassword': 'privacy_password', + } + + @property + def snmp_version(self): + if self._values['snmp_version'] is None: + return None + return str(self._values['snmp_version']) + + @property + def port(self): + if self._values['port'] is None: + return None + return int(self._values['port']) + + def to_return(self): + result = {} + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + + +class V3Parameters(Parameters): + updatables = [ + 'snmp_version', + 'community', + 'destination', + 'port', + 'network', + 'security_name', + 'auth_protocol', + 'security_level', + 'privacy_protocol', + ] + + returnables = [ + 'snmp_version', + 'community', + 'destination', + 'port', + 'network', + 'security_name', + 'auth_protocol', + 'auth_password', + 'security_level', + 'privacy_protocol', + 'privacy_password', + ] + + api_attributes = [ + 'version', + 'community', + 'host', + 'port', + 'network', + 'securityName', + 'authProtocol', + 'authPassword', + 'securityLevel', + 'privacyProtocol', + 'privacyPassword', + ] + + @property + def network(self): + if self._values['network'] is None: + return None + network = str(self._values['network']) + if network == 'management': + return 'mgmt' + elif network == 'default': + raise F5ModuleError( + "'default' is not a valid option for this version of BIG-IP. " + "Use either 'management', 'or 'other' instead." + ) + else: + return network + + +class V2Parameters(Parameters): + updatables = [ + 'snmp_version', + 'community', + 'destination', + 'port', + 'network', + ] + + returnables = [ + 'snmp_version', + 'community', + 'destination', + 'port', + 'network', + ] + + api_attributes = [ + 'version', + 'community', + 'host', + 'port', + 'network', + ] + + @property + def network(self): + if self._values['network'] is None: + return None + network = str(self._values['network']) + if network == 'management': + return 'mgmt' + elif network == 'default': + return '' + else: + return network + + +class V1Parameters(Parameters): + updatables = [ + 'snmp_version', + 'community', + 'destination', + 'port', + ] + + returnables = [ + 'snmp_version', + 'community', + 'destination', + 'port', + ] + + api_attributes = [ + 'version', + 'community', + 'host', + 'port', + ] + + @property + def network(self): + return None + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def exec_module(self): + if self.is_version_without_network(): + manager = V1Manager(**self.kwargs) + elif self.is_version_with_default_network(): + manager = V2Manager(**self.kwargs) + else: + manager = V3Manager(**self.kwargs) + + return manager.exec_module() + + def is_version_without_network(self): + """Is current BIG-IP version missing "network" value support + + Returns: + bool: True when it is missing. False otherwise. + """ + version = tmos_version(self.client) + if Version(version) < Version('12.1.0'): + return True + else: + return False + + def is_version_with_default_network(self): + """Is current BIG-IP version missing "default" network value support + + Returns: + bool: True when it is missing. False otherwise. + """ + version = tmos_version(self.client) + if Version(version) < Version('13.1.0'): + return True + else: + return False + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the snmp trap") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + if all(getattr(self.want, v) is None for v in self.required_resources): + raise F5ModuleError( + "You must specify at least one of " + ', '.join(self.required_resources) + ) + self.create_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/traps/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update_on_device(self): + params = self.want.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/traps/{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 create_on_device(self): + params = self.want.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/traps/".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 remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/traps/{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 + + +class V3Manager(BaseManager): + def __init__(self, *args, **kwargs): + super(V3Manager, self).__init__(**kwargs) + self.required_resources = [ + 'version', 'community', 'destination', 'port', 'network', 'security_name', 'auth_protocol', 'auth_password', + 'security_level', 'privacy_protocol', 'privacy_password' + ] + self.want = V3Parameters(params=self.module.params) + self.changes = V3Parameters() + + def _set_changed_options(self): + changed = {} + for key in V3Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = V3Parameters(params=changed) + + def _update_changed_options(self): + changed = {} + for key in V3Parameters.updatables: + if getattr(self.want, key) is not None: + attr1 = getattr(self.want, key) + attr2 = getattr(self.have, key) + if attr1 != attr2: + changed[key] = attr1 + if changed: + self.changes = V3Parameters(params=changed) + return True + return False + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/traps/{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) + return V3Parameters(params=response) + + +class V2Manager(BaseManager): + def __init__(self, *args, **kwargs): + super(V2Manager, self).__init__(**kwargs) + self.required_resources = [ + 'version', 'community', 'destination', 'port', 'network' + ] + self.want = V2Parameters(params=self.module.params) + self.changes = V2Parameters() + + def _set_changed_options(self): + changed = {} + for key in V2Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = V2Parameters(params=changed) + + def _update_changed_options(self): + changed = {} + for key in V2Parameters.updatables: + if getattr(self.want, key) is not None: + attr1 = getattr(self.want, key) + attr2 = getattr(self.have, key) + if attr1 != attr2: + changed[key] = attr1 + if changed: + self.changes = V2Parameters(params=changed) + return True + return False + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/traps/{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) + self._ensure_network(response) + return V2Parameters(params=response) + + def _ensure_network(self, result): + # BIG-IP's value for "default" is that the key does not + # exist. This conflicts with our purpose of having a key + # not exist (which we equate to "i dont want to change that" + # therefore, if we load the information from BIG-IP and + # find that there is no 'network' key, that is BIG-IP's + # way of saying that the network value is "default" + if 'network' not in result: + result['network'] = 'default' + + +class V1Manager(BaseManager): + def __init__(self, *args, **kwargs): + super(V1Manager, self).__init__(**kwargs) + self.required_resources = [ + 'version', 'community', 'destination', 'port' + ] + self.want = V1Parameters(params=self.module.params) + self.changes = V1Parameters() + + def _set_changed_options(self): + changed = {} + for key in V1Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = V1Parameters(params=changed) + + def _update_changed_options(self): + changed = {} + for key in V1Parameters.updatables: + if getattr(self.want, key) is not None: + attr1 = getattr(self.want, key) + attr2 = getattr(self.have, key) + if attr1 != attr2: + changed[key] = attr1 + if changed: + self.changes = V1Parameters(params=changed) + return True + return False + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/snmp/traps/{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) + return V1Parameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True + ), + snmp_version=dict( + choices=['1', '2c', '3'] + ), + community=dict(no_log=True), + destination=dict(), + security_name=dict(), + security_level=dict( + choices=['auth-no-privacy', 'auth-privacy'] + ), + auth_protocol=dict( + choices=['sha', 'md5'] + ), + auth_password=dict(no_log=True), + privacy_protocol=dict( + choices=['aes', 'des'] + ), + privacy_password=dict(no_log=True), + port=dict(), + network=dict( + choices=['other', 'management', 'default'] + ), + state=dict( + default='present', + choices=['absent', 'present'] + ), + 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.required_if = [ + ['snmp_version', '3', ['security_name']], + ['snmp_version', '3', ['security_level']], + ['security_level', 'auth-no-privacy', ['auth_protocol']], + ['security_level', 'auth-no-privacy', ['auth_password']], + ['security_level', 'auth-privacy', ['auth_protocol']], + ['security_level', 'auth-privacy', ['auth_password']], + ['security_level', 'auth-privacy', ['privacy_protocol']], + ['security_level', 'auth-privacy', ['privacy_password']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_image.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_image.py new file mode 100644 index 00000000..23f1543f --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_image.py @@ -0,0 +1,505 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_software_image +short_description: Manage software images on a BIG-IP +description: + - Manages software images on a BIG-IP. These images may include both base images + and hotfix images. +version_added: "1.0.0" +options: + force: + description: + - When C(yes), uploads the file every time and replaces the file on the + device. + - When C(no), the file is only uploaded if it does not already + exist. + - Generally should be C(yes) only in cases where you have reason + to believe the image was corrupted during upload. + type: bool + default: no + state: + description: + - When C(present), ensures the image is uploaded. + - When C(absent), ensures the image is removed. + type: str + choices: + - absent + - present + default: present + image: + description: + - The image to put on the remote device. + - This may be an absolute or relative location on the Ansible controller. + - Image names, whether they are base ISOs or hotfix ISOs, B(must) be unique. + type: str + required: True +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Upload relative image to the BIG-IP + bigip_software_image: + image: BIGIP-13.0.0.0.0.1645.iso + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Upload absolute image to the BIG-IP + bigip_software_image: + image: /path/to/images/BIGIP-13.0.0.0.0.1645.iso + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Upload image in a role to the BIG-IP + bigip_software_image: + image: "{{ role_path }}/files/BIGIP-13.0.0.0.0.1645.iso" + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +image_type: + description: Whether the image is a release or hotfix image. + returned: changed + type: str + sample: release +version: + description: Version of the software contained in the image. + returned: changed + type: str + sample: 13.1.0.8 +build: + description: Build version of the software contained in the image. + returned: changed + type: str + sample: 0.0.3 +checksum: + description: MD5 checksum of the ISO. + returned: changed + type: str + sample: 8cdbd094195fab4b2b47ff4285577b70 +file_size: + description: Size of the uploaded image in MB. + returned: changed + type: int + sample: 1948 +''' + +import os +import time +from datetime import datetime + +from ansible.module_utils.urls import urlparse +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import ( + upload_file, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'fileSize': 'file_size' + } + + api_attributes = [ + + ] + + returnables = [ + 'image_type', + 'version', + 'build', + 'checksum', + 'file_size', + ] + + updatables = [ + + ] + + +class ApiParameters(Parameters): + @property + def file_size(self): + if self._values['file_size'] is None: + return None + tmp = self._values['file_size'].split(' ') + return int(tmp[0]) + + +class ModuleParameters(Parameters): + @property + def filename(self): + return os.path.basename(self.image) + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + self.image_type = None + self.image_url = None + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + if self.image_exists() or self.hotfix_exists(): + return True + return False + + def _set_image_url(self, item): + path = urlparse(item['selfLink']).path + self.image_url = "https://{0}:{1}{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + path + ) + + def image_exists(self): + result = False + uri = "https://{0}:{1}/mgmt/tm/sys/software/image/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if 'items' in response: + for item in response['items']: + if item['name'].startswith(self.want.filename): + self._set_image_url(item) + self.image_type = 'release' + result = True + break + return result + + def hotfix_exists(self): + result = False + uri = "https://{0}:{1}/mgmt/tm/sys/software/hotfix/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if 'items' in response: + for item in response['items']: + if item['name'].startswith(self.want.filename): + self._set_image_url(item) + self.image_type = 'hotfix' + result = True + break + return result + + def update(self): + if self.module.check_mode: + return True + if self.want.force: + # The process of updating is a forced re-creation. + self.remove_from_device() + self.create_on_device() + return True + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + + # Deleting images involves a short period of inconsistency in the REST + # API due to needing to remove files from disk and update MCPD. + # + # This should not (realistically) take more than 30 seconds. + for x in range(0, 30): + if not self.exists(): + return True + time.sleep(1) + raise F5ModuleError("Failed to delete the resource.") + + def _set_mode_and_ownership(self): + url = 'https://{0}:{1}/mgmt/tm/util/bash'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + ownership = 'root:root' + image_path = f'/shared/images/{self.want.filename}' + file_mode = '0644' + args = dict( + command='run', + utilCmdArgs='-c "chown {0} {1};chmod {2} {1}"'.format(ownership, image_path, file_mode) + ) + + self.client.api.post(url, json=args) + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + + self.create_on_device() + + # Creating images involves a short period of inconsistency in the REST + # API likely due to having to move files into appropriate places on disk + # and update MCPD with information. + # + # This should not (realistically) take more than 30 seconds. + for x in range(0, 30): + if self.exists(): + # We want to return some information about the image that was just uploaded + # + # This must appear after the creation process because the information + # does not exist on the device (has been parsed by BIG-IP) until the + # ISO is uploaded. + self._set_mode_and_ownership() + self.want = self.read_current_from_device() + self._set_changed_options() + return True + time.sleep(1) + raise F5ModuleError("Failed to create the resource.") + + def create_on_device(self): + url = 'https://{0}:{1}/mgmt/cm/autodeploy/software-image-uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, self.want.image) + except F5ModuleError: + raise + + def read_current_from_device(self): + resp = self.client.api.get(self.image_url) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = ApiParameters(params=response) + result.update({'image_type': self.image_type}) + return result + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + if self.image_exists(): + self.remove_iso_from_device('image') + elif self.hotfix_exists(): + self.remove_iso_from_device('hotfix') + + def remove_iso_from_device(self, type): + uri = "https://{0}:{1}/mgmt/tm/sys/software/{2}/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + type, + self.want.filename + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + if 'code' in response and response['code'] in [400, 404]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + force=dict(type='bool', default='no'), + image=dict(required=True), + state=dict( + default='present', + choices=['present', 'absent'] + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_install.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_install.py new file mode 100644 index 00000000..3bec376b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_install.py @@ -0,0 +1,715 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_software_install +short_description: Install software images on a BIG-IP +description: + - Install new software images on a BIG-IP system. +version_added: "1.0.0" +options: + image: + description: + - Image to install on the remote device. + type: str + block_device_image: + description: + - Image to install on the remote device. In the case of a VCMP guest, + ensure this image is present on the VCMP host and is + referenced from there, and not from the VCMP guest. An ISO image + directly uploaded to the VCMP guest will not work. + type: str + version_added: "1.2.0" + volume: + description: + - The volume on which to install the software image. + type: str + state: + description: + - When C(installed), ensures the software is installed on the volume + and the volume is set to be booted from. The device is B(not) rebooted + into the new software. + - When C(activated), performs the same operation as C(installed), but + the system is rebooted to the new software. + type: str + choices: + - activated + - installed + default: activated + type: + description: + - The type of the BIG-IP. + - Defaults to C(standard), the other choice is C(vcmp). + type: str + default: standard + choices: + - standard + - vcmp + version_added: "1.2.0" +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) + - Nitin Khanna (@nitinthewiz) +''' +EXAMPLES = r''' +- name: Ensure an existing image is installed in specified volume + bigip_software_install: + image: BIGIP-13.0.0.0.0.1645.iso + volume: HD1.2 + state: installed + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Ensure an existing image is activated in specified volume + bigip_software_install: + image: BIGIP-13.0.0.0.0.1645.iso + state: activated + volume: HD1.2 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Ensure an existing image is activated in specified volume in a VCMP guest + bigip_software_install: + block_device_image: BIGIP-13.0.0.0.0.1645.iso + type: vcmp + state: activated + volume: HD1.2 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import time +import ssl +from datetime import datetime + +from ansible.module_utils.six.moves.urllib.error import URLError +from ansible.module_utils.urls import urlparse +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + 'options', + 'volume', + ] + + returnables = [ + + ] + + updatables = [ + + ] + + +class ApiParameters(Parameters): + @property + def image_names(self): + result = [] + result += self.read_image_from_device('image') + result += self.read_image_from_device('hotfix') + return result + + def read_image_from_device(self, t): + uri = "https://{0}:{1}/mgmt/tm/sys/software/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + t, + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return [] + + if 'code' in response and response['code'] == 400: + if 'message' in response: + return [] + else: + return [] + if 'items' not in response: + return [] + return [x['name'].split('/')[0] for x in response['items']] + + @property + def block_device_image_names(self): + result = [] + result += self.read_block_device_image_from_device() + result += self.read_block_device_hotfix_from_device() + return result + + def read_block_device_image_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/software/block-device-image/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return [] + + if 'code' in response and response['code'] == 400: + if 'message' in response: + return [] + else: + return [] + if 'items' not in response: + return [] + return [x['name'] for x in response['items']] + + def read_block_device_hotfix_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/software/block-device-hotfix/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return [] + + if 'code' in response and response['code'] == 400: + if 'message' in response: + return [] + else: + return [] + if 'items' not in response: + return [] + return [x['name'] for x in response['items']] + + +class ModuleParameters(Parameters): + @property + def version(self): + if self._values['version']: + return self._values['version'] + + if self._values['type'] == "standard": + self._values['version'] = self.image_info['version'] + elif self._values['type'] == "vcmp": + self._values['version'] = self.block_device_image_info['version'] + return self._values['version'] + + @property + def build(self): + # Return cached copy if we have it + if self._values['build']: + return self._values['build'] + + # Otherwise, get copy from image info cache + # self._values['build'] = self.image_info['build'] + + if self._values['type'] == "standard": + self._values['build'] = self.image_info['build'] + elif self._values['type'] == "vcmp": + self._values['build'] = self.block_device_image_info['build'] + return self._values['build'] + + @property + def image_info(self): + if self._values['image_info']: + image = self._values['image_info'] + else: + # Otherwise, get a new copy and store in cache + image = self.read_image() + self._values['image_info'] = image + return image + + @property + def block_device_image_info(self): + if self._values['block_device_image_info']: + block_device_image = self._values['block_device_image_info'] + else: + # Otherwise, get a new copy and store in cache + block_device_image = self.read_block_device_image() + self._values['block_device_image_info'] = block_device_image + return block_device_image + + @property + def image_type(self): + if self._values['image_type']: + return self._values['image_type'] + if 'software:image' in self.image_info['kind']: + self._values['image_type'] = 'image' + else: + self._values['image_type'] = 'hotfix' + return self._values['image_type'] + + @property + def block_device_image_type(self): + if self._values['block_device_image_type']: + return self._values['block_device_image_type'] + if 'software:block-device-image' in self.block_device_image_info['kind']: + self._values['block_device_image_type'] = 'block-device-image' + else: + self._values['block_device_image_type'] = 'block-device-hotfix' + return self._values['block_device_image_type'] + + def read_image(self): + image = self.read_image_from_device(type='image') + if image: + return image + image = self.read_image_from_device(type='hotfix') + if image: + return image + return None + + def read_image_from_device(self, type): + uri = "https://{0}:{1}/mgmt/tm/sys/software/{2}/".format( + self.client.provider['server'], + self.client.provider['server_port'], + type, + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'items' in response: + for item in response['items']: + if item['name'].startswith(self.image): + return item + + def read_block_device_image(self): + block_device_image = self.read_block_device_image_from_device() + if block_device_image: + return block_device_image + block_device_image = self.read_block_device_hotfix_from_device() + if block_device_image: + return block_device_image + return None + + def read_block_device_image_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/software/block-device-image/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'items' in response: + for item in response['items']: + if item['name'].startswith(self.block_device_image): + return item + + def read_block_device_hotfix_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/software/block-device-hotfix/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'items' in response: + for item in response['items']: + if item['name'].startswith(self.block_device_image): + return item + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params, client=self.client) + self.have = ApiParameters(client=self.client) + self.changes = UsableChanges() + self.volume_url = None + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + else: + return self.update() + + def _set_volume_url(self, item): + path = urlparse(item['selfLink']).path + self.volume_url = "https://{0}:{1}{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + path + ) + + def volume_exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/software/volume/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.get(uri) + + try: + collection = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in collection and collection['code'] in errors: + if 'message' in collection: + raise F5ModuleError(collection['message']) + else: + raise F5ModuleError(resp.content) + + for item in collection['items']: + if item['name'].startswith(self.want.volume): + self._set_volume_url(item) + break + + if not self.volume_url: + self.volume_url = uri + self.want.volume + + resp = self.client.api.get(self.volume_url) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + + return True + + def software_installation_exists(self): + if self.volume_url is None: + return False + + resp = self.client.api.get(self.volume_url) + + try: + resp_json = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp_json.get('status') and resp_json.get('status') == 404: + return False + + same_version = self.want.version == resp_json.get('version', None) + same_build = self.want.build == resp_json.get('build', None) + if (not same_build) or (not same_version): + return False + + return True + + def remove_volume(self): + vol_url = self.volume_url.split('~')[0] + delresp = self.client.api.delete(vol_url) + if delresp.status == 200: + time.sleep(60) + return True + + def exists(self): + if not self.volume_exists(): + self.want.update({'options': [{'create-volume': True}]}) + return False + if not self.software_installation_exists(): + return False + return True + + def update(self): + if self.module.check_mode: + return True + + if self.want.type == "standard": + if self.want.image and self.want.image not in self.have.image_names: + raise F5ModuleError( + "The specified image was not found on the device." + ) + elif self.want.type == "vcmp": + if self.want.block_device_image and not any( + have_block_device_image.startswith(self.want.block_device_image) + for have_block_device_image in self.have.block_device_image_names): + raise F5ModuleError( + "The specified block_device_image was not found on the device." + ) + + options = self.want.options if bool(self.want.options) else list() + if self.want.state == 'activated': + options.append({'reboot': True}) + self.want.update({'options': options}) + + self.update_on_device() + self.wait_for_software_install_on_device() + if self.want.state == 'activated': + self.wait_for_device_reboot() + return True + + def update_on_device(self): + if self.want.type == "standard": + params = { + "command": "install", + "name": self.want.image, + } + params.update(self.want.api_params()) + uri = "https://{0}:{1}/mgmt/tm/sys/software/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.image_type + ) + elif self.want.type == "vcmp": + params = { + "command": "install", + "name": transform_name(name=self.want.block_device_image), + } + params.update(self.want.api_params()) + uri = "https://{0}:{1}/mgmt/tm/sys/software/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=self.want.block_device_image_type) + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + if 'commandResult' in response and len(response['commandResult'].strip()) > 0: + raise F5ModuleError(response['commandResult']) + 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) + return True + + def wait_for_device_reboot(self): + while True: + time.sleep(5) + try: + self.client.reconnect() + volume = self.read_volume_from_device() + if volume is None: + continue + if 'active' in volume and volume['active'] is True: + break + except F5ModuleError: + # Handle all exceptions because if the system is offline (for a + # reboot) the REST client will raise exceptions about + # connections + pass + + def wait_for_software_install_on_device(self): + # We need to delay this slightly in case the the volume needs to be + # created first + for dummy in range(10): + try: + if self.volume_exists(): + break + except F5ModuleError: + pass + time.sleep(5) + while True: + time.sleep(10) + volume = self.read_volume_from_device() + if volume is None or 'status' not in volume: + self.client.reconnect() + continue + if volume['status'] == 'complete': + break + elif volume['status'] == 'failed': + raise F5ModuleError + + def read_volume_from_device(self): + try: + resp = self.client.api.get(self.volume_url) + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + except ssl.SSLError: + # Suggests BIG-IP is still in the middle of restarting itself or + # restjavad is restarting. + return None + except URLError: + # At times during reboot BIG-IP will reset or timeout connections so we catch and pass this here. + return None + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return response + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + image=dict(), + block_device_image=dict(), + volume=dict(), + state=dict( + default='activated', + choices=['activated', 'installed'] + ), + type=dict( + choices=['standard', 'vcmp'], + default='standard' + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_update.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_update.py new file mode 100644 index 00000000..bb314220 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_software_update.py @@ -0,0 +1,330 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_software_update +short_description: Manage the software update settings of a BIG-IP +description: + - Manage the software update settings of a BIG-IP. +version_added: "1.0.0" +options: + auto_check: + description: + - Specifies whether to automatically check for updates on the F5 + Networks downloads server. + type: bool + auto_phone_home: + description: + - Specifies whether to automatically send phone home data to the + F5 Networks PhoneHome server. + type: bool + frequency: + description: + - Specifies the schedule for the automatic update check. + type: str + choices: + - daily + - monthly + - weekly +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Enable automatic update checking + bigip_software_update: + auto_check: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Disable automatic update checking and phoning home + bigip_software_update: + auto_check: no + auto_phone_home: no + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +auto_check: + description: Whether the system automatically checks for updates. + returned: changed + type: bool + sample: True +auto_phone_home: + description: Whether the system automatically sends phone home data. + returned: changed + type: bool + sample: True +frequency: + description: Frequency of auto update checks. + returned: changed + type: str + sample: weekly +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'autoCheck': 'auto_check', + 'autoPhonehome': 'auto_phone_home' + } + + api_attributes = [ + 'autoCheck', 'autoPhonehome', 'frequency', + ] + + updatables = [ + 'auto_check', 'auto_phone_home', 'frequency', + ] + + returnables = [ + 'auto_check', 'auto_phone_home', 'frequency', + ] + + +class ApiParameters(Parameters): + @property + def auto_check(self): + if self._values['auto_check'] is None: + return None + return self._values['auto_check'] + + +class ModuleParameters(Parameters): + @property + def auto_check(self): + if self._values['auto_check'] is None: + return None + elif self._values['auto_check'] is True: + return 'enabled' + else: + return 'disabled' + + @property + def auto_phone_home(self): + if self._values['auto_phone_home'] is None: + return None + elif self._values['auto_phone_home'] is True: + return 'enabled' + else: + return 'disabled' + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def auto_check(self): + if self._values['auto_check'] == 'enabled': + return True + elif self._values['auto_check'] == 'disabled': + return False + + @property + def auto_phone_home(self): + if self._values['auto_phone_home'] == 'enabled': + return True + elif self._values['auto_phone_home'] == 'disabled': + return False + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.update() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/software/update/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + return ApiParameters(params=response) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/software/update/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + auto_check=dict( + type='bool' + ), + auto_phone_home=dict( + type='bool' + ), + frequency=dict( + choices=['daily', 'monthly', 'weekly'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_certificate.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_certificate.py new file mode 100644 index 00000000..76648c96 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_certificate.py @@ -0,0 +1,592 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_ssl_certificate +short_description: Import/Delete certificates from BIG-IP +description: + - This module imports/deletes SSL certificates on BIG-IP LTM. + Certificates can be imported from certificate and key files on the local + disk, in PEM format. +version_added: "1.0.0" +options: + content: + description: + - Sets the contents of a certificate directly to the specified value. + This is used with lookup plugins or for anything with formatting, or + - C(content) must be provided when C(state) is C(present). + type: str + aliases: ['cert_content'] + state: + description: + - Certificate state. This determines if the provided certificate + and key is to be made C(present) on the device or C(absent). + type: str + choices: + - present + - absent + default: present + name: + description: + - SSL Certificate Name. This is the cert name used when importing a certificate + into the BIG-IP. It also determines the filenames of the objects on the LTM. + type: str + required: True + issuer_cert: + description: + - Issuer certificate used for OCSP monitoring. + - This parameter is only valid on versions of BIG-IP 13.0.0 or above. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +notes: + - This module does not behave like other modules that you might include in + roles, where referencing files or templates first looks in the role's + files or templates directory. To have it behave that way, use the Ansible + file or template lookup (see Examples). The lookups behave as expected in + a role context. +extends_documentation_fragment: f5networks.f5_modules.f5 +requirements: + - BIG-IP >= v12 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Use a file lookup to import PEM Certificate + bigip_ssl_certificate: + name: certificate-name + state: present + content: "{{ lookup('file', '/path/to/cert.crt') }}" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Use a file lookup to import CA certificate chain + bigip_ssl_certificate: + name: ca-chain-name + state: present + content: "{{ lookup('file', '/path/to/ca-chain.crt') }}" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Delete Certificate + bigip_ssl_certificate: + name: certificate-name + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +cert_name: + description: The name of the certificate. + returned: created + type: str + sample: cert1 +filename: + description: + - The name of the SSL certificate. + returned: created + type: str + sample: cert1.crt +checksum: + description: SHA1 checksum of the cert. + returned: changed and created + type: str + sample: f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0 +source_path: + description: Path on BIG-IP where the source of the certificate is stored. + returned: created + type: str + sample: /var/config/rest/downloads/cert1.crt +''' + +import hashlib +import os +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import ( + upload_file, tmos_version +) +from ..module_utils.teem import send_teem + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + + +class Parameters(AnsibleF5Parameters): + download_path = '/var/config/rest/downloads' + + api_map = { + 'sourcePath': 'source_path', + 'issuerCert': 'issuer_cert', + } + + updatables = [ + 'content', + 'issuer_cert', + 'source_path', + ] + + returnables = [ + 'filename', + 'checksum', + 'source_path', + 'issuer_cert', + ] + + api_attributes = [ + 'issuerCert', + 'sourcePath', + ] + + +class ApiParameters(Parameters): + @property + def checksum(self): + if self._values['checksum'] is None: + return None + pattern = r'SHA1:\d+:(?P[\w+]{40})' + matches = re.match(pattern, self._values['checksum']) + if matches: + return matches.group('value') + else: + return None + + @property + def filename(self): + return self._values['name'] + + +class ModuleParameters(Parameters): + def _get_hash(self, content): + k = hashlib.sha1() + s = StringIO(content) + while True: + data = s.read(1024) + if not data: + break + k.update(data.encode('utf-8')) + return k.hexdigest() + + @property + def issuer_cert(self): + if self._values['issuer_cert'] is None: + return None + name = fq_name(self.partition, self._values['issuer_cert']) + if name.endswith('.crt'): + return name + else: + return name + '.crt' + + @property + def checksum(self): + if self.content is None: + return None + return self._get_hash(self.content) + + @property + def filename(self): + if self.name.endswith('.crt'): + return self.name + else: + return self.name + '.crt' + + @property + def source_path(self): + result = 'file://' + os.path.join( + self.download_path, + self.filename + ) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ReportableChanges(Changes): + pass + + +class UsableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def source_path(self): + if self.want.source_path is None: + return None + if self.want.source_path == self.have.source_path: + if self.content: + return self.want.source_path + if self.want.source_path != self.have.source_path: + return self.want.source_path + + @property + def content(self): + if self.want.checksum != self.have.checksum: + result = dict( + checksum=self.want.checksum, + content=self.want.content + ) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + self.remove_uploaded_file_from_device(self.want.filename) + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + self.remove_uploaded_file_from_device(self.want.filename) + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + return True + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def remove_uploaded_file_from_device(self, name): + filepath = '/var/config/rest/downloads/{0}'.format(name) + params = { + "command": "run", + "utilCmdArgs": filepath + } + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.filename) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def update_on_device(self): + content = StringIO(self.want.content) + self.upload_file_to_device(content, self.want.filename) + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.filename) + ) + resp = self.client.api.put(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + + raise F5ModuleError(resp.content) + + def create_on_device(self): + content = StringIO(self.want.content) + self.upload_file_to_device(content, self.want.filename) + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + params = dict( + sourcePath=self.want.source_path, + name=self.want.filename, + partition=self.want.partition + ) + + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + # This needs to be done because of the way that BIG-IP creates certificates. + # + # The extra params (such as OCSP and issuer stuff) are not available in the + # payload. In a nutshell, the available resource attributes *change* after + # a create so that *more* are available. + params = self.want.api_params() + if params: + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.filename) + ) + resp = self.client.api.put(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.filename) + ) + + query = '?expandSubcollections=true' + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.filename) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True + ), + content=dict(aliases=['cert_content']), + state=dict( + default='present', + choices=['absent', 'present'] + ), + issuer_cert=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_csr.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_csr.py new file mode 100644 index 00000000..92eb93ca --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_csr.py @@ -0,0 +1,477 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_ssl_csr +short_description: Create SSL CSR files on the BIG-IP +description: + - This module will create SSL CSR files on a BIG-IP. CSRs + require an associated SSL key to pre-exist on the BIG-IP. +version_added: "1.3.0" +options: + name: + description: + - The name of the CSR file. + type: str + required: True + common_name: + description: + - The certificate common name. + type: str + key_name: + description: + - The SSL key to be used to generate the CSR. + type: str + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource does not exist. + type: str + choices: + - present + - absent + default: present + dest: + description: + - Destination on your local filesystem when you want to save the CSR file. + type: path + required: True + force: + description: + - If C(no), the file will only be transferred if the destination does not + exist. + type: bool + default: yes +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Nitin Khanna (@nitinthewiz) +''' + +EXAMPLES = r''' +- name: Create an SSL csr + bigip_ssl_csr: + name: csr-name + key_name: key-name + common_name: csr-name + dest: /tmp/csr-name + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +csr_name: + description: The name of the CSR file. + returned: created + type: str + sample: csr-name +common_name: + description: The common name of the CSR file. + returned: created + type: str + sample: csr-name +''' + +import os +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import ( + download_file, tmos_version +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'commonName': 'common_name', + 'key': 'key_name' + } + + api_attributes = [ + 'commonName', + 'key' + ] + + returnables = [ + 'csr_name', + 'common_name' + ] + + updatables = [ + + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + self.remote_dir = '/var/config/rest/bulk' + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if self.version_is_less_than_14(version): + raise F5ModuleError( + "This module requires TMOS version 14.x and above." + ) + + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def version_is_less_than_14(self, version): + if Version(version) < Version('14.0.0'): + return True + else: + return False + + def present(self): + if os.path.exists(self.want.dest) and not self.want.force: + raise F5ModuleError( + "The specified 'dest' file already exists." + ) + if not os.path.exists(os.path.dirname(self.want.dest)): + raise F5ModuleError( + "The directory of your 'dest' file does not exist." + ) + if self.exists(): + return False + else: + return self.execute() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def execute(self): + response = self.create() + if not response: + raise F5ModuleError( + "Failed to create csr on device." + ) + + result = self._move_csr_to_download() + if not result: + raise F5ModuleError( + "Failed to move the csr file to a downloadable location" + ) + + self._download_file() + if not os.path.exists(self.want.dest): + raise F5ModuleError( + "Failed to save the csr file to local disk" + ) + + self._delete_csr() + result = self.file_exists() + if result: + raise F5ModuleError( + "Failed to remove the remote csr file" + ) + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/crypto/csr/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + params['key'] = self.want.key_name + uri = "https://{0}:{1}/mgmt/tm/sys/crypto/csr/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/crypto/csr/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + response = self.client.api.delete(uri) + + if response.status in [200, 201]: + return True + raise F5ModuleError(response.content) + + def file_exists(self): + tpath_name = '{0}/{1}'.format(self.remote_dir, self.want.name) + params = dict( + command='run', + utilCmdArgs=tpath_name + ) + uri = "https://{0}:{1}/mgmt/tm/util/unix-ls".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + + try: + if "No such file or directory" in response['commandResult']: + return False + if self.want.name in response['commandResult']: + return True + except KeyError: + return False + + def _download_file(self): + uri = "https://{0}:{1}/mgmt/shared/file-transfer/bulk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + download_file(self.client, uri, self.want.dest) + if os.path.exists(self.want.dest): + return True + return False + + def _delete_csr(self): + tpath_name = '{0}/{1}'.format(self.remote_dir, self.want.name) + params = dict( + command='run', + utilCmdArgs=tpath_name + ) + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + + def _move_csr_to_download(self): + uri = "https://{0}:{1}/mgmt/tm/util/unix-mv/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + args = dict( + command='run', + utilCmdArgs='/config/ssl/ssl.csr/{0} {1}/{0}'.format(self.want.name, self.remote_dir) + ) + self.client.api.post(uri, json=args) + return True + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True + ), + common_name=dict(), + key_name=dict(), + state=dict( + default='present', + choices=['present', 'absent'] + ), + dest=dict( + type='path', + required=True + ), + force=dict( + default=True, + type='bool' + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + self.required_if = [ + ['state', 'present', ['common_name', 'key_name']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_key.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_key.py new file mode 100644 index 00000000..f623d170 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_key.py @@ -0,0 +1,540 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_ssl_key +short_description: Import/Delete SSL keys from BIG-IP +description: + - This module imports/deletes SSL keys on a BIG-IP. Keys can be imported + from key files on the local disk, in PEM format. +version_added: "1.0.0" +options: + content: + description: + - Sets the contents of a key directly to the specified value. This is + used with lookup plugins or for anything with formatting or templating. + This must be provided when C(state) is C(present). + type: str + aliases: + - key_content + state: + description: + - When C(present), ensures the key is uploaded to the device. When + C(absent), ensures the key is removed from the device. If the key + is currently in use, the module is not able to remove the key. + type: str + choices: + - present + - absent + default: present + name: + description: + - The name of the key. + type: str + required: True + passphrase: + description: + - Passphrase on key. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +notes: + - This module does not behave like other modules you might include in + roles, where referencing files or templates first looks in the role's + files or templates directory. To have the module behave that way, use + the Ansible file or template lookup (see Examples). The lookups behave + as expected in a role context. +extends_documentation_fragment: f5networks.f5_modules.f5 +requirements: + - BIG-IP >= v12 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Use a file lookup to import key + bigip_ssl_key: + name: key-name + state: present + content: "{{ lookup('file', '/path/to/key.key') }}" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Delete key + bigip_ssl_key: + name: key-name + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +key_filename: + description: + - The name of the SSL certificate key. The C(key_filename) and + C(cert_filename) will be similar to each other, however the + C(key_filename) will have a C(.key) extension. + returned: created + type: str + sample: cert1.key +key_checksum: + description: SHA1 checksum of the key. + returned: changed and created + type: str + sample: cf23df2207d99a74fbe169e3eba035e633b65d94 +key_source_path: + description: Path on BIG-IP where the source of the key is stored. + returned: created + type: str + sample: /var/config/rest/downloads/cert1.key +''' + +import hashlib +import os +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.icontrol import ( + upload_file, tmos_version +) +from ..module_utils.teem import send_teem + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + + +class Parameters(AnsibleF5Parameters): + download_path = '/var/config/rest/downloads' + + api_map = { + 'sourcePath': 'key_source_path' + } + + updatables = [ + 'key_checksum', + 'key_source_path', + ] + + returnables = [ + 'key_filename', + 'key_checksum', + 'key_source_path', + ] + + api_attributes = [ + 'passphrase', + 'sourcePath', + ] + + +class ApiParameters(Parameters): + @property + def checksum(self): + if self._values['checksum'] is None: + return None + pattern = r'SHA1:\d+:(?P[\w+]{40})' + matches = re.match(pattern, self._values['checksum']) + if matches: + return matches.group('value') + else: + return None + + +class ModuleParameters(Parameters): + def _get_hash(self, content): + k = hashlib.sha1() + s = StringIO(content) + while True: + data = s.read(1024) + if not data: + break + k.update(data.encode('utf-8')) + return k.hexdigest() + + @property + def key_filename(self): + if self.name.endswith('.key'): + return self.name + else: + return self.name + '.key' + + @property + def key_checksum(self): + if self.content is None: + return None + return self._get_hash(self.content) + + @property + def key_source_path(self): + result = 'file://' + os.path.join( + self.download_path, + self.key_filename + ) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def key_checksum(self): + if self.want.key_checksum is None: + return None + if self.want.key_checksum != self.have.checksum: + return self.want.key_checksum + + @property + def key_source_path(self): + if self.want.key_source_path is None: + return None + if self.want.key_source_path == self.have.key_source_path: + if self.key_checksum: + return self.want.key_source_path + if self.want.key_source_path != self.have.key_source_path: + return self.want.key_source_path + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + self.remove_uploaded_file_from_device(self.want.key_filename) + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def create(self): + if self.want.content is None: + return False + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + self.remove_uploaded_file_from_device(self.want.key_filename) + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the key") + return True + + def remove_uploaded_file_from_device(self, name): + filepath = '/var/config/rest/downloads/{0}'.format(name) + params = { + "command": "run", + "utilCmdArgs": filepath + } + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.key_filename) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def update_on_device(self): + content = StringIO(self.want.content) + self.upload_file_to_device(content, self.want.key_filename) + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.key_filename) + ) + resp = self.client.api.put(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + content = StringIO(self.want.content) + self.upload_file_to_device(content, self.want.key_filename) + params['name'] = self.want.key_filename + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.key_filename) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.key_filename) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True + ), + content=dict( + aliases=['key_content'], + no_log=True + ), + passphrase=dict( + no_log=True + ), + state=dict( + required=False, + default='present', + choices=['absent', 'present'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_key_cert.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_key_cert.py new file mode 100644 index 00000000..311e29fd --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_key_cert.py @@ -0,0 +1,803 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2020, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_ssl_key_cert +short_description: Import/Delete SSL keys and certs from BIG-IP +description: + - This module imports/deletes SSL keys and certificates on a BIG-IP. + Keys can be imported from key files on the local disk, in PEM format. + Certificates can be imported from certificate and key files on the local + disk, in PEM format. +version_added: "1.6.0" +options: + key_content: + description: + - Sets the contents of a key directly to the specified value. This is + used with lookup plugins, or for anything with formatting or templating. + This must be provided when C(state) is C(present). + type: str + state: + description: + - When C(present), ensures the key and/or cert is uploaded to the + device. When C(absent), ensures the key and/or cert is removed + from the device. If the key and/or cert is currently in use, the module + will not be able to remove the key. + type: str + choices: + - present + - absent + default: present + key_name: + description: + - The name of the key. + type: str + passphrase: + description: + - Passphrase on key. + type: str + cert_content: + description: + - Sets the contents of a certificate directly to the specified value. + This is used with lookup plugins or for anything with formatting or + - C(content) must be provided when C(state) is C(present). + type: str + cert_name: + description: + - SSL certificate name. This is the cert name used when importing a certificate + into the BIG-IP. It also determines the filenames of the objects on the LTM. + type: str + issuer_cert: + description: + - Issuer certificate used for OCSP monitoring. + - This parameter is only valid on versions of BIG-IP 13.0.0 or above. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Nitin Khanna (@nitinthewiz) +''' + +EXAMPLES = r''' +- name: Import both key and cert + bigip_ssl_key_cert: + key_content: "{{ lookup('file', 'key.pem') }}" + key_name: cert1 + cert_content: "{{ lookup('file', 'cert.pem') }}" + cert_name: cert1 + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import hashlib +import os +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, + f5_argument_spec, fq_name, merge_two_dicts +) +from ..module_utils.icontrol import ( + TransactionContextManager, upload_file, tmos_version +) +from ..module_utils.teem import send_teem + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + + +class Parameters(AnsibleF5Parameters): + download_path = '/var/config/rest/downloads' + + api_map = { + 'sourcePath': 'source_path', + 'issuerCert': 'issuer_cert', + } + + api_attributes = [ + 'passphrase', + 'sourcePath', + 'issuerCert', + ] + + returnables = [ + 'checksum', + 'source_path', + 'issuer_cert', + ] + + updatables = [ + 'key_checksum', + 'cert_checksum', + 'content', + 'issuer_cert', + 'source_path', + ] + + +class ApiParameters(Parameters): + @property + def key_filename(self): + if self._values['name'] is None: + return None + if not self._values['name'].endswith('.key'): + return None + return self._values['name'] + + @property + def key_source_path(self): + if self.key_filename is None: + return None + if self._values['key_source_path'] is None: + return None + else: + return self._values['key_source_path'] + + @property + def cert_filename(self): + if self._values['name'] is None: + return None + if not self._values['name'].endswith('.crt'): + return None + return self._values['name'] + + @property + def cert_source_path(self): + if self.cert_filename is None: + return None + if self._values['cert_source_path'] is None: + return None + else: + return self._values['cert_source_path'] + + @property + def key_checksum(self): + if self._values['key_checksum'] is None: + return None + pattern = r'SHA1:\d+:(?P[\w+]{40})' + matches = re.match(pattern, self._values['key_checksum']) + if matches: + return matches.group('value') + + @property + def cert_checksum(self): + if self._values['cert_checksum'] is None: + return None + pattern = r'SHA1:\d+:(?P[\w+]{40})' + matches = re.match(pattern, self._values['cert_checksum']) + if matches: + return matches.group('value') + + +class ModuleParameters(Parameters): + def _get_hash(self, content): + k = hashlib.sha1() + s = StringIO(content) + while True: + data = s.read(1024) + if not data: + break + k.update(data.encode('utf-8')) + return k.hexdigest() + + @property + def issuer_cert(self): + if self._values['issuer_cert'] is None: + return None + name = fq_name(self.partition, self._values['issuer_cert']) + if name.endswith('.crt'): + return name + else: + return name + '.crt' + + @property + def key_filename(self): + if self.key_name is None: + return None + if self.key_name.endswith('.key'): + return self.key_name + else: + return self.key_name + '.key' + + @property + def cert_filename(self): + if self.cert_name is None: + return None + if self.cert_name.endswith('.crt'): + return self.cert_name + else: + return self.cert_name + '.crt' + + @property + def key_checksum(self): + if self.key_content is None: + return None + return self._get_hash(self.key_content) + + @property + def cert_checksum(self): + if self.cert_content is None: + return None + return self._get_hash(self.cert_content) + + @property + def key_source_path(self): + result = 'file://' + os.path.join( + self.download_path, + self.key_filename + ) + return result + + @property + def cert_source_path(self): + result = 'file://' + os.path.join( + self.download_path, + self.cert_filename + ) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def key_checksum(self): + if self.want.key_checksum is None: + return None + if self.want.key_checksum != self.have.key_checksum: + return self.want.key_checksum + + @property + def key_source_path(self): + if self.want.key_source_path is None: + return None + if self.want.key_source_path == self.have.key_source_path: + if self.key_checksum: + return self.want.key_source_path + if self.want.key_source_path != self.have.key_source_path: + return self.want.key_source_path + + @property + def cert_source_path(self): + if self.want.source_path is None: + return None + if self.want.source_path == self.have.source_path: + if self.cert_content: + return self.want.source_path + if self.want.source_path != self.have.source_path: + return self.want.source_path + + @property + def cert_content(self): + if self.want.cert_checksum != self.have.checksum: + result = dict( + checksum=self.want.cert_checksum, + content=self.want.cert_content + ) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + if self.want.key_filename: + self.remove_uploaded_file_from_device(self.want.key_filename) + if self.want.cert_filename: + self.remove_uploaded_file_from_device(self.want.cert_filename) + return True + + def remove_uploaded_file_from_device(self, name): + filepath = '/var/config/rest/downloads/{0}'.format(name) + params = { + "command": "run", + "utilCmdArgs": filepath + } + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def exists(self): + # Can't use TransactionContextManager here because + # it expects end result code to be 200 or so. 404 causes + # TransactionContextManager to fail. + if self.want.key_name: + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.key_filename) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if self.want.cert_name: + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.cert_filename) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + return True + + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def _prepare_links(self): + # this is to ensure no duplicates are in the provided collection + links = list() + + if self.want.key_name: + key_link = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.key_filename) + ) + links.append(key_link) + + if self.want.cert_name: + cert_link = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.cert_filename) + ) + links.append(cert_link) + + return links + + def _prepare_links_for_update(self, params_dict): + # this is to ensure no duplicates are in the provided collection + links_and_params = list() + + if self.want.key_name: + key_link = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.key_filename) + ) + key_params_dict = params_dict.copy() + key_params_dict['sourcePath'] = self.want.key_source_path + links_and_params.append({'link': key_link, 'params': key_params_dict}) + + if self.want.cert_name: + cert_link = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.cert_filename) + ) + cert_params_dict = params_dict.copy() + cert_params_dict['sourcePath'] = self.want.cert_source_path + + links_and_params.append({'link': cert_link, 'params': cert_params_dict}) + + return links_and_params + + def _prepare_links_for_create(self, params_dict): + # this is to ensure no duplicates are in the provided collection + links_and_params = list() + + if self.want.key_name: + key_link = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + key_params_dict = params_dict.copy() + key_params_dict['name'] = self.want.key_filename + key_params_dict['sourcePath'] = self.want.key_source_path + links_and_params.append({'link': key_link, 'params': key_params_dict}) + + if self.want.cert_name: + cert_link = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + cert_params_dict = params_dict.copy() + cert_params_dict['name'] = self.want.cert_filename + cert_params_dict['sourcePath'] = self.want.cert_source_path + + links_and_params.append({'link': cert_link, 'params': cert_params_dict}) + + return links_and_params + + def create_on_device(self): + params = self.changes.api_params() + params['partition'] = self.want.partition + + # params['name'] = self.want.name + + links_and_params = self._prepare_links_for_create(params) + + if self.want.key_name: + key_content = StringIO(self.want.key_content) + self.upload_file_to_device(key_content, self.want.key_filename) + + if self.want.cert_name: + cert_content = StringIO(self.want.cert_content) + self.upload_file_to_device(cert_content, self.want.cert_filename) + + with TransactionContextManager(self.client) as transact: + for link in links_and_params: + resp = transact.api.post(link['link'], json=link['params']) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if not (resp.status in [200, 201] or 'code' in response and + response['code'] in [200, 201]): + raise F5ModuleError(resp.content) + + # This needs to be done because of the way that BIG-IP creates certificates. + # + # The extra params (such as OCSP and issuer stuff) are not available in the + # payload. In a nutshell, the available resource attributes *change* after + # a create so that *more* are available. + if self.want.cert_name: + params = self.want.api_params() + if params: + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.cert_filename) + ) + resp = self.client.api.put(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if not (resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]): + raise F5ModuleError(resp.content) + + return True + + def update_on_device(self): + params = self.changes.api_params() + + if self.want.key_name: + key_content = StringIO(self.want.key_content) + self.upload_file_to_device(key_content, self.want.key_filename) + + if self.want.cert_name: + cert_content = StringIO(self.want.cert_content) + self.upload_file_to_device(cert_content, self.want.cert_filename) + + links_and_params = self._prepare_links_for_update(params) + with TransactionContextManager(self.client) as transact: + for link in links_and_params: + resp = transact.api.patch(link['link'], json=link['params']) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if not (resp.status in [200, 201] or 'code' in response and + response['code'] in [200, 201]): + raise F5ModuleError(resp.content) + return True + + def remove_from_device(self): + links = self._prepare_links() + with TransactionContextManager(self.client) as transact: + for link in links: + resp = transact.api.delete(link) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if not (resp.status in [200, 201]): + raise F5ModuleError(resp.content) + return True + + def read_current_from_device(self): + final_response = {} + # TransactionContextManager cannot be used for reading, for + # whatever reason + + if self.want.key_name: + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-key/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.key_filename) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response['key_checksum'] = response['checksum'] + response['key_source_path'] = response['sourcePath'] + final_response = merge_two_dicts(final_response, response) + else: + raise F5ModuleError(resp.content) + + if self.want.cert_name: + uri = "https://{0}:{1}/mgmt/tm/sys/file/ssl-cert/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.cert_filename) + ) + + query = '?expandSubcollections=true' + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + response['cert_checksum'] = response['checksum'] + response['cert_source_path'] = response['sourcePath'] + final_response = merge_two_dicts(final_response, response) + else: + raise F5ModuleError(resp.content) + + return ApiParameters(params=final_response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + key_name=dict(), + key_content=dict( + no_log=True + ), + passphrase=dict( + no_log=True + ), + cert_name=dict(), + cert_content=dict( + no_log=True + ), + issuer_cert=dict(), + state=dict( + required=False, + default='present', + choices=['absent', 'present'] + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_ocsp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_ocsp.py new file mode 100644 index 00000000..1d486e0a --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ssl_ocsp.py @@ -0,0 +1,787 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_ssl_ocsp +short_description: Manage OCSP configurations on BIG-IP +description: + - Manage OCSP configurations on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the OCSP certificate validator. + type: str + required: True + cache_error_timeout: + description: + - Specifies the lifetime of an error response in the cache, in seconds. + type: int + proxy_server_pool: + description: + - Specifies the proxy server pool the BIG-IP system uses to fetch the OCSP + response. + - This involves creating a pool with proxy-servers. + - Use this option when either the OCSP responder cannot be reached on any of + BIG-IP system's interfaces, or one or more servers can proxy an HTTP request + to an external server and fetch the response. + type: str + cache_timeout: + description: + - Specifies the lifetime of the OCSP response in the cache, in seconds. + type: str + clock_skew: + description: + - Specifies the tolerable absolute difference in the clocks of the responder + and the BIG-IP system, in seconds. + type: int + connections_limit: + description: + - Specifies the maximum number of connections per second allowed for the + OCSP certificate validator. + type: int + dns_resolver: + description: + - Specifies the internal DNS resolver the BIG-IP system uses to fetch the + OCSP response. + - This involves specifying one or more DNS servers in the DNS resolver + configuration. + - Use this option when either there is a DNS server that can do the + name-resolution of the OCSP responders, or the OCSP responder can be + reached on one of BIG-IP system's interfaces. + type: str + route_domain: + description: + - Specifies the route domain for fetching an OCSP response using HTTP + forward proxy. + type: str + hash_algorithm: + description: + - Specifies a hash algorithm used to sign an OCSP request. + type: str + choices: + - sha256 + - sha1 + certificate: + description: + - Specifies a certificate used to sign an OCSP request. + type: str + key: + description: + - Specifies a key used to sign an OCSP request. + type: str + passphrase: + description: + - Specifies a passphrase used to sign an OCSP request. + type: str + status_age: + description: + - Specifies the maximum allowed lag time the BIG-IP system accepts for + the 'thisUpdate' time in the OCSP response. + type: int + strict_responder_checking: + description: + - Specifies whether the responder's certificate is checked for an OCSP + signing extension. + type: bool + connection_timeout: + description: + - Specifies the time interval the BIG-IP system waits for before + ending the connection to the OCSP responder, in seconds. + type: int + trusted_responders: + description: + - Specifies the certificates used for validating the OCSP response + when the responder's certificate has been omitted from the response. + type: str + responder_url: + description: + - Specifies the absolute URL that overrides the OCSP responder URL + obtained from the certificate's AIA extensions. This should be an + HTTP-based URL. + type: str + update_password: + description: + - C(always) allows the user to update passwords. + C(on_create) only sets the password for newly created OCSP validators. + type: str + choices: + - always + - on_create + default: always + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource does not exist. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - Requires BIG-IP >= 13.x. +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a OCSP validator + bigip_ssl_ocsp: + name: foo + proxy_server_pool: validators-pool + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +cache_error_timeout: + description: The new Response Caching Error Timeout value. + returned: changed + type: int + sample: 3600 +cache_timeout: + description: The new Response Caching Timeout value. + returned: changed + type: str + sample: indefinite +clock_skew: + description: The new Response Validation Clock Skew value. + returned: changed + type: int + sample: 300 +connections_limit: + description: The new Concurrent Connections Limit value. + returned: changed + type: int + sample: 50 +dns_resolver: + description: The new DNS Resolver value. + returned: changed + type: str + sample: /Common/resolver1 +route_domain: + description: The new Route Domain value. + returned: changed + type: str + sample: /Common/0 +hash_algorithm: + description: The new Request Signing Hash Algorithm value. + returned: changed + type: str + sample: sha256 +certificate: + description: The new Request Signing Certificate value. + returned: changed + type: str + sample: /Common/cert1 +key: + description: The new Request Signing Key value. + returned: changed + type: str + sample: /Common/key1 +proxy_server_pool: + description: The new Proxy Server Pool value. + returned: changed + type: str + sample: /Common/pool1 +responder_url: + description: The new Connection Responder URL value. + returned: changed + type: str + sample: "http://responder.site.com" +status_age: + description: The new Response Validation Status Age value. + returned: changed + type: int + sample: 0 +strict_responder_checking: + description: The new Response Validation Strict Responder Certificate Checking value. + returned: changed + type: bool + sample: yes +connection_timeout: + description: The new Connection Timeout value. + returned: changed + type: int + sample: 8 +trusted_responders: + description: The new Response Validation Trusted Responders value. + returned: changed + type: int + sample: /Common/default +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'cacheErrorTimeout': 'cache_error_timeout', + 'cacheTimeout': 'cache_timeout', + 'clockSkew': 'clock_skew', + 'concurrentConnectionsLimit': 'connections_limit', + 'dnsResolver': 'dns_resolver', + 'proxyServerPool': 'proxy_server_pool', + 'responderUrl': 'responder_url', + 'routeDomain': 'route_domain', + 'signHash': 'hash_algorithm', + 'signerCert': 'certificate', + 'signerKey': 'key', + 'signerKeyPassphrase': 'passphrase', + 'statusAge': 'status_age', + 'strictRespCertCheck': 'strict_responder_checking', + 'timeout': 'connection_timeout', + 'trustedResponders': 'trusted_responders', + } + + api_attributes = [ + 'cacheErrorTimeout', + 'cacheTimeout', + 'clockSkew', + 'concurrentConnectionsLimit', + 'dnsResolver', + 'routeDomain', + 'proxyServerPool', + 'responderUrl', + 'signHash', + 'signerCert', + 'signerKey', + 'signerKeyPassphrase', + 'statusAge', + 'strictRespCertCheck', + 'timeout', + 'trustedResponders', + ] + + returnables = [ + 'cache_error_timeout', + 'cache_timeout', + 'clock_skew', + 'connections_limit', + 'dns_resolver', + 'route_domain', + 'hash_algorithm', + 'certificate', + 'key', + 'passphrase', + 'proxy_server_pool', + 'responder_url', + 'status_age', + 'strict_responder_checking', + 'connection_timeout', + 'trusted_responders', + ] + + updatables = [ + 'cache_error_timeout', + 'cache_timeout', + 'clock_skew', + 'connections_limit', + 'dns_resolver', + 'route_domain', + 'hash_algorithm', + 'certificate', + 'key', + 'passphrase', + 'proxy_server_pool', + 'responder_url', + 'status_age', + 'strict_responder_checking', + 'connection_timeout', + 'trusted_responders', + ] + + @property + def strict_responder_checking(self): + return flatten_boolean(self._values['strict_responder_checking']) + + @property + def cache_timeout(self): + if self._values['cache_timeout'] is None: + return None + try: + return int(self._values['cache_timeout']) + except ValueError: + return self._values['cache_timeout'] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def route_domain(self): + if self._values['route_domain'] is None: + return None + result = fq_name(self.partition, self._values['route_domain']) + return result + + @property + def dns_resolver(self): + if self._values['dns_resolver'] is None: + return None + result = fq_name(self.partition, self._values['dns_resolver']) + return result + + @property + def proxy_server_pool(self): + if self._values['proxy_server_pool'] is None: + return None + result = fq_name(self.partition, self._values['proxy_server_pool']) + return result + + @property + def responder_url(self): + if self._values['responder_url'] is None: + return None + if self._values['responder_url'] in ['', 'none']: + return '' + return self._values['responder_url'] + + @property + def certificate(self): + if self._values['certificate'] is None: + return None + if self._values['certificate'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['certificate']) + return result + + @property + def key(self): + if self._values['key'] is None: + return None + if self._values['key'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['key']) + return result + + @property + def trusted_responders(self): + if self._values['trusted_responders'] is None: + return None + if self._values['trusted_responders'] in ['', 'none']: + return '' + result = fq_name(self.partition, self._values['trusted_responders']) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def strict_responder_checking(self): + if self._values['strict_responder_checking'] == 'yes': + return 'enabled' + elif self._values['strict_responder_checking'] == 'no': + return 'disabled' + + +class ReportableChanges(Changes): + @property + def strict_responder_checking(self): + result = flatten_boolean(self._values['strict_responder_checking']) + return result + + @property + def passphrase(self): + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def responder_url(self): + if self.want.responder_url is None: + return None + if self.want.responder_url == '' and self.have.responder_url is None: + return None + if self.want.responder_url != self.have.responder_url: + return self.want.responder_url + + @property + def certificate(self): + if self.want.certificate is None: + return None + if self.want.certificate == '' and self.have.certificate is None: + return None + if self.want.certificate != self.have.certificate: + return self.want.certificate + + @property + def key(self): + if self.want.key is None: + return None + if self.want.key == '' and self.have.key is None: + return None + if self.want.key != self.have.key: + return self.want.key + + @property + def trusted_responders(self): + if self.want.trusted_responders is None: + return None + if self.want.trusted_responders == '' and self.have.trusted_responders is None: + return None + if self.want.trusted_responders != self.have.trusted_responders: + return self.want.trusted_responders + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + if Version(version) < Version('13.0.0'): + raise F5ModuleError( + "BIG-IP v13 or greater is required to use this module." + ) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/crypto/cert-validator/ocsp/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + + if self.want.update_password == 'always': + self.want.update({'passphrase': self.want.passphrase}) + else: + if self.want.passphrase: + del self.want._values['passphrase'] + + if not self.should_update(): + return False + + # these two params are mutually exclusive, and so one must be zeroed + # out so that the other can be set. This zeros the non-specified values + # out so that the PATCH can happen + if self.want.dns_resolver: + self.changes.update({'proxy_server_pool': ''}) + if self.want.proxy_server_pool: + self.changes.update({'dns_resolver': ''}) + + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/crypto/cert-validator/ocsp/".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/crypto/cert-validator/ocsp/{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 absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/crypto/cert-validator/ocsp/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/crypto/cert-validator/ocsp/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + cache_error_timeout=dict(type='int'), + proxy_server_pool=dict(), + cache_timeout=dict(), + clock_skew=dict(type='int'), + connections_limit=dict(type='int'), + dns_resolver=dict(), + route_domain=dict(), + hash_algorithm=dict( + choices=['sha256', 'sha1'] + ), + certificate=dict(), + key=dict(), + passphrase=dict(no_log=True), + status_age=dict(type='int'), + strict_responder_checking=dict(type='bool'), + connection_timeout=dict(type='int'), + trusted_responders=dict(), + responder_url=dict(), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + 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 = [ + ['dns_resolver', 'proxy_server_pool'] + ] + self.required_together = [ + ['certificate', 'key'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + required_together=spec.required_together, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_static_route.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_static_route.py new file mode 100644 index 00000000..3e8b8492 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_static_route.py @@ -0,0 +1,705 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_static_route +short_description: Manipulate static routes on a BIG-IP +description: + - Manipulate static routes on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Name of the static route. + type: str + required: True + description: + description: + - Descriptive text that identifies the route. + type: str + destination: + description: + - Specifies an IP address for the static entry in the routing table. + When creating a new static route, this value is required. + - This value cannot be changed once it is set. + type: str + netmask: + description: + - The netmask for the static route. When creating a new static route, this value + is required. + - This value can be in either IP or CIDR format. + - This value cannot be changed once it is set. + type: str + gateway_address: + description: + - Specifies the router for the system to use when forwarding packets + to the destination host or network. Also known as the next-hop router + address. This can be either an IPv4 or IPv6 address. When it is an + IPv6 address that starts with C(FE80:), the address is treated + as a link-local address. This requires the C(vlan) parameter + also be supplied. + type: str + vlan: + description: + - Specifies the VLAN or Tunnel through which the system forwards packets + to the destination. When C(gateway_address) is a link-local IPv6 + address, this value is required. + type: str + pool: + description: + - Specifies the pool through which the system forwards packets to the + destination. + type: str + reject: + description: + - Specifies the system drops packets sent to the destination. + type: bool + mtu: + description: + - Specifies a specific maximum transmission unit (MTU). + type: str + route_domain: + description: + - The route domain ID of the system. When creating a new static route, if + this value is not specified, the default value is C(0). + - This value cannot be changed once it is set. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the static route exists. + - When C(absent), ensures the static does not exist. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create static route with gateway address + bigip_static_route: + destination: 10.10.10.10 + netmask: 255.255.255.255 + gateway_address: 10.2.2.3 + name: test-route + provider: + password: secret + server: lb.mydomain.come + user: admin + validate_certs: no + delegate_to: localhost +''' + +RETURN = r''' +vlan: + description: The VLAN or Tunnel through which the system forwards packets to the destination. + returned: changed + type: str + sample: /Common/vlan1 +gateway_address: + description: The router for the system to use when forwarding packets to the destination host or network. + returned: changed + type: str + sample: 10.2.2.3 +destination: + description: An IP address for the static entry in the routing table. + returned: changed + type: str + sample: 0.0.0.0/0 +route_domain: + description: The route domain ID of the system. + returned: changed + type: int + sample: 1 +netmask: + description: Netmask of the destination. + returned: changed + type: str + sample: 255.255.255.255 +pool: + description: Whether the banner is enabled or not. + returned: changed + type: str + sample: yes +partition: + description: The partition that the static route was created on. + returned: changed + type: str + sample: Common +description: + description: Descriptive text that identifies the route. + returned: changed + type: str + sample: "Route tho DMZ" +reject: + description: Specifies the system drops packets sent to the destination. + returned: changed + type: bool + sample: true +''' + +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE + +from ipaddress import ( + ip_network, ip_interface, ip_address +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.ipaddress import ( + is_valid_ip, ipv6_netmask_to_cidr +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'tmInterface': 'vlan', + 'gw': 'gateway_address', + 'network': 'destination', + 'blackhole': 'reject' + } + + updatables = [ + 'description', + 'gateway_address', + 'vlan', + 'pool', + 'mtu', + 'reject', + 'destination', + 'route_domain', + 'netmask', + ] + + returnables = [ + 'vlan', + 'gateway_address', + 'destination', + 'pool', + 'description', + 'reject', + 'mtu', + 'netmask', + 'route_domain', + ] + + api_attributes = [ + 'tmInterface', + 'gw', + 'network', + 'blackhole', + 'description', + 'pool', + 'mtu', + ] + + def to_return(self): + result = {} + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + + @property + def reject(self): + if self._values['reject'] in BOOLEANS_TRUE: + return True + + +class ModuleParameters(Parameters): + @property + def vlan(self): + if self._values['vlan'] is None: + return None + return fq_name(self.partition, self._values['vlan']) + + @property + def gateway_address(self): + if self._values['gateway_address'] is None: + return None + try: + if '%' in self._values['gateway_address']: + addr = self._values['gateway_address'].split('%')[0] + ip_interface(u'%s' % str(addr)) + else: + addr = self._values['gateway_address'] + ip_interface(u'%s' % str(addr)) + if self.route_domain: + result = str(addr).lower() + '%' + str(self.route_domain) + return result + return str(self._values['gateway_address']).lower() + except ValueError: + raise F5ModuleError( + "The provided gateway_address is not an IP address" + ) + + @property + def route_domain(self): + if self._values['route_domain'] is None: + return None + result = int(self._values['route_domain']) + return result + + @property + def destination(self): + if self._values['destination'] is None: + return None + if self._values['destination'].startswith('default'): + self._values['destination'] = '0.0.0.0/0' + if self._values['destination'].startswith('default-inet6'): + self._values['destination'] = '::/0' + try: + ip = ip_network(u'%s' % str(self.destination_ip)) + if self.route_domain: + return '{0}%{1}/{2}'.format(str(ip.network_address), self.route_domain, ip.prefixlen) + else: + return '{0}/{1}'.format(str(ip.network_address), ip.prefixlen) + except ValueError: + raise F5ModuleError( + "The provided destination is not an IP address" + ) + + @property + def destination_ip(self): + if self._values['destination']: + ip = ip_network(u'{0}/{1}'.format(self._values['destination'], self.netmask)) + return '{0}/{1}'.format(str(ip.network_address), ip.prefixlen) + + @property + def netmask(self): + if self._values['netmask'] is None: + return None + try: + result = int(self._values['netmask']) + + # CIDRs between 0 and 128 are allowed + if 0 <= result <= 128: + return result + else: + raise F5ModuleError( + "The provided netmask must be between 0 and 32 for IPv4, or " + "0 and 128 for IPv6." + ) + except ValueError: + # not a number, but that's ok. Further processing necessary + pass + + if not is_valid_ip(self._values['netmask']): + raise F5ModuleError( + 'The provided netmask {0} is neither in IP or CIDR format'.format(result) + ) + + # Create a temporary address to check if the netmask IP is v4 or v6 + addr = ip_address(u'{0}'.format(str(self._values['netmask']))) + if addr.version == 4: + # Create a more real v4 address using a wildcard, so that we can determine + # the CIDR value from it. + ip = ip_network(u'0.0.0.0/%s' % str(self._values['netmask'])) + result = ip.prefixlen + else: + result = ipv6_netmask_to_cidr(self._values['netmask']) + + return result + + +class ApiParameters(Parameters): + @property + def route_domain(self): + if self._values['destination'] is None: + return None + pattern = r'([0-9a-zA-Z\:\-\.]+%(?P[0-9]+))' + matches = re.search(pattern, self._values['destination']) + if matches: + return int(matches.group('rd')) + return 0 + + @property + def destination_ip(self): + if self._values['destination'] is None: + return None + destination = self.destination_to_network() + + try: + pattern = r'(?P%[0-9]+)' + addr = re.sub(pattern, '', destination) + ip = ip_network(u'%s' % str(addr)) + return '{0}/{1}'.format(str(ip.network_address), ip.prefixlen) + except ValueError: + raise F5ModuleError( + "The provided destination is not an IP address." + ) + + @property + def netmask(self): + destination = self.destination_to_network() + ip = ip_network(u'%s' % str(destination)) + return int(ip.prefixlen) + + def destination_to_network(self): + destination = self._values['destination'] + if destination.startswith('default%'): + destination = '0.0.0.0%{0}/0'.format(destination.split('%')[1]) + elif destination.startswith('default-inet6%'): + destination = '::%{0}/0'.format(destination.split('%')[1]) + elif destination.startswith('default-inet6'): + destination = '::/0' + elif destination.startswith('default'): + destination = '0.0.0.0/0' + return destination + + +class Changes(Parameters): + pass + + +class UsableChanges(Parameters): + pass + + +class ReportableChanges(Parameters): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def destination(self): + if self.want.destination_ip is None: + return None + if self.want.destination_ip != self.have.destination_ip: + raise F5ModuleError( + "The destination cannot be changed. Delete and recreate " + "the static route if you need to do this." + ) + + @property + def route_domain(self): + if self.want.route_domain is None: + return None + if self.want.route_domain is None and self.have.route_domain == 0: + return None + if self.want.route_domain != self.have.route_domain: + raise F5ModuleError("You cannot change the route domain.") + + @property + def netmask(self): + if self.want.netmask is None: + return None + # It's easiest to just check the netmask by comparing dest IPs. + if self.want.destination_ip != self.have.destination_ip: + raise F5ModuleError( + "The netmask cannot be changed. Delete and recreate " + "the static route if you need to do this." + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if k in ['netmask', 'route_domain']: + changed['address'] = change + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/route/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + required_resources = ['pool', 'vlan', 'reject', 'gateway_address'] + self._set_changed_options() + if self.want.destination is None: + raise F5ModuleError( + 'destination must be specified when creating a static route' + ) + if self.want.netmask is None: + raise F5ModuleError( + 'netmask must be specified when creating a static route' + ) + if all(getattr(self.want, v) is None for v in required_resources): + raise F5ModuleError( + "You must specify at least one of " + ', '.join(required_resources) + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def update_on_device(self): + params = self.want.api_params() + + # The 'network' attribute is not updatable + params.pop('network', None) + + uri = "https://{0}:{1}/mgmt/tm/net/route/{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 read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/route/{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) + return ApiParameters(params=response) + + def create_on_device(self): + params = self.want.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/net/route/".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 absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the static route") + return True + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/route/{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 + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + destination=dict(), + netmask=dict(), + gateway_address=dict(), + vlan=dict(), + pool=dict(), + mtu=dict(), + reject=dict( + type='bool' + ), + state=dict( + default='present', + choices=['absent', 'present'] + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + route_domain=dict(type='int') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.mutually_exclusive = [ + ['gateway_address', 'pool', 'reject'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_daemon_log_tmm.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_daemon_log_tmm.py new file mode 100644 index 00000000..e6a356b2 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_daemon_log_tmm.py @@ -0,0 +1,488 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_sys_daemon_log_tmm +short_description: Manage BIG-IP tmm daemon log settings +description: + - Manage BIG-IP tmm log settings. +version_added: "1.0.0" +options: + arp_log_level: + description: + - Specifies the lowest level of ARP messages from the tmm daemon + to include in the system log. + type: str + choices: + - debug + - error + - informational + - notice + - warning + http_compression_log_level: + description: + - Specifies the lowest level of HTTP compression messages from the tmm daemon + to include in the system log. + type: str + choices: + - debug + - error + - informational + - notice + - warning + http_log_level: + description: + - Specifies the lowest level of HTTP messages from the tmm daemon + to include in the system log. + type: str + choices: + - debug + - error + - informational + - notice + - warning + ip_log_level: + description: + - Specifies the lowest level of IP address messages from the tmm daemon + to include in the system log. + type: str + choices: + - debug + - informational + - notice + - warning + irule_log_level: + description: + - Specifies the lowest level of iRule messages from the tmm daemon + to include in the system log. + type: str + choices: + - debug + - error + - informational + - notice + - warning + layer4_log_level: + description: + - Specifies the lowest level of Layer 4 messages from the tmm daemon + to include in the system log. + type: str + choices: + - debug + - informational + - notice + net_log_level: + description: + - Specifies the lowest level of network messages from the tmm daemon + to include in the system log. + type: str + choices: + - critical + - debug + - error + - informational + - notice + - warning + os_log_level: + description: + - Specifies the lowest level of operating system messages from the tmm daemon + to include in the system log. + type: str + choices: + - alert + - critical + - debug + - emergency + - error + - informational + - notice + - warning + pva_log_level: + description: + - Specifies the lowest level of PVA messages from the tmm daemon + to include in the system log. + type: str + choices: + - debug + - informational + - notice + ssl_log_level: + description: + - Specifies the lowest level of SSL messages from the tmm daemon + to include in the system log. + type: str + choices: + - alert + - critical + - debug + - emergency + - error + - informational + - notice + - warning + state: + description: + - The state of the log level on the system. When C(present), guarantees + an existing log level is set to C(value). + type: str + choices: + - present + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Set SSL log level to debug + bigip_sys_daemon_log_tmm: + provider: + password: secret + server: lb.mydomain.com + user: admin + ssl_log_level: debug + delegate_to: localhost +''' + +RETURN = r''' +arp_log_level: + description: Lowest level of ARP messages from the tmm daemon to log. + returned: changed + type: str + sample: error +http_compression_log_level: + description: Lowest level of HTTP compression messages from the tmm daemon to log. + returned: changed + type: str + sample: debug +http_log_level: + description: Lowest level of HTTP messages from the tmm daemon to log. + returned: changed + type: str + sample: notice +ip_log_level: + description: Lowest level of IP address messages from the tmm daemon to log. + returned: changed + type: str + sample: warning +irule_log_level: + description: Lowest level of iRule messages from the tmm daemon to log. + returned: changed + type: str + sample: error +layer4_log_level: + description: Lowest level of Layer 4 messages from the tmm daemon to log. + returned: changed + type: str + sample: notice +net_log_level: + description: Lowest level of network messages from the tmm daemon to log. + returned: changed + type: str + sample: critical +os_log_level: + description: Lowest level of operating system messages from the tmm daemon to log. + returned: changed + type: str + sample: critical +pva_log_level: + description: Lowest level of PVA messages from the tmm daemon to log. + returned: changed + type: str + sample: debug +ssl_log_level: + description: Lowest level of SSL messages from the tmm daemon to log. + returned: changed + type: str + sample: critical +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'arpLogLevel': 'arp_log_level', + 'httpCompressionLogLevel': 'http_compression_log_level', + 'httpLogLevel': 'http_log_level', + 'ipLogLevel': 'ip_log_level', + 'iruleLogLevel': 'irule_log_level', + 'layer4LogLevel': 'layer4_log_level', + 'netLogLevel': 'net_log_level', + 'osLogLevel': 'os_log_level', + 'pvaLogLevel': 'pva_log_level', + 'sslLogLevel': 'ssl_log_level', + } + + api_attributes = [ + 'arpLogLevel', + 'httpCompressionLogLevel', + 'httpLogLevel', + 'ipLogLevel', + 'iruleLogLevel', + 'layer4LogLevel', + 'netLogLevel', + 'osLogLevel', + 'pvaLogLevel', + 'sslLogLevel', + ] + + returnables = [ + 'arp_log_level', + 'http_compression_log_level', + 'http_log_level', + 'ip_log_level', + 'irule_log_level', + 'layer4_log_level', + 'net_log_level', + 'os_log_level', + 'pva_log_level', + 'ssl_log_level', + ] + + updatables = [ + 'arp_log_level', + 'http_compression_log_level', + 'http_log_level', + 'ip_log_level', + 'irule_log_level', + 'layer4_log_level', + 'net_log_level', + 'os_log_level', + 'pva_log_level', + 'ssl_log_level', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + return self.update() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/daemon-log-settings/tmm".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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 read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/daemon-log-settings/tmm".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.choices_min = ['debug', 'informational', 'notice'] + self.choices_common = self.choices_min + ['warning', 'error'] + self.choices_all = self.choices_common + ['alert', 'critical', 'emergency'] + argument_spec = dict( + arp_log_level=dict( + choices=self.choices_common + ), + http_compression_log_level=dict( + choices=self.choices_common + ), + http_log_level=dict( + choices=self.choices_common + ), + ip_log_level=dict( + choices=self.choices_min + ['warning'] + ), + irule_log_level=dict( + choices=self.choices_common + ), + layer4_log_level=dict( + choices=self.choices_min + ), + net_log_level=dict( + choices=self.choices_common + ['critical'] + ), + os_log_level=dict( + choices=self.choices_all + ), + pva_log_level=dict( + choices=self.choices_min + ), + ssl_log_level=dict( + choices=self.choices_all + ), + state=dict(default='present', choices=['present']) + ) + + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_db.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_db.py new file mode 100644 index 00000000..ac958063 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_db.py @@ -0,0 +1,398 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2016, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_sys_db +short_description: Manage BIG-IP system database variables +description: + - Manage BIG-IP system database variables. +version_added: "1.0.0" +options: + key: + description: + - The database variable to manipulate. + type: str + required: True + state: + description: + - The state of the variable on the system. When C(present), guarantees + an existing variable is set to C(value). When C(reset), sets the + variable back to the default value. At least one of value and state + C(reset) are required. + type: str + choices: + - present + - reset + default: present + value: + description: + - The value to set the key to. At least one of value and state C(reset) + are required. + type: str +notes: + - Requires BIG-IP version 12.0.0 or later. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Set the boot.quiet DB variable on the BIG-IP + bigip_sys_db: + key: boot.quiet + value: disable + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Disable the initial setup screen + bigip_sys_db: + key: setup.run + value: false + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Reset the initial setup screen + bigip_sys_db: + key: setup.run + state: reset + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +name: + description: The key in the system database. + returned: changed and success + type: str + sample: setup.run +default_value: + description: The default value of the key. + returned: changed and success + type: str + sample: true +value: + description: The value that you set the key to. + returned: changed and success + type: str + sample: false +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'defaultValue': 'default_value' + } + api_attributes = [ + 'value', + ] + updatables = [ + 'value', + ] + returnables = [ + 'name', + 'value', + 'default_value', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + change = getattr(self, returnable) + if isinstance(change, dict): + result.update(change) + else: + result[returnable] = change + result = self._filter_params(result) + except Exception: + pass + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def value(self): + if self.want.state == 'reset': + if str(self.have.value) != str(self.have.default_value): + return self.have.default_value + if self.want.value != self.have.value: + return self.want.value + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.pop('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + changed['name'] = self.want.key + changed['default_value'] = self.have.default_value + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "reset": + changed = self.reset() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return False + else: + return self.update() + + def reset(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.reset_on_device() + self.want.update({'key': self.want.key}) + self.want.update({'value': self.have.default_value}) + if self.exists(): + return True + else: + raise F5ModuleError( + "Failed to reset the DB variable" + ) + + def update(self): + if self.want.value is None: + raise F5ModuleError( + "When setting a key, a value must be supplied" + ) + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.key + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if str(response['value']) == str(self.want.value): + return True + return False + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.key + ) + + 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) + + return ApiParameters(params=response) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.key + ) + + 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 reset_on_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.key + ) + params = dict( + value=self.have.default_value + ) + + 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) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + key=dict(required=True), + state=dict( + default='present', + choices=['present', 'reset'] + ), + value=dict() + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_global.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_global.py new file mode 100644 index 00000000..aa5b498a --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_sys_global.py @@ -0,0 +1,498 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2016, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_sys_global +short_description: Manage BIG-IP global settings +description: + - Manage BIG-IP global settings. +version_added: "1.0.0" +options: + banner_text: + description: + - Specifies the text to present in the advisory banner. + type: str + console_timeout: + description: + - Specifies the number of seconds of inactivity before the system logs + off a user that is logged on. + type: int + gui_setup: + description: + - C(yes) or C(no), the Setup utility in the browser-based + Configuration utility. + type: bool + lcd_display: + description: + - When C(yes), specifies the system menu displays on the + LCD screen on the front of the unit. This setting has no effect + when used on the VE platform. + type: bool + mgmt_dhcp: + description: + - Specifies whether or not to enable DHCP client on the management + interface. + type: bool + net_reboot: + description: + - When C(yes), specifies the next time you reboot the system, + the system boots to an ISO image on the network, rather than an + internal media drive. + type: bool + quiet_boot: + description: + - When C(yes), specifies the system suppresses informational + text on the console during the boot cycle. When C(no), the + system presents messages and informational text on the console during + the boot cycle. + type: bool + security_banner: + description: + - Specifies whether the system displays an advisory message on the + login screen. + type: bool + state: + description: + - The state of the variable on the system. When C(present), guarantees + an existing variable is set to C(value). + type: str + choices: + - present + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Disable the setup utility + bigip_sys_global: + gui_setup: no + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +banner_text: + description: The new text to present in the advisory banner. + returned: changed + type: str + sample: This is a corporate device. Do not touch. +console_timeout: + description: + - The new number of seconds of inactivity before the system + logs off a user that is logged on. + returned: changed + type: int + sample: 600 +gui_setup: + description: The new setting for the Setup utility. + returned: changed + type: bool + sample: yes +lcd_display: + description: The new setting for displaying the system menu on the LCD. + returned: changed + type: bool + sample: yes +mgmt_dhcp: + description: The new setting for whether the mgmt interface should use DHCP or not. + returned: changed + type: bool + sample: yes +net_reboot: + description: The new setting for whether the system should boot to an ISO on the network or not. + returned: changed + type: bool + sample: yes +quiet_boot: + description: + - The new setting for whether the system should suppress information to + the console during boot or not. + returned: changed + type: bool + sample: yes +security_banner: + description: + - The new setting for whether the system should display an advisory message + on the login screen or not. + returned: changed + type: bool + sample: yes +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'guiSecurityBanner': 'security_banner', + 'guiSecurityBannerText': 'banner_text', + 'guiSetup': 'gui_setup', + 'lcdDisplay': 'lcd_display', + 'mgmtDhcp': 'mgmt_dhcp', + 'netReboot': 'net_reboot', + 'quietBoot': 'quiet_boot', + 'consoleInactivityTimeout': 'console_timeout', + } + + api_attributes = [ + 'guiSecurityBanner', + 'guiSecurityBannerText', + 'guiSetup', + 'lcdDisplay', + 'mgmtDhcp', + 'netReboot', + 'quietBoot', + 'consoleInactivityTimeout', + ] + + returnables = [ + 'security_banner', + 'banner_text', + 'gui_setup', + 'lcd_display', + 'mgmt_dhcp', + 'net_reboot', + 'quiet_boot', + 'console_timeout', + ] + + updatables = [ + 'security_banner', + 'banner_text', + 'gui_setup', + 'lcd_display', + 'mgmt_dhcp', + 'net_reboot', + 'quiet_boot', + 'console_timeout', + ] + + @property + def security_banner(self): + return flatten_boolean(self._values['security_banner']) + + @property + def gui_setup(self): + return flatten_boolean(self._values['gui_setup']) + + @property + def lcd_display(self): + return flatten_boolean(self._values['lcd_display']) + + @property + def mgmt_dhcp(self): + return flatten_boolean(self._values['mgmt_dhcp']) + + @property + def net_reboot(self): + return flatten_boolean(self._values['net_reboot']) + + @property + def quiet_boot(self): + return flatten_boolean(self._values['quiet_boot']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def security_banner(self): + if self._values['security_banner'] is None: + return None + if self._values['security_banner'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def gui_setup(self): + if self._values['gui_setup'] is None: + return None + if self._values['gui_setup'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def lcd_display(self): + if self._values['lcd_display'] is None: + return None + if self._values['lcd_display'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def mgmt_dhcp(self): + if self._values['mgmt_dhcp'] is None: + return None + if self._values['mgmt_dhcp'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def net_reboot(self): + if self._values['net_reboot'] is None: + return None + if self._values['net_reboot'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def quiet_boot(self): + if self._values['quiet_boot'] is None: + return None + if self._values['quiet_boot'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def security_banner(self): + return flatten_boolean(self._values['security_banner']) + + @property + def gui_setup(self): + return flatten_boolean(self._values['gui_setup']) + + @property + def lcd_display(self): + return flatten_boolean(self._values['lcd_display']) + + @property + def mgmt_dhcp(self): + return flatten_boolean(self._values['mgmt_dhcp']) + + @property + def net_reboot(self): + return flatten_boolean(self._values['net_reboot']) + + @property + def quiet_boot(self): + return flatten_boolean(self._values['quiet_boot']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + want = getattr(self.want, param) + try: + have = getattr(self.have, param) + if want != have: + return want + except AttributeError: + return want + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + return self.update() + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/global-settings/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + return ApiParameters(params=response) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/sys/global-settings/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.states = ['present'] + argument_spec = dict( + security_banner=dict( + type='bool' + ), + banner_text=dict(), + gui_setup=dict( + type='bool' + ), + lcd_display=dict( + type='bool' + ), + mgmt_dhcp=dict( + type='bool' + ), + net_reboot=dict( + type='bool' + ), + quiet_boot=dict( + type='bool' + ), + console_timeout=dict( + type='int' + ), + state=dict( + default='present', choices=['present'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_timer_policy.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_timer_policy.py new file mode 100644 index 00000000..b61d90b1 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_timer_policy.py @@ -0,0 +1,643 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_timer_policy +short_description: Manage timer policies on a BIG-IP +description: + - Manage timer policies on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the timer policy. + type: str + required: True + description: + description: + - Specifies descriptive text that identifies the timer policy. + type: str + rules: + description: + - Rules you want assigned to the timer policy. + type: list + elements: dict + suboptions: + name: + description: + - The name of the rule. + type: str + required: True + protocol: + description: + - Specifies the IP protocol entry for which the timer policy rule is being + configured. This could be a layer-4 protocol (such as C(tcp), C(udp) or + C(sctp). + - Only flows matching the configured protocol will make use of this rule. + - When C(all-other) is specified, if there are no specific ip-protocol rules + that match the flow, the flow matches all the other ip-protocol rules. + - When specifying rules, if this parameter is not specified, the default is + C(all-other). + type: str + default: all-other + choices: + - all-other + - ah + - bna + - esp + - etherip + - gre + - icmp + - ipencap + - ipv6 + - ipv6-auth + - ipv6-crypt + - ipv6-icmp + - isp-ip + - mux + - ospf + - sctp + - tcp + - udp + - udplite + destination_ports: + description: + - The list of destination ports on which to match the rule. + - Specify a port range by specifying start and end ports separated by a + dash (-). + - This field is only available if you have selected the C(sctp), C(tcp), or + C(udp) protocol. + type: list + elements: str + idle_timeout: + description: + - Specifies an idle timeout, in seconds, for protocol and port pairs that + match the timer policy rule. + - When C(infinite), specifies the protocol and port pairs that match + the timer policy rule have no idle timeout. + - When specifying rules, if this parameter is not specified, the default is + C(unspecified). + type: str + default: unspecified + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a timer policy + bigip_timer_policy: + name: timer1 + description: My timer policy + rules: + - name: rule1 + protocol: tcp + idle_timeout: indefinite + destination_ports: + - 443 + - 80 + - name: rule2 + protocol: 200 + - name: rule3 + protocol: sctp + idle_timeout: 200 + destination_ports: + - 21 + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove a timer policy and all its associated rules + bigip_timer_policy: + name: timer1 + description: My timer policy + state: absent + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the timer policy. + returned: changed + type: str + sample: true +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec +) +from ..module_utils.compare import compare_complex_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + 'description', + 'rules', + ] + + returnables = [ + 'description', + 'rules', + ] + + updatables = [ + 'description', + 'rules', + ] + + +class ApiParameters(Parameters): + @property + def rules(self): + if self._values['rules'] is None: + return None + results = [] + for rule in self._values['rules']: + result = dict() + result['name'] = rule['name'] + if 'ipProtocol' in rule: + result['protocol'] = str(rule['ipProtocol']) + if 'timers' in rule: + result['idle_timeout'] = str(rule['timers'][0]['value']) + if 'destinationPorts' in rule: + ports = list(set([str(x['name']) for x in rule['destinationPorts']])) + ports.sort() + result['destination_ports'] = ports + results.append(result) + results = sorted(results, key=lambda k: k['name']) + return results + + +class ModuleParameters(Parameters): + @property + def rules(self): + if self._values['rules'] is None: + return None + if len(self._values['rules']) == 1 and self._values['rules'][0] == '': + return '' + results = [] + for rule in self._values['rules']: + result = dict() + result['name'] = rule['name'] + if 'protocol' in rule and rule['protocol']: + result['protocol'] = str(rule['protocol']) + else: + result['protocol'] = 'all-other' + + if 'idle_timeout' in rule and rule['idle_timeout']: + result['idle_timeout'] = str(rule['idle_timeout']) + else: + result['idle_timeout'] = 'unspecified' + + if 'destination_ports' in rule and rule['destination_ports']: + ports = list(set([str(x) for x in rule['destination_ports']])) + ports.sort() + ports = [str(self._validate_port_entries(x)) for x in ports] + result['destination_ports'] = ports + results.append(result) + results = sorted(results, key=lambda k: k['name']) + return results + + def _validate_port_entries(self, port): + if port == 'all-other': + return 0 + if '-' in port: + parts = port.split('-') + if len(parts) != 2: + raise F5ModuleError( + "The correct format for a port range is X-Y, where X is the start" + "port and Y is the end port." + ) + try: + start = int(parts[0]) + end = int(parts[1]) + except ValueError: + raise F5ModuleError( + "The ports in a range must be numbers." + "You provided '{0}' and '{1}'.".format(parts[0], parts[1]) + ) + if start == end: + return start + if start > end: + return '{0}-{1}'.format(end, start) + else: + return port + else: + try: + return int(port) + except ValueError: + raise F5ModuleError( + "The specified destination port is not a number." + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def rules(self): + if self._values['rules'] is None: + return None + results = [] + for rule in self._values['rules']: + result = dict() + result['name'] = rule['name'] + if 'protocol' in rule: + result['ipProtocol'] = rule['protocol'] + + if 'destination_ports' in rule: + if rule['protocol'] not in ['tcp', 'udp', 'sctp']: + raise F5ModuleError( + "Only the 'tcp', 'udp', and 'sctp' protocols support 'destination_ports'." + ) + ports = [dict(name=str(x)) for x in rule['destination_ports']] + result['destinationPorts'] = ports + else: + result['destinationPorts'] = [] + + if 'idle_timeout' in rule: + if rule['idle_timeout'] in ['indefinite', 'immediate', 'unspecified']: + timeout = rule['idle_timeout'] + else: + try: + int(rule['idle_timeout']) + timeout = rule['idle_timeout'] + except ValueError: + raise F5ModuleError( + "idle_timeout must be a number, or, one of 'indefinite', 'immediate', or 'unspecified'." + ) + result['timers'] = [ + dict(name='flow-idle-timeout', value=timeout) + ] + results.append(result) + results = sorted(results, key=lambda k: k['name']) + return results + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def rules(self): + if self.want.rules is None: + return None + if self.have.rules is None and self.want.rules == '': + return None + if self.have.rules is not None and self.want.rules == '': + return [] + if self.have.rules is None: + return self.want.rules + + want = [tuple(x.pop('destination_ports')) for x in self.want.rules if 'destination_ports' in x] + have = [tuple(x.pop('destination_ports')) for x in self.have.rules if 'destination_ports' in x] + if set(want) != set(have): + return self.want.rules + if compare_complex_list(self.want.rules, self.have.rules): + return self.want.rules + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/timer-policy/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + 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/net/timer-policy/".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) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/net/timer-policy/{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/net/timer-policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name), + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/timer-policy/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + rules=dict( + type='list', + elements='dict', + options=dict( + name=dict(required=True), + protocol=dict( + default='all-other', + choices=[ + 'all-other', + 'ah', + 'bna', + 'esp', + 'etherip', + 'gre', + 'icmp', + 'ipencap', + 'ipv6', + 'ipv6-auth', + 'ipv6-crypt', + 'ipv6-icmp', + 'isp-ip', + 'mux', + 'ospf', + 'sctp', + 'tcp', + 'udp', + 'udplite', + ] + ), + idle_timeout=dict(default='unspecified'), + destination_ports=dict( + type='list', + elements='str', + ) + ) + ), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_traffic_selector.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_traffic_selector.py new file mode 100644 index 00000000..aff5dde0 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_traffic_selector.py @@ -0,0 +1,509 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_traffic_selector +short_description: Manage IPSec Traffic Selectors on BIG-IP +description: + - Manage IPSec Traffic Selectors on BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the traffic selector. + type: str + required: True + destination_address: + description: + - Specifies the host or network IP address to which the application traffic is destined. + - When creating a new traffic selector, this parameter is required. + type: str + source_address: + description: + - Specifies the host or network IP address from which the application traffic originates. + - When creating a new traffic selector, this parameter is required. + type: str + ipsec_policy: + description: + - Specifies the IPsec policy that tells the BIG-IP system how to handle the packets. + - When creating a new traffic selector, if this parameter is not specified, the default + is C(default-ipsec-policy). + type: str + order: + description: + - Specifies the order in which traffic is matched, if traffic can be matched to multiple + traffic selectors. + - Traffic is matched to the traffic selector with the highest priority (lowest order number). + - When creating a new traffic selector, if this parameter is not specified, the default + is C(last). + type: int + description: + description: + - Description of the traffic selector. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a traffic selector + bigip_traffic_selector: + name: selector1 + destination_address: 1.1.1.1 + ipsec_policy: policy1 + order: 1 + source_address: 2.2.2.2 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +destination_address: + description: The new Destination IP Address. + returned: changed + type: str + sample: 1.2.3.4/32 +source_address: + description: The new Source IP address. + returned: changed + type: str + sample: 2.3.4.5/32 +ipsec_policy: + description: The new IPSec policy. + returned: changed + type: str + sample: /Common/policy1 +order: + description: The new sort order. + returned: changed + type: int + sample: 1 +''' + +import re +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ipaddress import ip_interface + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name +) +from ..module_utils.compare import cmp_str_with_none +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'destinationAddress': 'destination_address', + 'sourceAddress': 'source_address', + 'ipsecPolicy': 'ipsec_policy', + } + + api_attributes = [ + 'destinationAddress', + 'sourceAddress', + 'ipsecPolicy', + 'order', + 'description', + ] + + returnables = [ + 'destination_address', + 'source_address', + 'ipsec_policy', + 'order', + 'description', + ] + + updatables = [ + 'destination_address', + 'source_address', + 'ipsec_policy', + 'order', + 'description', + ] + + +class ApiParameters(Parameters): + @property + def description(self): + if self._values['description'] in [None, 'none']: + return None + return self._values['description'] + + +class ModuleParameters(Parameters): + @property + def ipsec_policy(self): + if self._values['ipsec_policy'] is None: + return None + return fq_name(self.partition, self._values['ipsec_policy']) + + @property + def destination_address(self): + result = self._format_address('destination_address') + if result == -1: + raise F5ModuleError( + "No IP address found in 'destination_address'." + ) + return result + + @property + def source_address(self): + result = self._format_address('source_address') + if result == -1: + raise F5ModuleError( + "No IP address found in 'source_address'." + ) + return result + + @property + def description(self): + if self._values['description'] is None: + return None + elif self._values['description'] in ['none', '']: + return '' + return self._values['description'] + + def _format_address(self, type): + if self._values[type] is None: + return None + pattern = r'(?P[^%/]+)(%(?P\d+))?(/(?P\d+))?' + if '%' in self._values[type]: + # Handle route domains + matches = re.match(pattern, self._values[type]) + if not matches: + return None + addr = matches.group('addr') + if addr is None: + return -1 + cidr = matches.group('cidr') + rd = matches.group('rd') + if cidr is not None: + ip = ip_interface(u'{0}/{1}'.format(addr, cidr)) + else: + ip = ip_interface(u'{0}'.format(addr)) + if rd: + result = '{0}%{1}/{2}'.format(str(ip.ip), rd, ip.network.prefixlen) + else: + result = '{0}/{1}'.format(str(ip.ip), ip.network.prefixlen) + return result + return str(ip_interface(u'{0}'.format(self._values[type]))) + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def description(self): + return cmp_str_with_none(self.want.description, self.have.description) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/traffic-selector/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/net/ipsec/traffic-selector/".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/net/ipsec/traffic-selector/{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 absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/ipsec/traffic-selector/{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/net/ipsec/traffic-selector/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + destination_address=dict(), + source_address=dict(), + ipsec_policy=dict(), + order=dict(type='int'), + description=dict(), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_trunk.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_trunk.py new file mode 100644 index 00000000..5fead072 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_trunk.py @@ -0,0 +1,610 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_trunk +short_description: Manage trunks on a BIG-IP +description: + - Manages trunks on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the trunk. + type: str + required: True + interfaces: + description: + - The interfaces that are part of the trunk. + - To clear the list of interfaces, specify an empty list. + type: list + elements: str + description: + description: + - Description of the trunk. + type: str + link_selection_policy: + description: + - Once the trunk is configured, specifies the policy the trunk uses to determine + which member link (interface) can handle new traffic. + - When creating a new trunk, if this value is not specified, the default is C(auto). + - When C(auto), specifies the system automatically determines which interfaces + can handle new traffic. For the C(auto) option, the member links must all be the + same media type and speed. + - When C(maximum-bandwidth), specifies the system determines which interfaces + can handle new traffic based on the members' maximum bandwidth. + type: str + choices: + - auto + - maximum-bandwidth + frame_distribution_hash: + description: + - Specifies the basis for the hash the system uses as the frame distribution + algorithm. The system uses the resulting hash to determine which interface to + use for forwarding traffic. + - When creating a new trunk, if this parameter is not specified, the default is + C(source-destination-ip). + - When C(source-destination-mac), specifies the system bases the hash on the + combined MAC addresses of the source and the destination. + - When C(destination-mac), specifies the system bases the hash on the MAC + address of the destination. + - When C(source-destination-ip), specifies the system bases the hash on the + combined IP addresses of the source and the destination. + type: str + choices: + - destination-mac + - source-destination-ip + - source-destination-mac + lacp_enabled: + description: + - When C(yes), specifies the system supports the link aggregation control + protocol (LACP), which monitors the trunk by exchanging control packets over + the member links to determine the health of the links. + - If LACP detects a failure in a member link, it removes the link from the link + aggregation. + - When creating a new trunk, if this parameter is not specified, LACP is C(no). + - LACP is disabled by default for backward compatibility. If this does not apply + to your network, we recommend that you enable LACP. + type: bool + lacp_mode: + description: + - Specifies the operation mode for link aggregation control protocol (LACP), + if LACP is enabled for the trunk. + - When creating a new trunk, if this parameter is not specified, the default + is C(active). + - When C(active), specifies the system periodically sends control packets + regardless of whether the partner system has issued a request. + - When C(passive), specifies the system sends control packets only when + the partner system has issued a request. + type: str + choices: + - active + - passive + lacp_timeout: + description: + - Specifies the rate at which the system sends the LACP control packets. + - When creating a new trunk, if this parameter is not specified, the default is + C(long). + - When C(long), specifies the system sends an LACP control packet every 30 seconds. + - When C(short), specifies the system sends an LACP control packet every second. + type: str + choices: + - long + - short + qinq_ethertype: + description: + - Specifies the ether-type value used for the packets handled on this trunk when + it is a member in a QinQ VLAN. + - The ether-type can be set to any string containing a valid hexadecimal 16 bits + number, or any of the well known ether-types; C(0x8100), C(0x9100), C(0x88a8). + - This parameter is not supported on Virtual Editions. + - You should always wrap this value in quotes to prevent Ansible from interpreting + the value as a literal hexadecimal number and converting it to an integer. + type: raw + state: + description: + - When C(present), ensures the resource exists. + - When C(absent), ensures the resource is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a trunk on hardware + bigip_trunk: + name: trunk1 + interfaces: + - 1.1 + - 1.2 + link_selection_policy: maximum-bandwidth + frame_distribution_hash: destination-mac + lacp_enabled: yes + lacp_mode: passive + lacp_timeout: short + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +lacp_mode: + description: Operation mode for LACP if the lacp option is enabled for the trunk. + returned: changed + type: str + sample: active +lacp_timeout: + description: Rate at which the system sends the LACP control packets. + returned: changed + type: str + sample: long +link_selection_policy: + description: + - LACP policy the trunk uses to determine which member link (interface) + can handle new traffic. + returned: changed + type: str + sample: auto +frame_distribution_hash: + description: Hash the system uses as the frame distribution algorithm. + returned: changed + type: str + sample: src-dst-ipport +lacp_enabled: + description: Whether the system supports the link aggregation control protocol (LACP) or not. + returned: changed + type: bool + sample: yes +interfaces: + description: Interfaces that are part of the trunk. + returned: changed + type: list + sample: ['int1', 'int2'] +description: + description: Description of the trunk. + returned: changed + type: str + sample: My trunk +qinq_ethertype: + description: Ether-type value used for the packets handled on this trunk when it is a member in a QinQ VLAN. + returned: changed + type: str + sample: 0x9100 +''' +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.compare import cmp_simple_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'lacpMode': 'lacp_mode', + 'lacpTimeout': 'lacp_timeout', + 'linkSelectPolicy': 'link_selection_policy', + 'distributionHash': 'frame_distribution_hash', + 'lacp': 'lacp_enabled', + 'qinqEthertype': 'qinq_ethertype', + } + + api_attributes = [ + 'lacp', + 'lacpMode', + 'lacpTimeout', + 'linkSelectPolicy', + 'distributionHash', + 'interfaces', + 'description', + 'qinqEthertype', + ] + + returnables = [ + 'lacp_mode', + 'lacp_timeout', + 'link_selection_policy', + 'frame_distribution_hash', + 'lacp_enabled', + 'interfaces', + 'description', + 'qinq_ethertype', + ] + + updatables = [ + 'lacp_mode', + 'lacp_timeout', + 'link_selection_policy', + 'frame_distribution_hash', + 'lacp_enabled', + 'interfaces', + 'description', + 'qinq_ethertype', + ] + + +class ApiParameters(Parameters): + @property + def lacp_enabled(self): + if self._values['lacp_enabled'] is None: + return None + if self._values['lacp_enabled'] == 'enabled': + return True + return False + + @property + def interfaces(self): + if self._values['interfaces'] is None: + return None + result = list(set(self._values['interfaces'])) + result.sort() + return result + + +class ModuleParameters(Parameters): + @property + def frame_distribution_hash(self): + if self._values['frame_distribution_hash'] is None: + return None + elif self._values['frame_distribution_hash'] == 'source-destination-ip': + return 'src-dst-ipport' + elif self._values['frame_distribution_hash'] == 'source-destination-mac': + return 'src-dst-mac' + elif self._values['frame_distribution_hash'] == 'destination-mac': + return 'dst-mac' + + @property + def interfaces(self): + if self._values['interfaces'] is None: + return None + if len(self._values['interfaces']) == 1 and self._values['interfaces'][0] == '': + return '' + result = [str(x) for x in self._values['interfaces']] + result = list(set(result)) + result.sort() + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def lacp_enabled(self): + if self._values['lacp_enabled'] is None: + return None + if self._values['lacp_enabled']: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def frame_distribution_hash(self): + if self._values['frame_distribution_hash'] is None: + return None + elif self._values['frame_distribution_hash'] == 'src-dst-ipport': + return 'source-destination-ip' + elif self._values['frame_distribution_hash'] == 'src-dst-mac': + return 'source-destination-mac' + elif self._values['frame_distribution_hash'] == 'dst-mac': + return 'destination-mac' + + @property + def lacp_enabled(self): + if self._values['lacp_enabled'] is None: + return None + if self._values['lacp_enabled'] == 'enabled': + return True + return False + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def interfaces(self): + result = cmp_simple_list(self.want.interfaces, self.have.interfaces) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.link_selection_policy is None: + self.want.update({'link_selection_policy': 'auto'}) + if self.want.frame_distribution_hash is None: + self.want.update({'frame_distribution_hash': 'source-destination-ip'}) + if self.want.lacp_enabled is None: + self.want.update({'lacp_enabled': False}) + if self.want.lacp_mode is None: + self.want.update({'lacp_mode': 'active'}) + if self.want.lacp_timeout is None: + self.want.update({'lacp_timeout': 'long'}) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/trunk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + uri = "https://{0}:{1}/mgmt/tm/net/trunk/".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, 409]: + 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/net/trunk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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/net/trunk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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/net/trunk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + interfaces=dict( + type='list', + elements='str', + ), + link_selection_policy=dict( + choices=['auto', 'maximum-bandwidth'] + ), + frame_distribution_hash=dict( + choices=['destination-mac', 'source-destination-ip', 'source-destination-mac'] + ), + lacp_enabled=dict(type='bool'), + lacp_mode=dict(choices=['active', 'passive']), + lacp_timeout=dict(choices=['short', 'long']), + description=dict(), + state=dict( + default='present', + choices=['absent', 'present'] + ), + qinq_ethertype=dict(type='raw'), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_tunnel.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_tunnel.py new file mode 100644 index 00000000..e2387f15 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_tunnel.py @@ -0,0 +1,619 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_tunnel +short_description: Manage tunnels on a BIG-IP +description: + - Manages tunnels on a BIG-IP. Tunnels are usually based upon a tunnel profile which + defines both default arguments and constraints for the tunnel. + - Due to this, this module exposes a number of settings that may or may not be related + to the type of tunnel you are working with. It is important that you take this into + consideration when declaring your tunnel config. + - If a specific tunnel does not support the parameter you are considering, the documentation + of the parameter will usually make mention of this. Otherwise, when configuring that + parameter on the device, the device will notify you. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the tunnel. + type: str + required: True + description: + description: + - Description of the tunnel. + type: str + profile: + description: + - Specifies the profile to associate with the tunnel for handling traffic. + - Depending on your selection, other settings become available or disappear. + - This parameter may not be changed after it is set. + type: str + key: + description: + - When applied to a GRE tunnel, this value specifies an optional field in the GRE header, + used to authenticate the source of the packet. + - When applied to a VXLAN or Geneve tunnel, this value specifies the Virtual Network + Identifier (VNI). + - When applied to an NVGRE tunnel, this value specifies the Virtual Subnet Identifier (VSID). + - When creating a new tunnel, if this parameter is supported by the tunnel profile but not + specified, the default value is C(0). + type: int + local_address: + description: + - Specifies the IP address of the local endpoint of the tunnel. + type: str + remote_address: + description: + - Specifies the IP address of the remote endpoint of the tunnel. + - For C(dslite), C(fec) (when configuring the FEC tunnel for receiving traffic only), + C(v6rd) (configured as a border relay), or C(map), the tunnel must have an unspecified + remote address (any). + type: str + secondary_address: + description: + - Specifies a non-floating IP address for the tunnel, to be used with host-initiated traffic. + type: str + mtu: + description: + - Specifies the maximum transmission unit (MTU) of the tunnel. + - When creating a new tunnel, if this parameter is supported by the tunnel profile but not + specified, the default value is C(0). + - The valid range is from C(0) to C(65515). + type: int + use_pmtu: + description: + - Enables or disables the tunnel to use the PMTU (Path MTU) information provided by ICMP + NeedFrag error messages. + - If C(yes) and the tunnel C(mtu) is set to C(0), the tunnel will use the PMTU information. + - If C(yes) and the tunnel C(mtu) is fixed to a non-zero value, the tunnel will use the + minimum of PMTU and MTU. + - If C(no), the tunnel will use fixed MTU or calculate its MTU using tunnel encapsulation + configurations. + type: bool + tos: + description: + - Specifies the Type of Service (TOS) value to insert in the encapsulating header of + transmitted packets. + - When creating a new tunnel, if this parameter is supported by the tunnel profile but not + specified, the default value is C(preserve). + - When C(preserve), the system copies the TOS value from the inner header to the outer header. + - You may also specify a numeric value. The possible values are from C(0) to C(255). + type: str + auto_last_hop: + description: + - Allows you to configure auto last hop on a per-tunnel basis. + - When creating a new tunnel, if this parameter is supported by the tunnel profile but not + specified, the default is C(default). + - When C(default), means that the system uses the global auto-lasthop setting to send back + the request. + - When C(enabled), allows the system to send return traffic to the MAC address that transmitted + the request, even if the routing table points to a different network or interface. As a + result, the system can send return traffic to clients even when there is no matching route. + type: str + choices: + - default + - enabled + - disabled + traffic_group: + description: + - Specifies the traffic group to associate with the tunnel. + - This value cannot be changed after it is set. This is a limitation of BIG-IP. + type: str + mode: + description: + - Specifies how the tunnel carries traffic. + - When creating a new tunnel, if this parameter is supported by the tunnel profile but not + specified, the default is C(bidirectional). + - When C(bidirectional), specifies that the tunnel carries both inbound and outbound traffic. + - When C(inbound), specifies that the tunnel carries only incoming traffic. + - When C(outbound), specifies that the tunnel carries only outgoing traffic. + type: str + choices: + - bidirectional + - inbound + - outbound + transparent: + description: + - Specifies that the tunnel operates in transparent mode. + - When C(yes), you can inspect and manipulate the encapsulated traffic flowing through the BIG-IP + system. + - A transparent tunnel terminates a tunnel while presenting the illusion that the tunnel transits + the device unmodified (that is, the BIG-IP system appears as if it were an intermediate router + that simply routes IP traffic through the device). + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + state: + description: + - When C(present), ensures that the tunnel exists. + - When C(absent), ensures the tunnel is removed. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a VXLAN tunnel + bigip_tunnel: + name: openshift-tunnel + local_address: 192.1681.240 + key: 0 + secondary_address: 192.168.1.100 + mtu: 0 + use_pmtu: yes + tos: preserve + auto_last_hop: default + traffic_group: traffic-group-1 + state: present + 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. + returned: changed + type: str + sample: Foo is bar +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name, flatten_boolean +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'autoLasthop': 'auto_last_hop', + 'localAddress': 'local_address', + 'remoteAddress': 'remote_address', + 'secondaryAddress': 'secondary_address', + 'usePmtu': 'use_pmtu', + 'trafficGroup': 'traffic_group', + } + + api_attributes = [ + 'autoLasthop', + 'description', + 'key', + 'mtu', + 'profile', + 'transparent', + 'usePmtu', + 'tos', + 'secondaryAddress', + 'remoteAddress', + 'mode', + 'localAddress', + 'trafficGroup', + ] + + returnables = [ + 'auto_last_hop', + 'local_address', + 'mode', + 'remote_address', + 'secondary_address', + 'description', + 'key', + 'mtu', + 'profile', + 'transparent', + 'use_pmtu', + 'tos', + 'traffic_group', + ] + + updatables = [ + 'auto_last_hop', + 'local_address', + 'mode', + 'remote_address', + 'profile', + 'secondary_address', + 'description', + 'key', + 'mtu', + 'transparent', + 'use_pmtu', + 'tos', + 'traffic_group', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def transparent(self): + result = flatten_boolean(self._values['transparent']) + if result == 'yes': + return 'enabled' + elif result == 'no': + return 'disabled' + + @property + def use_pmtu(self): + result = flatten_boolean(self._values['use_pmtu']) + if result == 'yes': + return 'enabled' + elif result == 'no': + return 'disabled' + + @property + def profile(self): + if self._values['profile'] is None: + return None + return fq_name(self.partition, self._values['profile']) + + @property + def traffic_group(self): + if self._values['traffic_group'] is None: + return None + elif self._values['traffic_group'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['traffic_group']) + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def transparent(self): + result = flatten_boolean(self._values['transparent']) + return result + + @property + def use_pmtu(self): + result = flatten_boolean(self._values['use_pmtu']) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def profile(self): + if self.want.profile is None: + return None + if self.want.profile != self.have.profile: + raise F5ModuleError( + "'profile' cannot be changed after it is set." + ) + + @property + def traffic_group(self): + if self.want.traffic_group is None: + return None + if self.want.traffic_group in ['', None] and self.have.traffic_group is None: + return None + if self.want.traffic_group != self.have.traffic_group: + raise F5ModuleError( + "'traffic_group' cannot be changed after it is set." + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/tunnels/tunnel/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/net/tunnels/tunnel/".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) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/net/tunnels/tunnel/{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 absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/tunnels/tunnel/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/tunnels/tunnel/{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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + profile=dict(), + description=dict(), + key=dict(type='int'), + local_address=dict(), + remote_address=dict(), + secondary_address=dict(), + mtu=dict(type='int'), + use_pmtu=dict(type='bool'), + tos=dict(), + auto_last_hop=dict( + choices=['default', 'enabled', 'disabled'] + ), + traffic_group=dict(), + mode=dict( + choices=['bidirectional', 'inbound', 'outbound'] + ), + transparent=dict(type='bool'), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ucs.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ucs.py new file mode 100644 index 00000000..96f34c37 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ucs.py @@ -0,0 +1,753 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_ucs +short_description: Manage upload, installation, and removal of UCS files +description: + - Manage the upload, installation, and removal of UCS files on a BIG-IP system. + A user configuration set (UCS) is a backup file that contains BIG-IP configuration + data that can be used to fully restore a BIG-IP system in the event of a + failure or RMA replacement. +version_added: "1.0.0" +options: + include_chassis_level_config: + description: + - During restoration of the UCS file, includes chassis level configuration + that is shared among boot volume sets. For example, the cluster default + configuration. + type: bool + ucs: + description: + - The path to the UCS file to install. The parameter must be + provided if the C(state) is either C(installed) or C(activated). + When C(state) is C(absent), the full path for this parameter is + ignored and only the filename is used to select a UCS for removal. + Therefore you could specify C(/foo/bar/test.ucs) and this module + would only look for C(test.ucs). + type: str + required: True + force: + description: + - If C(yes), the system uploads the file every time and replaces the file on the + device. If C(no), the file is only uploaded if it does not already + exist. Generally should only be C(yes) in cases where you believe + the image was corrupted during upload. + type: bool + default: no + no_license: + description: + - Performs a full restore of the UCS file and all the files it contains, + with the exception of the license file. The option must be used to + restore a UCS on RMA (Returned Materials Authorization) devices. + type: bool + no_platform_check: + description: + - Bypasses the platform check and allows installation of a UCS that was + created using a different platform. By default (without this option), + installation of a UCS created from a different platform is not allowed. + type: bool + passphrase: + description: + - Specifies the passphrase that is necessary to load the specified UCS file. + type: str + reset_trust: + description: + - When specified, the device and trust domain certs and keys are not + loaded from the UCS. Instead, a new set is generated. + type: bool + state: + description: + - When C(installed), ensures the UCS is uploaded and installed + on the system. When C(present), ensures the UCS is uploaded. + When C(absent), the UCS is removed from the system. When + C(installed), the uploading of the UCS is idempotent, however the + installation of that configuration is not idempotent. + type: str + choices: + - absent + - installed + - present + default: present +notes: + - Only the most basic checks are performed by this module. Other checks and + considerations need to be taken into account. See + https://support.f5.com/kb/en-us/solutions/public/11000/300/sol11318.html + - This module does not handle devices with the FIPS 140 HSM. + - This module does not handle BIG-IPs systems on the 6400, 6800, 8400, or + 8800 hardware platforms. + - This module does not verify the new or replaced SSH keys from the + UCS file are synchronized between the BIG-IP system and the SCCP. + - This module does not support the 'rma' option. + - This module does not support restoring a UCS archive on a BIG-IP 1500, + 3400, 4100, 6400, 6800, or 8400 hardware platforms other than the system + from which the backup was created. + - The UCS restore operation restores the full configuration only if the + hostname of the target system matches the hostname on which the UCS + archive was created. If the hostname does not match, only the shared + configuration is restored. You can ensure hostnames match by using + the C(bigip_hostname) Ansible module in a task before using this module. + - This module does not support re-licensing a BIG-IP restored from a UCS. + - This module does not support restoring encrypted archives on replacement + RMA unit. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Upload UCS + bigip_ucs: + ucs: /root/bigip.localhost.localdomain.ucs + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Install (upload, install) UCS. + bigip_ucs: + ucs: /root/bigip.localhost.localdomain.ucs + state: installed + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Install (upload, install) UCS without installing the license portion + bigip_ucs: + ucs: /root/bigip.localhost.localdomain.ucs + state: installed + no_license: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Install (upload, install) UCS except the license, and bypassing the platform check + bigip_ucs: + ucs: /root/bigip.localhost.localdomain.ucs + state: installed + no_license: yes + no_platform_check: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Install (upload, install) UCS using a passphrase necessary to load the UCS + bigip_ucs: + ucs: /root/bigip.localhost.localdomain.ucs + state: installed + passphrase: MyPassphrase1234 + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove uploaded UCS file + bigip_ucs: + ucs: bigip.localhost.localdomain.ucs + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import os +import re +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import ( + upload_file, tmos_version +) +from ..module_utils.teem import send_teem + +try: + from collections import OrderedDict +except ImportError: + try: + from ordereddict import OrderedDict + except ImportError: + pass + + +class Parameters(AnsibleF5Parameters): + api_map = {} + updatables = [] + returnables = [] + api_attributes = [] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + def _check_required_if(self, parameter): + if self._values[parameter] is not True: + return self._values[parameter] + if self.state != 'installed': + raise F5ModuleError( + '"{0}" parameters requires "installed" state'.format(parameter) + ) + + @property + def basename(self): + return os.path.basename(self.ucs) + + @property + def options(self): + return { + 'include-chassis-level-config': self.include_chassis_level_config, + 'no-license': self.no_license, + 'no-platform-check': self.no_platform_check, + 'passphrase': self.passphrase, + 'reset-trust': self.reset_trust + } + + @property + def reset_trust(self): + self._check_required_if('reset_trust') + return self._values['reset_trust'] + + @property + def passphrase(self): + self._check_required_if('passphrase') + return self._values['passphrase'] + + @property + def no_platform_check(self): + self._check_required_if('no_platform_check') + return self._values['no_platform_check'] + + @property + def no_license(self): + self._check_required_if('no_license') + return self._values['no_license'] + + @property + def include_chassis_level_config(self): + self._check_required_if('include_chassis_level_config') + return self._values['include_chassis_level_config'] + + @property + def install_command(self): + cmd = 'tmsh load sys ucs /var/local/ucs/{0}'.format(self.basename) + # Append any options that might be specified + options = OrderedDict(sorted(self.options.items(), key=lambda t: t[0])) + for k, v in iteritems(options): + if v is False or v is None: + continue + elif k == 'passphrase': + cmd += ' %s %s' % (k, v) + else: + cmd += ' %s' % (k) + return cmd + + +class Changes(Parameters): + 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 ReportableChanges(Changes): + pass + + +class UsableChanges(Changes): + pass + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + + def exec_module(self): + if self.is_version_v1(): + manager = V1Manager(**self.kwargs) + else: + manager = V2Manager(**self.kwargs) + + return manager.exec_module() + + def is_version_v1(self): + """Checks to see if the TMOS version is less than 12.1.0 + + Versions prior to 12.1.0 have a bug which prevents the REST + API from properly listing any UCS files when you query the + /mgmt/tm/sys/ucs endpoint. Therefore you need to do everything + through tmsh over REST. + + :return: Bool + """ + version = tmos_version(self.client) + if Version(version) < Version('12.1.0'): + return True + else: + return False + + +class Difference(object): + pass + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ['present', 'installed']: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def update(self): + if self.module.check_mode: + if self.want.force: + return True + return False + elif self.want.force: + self.remove() + return self.create() + elif self.want.state == 'installed': + return self.install_on_device() + else: + return False + + def create(self): + if self.module.check_mode: + return True + self.create_on_device() + if not self.exists(): + raise F5ModuleError("Failed to upload the UCS file") + if self.want.state == 'installed': + self.install_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the UCS file") + return True + + def wait_for_rest_api_restart(self): + time.sleep(5) + for x in range(0, 60): + try: + self.client.reconnect() + break + except Exception: + time.sleep(3) + + def wait_for_configuration_reload(self): + noops = 0 + while noops < 4: + time.sleep(3) + try: + params = dict(command="run", + utilCmdArgs='-c "tmsh show sys mcp-state"' + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=params) + try: + output = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in output and output['code'] in [400, 403]: + if 'message' in output: + raise F5ModuleError(output['message']) + else: + raise F5ModuleError(resp.content) + except Exception: + # This can be caused by restjavad restarting. + continue + + if 'commandResult' not in output: + continue + + # Need to re-connect here because the REST framework will be restarting + # and thus be clearing its authorization cache + result = output['commandResult'] + if self._is_config_reloading_failed_on_device(result): + raise F5ModuleError( + "Failed to reload the configuration. This may be due " + "to a cross-version incompatibility. {0}".format(result) + ) + if self._is_config_reloading_success_on_device(result): + if self._is_config_reloading_running_on_device(result): + noops += 1 + continue + noops = 0 + + def _is_config_reloading_success_on_device(self, output): + succeed = r'Last Configuration Load Status\s+full-config-load-succeed' + matches = re.search(succeed, output) + if matches: + return True + return False + + def _is_config_reloading_running_on_device(self, output): + running = r'Running Phase\s+running' + matches = re.search(running, output) + if matches: + return True + return False + + def _is_config_reloading_failed_on_device(self, output): + failed = r'Last Configuration Load Status\s+base-config-load-failed' + matches = re.search(failed, output) + if matches: + return True + return False + + +class V1Manager(BaseManager): + """Manager class for V1 product + + V1 products include versions of BIG-IP < 12.1.0, but >= 12.0.0. + + These versions had a number of API deficiencies. These include, but + are not limited to, + + * UCS collection endpoint listed no items + * No API to upload UCS files + + """ + + def _set_mode_and_ownership(self): + url = 'https://{0}:{1}/mgmt/tm/util/bash'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + ownership = 'root:root' + ucs_path = f'/var/local/ucs/{self.want.basename}' + file_mode = oct(os.stat(self.want.ucs).st_mode)[-3:] + args = dict( + command='run', + utilCmdArgs='-c "chown {0} {1};chmod {2} {1}"'.format(ownership, ucs_path, file_mode) + ) + + self.client.api.post(url, json=args) + + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def create_on_device(self): + remote_path = "/var/local/ucs" + tpath_name = '/var/config/rest/downloads' + + self.upload_file_to_device(self.want.ucs, self.want.basename) + + uri = "https://{0}:{1}/mgmt/tm/util/unix-mv/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='{0}/{2} {1}/{2}'.format( + tpath_name, remote_path, self.want.basename + ) + ) + resp = self.client.api.post(uri, json=args) + 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) + + self._set_mode_and_ownership() + return True + + def read_current_from_device(self): + result = [] + params = dict(command="run", + utilCmdArgs='-c "tmsh list sys ucs"' + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=params) + try: + output = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in output and output['code'] in [400, 403]: + if 'message' in output: + raise F5ModuleError(output['message']) + else: + raise F5ModuleError(resp.content) + if 'commandResult' in output: + lines = output['commandResult'].split("\n") + result = [x.strip() for x in lines] + result = list(set(result)) + return result + + def exists(self): + collection = self.read_current_from_device() + if self.want.basename in collection: + return True + return False + + def remove_from_device(self): + params = dict(command="run", + utilCmdArgs='-c "tmsh delete sys ucs {0}"'.format(self.want.basename) + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=params) + try: + output = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in output and output['code'] in [400, 403]: + if 'message' in output: + raise F5ModuleError(output['message']) + else: + raise F5ModuleError(resp.content) + if 'commandResult' in output: + if '{0} is deleted'.format(self.want.basename) in output['commandResult']: + return True + return False + + def install_on_device(self): + try: + params = dict(command="run", + utilCmdArgs='-c "{0}"'.format(self.want.install_command) + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=params) + try: + output = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in output and output['code'] in [400, 403]: + if 'message' in output: + raise F5ModuleError(output['message']) + else: + raise F5ModuleError(resp.content) + except Exception as ex: + # Reloading a UCS configuration will cause restjavad to restart, + # aborting the connection. + if 'Connection aborted' in str(ex): + pass + elif 'TimeoutException' in str(ex): + # Timeouts appear to be able to happen in 12.1.2 + pass + elif 'remoteSender' in str(ex): + # catching some edge cases where API becomes unstable after installation + pass + else: + raise F5ModuleError(str(ex)) + self.wait_for_rest_api_restart() + self.wait_for_configuration_reload() + return True + + +class V2Manager(V1Manager): + """Manager class for V2 product + + V2 products include versions of BIG-IP >= 12.1.0 but < 13.0.0. + + These versions fixed the collection bug in V1, but had yet to add the + ability to upload files using a dedicated UCS upload API. + + """ + + def read_current_from_device(self): + result = [] + uri = "https://{0}:{1}/mgmt/tm/sys/ucs/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + items = response.get('items', []) + for item in items: + result.append(os.path.basename(item['apiRawValues']['filename'])) + return result + + def exists(self): + collection = self.read_current_from_device() + if self.want.basename in collection: + return True + return False + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + force=dict( + type='bool', + default='no' + ), + include_chassis_level_config=dict( + type='bool' + ), + no_license=dict( + type='bool' + ), + no_platform_check=dict( + type='bool' + ), + passphrase=dict(no_log=True), + reset_trust=dict(type='bool'), + state=dict( + default='present', + choices=['absent', 'installed', 'present'] + ), + ucs=dict(required=True) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ucs_fetch.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ucs_fetch.py new file mode 100644 index 00000000..b4966b4a --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_ucs_fetch.py @@ -0,0 +1,755 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_ucs_fetch +short_description: Fetches a UCS file from remote nodes +description: + - This module is used for fetching UCS files from remote machines and + storing them locally in a file tree, organized by hostname. This module + was written to create and transfer UCS files that might not be present, + it does not require UCS file to be pre-created. So a missing remote UCS + is not an error unless C(fail_on_missing) is set to 'yes'. +version_added: "1.0.0" +options: + backup: + description: + - Creates a backup file including the timestamp information so you can + get the original file back if you overwrote it incorrectly. + type: bool + default: no + create_on_missing: + description: + - Creates the UCS based on the value of C(src), if the file does not already + exist on the remote system. + type: bool + default: yes + dest: + description: + - A directory to save the UCS file into. + - This option is mandatory when C(only_create_file) is set to C(no). + type: path + encryption_password: + description: + - Password to use to encrypt the UCS file if desired. + type: str + fail_on_missing: + description: + - Make the module fail if the UCS file on the remote system is missing. + type: bool + default: no + force: + description: + - If C(no), the file is only transferred if the destination does not + exist. + type: bool + default: yes + src: + description: + - The name of the UCS file to create on the remote server for downloading. + - The file is retrieved or created in /var/local/ucs/. + - This option is mandatory when C(only_create_file) is set to C(yes). + type: str + async_timeout: + description: + - Parameter used when creating new UCS file on a device. + - The number of seconds to wait for the API async interface to complete its task. + - The accepted value range is between C(150) and C(1800) seconds. + type: int + default: 150 + only_create_file: + description: + - If C(yes), the file is created on the device and not downloaded. If the UCS archive exists on the device, + no change is made and the file is not downloaded. + - To recreate UCS files left on the device, remove them with the C(bigip_ucs) module before running this + module with C(only_create_file) set to C(yes). + type: bool + default: no + version_added: "1.12.0" +notes: + - BIG-IP provides no way to get a checksum of the UCS files on the system + via any interface with the possible exception of logging in directly to the box (which + would not support appliance mode). Therefore, the best this module can + do is check for the existence of the file on disk; no check-summing. + - If you are using this module with either Ansible Tower or Ansible AWX, you + should be aware of how these Ansible products execute jobs in restricted + environments. More information can be found here + https://clouddocs.f5.com/products/orchestration/ansible/devel/usage/module-usage-with-tower.html + - Some longer running tasks might cause the REST interface on BIG-IP to time out, to avoid this adjust the timers as + per this KB article https://support.f5.com/csp/article/K94602685 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Download a new UCS + bigip_ucs_fetch: + src: cs_backup.ucs + dest: /tmp/cs_backup.ucs + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Only create new UCS, no download + bigip_ucs_fetch: + src: cs_backup.ucs + only_create_file: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Recreate UCS file left on device - remove file first + bigip_ucs: + ucs: cs_backup.ucs + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Recreate UCS file left on device - create new file + bigip_ucs_fetch: + src: cs_backup.ucs + only_create_file: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +checksum: + description: The SHA1 checksum of the downloaded file. + returned: success or changed + type: str + sample: 7b46bbe4f8ebfee64761b5313855618f64c64109 +dest: + description: Location on the Ansible host the UCS was saved to. + returned: success + type: str + sample: /path/to/file.txt +src: + description: + - Name of the UCS file on the remote BIG-IP to download. If not + specified, this is a randomly generated filename. + returned: changed + type: str + sample: cs_backup.ucs +backup_file: + description: Name of the backup file. + returned: changed and if backup=yes + type: str + sample: /path/to/file.txt.2015-02-12@22:09~ +gid: + description: Group ID of the UCS file, after execution. + returned: success + type: int + sample: 100 +group: + description: Group of the UCS file, after execution. + returned: success + type: str + sample: httpd +owner: + description: Owner of the UCS file, after execution. + returned: success + type: str + sample: httpd +uid: + description: Owner ID of the UCS file, after execution. + returned: success + type: int + sample: 100 +md5sum: + description: The MD5 checksum of the downloaded file. + returned: changed or success + type: str + sample: 96cacab4c259c4598727d7cf2ceb3b45 +mode: + description: Permissions of the target UCS, after execution. + returned: success + type: str + sample: 0644 +size: + description: Size of the target UCS, after execution. + returned: success + type: int + sample: 1220 +''' + +import os +import re +import tempfile +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import ( + tmos_version, download_file +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + updatables = [] + returnables = [ + 'dest', + 'src', + 'md5sum', + 'checksum', + 'backup_file' + ] + api_attributes = [] + api_map = {} + + +class ModuleParameters(Parameters): + @property + def options(self): + result = [] + if self.passphrase: + result.append(dict( + passphrase=self.want.passphrase + )) + return result + + @property + def src(self): + if self._values['src'] is not None: + return self._values['src'] + result = next(tempfile._get_candidate_names()) + '.ucs' + self._values['src'] = result + return result + + @property + def fulldest(self): + result = None + if os.path.isdir(self.dest): + result = os.path.join(self.dest, self.src) + else: + if os.path.exists(os.path.dirname(self.dest)): + result = self.dest + else: + try: + # os.path.exists() can return false in some + # circumstances where the directory does not have + # the execute bit for the current user set, in + # which case the stat() call will raise an OSError + os.stat(os.path.dirname(self.dest)) + except OSError as e: + if "permission denied" in str(e).lower(): + raise F5ModuleError( + "Destination directory {0} is not accessible".format(os.path.dirname(self.dest)) + ) + raise F5ModuleError( + "Destination directory {0} does not exist".format(os.path.dirname(self.dest)) + ) + + if not os.access(os.path.dirname(result), os.W_OK): + raise F5ModuleError( + "Destination {0} not writable".format(os.path.dirname(result)) + ) + return result + + @property + def async_timeout(self): + divisor = 100 + timeout = self._values['async_timeout'] + if timeout < 150 or timeout > 1800: + raise F5ModuleError( + "Timeout value must be between 150 and 1800 seconds." + ) + + delay = timeout / divisor + + return delay, divisor + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + + def exec_module(self): + if self.is_version_v1(): + manager = self.get_manager('v1') + else: + manager = self.get_manager('v2') + return manager.exec_module() + + def get_manager(self, type): + if type == 'v1': + return V1Manager(**self.kwargs) + elif type == 'v2': + return V2Manager(**self.kwargs) + + def is_version_v1(self): + """Checks to see if the TMOS version is less than 12.1.0 + + Versions prior to 12.1.0 have a bug which prevents the REST + API from properly listing any UCS files when you query the + /mgmt/tm/sys/ucs endpoint. Therefore you need to do everything + through tmsh over REST. + + :return: bool + """ + version = tmos_version(self.client) + if Version(version) < Version('12.1.0'): + return True + else: + return False + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + result = dict() + + self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=True)) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + if not self.want.only_create_file: + self.update() + else: + self.create() + + def update(self): + if os.path.exists(self.want.fulldest): + if not self.want.force: + raise F5ModuleError( + "File '{0}' already exists".format(self.want.fulldest) + ) + self.execute() + + def _get_backup_file(self): + return self.module.backup_local(self.want.fulldest) + + def execute(self): + try: + if self.want.backup: + if os.path.exists(self.want.fulldest): + backup_file = self._get_backup_file() + self.changes.update({'backup_file': backup_file}) + self.download() + except IOError: + raise F5ModuleError( + "Failed to copy: {0} to {1}".format(self.want.src, self.want.fulldest) + ) + self._set_checksum() + self._set_md5sum() + self.changes.update({'src': self.want.src}) + self.changes.update({'md5sum': self.want.md5sum}) + self.changes.update({'checksum': self.want.checksum}) + file_args = self.module.load_file_common_arguments(self.module.params) + return self.module.set_fs_attributes_if_different(file_args, True) + + def _set_checksum(self): + try: + result = self.module.sha1(self.want.fulldest) + self.want.update({'checksum': result}) + except ValueError: + pass + + def _set_md5sum(self): + try: + result = self.module.md5(self.want.fulldest) + self.want.update({'md5sum': result}) + except ValueError: + pass + + def create(self): + if self.want.fail_on_missing: + raise F5ModuleError( + "UCS '{0}' was not found".format(self.want.src) + ) + + if not self.want.create_on_missing: + raise F5ModuleError( + "UCS '{0}' was not found".format(self.want.src) + ) + + if self.module.check_mode: + return True + if self.want.create_on_missing: + self.create_on_device() + if not self.want.only_create_file: + self.execute() + return True + + def create_on_device(self): + task = self.create_async_task_on_device() + self._start_task_on_device(task) + self.async_wait(task) + + def create_async_task_on_device(self): + if self.want.passphrase: + params = dict( + command='save', + name=self.want.src, + options=[{'passphrase': self.want.encryption_password}] + ) + else: + params = dict( + command='save', + name=self.want.src, + ) + + uri = "https://{0}:{1}/mgmt/tm/task/sys/ucs".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return response['_taskId'] + raise F5ModuleError(resp.content) + + def _start_task_on_device(self, task): + payload = {"_taskState": "VALIDATING"} + uri = "https://{0}:{1}/mgmt/tm/task/sys/ucs/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + task + ) + resp = self.client.api.put(uri, json=payload) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201, 202] or 'code' in response and response['code'] in [200, 201, 202]: + return True + raise F5ModuleError(resp.content) + + def async_wait(self, task): + delay, period = self.want.async_timeout + uri = "https://{0}:{1}/mgmt/tm/task/sys/ucs/{2}/result".format( + self.client.provider['server'], + self.client.provider['server_port'], + task + ) + for x in range(0, period): + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + # It is possible that the API call can return invalid JSON. + # This invalid JSON appears to be just empty strings. + continue + if resp.status in [200, 201, 202] or 'code' in response and response['code'] in [200, 201, 202]: + if response['_taskState'] == 'FAILED': + raise F5ModuleError("Task failed unexpectedly.") + if response['_taskState'] == 'COMPLETED': + return True + + time.sleep(delay) + # at times we time out waiting on task as sometimes task is gone from async queue after services reboot + # we are adding existence check here to catch where the file is created but async task is removed. + if not self.exists(): + raise F5ModuleError( + "Module timeout reached, state change is unknown, " + "please increase the async_timeout parameter for long lived actions." + ) + + def download(self): + self.download_from_device(self.want.dest) + if os.path.exists(self.want.dest): + return True + raise F5ModuleError( + "Failed to download the remote file" + ) + + +class V1Manager(BaseManager): + def __init__(self, *args, **kwargs): + super(V1Manager, self).__init__(**kwargs) + self.remote_dir = '/var/config/rest/madm' + + def read_current(self): + result = None + output = self.read_current_from_device() + if 'commandResult' in output: + result = self._read_ucs_files_from_output(output['commandResult']) + return result + + def read_current_from_device(self): + params = dict( + command='run', + utilCmdArgs='-c "tmsh list sys ucs"' + ) + + uri = "https://{0}:{1}/mgmt/tm/util/bash".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) + return response + + def _read_ucs_files_from_output(self, output): + search = re.compile(r'filename\s+(.*)').search + lines = output.split("\n") + result = [m.group(1) for m in map(search, lines) if m] + return result + + def exists(self): + collection = self.read_current() + base = os.path.basename(self.want.src) + if any(base == os.path.basename(x) for x in collection): + return True + return False + + def download_from_device(self, dest): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/madm/{2}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.filename + ) + try: + download_file(self.client, url, dest) + except F5ModuleError: + raise F5ModuleError( + "Failed to download the file." + ) + if os.path.exists(self.want.dest): + return True + return False + + def _move_to_download(self): + move_path = '/var/local/ucs/{0} {1}/{0}'.format( + self.want.filename, self.remote_dir + ) + params = dict( + command='run', + utilCmdArgs=move_path + ) + + uri = "https://{0}:{1}/mgmt/tm/util/unix-mv/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + resp = self.client.api.post(uri, json=params) + + try: + response = resp.json() + if 'commandResult' in response: + if 'cannot stat' in response['commandResult']: + raise F5ModuleError(response['commandResult']) + 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) + + return True + + +class V2Manager(BaseManager): + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/ucs".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + + return response + + def read_current(self): + collection = self.read_current_from_device() + if 'items' not in collection: + return [] + resources = collection['items'] + result = [x['apiRawValues']['filename'] for x in resources] + return result + + def exists(self): + collection = self.read_current() + base = os.path.basename(self.want.src) + if any(base == os.path.basename(x) for x in collection): + return True + return False + + def download_from_device(self, dest): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/ucs-downloads/{2}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.src + ) + try: + download_file(self.client, url, dest) + except F5ModuleError: + raise F5ModuleError( + "Failed to download the file." + ) + if os.path.exists(self.want.dest): + return True + return False + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + backup=dict( + default='no', + type='bool' + ), + create_on_missing=dict( + default='yes', + type='bool' + ), + encryption_password=dict(no_log=True), + dest=dict( + type='path' + ), + force=dict( + default='yes', + type='bool' + ), + fail_on_missing=dict( + default='no', + type='bool' + ), + src=dict(), + only_create_file=dict( + default='no', + type='bool' + ), + async_timeout=dict( + type='int', + default=150 + ), + ) + self.required_if = [ + ['only_create_file', 'no', ['dest']], + ['only_create_file', 'yes', ['src']] + ] + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.add_file_common_args = True + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if, + add_file_common_args=spec.add_file_common_args + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_user.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_user.py new file mode 100644 index 00000000..ac5f41ed --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_user.py @@ -0,0 +1,1128 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_user +short_description: Manage user accounts and user attributes on a BIG-IP +description: + - Manage user accounts and user attributes on a BIG-IP system. Typically this + module operates only on REST API users and not CLI users. + When specifying C(root), you may only change the password. + Your other parameters are ignored in this case. Changing the C(root) + password is not an idempotent operation. Therefore, it changes the + password every time this module attempts to change it. +version_added: "1.0.0" +options: + full_name: + description: + - Full name of the user. + type: str + username_credential: + description: + - Name of the user to create, remove, or modify. + - The C(root) user may not be removed. + type: str + required: True + aliases: + - name + password_credential: + description: + - Set the user's password to this unencrypted value. + C(password_credential) is required when creating a new account. + type: str + shell: + description: + - Optionally set the users shell. + type: str + choices: + - bash + - none + - tmsh + partition_access: + description: + - Specifies the administrative partition to which the user has access. + C(partition_access) is required when creating a new account, and + should be in the form "partition:role". + - Valid roles include C(acceleration-policy-editor), C(admin), C(application-editor), + C(auditor), C(certificate-manager), C(guest), C(irule-manager), C(manager), C(no-access), + C(operator), C(resource-admin), C(user-manager), C(web-application-security-administrator), + and C(web-application-security-editor). + - The partition portion the of tuple should be an existing partition or the value 'all'. + type: list + elements: str + state: + description: + - Whether the account should exist or not, taking action if the state is + different from what is stated. + type: str + choices: + - present + - absent + default: present + update_password: + description: + - C(always) allows the user to update passwords. + C(on_create) only sets the password for newly created users. + - When C(username_credential) is C(root), this value is forced to C(always). + type: str + choices: + - always + - on_create + default: always + partition: + description: + - Device partition to manage resources on. + type: str + default: Common +notes: + - Requires BIG-IP versions >= 12.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add the user 'johnd' as an admin + bigip_user: + username_credential: johnd + password_credential: password + full_name: John Doe + partition_access: + - all:admin + update_password: on_create + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Change the user "johnd's" role and shell + bigip_user: + username_credential: johnd + partition_access: + - NewPartition:manager + shell: tmsh + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Make the user 'johnd' an admin and set to advanced shell + bigip_user: + name: johnd + partition_access: + - all:admin + shell: bash + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove the user 'johnd' + bigip_user: + name: johnd + state: absent + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Update password + bigip_user: + state: present + username_credential: johnd + password_credential: newsupersecretpassword + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +# Note that the second time this task runs, it would fail because +# The password has been changed. Therefore, it is recommended that +# you either, +# +# * Put this in its own playbook that you run when you need to +# * Put this task in a `block` +# * Include `ignore_errors` on this task +- name: Change the Admin password + bigip_user: + state: present + username_credential: admin + password_credential: NewSecretPassword + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Change the root user's password + bigip_user: + username_credential: root + password_credential: secret + state: present + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +full_name: + description: Full name of the user. + returned: changed and success + type: str + sample: John Doe +partition_access: + description: + - List of strings containing the user's roles and to which partitions they + are applied. They are specified in the form "partition:role". + returned: changed and success + type: list + sample: ['all:admin'] +shell: + description: The shell assigned to the user account. + returned: changed and success + type: str + sample: tmsh +''' + +import os +import tempfile +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +try: + from BytesIO import BytesIO +except ImportError: + from io import BytesIO + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback, missing_required_lib +) +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_bytes + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, is_empty_list +) +from ..module_utils.icontrol import ( + tmos_version, upload_file +) +from ..module_utils.teem import send_teem + +try: + # Crypto is used specifically for changing the root password via + # tmsh over REST. + # + # We utilize the crypto library to encrypt the contents of a file + # before we upload it, and then decrypt it on-box to change the + # password. + # + # To accomplish such a process, we need to be able to encrypt the + # temporary file with the public key found on the box. + # + # These libraries are used to do the encryption. + # + # Note that, if these are not available, the ability to change the + # root password is disabled and the user will be notified as such + # by a failure of the module. + # + # These libraries *should* be available on most Ansible controllers + # by default though as crypto is a dependency of Ansible. + # + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import padding + HAS_CRYPTO = True +except ImportError: + HAS_CRYPTO = False + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'partitionAccess': 'partition_access', + 'description': 'full_name', + } + + updatables = [ + 'partition_access', + 'full_name', + 'shell', + 'password_credential', + ] + + returnables = [ + 'shell', + 'partition_access', + 'full_name', + 'username_credential', + 'password_credential', + ] + + api_attributes = [ + 'shell', + 'partitionAccess', + 'description', + 'name', + 'password', + ] + + @property + def temp_upload_file(self): + if self._values['temp_upload_file'] is None: + f = tempfile.NamedTemporaryFile() + name = os.path.basename(f.name) + self._values['temp_upload_file'] = name + return self._values['temp_upload_file'] + + +class ApiParameters(Parameters): + @property + def partition_access(self): + if self._values['partition_access'] is None: + return None + result = [] + part_access = self._values['partition_access'] + for access in part_access: + if isinstance(access, dict): + if 'nameReference' in access: + del access['nameReference'] + result.append(access) + else: + result.append(access) + return result + + +class ModuleParameters(Parameters): + @property + def partition_access(self): + if self._values['partition_access'] is None: + return None + if is_empty_list(self._values['partition_access']): + return [] + result = [] + part_access = self._values['partition_access'] + for access in part_access: + if isinstance(access, string_types): + acl = access.split(':') + if acl[0].lower() == 'all': + acl[0] = 'all-partitions' + value = dict( + name=acl[0], + role=acl[1] + ) + result.append(value) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + for returnable in self.returnables: + try: + result[returnable] = getattr(self, returnable) + except Exception: + pass + result = self._filter_params(result) + return result + + +class UsableChanges(Changes): + @property + def password(self): + if self._values['password_credential'] is None: + return None + return self._values['password_credential'] + + +class ReportableChanges(Changes): + @property + def partition_access(self): + if self._values['partition_access'] is None: + return None + result = [] + part_access = self._values['partition_access'] + for access in part_access: + if access['name'] == 'all-partitions': + name = 'all' + else: + name = access['name'] + role = access['role'] + result.append(('{0}:{1}'.format(name, role))) + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def password_credential(self): + if self.want.password_credential is None: + return None + if self.want.update_password in ['always']: + return self.want.password_credential + return None + + @property + def shell(self): + if self.want.shell == 'none' and self.have.shell is None: + return None + if self.want.shell == 'bash': + self._validate_shell_parameter() + if self.want.shell == self.have.shell: + return None + else: + return self.want.shell + if self.want.shell != self.have.shell: + return self.want.shell + + def _validate_shell_parameter(self): + """Method to validate shell parameters. + + Raise when shell attribute is set to 'bash' with roles set to + either 'admin' or 'resource-admin'. + + NOTE: Admin and Resource-Admin roles automatically enable access to + all partitions, removing any other roles that the user might have + had. There are few other roles which do that but those roles, + do not allow bash. + """ + + err = "Shell access is only available to " \ + "'admin' or 'resource-admin' roles." + permit = ['admin', 'resource-admin'] + + have = self.have.partition_access + if not any(r['role'] for r in have if r['role'] in permit): + raise F5ModuleError(err) + + if self.want.partition_access is not None: + want = self.want.partition_access + if not any(r['role'] for r in want if r['role'] in permit): + raise F5ModuleError(err) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.kwargs = kwargs + + def exec_module(self): + if self.is_root_username_credential(): + manager = self.get_manager('root') + elif self.is_version_less_than_13(): + manager = self.get_manager('v1') + else: + manager = self.get_manager('v2') + return manager.exec_module() + + def get_manager(self, type): + if type == 'root': + return RootUserManager(**self.kwargs) + elif type == 'v1': + return UnpartitionedManager(**self.kwargs) + elif type == 'v2': + return PartitionedManager(**self.kwargs) + + def is_version_less_than_13(self): + """Checks to see if the TMOS version is less than 13 + + Anything less than BIG-IP 13.x does not support users + on different partitions. + + :return: Bool + """ + version = tmos_version(self.client) + if Version(version) < Version('13.0.0'): + return True + else: + return False + + def is_root_username_credential(self): + user = self.module.params.get('username_credential', None) + if user == 'root': + return True + return False + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the user.") + return True + + def create(self): + self.validate_create_parameters() + if self.want.shell == 'bash': + self.validate_shell_parameter() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def validate_shell_parameter(self): + """Method to validate shell parameters. + + Raise when shell attribute is set to 'bash' with roles set to + either 'admin' or 'resource-admin'. + + NOTE: Admin and Resource-Admin roles automatically enable access to + all partitions, removing any other roles that the user might have + had. There are few other roles which do that but those roles, + do not allow bash. + """ + + err = "Shell access is only available to " \ + "'admin' or 'resource-admin' roles." + permit = ['admin', 'resource-admin'] + + if self.want.partition_access is not None: + want = self.want.partition_access + if not any(r['role'] for r in want if r['role'] in permit): + raise F5ModuleError(err) + + def validate_create_parameters(self): + if self.want.partition_access is None: + err = "The 'partition_access' option " \ + "is required when creating a resource." + raise F5ModuleError(err) + + +class UnpartitionedManager(BaseManager): + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/auth/user/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.username_credential + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.username_credential + uri = "https://{0}:{1}/mgmt/tm/auth/user/".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) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/auth/user/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.username_credential + ) + 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/auth/user/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.username_credential + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/user/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.username_credential + ) + 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) + return ApiParameters(params=response) + + +class PartitionedManager(BaseManager): + def exists(self): + response = self.list_users_on_device() + if 'items' in response: + collection = [x for x in response['items'] if x['name'] == self.want.username_credential] + if len(collection) == 1: + return True + elif len(collection) == 0: + return False + else: + raise F5ModuleError( + "Multiple users with the provided name were found!" + ) + return False + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.username_credential + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/auth/user/".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, 404, 409, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return True + + def read_current_from_device(self): + response = self.list_users_on_device() + collection = [x for x in response['items'] if x['name'] == self.want.username_credential] + if len(collection) == 1: + user = collection.pop() + return ApiParameters(params=user) + elif len(collection) == 0: + raise F5ModuleError( + "No accounts with the provided name were found." + ) + else: + raise F5ModuleError( + "Multiple users with the provided name were found!" + ) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/auth/user/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.username_credential + ) + 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'] in [400, 404, 409, 403]: + if 'message' in response: + if 'updated successfully' not in response['message']: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/user/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.username_credential + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def list_users_on_device(self): + uri = "https://{0}:{1}/mgmt/tm/auth/user/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + query = "?$filter=partition+eq+'{0}'".format(self.want.partition) + resp = self.client.api.get(uri + query) + 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) + return response + + +class RootUserManager(BaseManager): + def exec_module(self): + if not HAS_CRYPTO: + raise F5ModuleError( + "An installed and up-to-date python 'cryptography' package is " + "required to change the 'root' password." + ) + + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + raise F5ModuleError( + "You may not remove the root user." + ) + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def exists(self): + return True + + def update(self): + public_key = self.get_public_key_from_device() + public_key = self.extract_key(public_key) + encrypted = self.encrypt_password_change_file( + public_key, self.want.password_credential + ) + self.upload_to_device(encrypted, self.want.temp_upload_file) + result = self.update_on_device() + self.remove_uploaded_file_from_device(self.want.temp_upload_file) + return result + + def encrypt_password_change_file(self, public_key, password): + # This function call requires that the public_key be expressed in bytes + pub = serialization.load_pem_public_key( + to_bytes(public_key), + backend=default_backend() + ) + + message = to_bytes("{0}\n{0}\n".format(password)) + ciphertext = pub.encrypt( + message, + + # OpenSSL craziness + # + # Using this padding because it is the only one that works with + # the OpenSSL on BIG-IP at this time. + padding.PKCS1v15(), + + # + # OAEP is the recommended padding to use for encrypting, however, two + # things are wrong with it on BIG-IP. + # + # The first is that one of the parameters required to decrypt the data + # is not supported by the OpenSSL version on BIG-IP. A "parameter setting" + # error is raised when you attempt to use the OAEP parameters to specify + # hashing algorithms. + # + # This is validated by this thread here + # + # https://mta.openssl.org/pipermail/openssl-dev/2017-September/009745.html + # + # Were is supported, we could use OAEP, but the second problem is that OAEP + # is not the default mode of the ``openssl`` command. Therefore, we need + # to adjust the command we use to decrypt the encrypted file when it is + # placed on BIG-IP. + # + # The correct (and recommended if BIG-IP ever upgrades OpenSSL) code is + # shown below. + # + # padding.OAEP( + # mgf=padding.MGF1(algorithm=hashes.SHA256()), + # algorithm=hashes.SHA256(), + # label=None + # ) + # + # Additionally, the code in ``update_on_device()`` would need to be changed + # to pass the correct command line arguments to decrypt the file. + ) + return BytesIO(ciphertext) + + def extract_key(self, content): + """Extracts the public key from the openssl command output over REST + + The REST output includes some extra output that is not relevant to the + public key. This function attempts to only return the valid public key + data from the openssl output + + Args: + content: The output from the REST API command to view the public key. + + Returns: + string: The discovered public key + """ + + lines = content.split("\n") + start = lines.index('-----BEGIN PUBLIC KEY-----') + end = lines.index('-----END PUBLIC KEY-----') + result = "\n".join(lines[start:end + 1]) + return result + + def update_on_device(self): + errors = ['Bad password', 'password change canceled', 'based on a dictionary word'] + + # Decrypting logic + # + # The following commented out command will **not** work on BIG-IP versions + # utilizing OpenSSL 1.0.11-fips (15 Jan 2015). + # + # The reason is because that version of OpenSSL does not support the various + # ``-pkeyopt`` parameters shown below. + # + # Nevertheless, I am including it here as a possible future enhancement in + # case the method currently in use stops working. + # + # This command overrides defaults provided by OpenSSL because I am not + # sure how long the defaults will remain the defaults. Probably as long + # as it took OpenSSL to reach 1.0... + # + # openssl = [ + # 'openssl', 'pkeyutl', '-in', '/var/config/rest/downloads/{0}'.format(self.want.temp_upload_file), + # '-decrypt', '-inkey', '/config/ssl/ssl.key/default.key', + # '-pkeyopt', 'rsa_padding_mode:oaep', '-pkeyopt', 'rsa_oaep_md:sha256', + # '-pkeyopt', 'rsa_mgf1_md:sha256' + # ] + # + # The command we actually use is (while not recommended) also the only one + # that works. It forgoes the usage of OAEP and uses the defaults that come + # with OpenSSL (PKCS1v15) + # + # See this link for information on the parameters used + # + # https://www.openssl.org/docs/manmaster/man1/pkeyutl.html + # + # If you change the command below, you will need to additionally change + # how the encryption is done in ``encrypt_password_change_file()``. + # + openssl = [ + 'openssl', 'pkeyutl', '-in', '/var/config/rest/downloads/{0}'.format(self.want.temp_upload_file), + '-decrypt', '-inkey', '/config/ssl/ssl.key/default.key', + ] + cmd = '-c "{0} | tmsh modify auth password root"'.format(' '.join(openssl)) + + params = dict( + command='run', + utilCmdArgs=cmd + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + if 'commandResult' in response: + if any(x for x in errors if x in response['commandResult']): + raise F5ModuleError(response['commandResult']) + 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) + return True + + def upload_to_device(self, content, name): + """Uploads a file-like object via the REST API to a given filename + + Args: + content: The file-like object whose content to upload + name: The remote name of the file to store the content in. The + final location of the file will be in /var/config/rest/downloads. + + Returns: + void + """ + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + + def remove_uploaded_file_from_device(self, name): + filepath = '/var/config/rest/downloads/{0}'.format(name) + params = { + "command": "run", + "utilCmdArgs": filepath + } + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".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 get_public_key_from_device(self): + cmd = '-c "openssl rsa -in /config/ssl/ssl.key/default.key -pubout"' + + params = dict( + command='run', + utilCmdArgs=cmd + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".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) + if 'commandResult' in response: + return response['commandResult'] + return None + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + username_credential=dict( + required=True, + aliases=['name'] + ), + password_credential=dict( + no_log=True, + ), + partition_access=dict( + type='list', + elements='str', + ), + full_name=dict(), + shell=dict( + choices=['none', 'bash', 'tmsh'] + ), + update_password=dict( + default='always', + choices=['always', 'on_create'], + no_log=False + ), + state=dict(default='present', choices=['absent', 'present']), + 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) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_vcmp_guest.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_vcmp_guest.py new file mode 100644 index 00000000..ddccc7be --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_vcmp_guest.py @@ -0,0 +1,1025 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_vcmp_guest +short_description: Manages vCMP guests on a BIG-IP +description: + - Manages vCMP (Virtual Clustered Multiprocessing) guests on a BIG-IP. This functionality + only exists on actual hardware and must be enabled by provisioning C(vcmp) with the + C(bigip_provision) module. +version_added: "1.0.0" +options: + name: + description: + - The name of the vCMP guest to manage. + type: str + required: True + vlans: + description: + - VLANs the guest uses to communicate with other guests, the host, and with + the external network. The available VLANs in the list are those that are + currently configured on the vCMP host. + - The order of these VLANs is not important and is ignored. This module + orders the VLANs automatically. Therefore, if you deliberately re-order them + in subsequent tasks, this module does B(not) register a change. + type: list + elements: str + initial_image: + description: + - Specifies the base software release ISO image file for installing the TMOS + hypervisor instance and any licensed BIG-IP modules onto the guest's virtual + disk. When creating a new guest, this parameter is required. Ensure + this image is present on the VCMP host and not only on the VCMP guest. Also, the + file reference for this image should be the one present on the host and not on the + guest. + type: str + initial_hotfix: + description: + - Specifies the hotfix ISO image file which is applied on top of the base + image. + type: str + mgmt_network: + description: + - Specifies the method by which the management address is used in the vCMP guest. + - When C(bridged), specifies the guest can communicate with the vCMP host's + management network. + - When C(isolated), specifies the guest is isolated from the vCMP host's + management network. In this case, the only way a guest can communicate + with the vCMP host is through the console port or through a self IP address + on the guest that allows traffic through port 22. + - When C(host only), prevents the guest from installing images and hotfixes other + than those provided by the hypervisor. + - If the guest setting is C(isolated) or C(host only), the C(mgmt_address) does + not apply. + - For mode changing, changing C(bridged) to C(isolated) causes the vCMP + host to remove all of the guest's management interfaces from its bridged + management network. This immediately disconnects the guest's VMs from the + physical management network. Changing C(isolated) to C(bridged) causes the + vCMP host to dynamically add the guest's management interfaces to the bridged + management network. This immediately connects all of the guest's VMs to the + physical management network. Changing this property while the guest is in the + C(configured) or C(provisioned) state has no immediate effect. + type: str + choices: + - bridged + - isolated + - host only + delete_virtual_disk: + description: + - When C(state) is C(absent), the system additionally deletes the virtual disk associated + with the vCMP guest. By default, this value is C(no). + type: bool + default: no + mgmt_address: + description: + - Specifies the IP address and subnet or subnet mask you use to access + the guest when you want to manage a module running within the guest. This + parameter is required if the C(mgmt_network) parameter is C(bridged). + - When creating a new guest, if you do not specify a network or network mask, + a default of C(/24) (C(255.255.255.0)) is used. + type: str + mgmt_route: + description: + - Specifies the gateway address for the C(mgmt_address). + - If this value is not specified when creating a new guest, it is set to C(none). + - The value C(none) can be used during an update to remove this value. + type: str + state: + description: + - The state of the vCMP guest on the system. Each state implies the actions of + all states before it. + - When C(configured), guarantees the vCMP guest exists with the provided + attributes. Additionally, ensures the vCMP guest is turned off. + - When C(disabled), behaves the same way as C(configured), it is just a more + user-friendly name. + - When C(provisioned), ensures the guest is created and installed. + This state does not start the guest; use C(deployed) for that purpose. This state + is one step beyond C(present), as C(present) does not install the guest; + only sets up the configuration for it to be installed. + - When C(present), ensures the guest is properly provisioned and starts + the guest so that it is in a running state. + - When C(absent), removes the vCMP from the system. + type: str + choices: + - configured + - disabled + - provisioned + - present + - absent + default: present + cores_per_slot: + description: + - Specifies the number of cores the system allocates to the guest. + - Each core represents a portion of CPU and memory. Therefore, the amount of + memory allocated per core is directly tied to the amount of CPU. This amount + of memory varies per hardware platform type. + - The number you can specify depends on the type of hardware you have. + - In the event of a reboot, the system persists the guest to the same slot on + which it ran prior to the reboot. + type: int + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + number_of_slots: + description: + - Specifies the number of slots for the system to use when creating the guest. + - This value dictates how many cores a guest is allocated from each slot to which + it is assigned. + - Possible values are dependent on the type of blades being used in this cluster. + - The default value depends on the type of blades being used in this cluster. + type: int + min_number_of_slots: + description: + - Specifies the minimum number of slots the guest must be assigned to in + order to deploy. + - This field dictates the number of slots to which the guest must be assigned. + - At the end of any allocation attempt, if the guest is not assigned to at least + this many slots, the attempt fails and the change that initiated it is reverted. + - A guest's C(min_number_of_slots) value cannot be greater than its C(number_of_slots). + type: int + allowed_slots: + description: + - Contains those slots to which the guest is allowed to be assigned. + - When the host determines which slots this guest should be assigned, only slots + in this list are considered. + - This is a good way to force guests to be assigned only to particular slots, or, + by configuring disjoint C(allowed_slots) on two guests, that those guests are + never assigned to the same slot. + - By default, this list includes every available slot in the cluster. This means + the guest may be assigned to any slot by default. + type: list + elements: str +notes: + - This module can take a lot of time to deploy vCMP guests. This is an intrinsic + limitation of the vCMP system, because it is booting real VMs on the BIG-IP + device. This boot time is very similar in length to the time it takes to + boot VMs on any other virtualization platform; public or private. + - When BIG-IP starts, the VMs are booted sequentially; not in parallel. This + means it is not unusual for a vCMP host with many guests to take a + long time (60+ minutes) to reboot and bring all the guests online. The + BIG-IP chassis will be available before all vCMP guests are online. +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create a vCMP guest + bigip_vcmp_guest: + name: foo + mgmt_network: bridge + mgmt_address: 10.20.30.40/24 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a vCMP guest with specific VLANs + bigip_vcmp_guest: + name: foo + mgmt_network: bridge + mgmt_address: 10.20.30.40/24 + vlans: + - vlan1 + - vlan2 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove vCMP guest and disk + bigip_vcmp_guest: + name: guest1 + state: absent + delete_virtual_disk: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + register: result +''' + +RETURN = r''' +vlans: + description: The VLANs assigned to the vCMP guest, in their full path format. + returned: changed + type: list + sample: ['/Common/vlan1', '/Common/vlan2'] +''' + +import time +from collections import namedtuple +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ipaddress import ip_interface + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, fq_name, f5_argument_spec +) +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.urls import parseStats +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'managementGw': 'mgmt_route', + 'managementNetwork': 'mgmt_network', + 'managementIp': 'mgmt_address', + 'initialImage': 'initial_image', + 'initialHotfix': 'initial_hotfix', + 'virtualDisk': 'virtual_disk', + 'coresPerSlot': 'cores_per_slot', + 'slots': 'number_of_slots', + 'minSlots': 'min_number_of_slots', + 'allowedSlots': 'allowed_slots', + } + + api_attributes = [ + 'vlans', + 'managementNetwork', + 'managementIp', + 'initialImage', + 'initialHotfix', + 'managementGw', + 'state', + 'coresPerSlot', + 'slots', + 'minSlots', + 'allowedSlots', + ] + + returnables = [ + 'vlans', + 'mgmt_network', + 'mgmt_address', + 'initial_image', + 'initial_hotfix', + 'mgmt_route', + 'name', + 'cores_per_slot', + 'number_of_slots', + 'min_number_of_slots', + 'allowed_slots', + ] + + updatables = [ + 'vlans', + 'mgmt_network', + 'mgmt_address', + 'initial_image', + 'initial_hotfix', + 'mgmt_route', + 'state', + 'cores_per_slot', + 'number_of_slots', + 'min_number_of_slots', + 'allowed_slots', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def mgmt_route(self): + if self._values['mgmt_route'] is None: + return None + elif self._values['mgmt_route'] == 'none': + return 'none' + if is_valid_ip(self._values['mgmt_route']): + return self._values['mgmt_route'] + else: + raise F5ModuleError( + "The specified 'mgmt_route' is not a valid IP address." + ) + + @property + def mgmt_address(self): + if self._values['mgmt_address'] is None: + return None + try: + addr = ip_interface(u'%s' % str(self._values['mgmt_address'])) + return str(addr.with_prefixlen) + except ValueError: + raise F5ModuleError( + "The specified 'mgmt_address' is not a valid IP address." + ) + + @property + def mgmt_tuple(self): + Destination = namedtuple('Destination', ['ip', 'subnet']) + try: + parts = self._values['mgmt_address'].split('/') + if len(parts) == 2: + result = Destination(ip=parts[0], subnet=parts[1]) + elif len(parts) < 2: + result = Destination(ip=parts[0], subnet=None) + else: + raise F5ModuleError( + "The provided mgmt_address is malformed." + ) + except ValueError: + result = Destination(ip=None, subnet=None) + return result + + @property + def state(self): + if self._values['state'] == 'present': + return 'deployed' + elif self._values['state'] in ['configured', 'disabled']: + return 'configured' + return self._values['state'] + + @property + def vlans(self): + if self._values['vlans'] is None: + return None + result = [fq_name(self.partition, x) for x in self._values['vlans']] + result.sort() + return result + + @property + def initial_image(self): + if self._values['initial_image'] is None: + return None + if self.initial_image_exists(self._values['initial_image']): + return self._values['initial_image'] + raise F5ModuleError( + "The specified 'initial_image' does not exist on the remote device." + ) + + @property + def initial_hotfix(self): + if self._values['initial_hotfix'] is None: + return None + if self.initial_hotfix_exists(self._values['initial_hotfix']): + return self._values['initial_hotfix'] + raise F5ModuleError( + "The specified 'initial_hotfix' does not exist on the remote device." + ) + + def initial_image_exists(self, image): + uri = "https://{0}:{1}/mgmt/tm/sys/software/image/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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 + for resource in response['items']: + if resource['name'].startswith(image): + return True + return False + + def initial_hotfix_exists(self, hotfix): + uri = "https://{0}:{1}/mgmt/tm/sys/software/hotfix/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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 + for resource in response['items']: + if resource['name'].startswith(hotfix): + return True + return False + + @property + def allowed_slots(self): + if self._values['allowed_slots'] is None: + return None + result = self._values['allowed_slots'] + result.sort() + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + change = getattr(self, returnable) + if isinstance(change, dict): + result.update(change) + else: + result[returnable] = change + result = self._filter_params(result) + except Exception: + pass + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def mgmt_address(self): + want = self.want.mgmt_tuple + if want.subnet is None: + raise F5ModuleError( + "A subnet must be specified when changing the mgmt_address." + ) + if self.want.mgmt_address != self.have.mgmt_address: + return self.want.mgmt_address + + @property + def allowed_slots(self): + if self.want.allowed_slots is None: + return None + if self.have.allowed_slots is None: + return self.want.allowed_slots + if set(self.want.allowed_slots) != set(self.have.allowed_slots): + return self.want.allowed_slots + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(client=self.client, params=self.module.params) + self.have = None + self.changes = ReportableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ['configured', 'provisioned', 'deployed']: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + if self.changes.cores_per_slot: + if not self.is_configured(): + self.configure() + self.update_on_device() + if self.want.state == 'provisioned': + self.provision() + elif self.want.state == 'deployed': + self.deploy() + elif self.want.state == 'configured': + self.configure() + return True + + def remove(self): + if self.module.check_mode: + return True + if self.want.delete_virtual_disk: + self.have = self.read_current_from_device() + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + if self.want.delete_virtual_disk: + self.remove_virtual_disk() + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + if self.want.mgmt_tuple.subnet is None: + self.want.update(dict( + mgmt_address='{0}/255.255.255.0'.format(self.want.mgmt_tuple.ip) + )) + self.create_on_device() + if self.want.state == 'provisioned': + self.provision() + elif self.want.state == 'deployed': + self.deploy() + elif self.want.state == 'configured': + self.configure() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/".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) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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/vcmp/guest/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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) + return ApiParameters(params=response) + + def remove_virtual_disk(self): + if self.virtual_disk_exists(): + return self.remove_virtual_disk_from_device() + return False + + def get_virtual_disks_on_device(self): + uri = "https://{0}:{1}/mgmt/tm/vcmp/virtual-disk/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + + if 'items' in response: + return response + + def virtual_disk_exists(self): + """Checks if a virtual disk exists for a guest + + The virtual disk names can differ based on the device vCMP is installed on. + For instance, on a shuttle-series device with no slots, you will see disks + that resemble the following + + guest1.img + + On an 8-blade Viprion with slots though, you will see + + guest1.img/1 + + The "/1" in this case is the slot that it is a part of. This method looks + for the virtual-disk without the trailing slot. + + Returns: + dict + """ + response = self.get_virtual_disks_on_device() + check = '{0}'.format(self.have.virtual_disk) + for resource in response['items']: + if resource['name'].startswith(check): + return True + else: + return False + + def remove_virtual_disk_from_device(self): + check = '{0}'.format(self.have.virtual_disk) + response = self.get_virtual_disks_on_device() + for resource in response['items']: + if resource['name'].startswith(check): + uri = "https://{0}:{1}/mgmt/tm/vcmp/virtual-disk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + resource['name'].replace('/', '~') + ) + response = self.client.api.delete(uri) + + if response.status == 200: + continue + else: + raise F5ModuleError(response.content) + + return True + + def is_configured(self): + """Checks to see if guest is disabled + + A disabled guest is fully disabled once their Stats go offline. + Until that point they are still in the process of disabling. + + :return: + """ + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status == 404 or 'code' in response and response['code'] == 404: + return True + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + return False + + def is_provisioned(self): + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError: + return False + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + + result = parseStats(response) + + if 'stats' in result: + if result['stats']['requestedState'] == 'provisioned': + if result['stats']['vmStatus'] == 'stopped': + return True + return False + + def is_deployed(self): + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.name + ) + resp = self.client.api.get(uri) + + try: + response = resp.json() + except ValueError: + return False + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + + result = parseStats(response) + + if 'stats' in result: + if result['stats']['requestedState'] == 'deployed': + if result['stats']['vmStatus'] == 'running': + return True + return False + + def configure(self): + if self.is_configured(): + return False + self.configure_on_device() + self.wait_for_configured() + return True + + def configure_on_device(self): + params = dict(state='configured') + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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 wait_for_configured(self): + nops = 0 + while nops < 3: + if self.is_configured(): + nops += 1 + time.sleep(1) + + def provision(self): + if self.is_provisioned(): + return False + self.provision_on_device() + self.wait_for_provisioned() + + def provision_on_device(self): + params = dict(state='provisioned') + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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 wait_for_provisioned(self): + nops = 0 + while nops < 3: + if self.is_provisioned(): + nops += 1 + time.sleep(1) + + def deploy(self): + if self.is_deployed(): + return False + self.deploy_on_device() + self.wait_for_deployed() + + def deploy_on_device(self): + params = dict(state='deployed') + uri = "https://{0}:{1}/mgmt/tm/vcmp/guest/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 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 wait_for_deployed(self): + nops = 0 + while nops < 3: + if self.is_deployed(): + nops += 1 + time.sleep(1) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + vlans=dict( + type='list', + elements='str', + ), + mgmt_network=dict(choices=['bridged', 'isolated', 'host only']), + mgmt_address=dict(), + mgmt_route=dict(), + initial_image=dict(), + initial_hotfix=dict(), + state=dict( + default='present', + choices=['configured', 'disabled', 'provisioned', 'absent', 'present'] + ), + delete_virtual_disk=dict( + type='bool', + default='no' + ), + cores_per_slot=dict(type='int'), + number_of_slots=dict(type='int'), + min_number_of_slots=dict(type='int'), + allowed_slots=dict( + type='list', + elements='str', + ), + 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.required_if = [ + ['mgmt_network', 'bridged', ['mgmt_address']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_address.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_address.py new file mode 100644 index 00000000..28e0d7f8 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_address.py @@ -0,0 +1,902 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_virtual_address +short_description: Manage LTM virtual addresses on a BIG-IP +description: + - Manage LTM virtual addresses on a BIG-IP system. +version_added: "1.0.0" +options: + name: + description: + - Name of the virtual address. + - If this parameter is not provided, the system uses the value of C(address). + type: str + address: + description: + - Specifies the virtual address. This value cannot be modified after it is set. + - If you never created a virtual address, but did create virtual servers, + a virtual address for each virtual server was created automatically. The name + of this virtual address is its IP address value. + type: str + netmask: + description: + - Specifies the netmask of the provided virtual address. This value cannot be + modified after it is set. + - When creating a new virtual address, if this parameter is not specified, the + default value is C(255.255.255.255) for IPv4 addresses and + C(ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) for IPv6 addresses. + type: str + connection_limit: + description: + - Specifies the number of concurrent connections the system + allows on this virtual address. + type: int + arp: + description: + - Specifies whether the system accepts ARP requests. + - When C(no), specifies the system does not accept ARP requests. + - When C(yes), the packets are dropped. + - Both ARP and ICMP Echo must be disabled in order for forwarding + virtual servers using that virtual address to forward ICMP packets. + - When creating a new virtual address, if this parameter is not specified, + the default value is C(yes). + type: bool + auto_delete: + description: + - Specifies whether the system automatically deletes the virtual + address with the deletion of the last associated virtual server. + When C(no), specifies the system leaves the virtual + address, even when all associated virtual servers have been deleted. + When creating the virtual address, the default value is C(yes). + type: bool + icmp_echo: + description: + - Specifies how the system sends responses to (ICMP) echo requests + on a per-virtual address basis for enabling route advertisement. + When C(enabled), the BIG-IP system intercepts ICMP echo request + packets and responds to them directly. When C(disabled), the BIG-IP + system passes ICMP echo requests through to the backend servers. + When (selective), causes the BIG-IP system to internally enable or + disable responses based on virtual server state; C(when_any_available), + C(when_all_available, or C(always), regardless of the state of any + virtual servers. + type: str + choices: + - enabled + - disabled + - selective + state: + description: + - The virtual address state. If C(absent), the system makes an attempt + to delete the virtual address. This will only succeed if this + virtual address is not in use by a virtual server. C(present) creates + the virtual address and enables it. If C(enabled), enables the virtual + address if it exists. If C(disabled), creates the virtual address if + needed, and sets the state to C(disabled). + type: str + choices: + - present + - absent + - enabled + - disabled + default: present + availability_calculation: + description: + - Specifies which routes of the virtual address the system advertises. + When C(when_any_available), advertises the route when any virtual + server is available. When C(when_all_available), advertises the + route when all virtual servers are available. When (always), always + advertises the route regardless of the virtual servers available. + type: str + choices: + - always + - when_all_available + - when_any_available + aliases: ['advertise_route'] + route_advertisement: + description: + - Specifies whether the system uses route advertisement for this + virtual address. + - When disabled, the system does not advertise routes for this virtual address. + - The majority of these options are only supported on versions 13.0.0-HF1 or + later. On versions prior than this, all choices expect C(disabled) + translate to C(enabled). + - When C(always), the BIG-IP system always advertises the route for the + virtual address, regardless of availability status. This requires an C(enabled) + virtual address. + - When C(enabled), the BIG-IP system advertises the route for the available + virtual address, based on the calculation method in the availability calculation. + - When C(disabled), the BIG-IP system does not advertise the route for the virtual + address, regardless of the availability status. + - When C(selective), you can also selectively enable ICMP echo responses, which + causes the BIG-IP system to internally enable or disable responses based on + virtual server state. + - When C(any), the BIG-IP system advertises the route for the virtual address + when any virtual server is available. + - When C(all), the BIG-IP system advertises the route for the virtual address + when all virtual servers are available. + type: str + choices: + - disabled + - enabled + - always + - selective + - any + - all + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + traffic_group: + description: + - The traffic group for the virtual address. When creating a new address, + if this value is not specified, the default is C(/Common/traffic-group-1). + type: str + route_domain: + description: + - The route domain of the C(address) you want to use. + - This value cannot be modified after it is set. + type: str + spanning: + description: + - Enables all BIG-IP systems in a device group to listen for and process traffic + on the same virtual address. + - Spanning for a virtual address occurs when you enable the C(spanning) option on a + device, and then sync the virtual address to the other members of the device group. + - Spanning also relies on the upstream router to distribute application flows to the + BIG-IP systems using ECMP routes. ECMP defines a route to the virtual address using + distinct Floating self-IP addresses configured on each BIG-IP system. + - You must also configure MAC masquerade addresses and disable C(arp) on the virtual + address when Spanning is enabled. + - When creating a new virtual address, if this parameter is not specified, the default + valus is C(no). + type: bool +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add virtual address + bigip_virtual_address: + state: present + partition: Common + address: 10.10.10.10 + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Enable route advertisement on the virtual address + bigip_virtual_address: + state: present + address: 10.10.10.10 + route_advertisement: any + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +availability_calculation: + description: Specifies which routes of the virtual address the system advertises. + returned: changed + type: str + sample: always +auto_delete: + description: New setting for auto deleting virtual address. + returned: changed + type: bool + sample: yes +icmp_echo: + description: New ICMP echo setting applied to virtual address. + returned: changed + type: str + sample: disabled +connection_limit: + description: The new connection limit of the virtual address. + returned: changed + type: int + sample: 1000 +netmask: + description: The netmask of the virtual address. + returned: created + type: int + sample: 2345 +arp: + description: The new way the virtual address handles ARP requests. + returned: changed + type: bool + sample: yes +address: + description: The address of the virtual address. + returned: created + type: int + sample: 2345 +state: + description: The new state of the virtual address. + returned: changed + type: str + sample: disabled +spanning: + description: Whether spanning is enabled or not. + returned: changed + type: str + sample: disabled +''' +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) +from ansible.module_utils.parsing.convert_bool import ( + BOOLEANS_TRUE, BOOLEANS_FALSE +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name, flatten_boolean +) +from ..module_utils.icontrol import tmos_version +from ..module_utils.ipaddress import ( + is_valid_ip, compress_address +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'routeAdvertisement': 'route_advertisement_type', + 'autoDelete': 'auto_delete', + 'icmpEcho': 'icmp_echo', + 'connectionLimit': 'connection_limit', + 'serverScope': 'availability_calculation', + 'mask': 'netmask', + 'trafficGroup': 'traffic_group', + } + + updatables = [ + 'route_advertisement_type', + 'auto_delete', + 'icmp_echo', + 'connection_limit', + 'arp', + 'enabled', + 'availability_calculation', + 'traffic_group', + 'spanning', + ] + + returnables = [ + 'route_advertisement_type', + 'auto_delete', + 'icmp_echo', + 'connection_limit', + 'netmask', + 'arp', + 'address', + 'state', + 'traffic_group', + 'route_domain', + 'spanning', + 'availability_calculation', + ] + + api_attributes = [ + 'routeAdvertisement', + 'autoDelete', + 'icmpEcho', + 'connectionLimit', + 'advertiseRoute', + 'arp', + 'mask', + 'enabled', + 'serverScope', + 'trafficGroup', + 'spanning', + 'serverScope', + ] + + @property + def availability_calculation(self): + if self._values['availability_calculation'] is None: + return None + elif self._values['availability_calculation'] in ['any', 'when_any_available']: + return 'any' + elif self._values['availability_calculation'] in ['all', 'when_all_available']: + return 'all' + elif self._values['availability_calculation'] in ['none', 'always']: + return 'none' + + @property + def connection_limit(self): + if self._values['connection_limit'] is None: + return None + return int(self._values['connection_limit']) + + @property + def enabled(self): + if self._values['state'] in ['enabled', 'present']: + return 'yes' + elif self._values['enabled'] in BOOLEANS_TRUE: + return 'yes' + elif self._values['state'] == 'disabled': + return 'no' + elif self._values['enabled'] in BOOLEANS_FALSE: + return 'no' + else: + return None + + @property + def netmask(self): + if self._values['netmask'] is None: + return None + if is_valid_ip(self._values['netmask']): + return self._values['netmask'] + else: + raise F5ModuleError( + "The provided 'netmask' is not a valid IP address" + ) + + @property + def auto_delete(self): + result = flatten_boolean(self._values['auto_delete']) + if result == 'yes': + return 'true' + if result == 'no': + return 'false' + + @property + def state(self): + if self.enabled == 'yes' and self._values['state'] != 'present': + return 'enabled' + elif self.enabled == 'no': + return 'disabled' + else: + return self._values['state'] + + @property + def traffic_group(self): + if self._values['traffic_group'] is None: + return None + else: + result = fq_name(self.partition, self._values['traffic_group']) + if result.startswith('/Common/'): + return result + else: + raise F5ModuleError( + "Traffic groups can only exist in /Common" + ) + + @property + def route_advertisement_type(self): + if self.route_advertisement: + return self.route_advertisement + else: + return self._values['route_advertisement_type'] + + @property + def route_advertisement(self): + if self._values['route_advertisement'] is None: + return None + version = tmos_version(self.client) + if Version(version) <= Version('13.0.0'): + if self._values['route_advertisement'] == 'disabled': + return 'disabled' + else: + return 'enabled' + else: + return self._values['route_advertisement'] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def address(self): + if self._values['address'] is None: + return None + if is_valid_ip(self._values['address']): + return compress_address(self._values['address']) + else: + raise F5ModuleError( + "The provided 'address' is not a valid IP address" + ) + + @property + def full_address(self): + if self.route_domain is not None: + return '{0}%{1}'.format(self.address, self.route_domain) + return self.address + + @property + def name(self): + if self._values['name'] is None: + result = str(self.address) + if self.route_domain: + result = "{0}%{1}".format(result, self.route_domain) + else: + result = self._values['name'] + return result + + @property + def route_domain(self): + if self._values['route_domain'] is None: + return None + try: + return int(self._values['route_domain']) + except ValueError: + uri = "https://{0}:{1}/mgmt/tm/net/route-domain/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self._values['partition'], self._values['route_domain']) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + raise F5ModuleError( + "The specified 'route_domain' was not found." + ) + if resp.status == 404 or 'code' in response and response['code'] == 404: + raise F5ModuleError( + "The specified 'route_domain' was not found." + ) + + return int(response['id']) + + @property + def arp(self): + result = flatten_boolean(self._values['arp']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + @property + def spanning(self): + result = flatten_boolean(self._values['spanning']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def address(self): + if self._values['address'] is None: + return None + if self._values['route_domain'] is None: + return self._values['address'] + result = "{0}%{1}".format(self._values['address'], self.route_domain) + return result + + +class ReportableChanges(Changes): + @property + def arp(self): + if self._values['arp'] == 'disabled': + return 'no' + elif self._values['arp'] == 'enabled': + return 'yes' + + @property + def spanning(self): + if self._values['spanning'] == 'disabled': + return 'no' + elif self._values['spanning'] == 'enabled': + return 'yes' + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def traffic_group(self): + if self.want.traffic_group != self.have.traffic_group: + return self.want.traffic_group + + @property + def spanning(self): + if self.want.spanning is None: + return None + if self.want.spanning != self.have.spanning: + return self.want.spanning + + @property + def arp(self): + if self.want.arp is None: + return None + if self.want.arp != self.have.arp: + return self.want.arp + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = ApiParameters() + self.want = ModuleParameters(client=self.client, params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state in ['present', 'enabled', 'disabled']: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + + if self.module._diff and self.have: + result['diff'] = self.make_diff() + + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _grab_attr(self, item): + result = dict() + updatables = Parameters.updatables + for k in updatables: + if getattr(item, k) is not None: + result[k] = getattr(item, k) + return result + + def make_diff(self): + result = dict(before=self._grab_attr(self.have), after=self._grab_attr(self.want)) + return result + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + changed = False + if self.exists(): + changed = self.remove() + return changed + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the virtual address") + return True + + def create(self): + self._set_changed_options() + + if self.want.traffic_group is None: + self.want.update({'traffic_group': '/Common/traffic-group-1'}) + if self.want.arp is None: + self.want.update({'arp': True}) + if self.want.spanning is None: + self.want.update({'spanning': False}) + + if self.want.netmask is None: + if is_valid_ip(self.want.address, type='ipv4'): + self.want.update({'netmask': '255.255.255.255'}) + else: + self.want.update({'netmask': 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'}) + + if self.want.arp == 'enabled' and self.want.spanning == 'enabled': + raise F5ModuleError( + "'arp' and 'spanning' cannot both be enabled on virtual address." + ) + if self.module.check_mode: + return True + self.create_on_device() + if self.exists(): + return True + else: + raise F5ModuleError("Failed to create the virtual address") + + def update(self): + self.have = self.read_current_from_device() + if self.want.netmask is not None: + if self.have.netmask != self.want.netmask: + raise F5ModuleError( + "The netmask cannot be changed. Delete and recreate " + "the virtual address if you need to do this." + ) + if self.want.address is not None: + if self.have.address != self.want.full_address: + raise F5ModuleError( + "The address cannot be changed. Delete and recreate " + "the virtual address if you need to do this." + ) + if self.changes.arp == 'enabled' and self.changes.spanning == 'enabled': + raise F5ModuleError( + "'arp' and 'spanning' cannot both be enabled on virtual address." + ) + + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual-address/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual-address/{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) + return ApiParameters(params=response) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual-address/{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 create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + params['address'] = self.changes.address + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual-address/".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + return response['selfLink'] + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual-address/{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 + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + state=dict( + default='present', + choices=['present', 'absent', 'disabled', 'enabled'] + ), + name=dict(), + address=dict(), + netmask=dict(), + connection_limit=dict( + type='int' + ), + + auto_delete=dict( + type='bool' + ), + icmp_echo=dict( + choices=['enabled', 'disabled', 'selective'], + ), + availability_calculation=dict( + choices=['always', 'when_all_available', 'when_any_available'], + aliases=['advertise_route'] + ), + traffic_group=dict(), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + route_domain=dict(), + spanning=dict(type='bool'), + route_advertisement=dict( + choices=[ + 'disabled', + 'enabled', + 'always', + 'selective', + 'any', + 'all', + ] + ), + arp=dict(type='bool'), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_one_of = [ + ['name', 'address'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_one_of=spec.required_one_of + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() 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 new file mode 100644 index 00000000..29a0808d --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_virtual_server.py @@ -0,0 +1,3822 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_virtual_server +short_description: Manage LTM virtual servers on a BIG-IP +description: + - Manage LTM virtual servers on a BIG-IP system. +version_added: "1.0.0" +options: + state: + description: + - The virtual server state. If C(absent), deletes the virtual server + if it exists. If C(present), creates the virtual server and enables it. + If C(enabled), enables the virtual server if it exists. If C(disabled), + creates the virtual server if needed, and sets the state to C(disabled). + - Attempting to change C(state) on a virtual server that belongs to an iAPP with strict updates enabled will result + in an error message returned by device, unless C(insert_metadata) parameter is set to C(no). + type: str + choices: + - present + - absent + - enabled + - disabled + default: present + type: + description: + - Specifies the network service provided by this virtual server. + - When creating a new virtual server, if this parameter is not provided, the + default is C(standard). + - This value cannot be changed after it is set. + - When C(standard), specifies a virtual server that directs client traffic to + a load balancing pool, and is the most basic type of virtual server. When you + first create the virtual server, you assign an existing default pool to it. + From then on, the virtual server automatically directs traffic to that default pool. + - When C(forwarding-l2), specifies a virtual server that shares the same IP address as a + node in an associated VLAN. + - When C(forwarding-ip), specifies a virtual server like other virtual servers, except + the virtual server has no pool members to load balance. The virtual server simply + forwards the packet directly to the destination IP address specified in the client request. + - When C(performance-http), specifies a virtual server with which you associate a Fast HTTP + profile. Together, the virtual server and profile increase the speed at which the virtual + server processes HTTP requests. + - When C(performance-l4), specifies a virtual server with which you associate a Fast L4 profile. + Together, the virtual server and profile increase the speed at which the virtual server + processes layer 4 requests. + - When C(stateless), specifies a virtual server that accepts traffic matching the virtual + server address and load balances the packet to the pool members without attempting to + match the packet to a pre-existing connection in the connection table. New connections + are immediately removed from the connection table. This addresses the requirement for + one-way UDP traffic that needs to be processed at very high throughput levels, for example, + load balancing syslog traffic to a pool of syslog servers. Stateless virtual servers are + not suitable for processing traffic requiring stateful tracking, such as TCP traffic. + Stateless virtual servers do not support iRules, persistence, connection mirroring, + rateshaping, or SNAT automap. + - When C(reject), specifies the BIG-IP system rejects any traffic destined for the + virtual server IP address. + - When C(dhcp), specifies a virtual server that relays Dynamic Host Control Protocol (DHCP) + client requests for an IP address to one or more DHCP servers, and provides DHCP server + responses with an available IP address for the client. + - When C(internal), specifies a virtual server that supports modification of HTTP requests + and responses. Internal virtual servers enable the use of ICAP (Internet Content Adaptation + Protocol) servers to modify HTTP requests and responses by creating and applying an ICAP + profile and adding Request Adapt or Response Adapt profiles to the virtual server. + - When C(message-routing), specifies a virtual server that uses a SIP application protocol + and functions in accordance with a SIP session profile and SIP router profile. + type: str + choices: + - standard + - forwarding-l2 + - forwarding-ip + - performance-http + - performance-l4 + - stateless + - reject + - dhcp + - internal + - message-routing + default: standard + name: + description: + - Virtual server name. + type: str + required: True + aliases: + - vs + destination: + description: + - Destination IP of the virtual server. + - Required when C(state) is C(present) and the virtual server does not exist. + - When C(type) is C(internal), this parameter is ignored. For all other types, + it is required. + - Destination can also be specified as a name for an existing Virtual Address. + type: str + aliases: + - address + - ip + source: + description: + - Specifies an IP address or network from which the virtual server accepts traffic. + - The virtual server accepts clients only from one of these IP addresses. + - For this setting to function effectively, specify a value other than 0.0.0.0/0 or ::/0 + (that is, any/0, any6/0). + - In order to maximize the utility of this setting, specify the most specific address + prefixes covering all customer addresses and no others. + - Specify the IP address in Classless Inter-Domain Routing (CIDR) format; address/prefix, + where the prefix length is in bits. For example, for IPv4, 10.0.0.1/32 or 10.0.0.0/24, + and for IPv6, ffe1::0020/64 or 2001:ed8:77b5:2:10:10:100:42/64. + type: str + port: + description: + - Port of the virtual server. Required when C(state) is C(present) + and the virtual server does not exist. + - If you do not want to specify a particular port, use the value C(0). + This means the virtual server listens on any port. + - When C(type) is C(dhcp), this module forces the C(port) parameter to C(67). + - When C(type) is C(internal), this module forces the C(port) parameter to C(0). + - In addition to specifying a port number, a select number of service names may also + be provided. + - The string C(ftp) may be substituted for for port C(21). + - The string C(http) may be substituted for for port C(80). + - The string C(https) may be substituted for for port C(443). + - The string C(telnet) may be substituted for for port C(23). + - The string C(smtp) may be substituted for for port C(25). + - The string C(snmp) may be substituted for for port C(161). + - The string C(snmp-trap) may be substituted for for port C(162). + - The string C(ssh) may be substituted for for port C(22). + - The string C(tftp) may be substituted for for port C(69). + - The string C(isakmp) may be substituted for for port C(500). + - The string C(mqtt) may be substituted for for port C(1883). + - The string C(mqtt-tls) may be substituted for for port C(8883). + type: str + profiles: + description: + - List of profiles (HTTP, ClientSSL, ServerSSL, etc) to apply to both sides + of the connection (client-side and server-side). + - If you only want to apply a particular profile to the client-side of + the connection, specify C(client-side) for the profile's C(context). + - If you only want to apply a particular profile to the server-side of + the connection, specify C(server-side) for the profile's C(context). + - If C(context) is not provided, it will default to C(all). + - If you want to remove a profile from the list of profiles currently active + on the virtual, simply remove it from the C(profiles) list. See + examples for an illustration of this. + - If you want to add a profile to the list of profiles currently active + on the virtual, simply add it to the C(profiles) list. See + examples for an illustration of this. + - B(Profiles are important). This module will fail to configure a BIG-IP if you mix up + your profiles, or if you attempt to set an IP protocol which your current, + or new, profiles do not support. Both this module, and BIG-IP, will report an error + if this is incorrect, resembling C(lists profiles incompatible with its protocol). + - If you are unsure what the correct profile combinations are, we suggest having a BIG-IP + available in which you can make changes and copy what the correct combinations are. + - To use C(http2) in full proxy to enable C(HTTP MRF Router) option seen in the GUI you need to assign + C(/Common/httprouter) profile with C(context) set to C(all). See the bottom of examples section below. + type: raw + aliases: + - all_profiles + suboptions: + name: + description: + - Name of the profile. + - This must be specified if a context is specified. + - If this is not specified, it is assumed the profile item is + only a name of a profile. + type: str + context: + description: + - The side of the connection on which the profile should be applied. + type: str + choices: + - all + - server-side + - client-side + default: all + irules: + description: + - Specifies a list of rules to be applied in priority order. + - If you want to remove existing iRules, specify a single empty value; C(""). + See the documentation for an example. + - The order in which iRules are specified does matter, so a list that contains + the same list elements but in a different order in the playbook will make changes + on the device. + - When C(type) is C(dhcp), C(stateless), C(reject), or C(internal), this parameter is ignored. + type: list + elements: str + aliases: + - all_rules + enabled_vlans: + description: + - List of VLANs to enable. When a VLAN named C(all) is used, all + VLANs will be allowed. VLANs can be specified with or without the + leading partition. If the partition is not specified in the VLAN, + the C(partition) option of this module is used. + - This parameter is mutually exclusive with the C(disabled_vlans) parameter. + type: list + elements: str + disabled_vlans: + description: + - List of VLANs to be disabled. If the partition is not specified in the VLAN, + the C(partition) option of this module is used. + - This parameter is mutually exclusive with the C(enabled_vlans) parameters. + type: list + elements: str + pool: + description: + - Default pool for the virtual server. + - If you want to remove the existing pool, specify an empty value; C(""). + See the documentation for an example. + - When creating a new virtual server, and C(type) is C(stateless), this parameter + is required. + - If C(type) is C(stateless), the C(pool) must not have any members + which define a C(rate_limit). + type: str + policies: + description: + - Specifies the policies for the virtual server. + - When C(type) is C(dhcp), C(reject), or C(internal), this parameter is ignored. + type: list + elements: str + aliases: + - all_policies + snat: + description: + - Source network address policy. + - When C(type) is C(dhcp), C(reject), or C(internal), this parameter is ignored. + - The name of a SNAT pool (like "/Common/snat_pool_name") can be specified to enable SNAT + with the specific pool. + - To remove SNAT, specify the word C(none). + - To specify automap, use the word C(automap). + type: str + default_persistence_profile: + description: + - Default profile which manages the session persistence. + - If you want to remove the existing default persistence profile, specify an + empty value; C(""). See the documentation for an example. + - When C(type) is C(dhcp), this parameter is ignored. + type: str + description: + description: + - Virtual server description. + type: str + fallback_persistence_profile: + description: + - Specifies the persistence profile you want the system to use if it + cannot use the specified default persistence profile. + - If you want to remove the existing fallback persistence profile, specify an + empty value; C(""). See the documentation for an example. + - When C(type) is C(dhcp), this parameter is ignored. + type: str + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + metadata: + description: + - Arbitrary key/value pairs you can attach to a virtual server. This is useful in + situations where you want to annotate a virtual to be managed by Ansible. + - Key names are stored as strings; this includes names that are numbers. + - Values for all of the keys are stored as strings; this includes values + that are numbers. + - Data is persisted, not ephemeral. + type: raw + insert_metadata: + description: + - When set to C(no), the module does not set metadata on the device. + - Currently there is a limitation that non-admin users cannot set metadata on the object, despite being + able to create and modify virtual server objects. Setting this option to C(no) allows + such users to use this module to manage virtual server objects on the device. + - Attempting to change C(state) on Virtual Server that belongs to an iAPP with strict updates enabled will result + in error message returned by device, unless C(insert_metadata) parameter is set to C(no). + type: bool + default: yes + address_translation: + description: + - When C(enabled), specifies the system translates the address of the + virtual server. + - When C(disabled), specifies the system uses the address without translation. + - This option is useful when the system is load balancing devices that have the + same IP address. + - When creating a new virtual server, the default is C(enabled). + type: bool + port_translation: + description: + - When C(enabled), specifies the system translates the port of the virtual + server. + - When C(disabled), specifies the system uses the port without translation. + Turning off port translation for a virtual server is useful if you want to use + the virtual server to load balance connections to any service. + - When creating a new virtual server, the default is C(enabled). + type: bool + source_port: + description: + - Specifies whether the system preserves the source port of the connection. + - When creating a new virtual server, if this parameter is not specified, the default is C(preserve). + type: str + choices: + - preserve + - preserve-strict + - change + mirror: + description: + - Specifies the system mirrors connections on each member of a redundant pair. + - When creating a new virtual server, if this parameter is not specified, the default is C(disabled). + type: bool + auto_last_hop: + description: + - Allows the BIG-IP system to track the source MAC address of incoming connections and return traffic from + pools to the source MAC address, regardless of the routing table. + type: str + choices: + - default + - enabled + - disabled + version_added: "1.13.0" + mask: + description: + - Specifies the destination address network mask. This parameter works with IPv4 and IPv6 addresses. + - This is an optional parameter which can be specified when creating or updating virtual server. + - If C(destination) is set in CIDR notation format and C(mask) is provided, the C(mask) parameter takes precedence. + - If you specify a catchall destination (for example, C(0.0.0.0) for IPv4, C(::) for IPv6) + the mask parameter is set to C(any) or C(any6) respectively. + - When the C(destination) is not in CIDR notation and a C(mask) is not specified, C(255.255.255.255) or + C(ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) is set for IPv4 and IPv6 addresses respectively. + - When C(destination) is provided in CIDR notation format and a C(mask) is not specified, the mask parameter is + inferred from C(destination). + - When C(destination) is provided as a virtual address name, and a C(mask) is not specified, + the mask will be C(None), allowing device set it with its internal defaults. + type: str + ip_protocol: + description: + - Specifies a network protocol name you want the system to use to direct traffic + on this virtual server. + - When creating a new virtual server, if this parameter is not specified, the default is C(tcp). + - The Protocol setting is not available when you select Performance (HTTP) as the C(Type). + - The value of this argument can be specified in either its numeric value, or in a select number + of named values. Refer to C(choices) for examples. + - For a list of valid IP protocol numbers, refer to https://en.wikipedia.org/wiki/List_of_IP_protocol_numbers. + - When C(type) is C(dhcp), this module forces the C(ip_protocol) parameter to C(17) (UDP). + type: str + choices: + - ah + - any + - bna + - esp + - etherip + - gre + - icmp + - ipencap + - ipv6 + - ipv6-auth + - ipv6-crypt + - ipv6-icmp + - isp-ip + - mux + - ospf + - sctp + - tcp + - udp + - udplite + firewall_enforced_policy: + description: + - Applies the specified AFM policy to the virtual in an enforcing way. + - When creating a new virtual, if this parameter is not specified, the enforced + policy is disabled. + type: str + firewall_staged_policy: + description: + - Applies the specified AFM policy to the virtual in an enforcing way. + - A staged policy shows the results of the policy rules in the log, while not + actually applying the rules to traffic. + - When creating a new virtual, if this parameter is not specified, the staged + policy is disabled. + type: str + security_log_profiles: + description: + - Specifies the log profile applied to the virtual server. + - To make use of this feature, the AFM module must be licensed and provisioned. + - The C(Log all requests) and C(Log illegal requests) are mutually exclusive and + therefore, this module raises an error if the two are specified together. + type: list + elements: str + security_nat_policy: + description: + - Specify the Firewall NAT policies for the virtual server. + - You can specify one or more NAT policies to use. + - The most specific policy is used. For example, if you specify the + virtual server should use the device policy and the route domain policy, the route + domain policy overrides the device policy. + suboptions: + policy: + description: + - Specifies the policy to apply a NAT policy directly to the virtual server. + - The virtual server NAT policy is the most specific, and overrides a + route domain and device policy, if specified. + - To remove the policy, specify an empty string value. + type: str + use_device_policy: + description: + - Specifies the virtual server uses the device NAT policy, as specified + in the Firewall Options. + - The device policy is used if no route domain or virtual server NAT + setting is specified. + type: bool + use_route_domain_policy: + description: + - Specifies the virtual server uses the route domain policy, as + specified in the Route Domain Security settings. + - When specified, the route domain policy overrides the device policy, and + is overridden by a virtual server policy. + type: bool + type: dict + ip_intelligence_policy: + description: + - Specifies the IP intelligence policy applied to the virtual server. + - This parameter requires a valid BIG-IP security module is provisioned, + such as ASM or AFM. + type: str + rate_limit: + description: + - Virtual server rate limit (connections-per-second). Setting this to C(0) + disables the limit. + - The valid value range is C(0) - C(4294967295). + type: int + rate_limit_dst_mask: + description: + - Specifies a mask, in bits, to be applied to the destination address as part of the rate limiting. + - The default value is C(0), which is equivalent to using the entire address - C(32) in IPv4, or C(128) in IPv6. + - The valid value range is C(0) - C(4294967295). + type: int + rate_limit_src_mask: + description: + - Specifies a mask, in bits, to be applied to the source address as part of the rate limiting. + - The default value is C(0), which is equivalent to using the entire address - C(32) in IPv4, or C(128) in IPv6. + - The valid value range is C(0) - C(4294967295). + type: int + rate_limit_mode: + description: + - Indicates whether the rate limit is applied per virtual object, per source address, per destination address, + or some combination thereof. + - The default value is C(object), which does not use the source or destination address as part of the key. + type: str + choices: + - object + - object-source + - object-destination + - object-source-destination + - destination + - source + - source-destination + default: object + clone_pools: + description: + - Specifies a pool or list of pools that the virtual server uses to replicate either client-side + or server-side traffic. + - Typically this option is used for intrusion detection. + type: list + elements: dict + suboptions: + pool_name: + description: + - The pool name to which the server replicates the traffic. + - Only pools created on the Common partition or on the same partition as the virtual server can be used. + - Referencing a pool on the Common partition needs to be done in the full path format, + for example, C(/Common/pool_name). + type: str + required: True + context: + description: + - The context option for a clone pool to replicate either client-side or server-side traffic. + type: str + required: True + choices: + - clientside + - serverside + service_down_immediate_action: + description: + - Specifies the immediate action to take upon the receipt of the initial SYN packet if the + availability status of the virtual server is Offline or Unavailable. + - Supported for virtual servers with a Type of C(standard) and Protocol of C(TCP). + type: str + choices: + - none + - reset + - drop + version_added: "1.16.0" + check_profiles: + description: + - Specifies whether the client and server SSL profiles specified by the user should be verified to be + correct against the existing profiles. This is useful in cases where a large number of profiles + are being added at once. + - Not recommended for common use. In case of duplicate profiles, or erroneous profiles, + the BIG-IP throws an error. + type: bool + default: yes + version_added: "1.2.0" + bypass_module_checks: + description: + - Disables all built-in module verification checks that require BIG-IP device calls. Using this option cuts + down on the number of REST calls made by this module. The trade off is that most parameters are sent as is, + which requires extra care when defining them. + - The device is the final source of truth for such configurations, usable in cases where speed is + preferred over accuracy. + - If set to C(yes), the module ignores the value op C(check_profiles) parameter. + - This parameter can be used when creating new or updating existing resources. + type: bool + default: no + version_added: "1.3.0" +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) + - Nitin Khanna (@nitinthewiz) +''' + +EXAMPLES = r''' +- name: Modify Port of the Virtual Server + bigip_virtual_server: + state: present + partition: Common + name: my-virtual-server + port: 8080 + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Delete virtual server + bigip_virtual_server: + state: absent + partition: Common + name: my-virtual-server + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Add virtual server + bigip_virtual_server: + state: present + partition: Common + name: my-virtual-server + destination: 10.10.10.10 + port: 443 + pool: my-pool + snat: Automap + description: Test Virtual Server + profiles: + - http + - fix + - name: clientssl + context: server-side + - name: ilx + context: client-side + policies: + - my-ltm-policy-for-asm + - ltm-uri-policy + - ltm-policy-2 + - ltm-policy-3 + enabled_vlans: + - /Common/vlan2 + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Add FastL4 virtual server + bigip_virtual_server: + destination: 1.1.1.1 + name: fastl4_vs + port: 80 + profiles: + - fastL4 + state: present + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Add iRules to the Virtual Server + bigip_virtual_server: + name: my-virtual-server + irules: + - irule1 + - irule2 + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Remove one iRule from the Virtual Server + bigip_virtual_server: + name: my-virtual-server + irules: + - irule2 + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Remove all iRules from the Virtual Server + bigip_virtual_server: + name: my-virtual-server + irules: "" + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Remove pool from the Virtual Server + bigip_virtual_server: + name: my-virtual-server + pool: "" + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Add metadata to virtual + bigip_virtual_server: + name: my-virtual-server + partition: Common + metadata: + ansible: 2.4 + updated_at: 2017-12-20T17:50:46Z + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add virtual with two profiles + bigip_virtual_server: + name: my-virtual-server + partition: Common + profiles: + - http + - tcp + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Remove HTTP profile from previous virtual + bigip_virtual_server: + name: my-virtual-server + partition: Common + profiles: + - tcp + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add the HTTP profile back to the previous virtual + bigip_virtual_server: + name: my-virtual-server + partition: Common + profiles: + - http + - tcp + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Add virtual server with rate limit + bigip_virtual_server: + state: present + partition: Common + name: my-virtual-server + destination: 10.10.10.10 + port: 443 + pool: my-pool + snat: Automap + description: Test Virtual Server + profiles: + - http + - fix + - name: clientssl + context: server-side + - name: ilx + context: client-side + policies: + - my-ltm-policy-for-asm + - ltm-uri-policy + - ltm-policy-2 + - ltm-policy-3 + enabled_vlans: + - /Common/vlan2 + rate_limit: 400 + rate_limit_mode: destination + rate_limit_dst_mask: 32 + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Add FastL4 virtual server with clone_pools + bigip_virtual_server: + destination: 1.1.1.1 + name: fastl4_vs + port: 80 + profiles: + - fastL4 + state: present + clone_pools: + - pool_name: FooPool + context: clientside + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost + +- name: Add virtual with MRF router option set + bigip_virtual_server: + name: my-virtual-server + destination: 10.10.10.10 + port: 443 + partition: Common + profiles: + - http + - tcp + - name: noneg-ssl + context: client-side + - name: http2 + context: client-side + - name: httprouter + context: all + provider: + server: lb.mydomain.net + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +description: + description: New description of the virtual server. + returned: changed + type: str + sample: This is my description +default_persistence_profile: + description: Default persistence profile set on the virtual server. + returned: changed + type: str + sample: /Common/dest_addr +destination: + description: Destination of the virtual server. + returned: changed + type: str + sample: 1.1.1.1 +disabled: + description: Whether the virtual server is disabled or not. + returned: changed + type: bool + sample: True +disabled_vlans: + description: List of VLANs that the virtual is disabled for. + returned: changed + type: list + sample: ['/Common/vlan1', '/Common/vlan2'] +enabled: + description: Whether the virtual server is enabled or not. + returned: changed + type: bool + sample: False +enabled_vlans: + description: List of VLANs that the virtual is enabled for. + returned: changed + type: list + sample: ['/Common/vlan5', '/Common/vlan6'] +fallback_persistence_profile: + description: Fallback persistence profile set on the virtual server. + returned: changed + type: str + sample: /Common/source_addr +irules: + description: iRules set on the virtual server. + returned: changed + type: list + sample: ['/Common/irule1', '/Common/irule2'] +pool: + description: Pool the virtual server is attached to. + returned: changed + type: str + sample: /Common/my-pool +policies: + description: List of policies attached to the virtual. + returned: changed + type: list + sample: ['/Common/policy1', '/Common/policy2'] +port: + description: Port the virtual server is configured to listen on. + returned: changed + type: int + sample: 80 +profiles: + description: List of profiles set on the virtual server. + returned: changed + type: list + sample: [{'name': 'tcp', 'context': 'server-side'}, {'name': 'tcp-legacy', 'context': 'client-side'}] +snat: + description: SNAT setting of the virtual server. + returned: changed + type: str + sample: Automap +source: + description: Source address set on the virtual server, in CIDR format. + returned: changed + type: str + sample: 1.2.3.4/32 +metadata: + description: The new value of the virtual. + returned: changed + type: dict + sample: {'key1': 'foo', 'key2': 'bar'} +address_translation: + description: The new value specifying whether address translation is on or off. + returned: changed + type: bool + sample: True +port_translation: + description: The new value specifying whether port translation is on or off. + returned: changed + type: bool + sample: True +source_port: + description: Specifies whether the system preserves the source port of the connection. + returned: changed + type: str + sample: change +mirror: + description: Specifies the system mirrors connections on each member of a redundant pair. + returned: changed + type: bool + sample: True +auto_last_hop: + description: Specifies the autoLasthop value of the virtual server + returned: changed + type: str + sample: enabled +ip_protocol: + description: The new value of the IP protocol. + returned: changed + type: int + sample: 6 +firewall_enforced_policy: + description: The new enforcing firewall policy. + returned: changed + type: str + sample: /Common/my-enforced-fw +firewall_staged_policy: + description: The new staging firewall policy. + returned: changed + type: str + sample: /Common/my-staged-fw +security_log_profiles: + description: The new list of security log profiles. + returned: changed + type: list + sample: ['/Common/profile1', '/Common/profile2'] +ip_intelligence_policy: + description: The new IP Intelligence Policy assigned to the virtual. + returned: changed + type: str + sample: /Common/ip-intelligence +rate_limit: + description: The maximum number of connections per second allowed for a virtual server. + returned: changed + type: int + sample: 5000 +rate_limit_src_mask: + description: Specifies a mask, in bits, to be applied to the source address as part of the rate limiting. + returned: changed + type: int + sample: 32 +rate_limit_dst_mask: + description: Specifies a mask, in bits, to be applied to the destination address as part of the rate limiting. + returned: changed + type: int + sample: 32 +rate_limit_mode: + description: Sets the type of rate limiting to be used on the virtual server. + returned: changed + type: str + sample: object-source +clone_pools: + description: Pools to which virtual server copies traffic. + returned: changed + type: list + sample: [{'pool_name':'/Common/Pool1', 'context': 'clientside'}] +service_down_immediate_action: + description: Action to take upon the receipt of the initial SYN packet if server is Offline or Unavailable. + returned: changed + type: str + sample: drop +''' +import os +import re +import traceback +from collections import namedtuple +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib, env_fallback +) +from ansible.module_utils.six import iteritems + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name, + only_has_managed_metadata, mark_managed_by, is_empty_list +) +from ..module_utils.constants import ( + MANAGED_BY_ANNOTATION_MODIFIED, MANAGED_BY_ANNOTATION_VERSION +) +from ..module_utils.compare import cmp_simple_list +from ..module_utils.icontrol import ( + modules_provisioned, tmos_version +) +from ..module_utils.ipaddress import ( + is_valid_ip, is_valid_ip_interface, ip_interface, validate_ip_v6_address, get_netmask, compress_address +) +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'sourceAddressTranslation': 'snat', + 'fallbackPersistence': 'fallback_persistence_profile', + 'persist': 'default_persistence_profile', + 'vlansEnabled': 'vlans_enabled', + 'vlansDisabled': 'vlans_disabled', + 'profilesReference': 'profiles', + 'policiesReference': 'policies', + 'rules': 'irules', + 'translateAddress': 'address_translation', + 'translatePort': 'port_translation', + 'ipProtocol': 'ip_protocol', + 'fwEnforcedPolicy': 'firewall_enforced_policy', + 'fwStagedPolicy': 'firewall_staged_policy', + 'securityLogProfiles': 'security_log_profiles', + 'securityNatPolicy': 'security_nat_policy', + 'sourcePort': 'source_port', + 'ipIntelligencePolicy': 'ip_intelligence_policy', + 'rateLimit': 'rate_limit', + 'rateLimitMode': 'rate_limit_mode', + 'rateLimitDstMask': 'rate_limit_dst_mask', + 'rateLimitSrcMask': 'rate_limit_src_mask', + 'clonePools': 'clone_pools', + 'autoLasthop': 'auto_last_hop', + 'serviceDownImmediateAction': 'service_down_immediate_action' + } + + api_attributes = [ + 'description', + 'destination', + 'disabled', + 'enabled', + 'fallbackPersistence', + 'ipProtocol', + 'metadata', + 'persist', + 'policies', + 'pool', + 'profiles', + 'rules', + 'source', + 'sourceAddressTranslation', + 'serviceDownImmediateAction', + 'vlans', + 'vlansEnabled', + 'vlansDisabled', + 'translateAddress', + 'translatePort', + 'l2Forward', + 'ipForward', + 'stateless', + 'reject', + 'dhcpRelay', + 'internal', + 'fwEnforcedPolicy', + 'fwStagedPolicy', + 'securityLogProfiles', + 'securityNatPolicy', + 'sourcePort', + 'mirror', + 'mask', + 'ipIntelligencePolicy', + 'rateLimit', + 'rateLimitMode', + 'rateLimitDstMask', + 'rateLimitSrcMask', + 'clonePools', + 'autoLasthop', + ] + + updatables = [ + 'address_translation', + 'description', + 'default_persistence_profile', + 'destination', + 'disabled_vlans', + 'enabled', + 'enabled_vlans', + 'fallback_persistence_profile', + 'ip_protocol', + 'irules', + 'metadata', + 'pool', + 'policies', + 'port', + 'port_translation', + 'profiles', + 'service_down_immediate_action', + 'snat', + 'source', + 'type', + 'firewall_enforced_policy', + 'firewall_staged_policy', + 'security_log_profiles', + 'security_nat_policy', + 'source_port', + 'mirror', + 'mask', + 'ip_intelligence_policy', + 'rate_limit', + 'rate_limit_mode', + 'rate_limit_src_mask', + 'rate_limit_dst_mask', + 'clone_pools', + 'auto_last_hop', + ] + + returnables = [ + 'address_translation', + 'description', + 'default_persistence_profile', + 'destination', + 'disabled', + 'disabled_vlans', + 'enabled', + 'enabled_vlans', + 'fallback_persistence_profile', + 'ip_protocol', + 'irules', + 'metadata', + 'pool', + 'policies', + 'port', + 'port_translation', + 'profiles', + 'service_down_immediate_action', + 'snat', + 'source', + 'vlans', + 'vlans_enabled', + 'vlans_disabled', + 'type', + 'firewall_enforced_policy', + 'firewall_staged_policy', + 'security_log_profiles', + 'security_nat_policy', + 'source_port', + 'mirror', + 'mask', + 'ip_intelligence_policy', + 'rate_limit', + 'rate_limit_mode', + 'rate_limit_src_mask', + 'rate_limit_dst_mask', + 'clone_pools', + 'auto_last_hop', + ] + + profiles_mutex = [ + 'sip', + 'sipsession', + 'iiop', + 'rtsp', + 'http', + 'diameter', + 'diametersession', + 'radius', + 'ftp', + 'tftp', + 'dns', + 'pptp', + 'fix', + ] + + ip_protocols_map = [ + ('ah', 51), + ('bna', 49), + ('esp', 50), + ('etherip', 97), + ('gre', 47), + ('icmp', 1), + ('ipencap', 4), + ('ipv6', 41), + ('ipv6-auth', 51), # not in the official list + ('ipv6-crypt', 50), # not in the official list + ('ipv6-icmp', 58), + ('iso-ip', 80), + ('mux', 18), + ('ospf', 89), + ('sctp', 132), + ('tcp', 6), + ('udp', 17), + ('udplite', 136), + ] + + def _format_port_for_destination(self, ip, port): + if validate_ip_v6_address(ip): + result = '.{0}'.format(port) + else: + result = ':{0}'.format(port) + return result + + def _format_destination(self, address, port, route_domain): + if port is None: + if route_domain is None: + result = '{0}'.format( + fq_name(self.partition, address) + ) + else: + result = '{0}%{1}'.format( + fq_name(self.partition, address), + route_domain + ) + else: + port = self._format_port_for_destination(address, port) + if route_domain is None: + result = '{0}{1}'.format( + fq_name(self.partition, address), + port + ) + else: + result = '{0}%{1}{2}'.format( + fq_name(self.partition, address), + route_domain, + port + ) + return result + + @property + def ip_protocol(self): + if self._values['ip_protocol'] is None: + return None + if self._values['ip_protocol'] == 'any': + return 'any' + for x in self.ip_protocols_map: + if x[0] == self._values['ip_protocol']: + return int(x[1]) + try: + return int(self._values['ip_protocol']) + except ValueError: + raise F5ModuleError( + "Specified ip_protocol was neither a number nor in the list of common protocols." + ) + + @property + def has_message_routing_profiles(self): + if self.profiles is None: + return None + current = self._read_current_message_routing_profiles_from_device() + result = [x['name'] for x in self.profiles if x['name'] in current] + if len(result) > 0: + return True + return False + + @property + def has_fastl4_profiles(self): + if self.profiles is None: + return None + current = self._read_current_fastl4_profiles_from_device() + result = [x['name'] for x in self.profiles if x['name'] in current] + if len(result) > 0: + return True + return False + + @property + def has_fasthttp_profiles(self): + """Check if ``fasthttp`` profile is in API profiles + + This method is used to determine the server type when doing comparisons + in the Difference class. + + Returns: + bool: True if server has ``fasthttp`` profiles. False otherwise. + """ + if self.profiles is None: + return None + current = self._read_current_fasthttp_profiles_from_device() + result = [x['name'] for x in self.profiles if x['name'] in current] + if len(result) > 0: + return True + return False + + def _read_current_message_routing_profiles_from_device(self): + result = [] + result += self._read_diameter_profiles_from_device() + result += self._read_sip_profiles_from_device() + result += self._read_legacy_sip_profiles_from_device() + return result + + def _read_diameter_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/diameter/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [x['name'] for x in response['items']] + return result + + def _read_legacy_sip_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/sip/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [x['name'] for x in response['items']] + return result + + def _read_sip_profiles_from_device(self): + version = tmos_version(self.client) + if Version(version) < Version('14.0.0'): + return [] + uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/sip/profile/session/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [x['name'] for x in response['items']] + return result + + def _read_current_fastl4_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [x['name'] for x in response['items']] + return result + + def _read_current_fasthttp_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fasthttp/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [x['name'] for x in response['items']] + return result + + def _read_current_clientssl_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/client-ssl/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [x['name'] for x in response['items']] + return result + + def _read_current_serverssl_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/server-ssl/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [x['name'] for x in response['items']] + return result + + def _is_client_ssl_profile(self, profile): + if profile['name'] in self._read_current_clientssl_profiles_from_device(): + return True + return False + + def _is_server_ssl_profile(self, profile): + if profile['name'] in self._read_current_serverssl_profiles_from_device(): + return True + return False + + def _check_pool(self, item): + pool = transform_name(name=fq_name(self.partition, item)) + uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + pool + ) + 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: + raise F5ModuleError( + 'The specified pool {0} does not exist.'.format(pool) + ) + return item + + +class ApiParameters(Parameters): + @property + def type(self): + """Attempt to determine the current server type + + This check is very unscientific. It turns out that this information is not + exactly available anywhere on a BIG-IP. Instead, we rely on a semi-reliable + means for determining what the type of the virtual server is. Hopefully it + always works. + + There are a handful of attributes that can be used to determine a specific + type. There are some types though that can only be determined by looking at + the profiles that are assigned to them. We follow that method for those + complicated types; message-routing, fasthttp, and fastl4. + + Because type determination is an expensive operation, we cache the result + from the operation. + + Returns: + string: The server type. + """ + if self._values['type']: + return self._values['type'] + if self.l2Forward is True: + result = 'forwarding-l2' + elif self.ipForward is True: + result = 'forwarding-ip' + elif self.stateless is True: + result = 'stateless' + elif self.reject is True: + result = 'reject' + elif self.dhcpRelay is True: + result = 'dhcp' + elif self.internal is True: + result = 'internal' + elif self.has_fasthttp_profiles: + result = 'performance-http' + elif self.has_fastl4_profiles: + result = 'performance-l4' + elif self.has_message_routing_profiles: + result = 'message-routing' + else: + result = 'standard' + self._values['type'] = result + return result + + @property + def destination(self): + if self._values['destination'] is None: + return None + destination = self.destination_tuple + result = self._format_destination(destination.ip, destination.port, destination.route_domain) + return result + + @property + def destination_tuple(self): + Destination = namedtuple('Destination', ['ip', 'port', 'route_domain', 'mask']) + + # Remove the partition + if self._values['destination'] is None: + result = Destination(ip=None, port=None, route_domain=None, mask=None) + return result + destination = re.sub(r'^/[a-zA-Z0-9_.-]+/', '', self._values['destination']) + # Covers the following examples + # + # /Common/2700:bc00:1f10:101::6%2.80 + # 2700:bc00:1f10:101::6%2.80 + # 1.1.1.1%2:80 + # /Common/1.1.1.1%2:80 + # /Common/2700:bc00:1f10:101::6%2.any + # + pattern = r'(?P[^%]+)%(?P[0-9]+)[:.](?P[0-9]+|any)' + matches = re.search(pattern, destination) + if matches: + try: + port = int(matches.group('port')) + except ValueError: + # Can be a port of "any". This only happens with IPv6 + port = matches.group('port') + if port == 'any': + port = 0 + result = Destination( + ip=matches.group('ip'), + port=port, + route_domain=int(matches.group('route_domain')), + mask=self.mask + ) + return result + + pattern = r'(?P[^%]+)%(?P[0-9]+)' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('ip'), + port=None, + route_domain=int(matches.group('route_domain')), + mask=self.mask + ) + return result + + # this will match any IPV4 Address and port, no RD + pattern = r'^(?P(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4]' \ + r'[0-9]|25[0-5])):(?P[0-9]+)' + + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('ip'), + port=int(matches.group('port')), + route_domain=None, + mask=self.mask + ) + return result + + # match standalone IPV6 address, no port + pattern = r'^([0-9a-f]{0,4}:){2,7}(:|[0-9a-f]{1,4})$' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=destination, + port=None, + route_domain=None, + mask=self.mask + ) + return result + + # match IPV6 address with port + pattern = r'(?P([0-9a-f]{0,4}:){2,7}(:|[0-9a-f]{1,4}).(?P[0-9]+|any))' + matches = re.search(pattern, destination) + if matches: + ip = matches.group('ip').split('.')[0] + try: + port = int(matches.group('port')) + except ValueError: + # Can be a port of "any". This only happens with IPv6 + port = matches.group('port') + if port == 'any': + port = 0 + result = Destination( + ip=ip, + port=port, + route_domain=None, + mask=self.mask + ) + return result + + # this will match any alphanumeric Virtual Address and port + pattern = r'(?P^[a-zA-Z0-9_.-]+):(?P[0-9]+)' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('name'), + port=int(matches.group('port')), + route_domain=None, + mask=self.mask + ) + return result + + # this will match any alphanumeric Virtual Address + pattern = r'(?P^[a-zA-Z0-9_.-]+)' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('name'), + port=None, + route_domain=None, + mask=self.mask + ) + return result + + # match IPv6 wildcard with port without RD + pattern = r'(?P[^.]+).(?P[0-9]+|any)' + matches = re.search(pattern, destination) + if matches: + result = Destination( + ip=matches.group('ip'), + port=matches.group('port'), + route_domain=None, + mask=self.mask + ) + return result + + result = Destination(ip=None, port=None, route_domain=None, mask=None) + return result + + @property + def port(self): + destination = self.destination_tuple + self._values['port'] = destination.port + return destination.port + + @property + def route_domain(self): + """Return a route domain number from the destination + + Returns: + int: The route domain number + """ + destination = self.destination_tuple + self._values['route_domain'] = destination.route_domain + return int(destination.route_domain) + + @property + def profiles(self): + """Returns a list of profiles from the API + + The profiles are formatted so that they are usable in this module and + are able to be compared by the Difference engine. + + Returns: + list (:obj:`list` of :obj:`dict`): List of profiles. + + Each dictionary in the list contains the following three (3) keys. + + * name + * context + * fullPath + + Raises: + F5ModuleError: If the specified context is a value other that + ``all``, ``serverside``, or ``clientside``. + """ + if 'items' not in self._values['profiles']: + return None + result = [] + for item in self._values['profiles']['items']: + context = item['context'] + name = item['name'] + if context in ['all', 'serverside', 'clientside']: + result.append(dict(name=name, context=context, fullPath=item['fullPath'])) + else: + raise F5ModuleError( + "Unknown profile context found: '{0}'".format(context) + ) + return result + + @property + def profile_types(self): + return [x['name'] for x in iteritems(self.profiles)] + + @property + def policies(self): + if 'items' not in self._values['policies']: + return None + result = [] + for item in self._values['policies']['items']: + name = item['name'] + partition = item['partition'] + result.append(dict(name=name, partition=partition)) + return result + + @property + def default_persistence_profile(self): + """Get the name of the current default persistence profile + + These persistence profiles are always lists when we get them + from the REST API even though there can only be one. We'll + make it a list again when we get to the Difference engine. + + Returns: + string: The name of the default persistence profile + """ + if self._values['default_persistence_profile'] is None: + return None + return self._values['default_persistence_profile'][0] + + @property + def enabled(self): + if 'enabled' in self._values: + return True + return False + + @property + def disabled(self): + if 'disabled' in self._values: + return True + return False + + @property + def metadata(self): + if self._values['metadata'] is None: + return None + if only_has_managed_metadata(self._values['metadata']): + return None + result = [] + for md in self._values['metadata']: + if md['name'] in [MANAGED_BY_ANNOTATION_VERSION, MANAGED_BY_ANNOTATION_MODIFIED]: + continue + + tmp = dict(name=str(md['name'])) + if 'value' in md: + tmp['value'] = str(md['value']) + else: + tmp['value'] = '' + result.append(tmp) + return result + + @property + def security_log_profiles(self): + if self._values['security_log_profiles'] is None: + return None + # At the moment, BIG-IP wraps the names of log profiles in double-quotes if + # the profile name contains spaces. This is likely due to the REST code being + # too close to actual tmsh code and, at the tmsh level, a space in the profile + # name would cause tmsh to see the 2nd word (and beyond) as "the next parameter". + # + # This seems like a bug to me. + result = list(set([x.strip('"') for x in self._values['security_log_profiles']])) + result.sort() + return result + + @property + def sec_nat_use_device_policy(self): + if self._values['security_nat_policy'] is None: + return None + if 'useDevicePolicy' not in self._values['security_nat_policy']: + return None + if self._values['security_nat_policy']['useDevicePolicy'] == "no": + return False + return True + + @property + def sec_nat_use_rd_policy(self): + if self._values['security_nat_policy'] is None: + return None + if 'useRouteDomainPolicy' not in self._values['security_nat_policy']: + return None + if self._values['security_nat_policy']['useRouteDomainPolicy'] == "no": + return False + return True + + @property + def sec_nat_policy(self): + if self._values['security_nat_policy'] is None: + return None + if 'policy' not in self._values['security_nat_policy']: + return None + return self._values['security_nat_policy']['policy'] + + @property + def irules(self): + if self._values['irules'] is None: + return [] + return self._values['irules'] + + @property + def rate_limit(self): + if self._values['rate_limit'] is None: + return None + if self._values['rate_limit'] == 'disabled': + return 0 + return int(self._values['rate_limit']) + + @property + def clone_pools(self): + if self._values['clone_pools'] is None: + return None + result = [] + for item in self._values['clone_pools']: + pool_name = fq_name(item['partition'], item['name']) + context = item['context'] + tmp = { + 'name': pool_name, + 'context': context + } + result.append(tmp) + return result + + +class ModuleParameters(Parameters): + services_map = { + 'ftp': 21, + 'http': 80, + 'https': 443, + 'telnet': 23, + 'pptp': 1723, + 'smtp': 25, + 'snmp': 161, + 'snmp-trap': 162, + 'ssh': 22, + 'tftp': 69, + 'isakmp': 500, + 'mqtt': 1883, + 'mqtt-tls': 8883, + 'rtsp': 554 + } + + def _handle_profile_context(self, tmp): + if 'context' not in tmp: + tmp['context'] = 'all' + else: + if 'name' not in tmp: + raise F5ModuleError( + "A profile name must be specified when a context is specified." + ) + tmp['context'] = tmp['context'].replace('server-side', 'serverside') + tmp['context'] = tmp['context'].replace('client-side', 'clientside') + + def _handle_ssl_profile_nuances(self, profile): + if self.check_profiles: + if profile['name'] == 'serverssl' or self._is_server_ssl_profile(profile): + if profile['context'] != 'serverside': + profile['context'] = 'serverside' + if profile['name'] == 'clientssl' or self._is_client_ssl_profile(profile): + if profile['context'] != 'clientside': + profile['context'] = 'clientside' + else: + if profile['name'] == 'serverssl': + if profile['context'] != 'serverside': + profile['context'] = 'serverside' + if profile['name'] == 'clientssl': + if profile['context'] != 'clientside': + profile['context'] = 'clientside' + + def _check_port(self): + try: + port = int(self._values['port']) + except ValueError: + raise F5ModuleError( + "The specified port was not a valid integer" + ) + if 0 <= port <= 65535: + return port + raise F5ModuleError( + "Valid ports must be in range 0 - 65535" + ) + + def _check_clone_pool_contexts(self): + client = 0 + server = 0 + for item in self._values['clone_pools']: + if item['context'] == 'clientside': + client += 1 + if item['context'] == 'serverside': + server += 1 + if client > 1 or server > 1: + raise F5ModuleError( + 'You must specify only one clone pool for each context.' + ) + + @property + def check_profiles(self): + result = flatten_boolean(self._values['check_profiles']) + if result == 'yes': + return True + return False + + @property + def bypass_module_checks(self): + result = flatten_boolean(self._values['bypass_module_checks']) + if result == 'yes': + return True + return False + + @property + def source(self): + if self._values['source'] is None: + return None + source = self.source_tuple + if is_valid_ip_interface(u'{0}/{1}'.format(source.ip, source.cidr)): + if source.route_domain: + result = '{0}%{1}/{2}'.format(source.ip, source.route_domain, source.cidr) + else: + result = '{0}/{1}'.format(source.ip, source.cidr) + return result + raise F5ModuleError( + "The source IP address must be a valid CIDR format: address/prefix." + ) + + @property + def source_tuple(self): + Source = namedtuple('Source', ['ip', 'route_domain', 'cidr']) + if self._values['source'] is None: + result = Source(ip=None, route_domain=None, cidr=None) + return result + # match source with RD + pattern = r'(?P[^%]+)%(?P[0-9]+)/(?P[0-9]+)' + matches = re.search(pattern, self._values['source']) + if matches: + result = Source( + ip=matches.group('ip'), + route_domain=matches.group('route_domain'), + cidr=matches.group('cidr') + ) + return result + # match source without RD + pattern = r'(?P[^%]+)/(?P[0-9]+)' + matches = re.search(pattern, self._values['source']) + if matches: + result = Source( + ip=matches.group('ip'), + route_domain=None, + cidr=matches.group('cidr') + ) + return result + + result = Source(ip=None, route_domain=None, cidr=None) + return result + + @property + def destination(self): + pattern = r'^[a-zA-Z0-9_.-]+' + if len(self._values['destination'].split('/')) > 1: + addr, dud = self._values['destination'].split('/') + if '%' in addr: + addr = addr.split('%')[0] + else: + addr = self._values['destination'].split('%')[0] + if not is_valid_ip(addr): + matches = re.search(pattern, addr) + if not matches: + raise F5ModuleError( + "The provided destination is not a valid IP address or a Virtual Address name." + ) + result = self._format_destination(addr, self.port, self.route_domain) + return result + + @property + def route_domain(self): + if self._values['destination'] is None: + return None + result = None + if len(self._values['destination'].split('/')) > 1: + addr, dud = self._values['destination'].split('/') + if '%' in addr: + result = addr.split('%') + else: + result = self._values['destination'].split('%') + if result and len(result) > 1: + pattern = r'^[a-zA-Z0-9_.-]+' + matches = re.search(pattern, result[0]) + if matches and not is_valid_ip(result[0]): + # we need to strip RD because when using Virtual Address names the RD is not needed. + return None + return int(result[1]) + return None + + @property + def destination_tuple(self): + pattern = r'^[a-zA-Z0-9_.-]+' + Destination = namedtuple('Destination', ['ip', 'port', 'route_domain', 'mask', 'not_ip']) + if self._values['destination'] is None: + result = Destination(ip=None, port=None, route_domain=None, mask=None, not_ip=None) + return result + addr = self._values['destination'].split("%")[0].split('/')[0] + if is_valid_ip(addr): + addr = compress_address(u'{0}'.format(addr)) + result = Destination(ip=addr, port=self.port, route_domain=self.route_domain, mask=self.mask, not_ip=False) + return result + else: + matches = re.search(pattern, addr) + if matches: + result = Destination(ip=addr, port=self.port, route_domain=self.route_domain, + mask=self.mask, not_ip=True) + return result + result = Destination(ip=addr, port=self.port, route_domain=self.route_domain, mask=self.mask, not_ip=False) + return result + + @property + def mask(self): + if self._values['destination'] is None: + return None + if len(self._values['destination'].split('/')) > 1: + addr, cidr = self._values['destination'].split('/') + if '%' in addr: + addr = addr.split('%')[0] + '/' + cidr + else: + addr = self._values['destination'] + else: + addr = self._values['destination'].split('%')[0] + if addr in ['0.0.0.0', '0.0.0.0/any', '0.0.0.0/0']: + return 'any' + if addr in ['::', '::/0', '::/any6']: + return 'any6' + if self._values['mask'] is None: + if is_valid_ip_interface(addr): + return get_netmask(addr) + else: + return None + return compress_address(self._values['mask']) + + @property + def port(self): + if self._values['port'] is None: + return None + if self._values['port'] in ['*', 'any', '0']: + return 0 + if self._values['port'] in self.services_map: + port = self._values['port'] + self._values['port'] = self.services_map[port] + self._check_port() + return int(self._values['port']) + + @property + def irules(self): + results = [] + if self._values['irules'] is None: + return None + if is_empty_list(self._values['irules']): + return '' + for irule in self._values['irules']: + result = fq_name(self.partition, irule) + results.append(result) + return results + + @property + def profiles(self): + if self._values['profiles'] is None: + return None + if is_empty_list(self._values['profiles']): + return '' + result = [] + for profile in self._values['profiles']: + tmp = dict() + if isinstance(profile, dict): + tmp.update(profile) + self._handle_profile_context(tmp) + tmp['name'] = profile + if 'name' in profile: + tmp['name'] = profile['name'] + if 'partition' not in profile: + if isinstance(tmp['name'], str) and len(tmp["name"].split("/")) > 1: + tmp["partition"] = tmp["name"].split("/")[1] + tmp['name'] = os.path.basename(tmp['name']) + else: + tmp['partition'] = "Common" + tmp['fullPath'] = fq_name(tmp['partition'], tmp['name']) + if not self.bypass_module_checks: + self._handle_ssl_profile_nuances(tmp) + else: + if len(profile.split("/")) > 1: + full_path = profile + tmp["partition"] = profile.split("/")[1] + else: + full_path = fq_name("Common", profile) + tmp["partition"] = "Common" + tmp['name'] = os.path.basename(profile) + tmp['context'] = 'all' + tmp['fullPath'] = full_path + if not self.bypass_module_checks: + self._handle_ssl_profile_nuances(tmp) + result.append(tmp) + mutually_exclusive = [x['name'] for x in result if x in self.profiles_mutex] + if len(mutually_exclusive) > 1: + raise F5ModuleError( + "Profiles {0} are mutually exclusive".format( + ', '.join(self.profiles_mutex).strip() + ) + ) + return result + + @property + def policies(self): + if self._values['policies'] is None: + return None + if is_empty_list(self._values['policies']): + return '' + result = [] + policies = [fq_name(self.partition, p) for p in self._values['policies']] + policies = set(policies) + for policy in policies: + parts = policy.split('/') + if len(parts) != 3: + raise F5ModuleError( + "The specified policy '{0}' is malformed".format(policy) + ) + tmp = dict( + name=parts[2], + partition=parts[1] + ) + result.append(tmp) + return result + + @property + def pool(self): + if self._values['pool'] is None: + return None + if self._values['pool'] == '': + return '' + return fq_name(self.partition, self._values['pool']) + + @property + def vlans_enabled(self): + if self._values['enabled_vlans'] is None: + return None + elif self._values['vlans_enabled'] is False: + # This is a special case for "all" enabled VLANs + return False + if self._values['disabled_vlans'] is None: + return True + return False + + @property + def vlans_disabled(self): + if self._values['disabled_vlans'] is None: + return None + elif self._values['vlans_disabled'] is True: + # This is a special case for "all" enabled VLANs + return True + elif self._values['enabled_vlans'] is None: + return True + return False + + @property + def enabled_vlans(self): + if self._values['enabled_vlans'] is None: + return None + elif any(x.lower() for x in self._values['enabled_vlans'] if x.lower() in ['all', '*']): + result = [fq_name(self.partition, 'all')] + return result + results = list(set([fq_name(self.partition, x) for x in self._values['enabled_vlans']])) + results.sort() + return results + + @property + def disabled_vlans(self): + if self._values['disabled_vlans'] is None: + return None + elif any(x.lower() for x in self._values['disabled_vlans'] if x.lower() in ['all', '*']): + raise F5ModuleError( + "You cannot disable all VLANs. You must name them individually." + ) + results = list(set([fq_name(self.partition, x) for x in self._values['disabled_vlans']])) + results.sort() + return results + + @property + def vlans(self): + disabled = self.disabled_vlans + if disabled: + return self.disabled_vlans + return self.enabled_vlans + + @property + def state(self): + if self._values['state'] == 'present': + return 'enabled' + return self._values['state'] + + @property + def snat(self): + if self._values['snat'] is None: + return None + lowercase = self._values['snat'].lower() + if lowercase in ['automap', 'none']: + return dict(type=lowercase) + snat_pool = fq_name(self.partition, self._values['snat']) + return dict(pool=snat_pool, type='snat') + + @property + def default_persistence_profile(self): + if self._values['default_persistence_profile'] is None: + return None + if self._values['default_persistence_profile'] == '': + return '' + profile = fq_name(self.partition, self._values['default_persistence_profile']) + parts = profile.split('/') + if len(parts) != 3: + raise F5ModuleError( + "The specified 'default_persistence_profile' is malformed" + ) + result = dict( + name=parts[2], + partition=parts[1] + ) + return result + + @property + def fallback_persistence_profile(self): + if self._values['fallback_persistence_profile'] is None: + return None + if self._values['fallback_persistence_profile'] == '': + return '' + result = fq_name(self.partition, self._values['fallback_persistence_profile']) + return result + + @property + def enabled(self): + if self._values['state'] == 'enabled': + return True + elif self._values['state'] == 'disabled': + return False + else: + return None + + @property + def disabled(self): + if self._values['state'] == 'enabled': + return False + elif self._values['state'] == 'disabled': + return True + else: + return None + + @property + def metadata(self): + if self._values['metadata'] is None: + return None + if self._values['metadata'] == '': + return [] + result = [] + try: + for k, v in iteritems(self._values['metadata']): + tmp = dict(name=str(k)) + if v: + tmp['value'] = str(v) + else: + tmp['value'] = '' + result.append(tmp) + except AttributeError: + raise F5ModuleError( + "The 'metadata' parameter must be a dictionary of key/value pairs." + ) + return result + + @property + def address_translation(self): + if self._values['address_translation'] is None: + return None + if self._values['address_translation']: + return 'enabled' + return 'disabled' + + @property + def port_translation(self): + if self._values['port_translation'] is None: + return None + if self._values['port_translation']: + return 'enabled' + return 'disabled' + + @property + def firewall_enforced_policy(self): + if self._values['firewall_enforced_policy'] is None: + return None + return fq_name(self.partition, self._values['firewall_enforced_policy']) + + @property + def firewall_staged_policy(self): + if self._values['firewall_staged_policy'] is None: + return None + return fq_name(self.partition, self._values['firewall_staged_policy']) + + @property + def ip_intelligence_policy(self): + if self._values['ip_intelligence_policy'] is None: + return None + if self._values['ip_intelligence_policy'] in ['', 'none']: + return '' + return fq_name(self.partition, self._values['ip_intelligence_policy']) + + @property + def security_log_profiles(self): + if self._values['security_log_profiles'] is None: + return None + if len(self._values['security_log_profiles']) == 1 and self._values['security_log_profiles'][0] == '': + return '' + result = list(set([fq_name(self.partition, x) for x in self._values['security_log_profiles']])) + result.sort() + return result + + @property + def sec_nat_use_device_policy(self): + if self._values['security_nat_policy'] is None: + return None + if 'use_device_policy' not in self._values['security_nat_policy']: + return None + return self._values['security_nat_policy']['use_device_policy'] + + @property + def sec_nat_use_rd_policy(self): + if self._values['security_nat_policy'] is None: + return None + if 'use_route_domain_policy' not in self._values['security_nat_policy']: + return None + return self._values['security_nat_policy']['use_route_domain_policy'] + + @property + def sec_nat_policy(self): + if self._values['security_nat_policy'] is None: + return None + if 'policy' not in self._values['security_nat_policy']: + return None + if self._values['security_nat_policy']['policy'] == '': + return '' + return fq_name(self.partition, self._values['security_nat_policy']['policy']) + + @property + def security_nat_policy(self): + result = dict() + if self.sec_nat_policy: + result['policy'] = self.sec_nat_policy + if self.sec_nat_use_device_policy is not None: + result['use_device_policy'] = self.sec_nat_use_device_policy + if self.sec_nat_use_rd_policy is not None: + result['use_route_domain_policy'] = self.sec_nat_use_rd_policy + if result: + return result + return None + + @property + def mirror(self): + result = flatten_boolean(self._values['mirror']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def rate_limit(self): + if self._values['rate_limit'] is None: + return None + if 0 <= int(self._values['rate_limit']) <= 4294967295: + return int(self._values['rate_limit']) + raise F5ModuleError( + "Valid 'rate_limit' must be in range 0 - 4294967295." + ) + + @property + def rate_limit_src_mask(self): + if self._values['rate_limit_src_mask'] is None: + return None + if 0 <= int(self._values['rate_limit_src_mask']) <= 4294967295: + return int(self._values['rate_limit_src_mask']) + raise F5ModuleError( + "Valid 'rate_limit_src_mask' must be in range 0 - 4294967295." + ) + + @property + def rate_limit_dst_mask(self): + if self._values['rate_limit_dst_mask'] is None: + return None + if 0 <= int(self._values['rate_limit_dst_mask']) <= 4294967295: + return int(self._values['rate_limit_dst_mask']) + raise F5ModuleError( + "Valid 'rate_limit_dst_mask' must be in range 0 - 4294967295." + ) + + @property + def clone_pools(self): + if self._values['clone_pools'] is None: + return None + if not self._values['clone_pools']: + return [] + self._check_clone_pool_contexts() + result = [] + for item in self._values['clone_pools']: + pool_name = fq_name(self.partition, self._check_pool(item['pool_name'])) + context = item['context'] + tmp = { + 'name': pool_name, + 'context': context + } + result.append(tmp) + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + for returnable in self.returnables: + try: + result[returnable] = getattr(self, returnable) + except Exception: + raise + result = self._filter_params(result) + return result + + +class UsableChanges(Changes): + @property + def destination(self): + if self._values['type'] == 'internal': + return None + return self._values['destination'] + + @property + def vlans(self): + if self._values['vlans'] is None: + return None + elif len(self._values['vlans']) == 0: + return [] + elif any(x for x in self._values['vlans'] if x.lower() in ['/common/all', 'all']): + return [] + return self._values['vlans'] + + @property + def irules(self): + if self._values['irules'] is None: + return None + if self._values['type'] in ['dhcp', 'stateless', 'reject', 'internal']: + return None + if self._values['irules'] == '': + return [] + return self._values['irules'] + + @property + def policies(self): + if self._values['policies'] is None: + return None + if self._values['type'] in ['dhcp', 'reject', 'internal']: + return None + if self._values['policies'] == '': + return [] + return self._values['policies'] + + @property + def default_persistence_profile(self): + if self._values['default_persistence_profile'] is None: + return None + if self._values['type'] == 'dhcp': + return None + if not self._values['default_persistence_profile']: + return [] + return [self._values['default_persistence_profile']] + + @property + def fallback_persistence_profile(self): + if self._values['fallback_persistence_profile'] is None: + return None + if self._values['type'] == 'dhcp': + return None + return self._values['fallback_persistence_profile'] + + @property + def snat(self): + if self._values['snat'] is None: + return None + if self._values['type'] in ['dhcp', 'reject', 'internal']: + return None + return self._values['snat'] + + @property + def dhcpRelay(self): + if self._values['type'] == 'dhcp': + return True + + @property + def reject(self): + if self._values['type'] == 'reject': + return True + + @property + def stateless(self): + if self._values['type'] == 'stateless': + return True + + @property + def internal(self): + if self._values['type'] == 'internal': + return True + + @property + def ipForward(self): + if self._values['type'] == 'forwarding-ip': + return True + + @property + def l2Forward(self): + if self._values['type'] == 'forwarding-l2': + return True + + @property + def security_log_profiles(self): + if self._values['security_log_profiles'] is None: + return None + mutex = ('Log all requests', 'Log illegal requests') + if len([x for x in self._values['security_log_profiles'] if x.endswith(mutex)]) >= 2: + raise F5ModuleError( + "The 'Log all requests' and 'Log illegal requests' are mutually exclusive." + ) + return self._values['security_log_profiles'] + + @property + def security_nat_policy(self): + if self._values['security_nat_policy'] is None: + return None + result = dict() + sec = self._values['security_nat_policy'] + if 'policy' in sec: + result['policy'] = sec['policy'] + if 'use_device_policy' in sec: + result['useDevicePolicy'] = 'yes' if sec['use_device_policy'] else 'no' + if 'use_route_domain_policy' in sec: + result['useRouteDomainPolicy'] = 'yes' if sec['use_route_domain_policy'] else 'no' + if result: + return result + return None + + +class ReportableChanges(Changes): + @property + def mirror(self): + if self._values['mirror'] is None: + return None + elif self._values['mirror'] == 'enabled': + return 'yes' + return 'no' + + @property + def snat(self): + if self._values['snat'] is None: + return None + result = self._values['snat'].get('type', None) + if result == 'automap': + return 'Automap' + elif result == 'none': + return 'none' + result = self._values['snat'].get('pool', None) + return result + + @property + def destination(self): + params = ApiParameters(params=dict(destination=self._values['destination'])) + result = params.destination_tuple.ip + return result + + @property + def port(self): + params = ApiParameters(params=dict(destination=self._values['destination'])) + result = params.destination_tuple.port + return result + + @property + def default_persistence_profile(self): + if self._values['default_persistence_profile'] is None: + return None + if len(self._values['default_persistence_profile']) == 0: + return [] + profile = self._values['default_persistence_profile'][0] + result = '/{0}/{1}'.format(profile['partition'], profile['name']) + return result + + @property + def policies(self): + if self._values['policies'] is None: + return None + if len(self._values['policies']) == 0: + return [] + if len(self._values['policies']) == 1 and self._values['policies'][0] == '': + return '' + result = ['/{0}/{1}'.format(x['partition'], x['name']) for x in self._values['policies']] + return result + + @property + def irules(self): + if self._values['irules'] is None: + return None + if len(self._values['irules']) == 0: + return [] + if len(self._values['irules']) == 1 and self._values['irules'][0] == '': + return '' + return self._values['irules'] + + @property + def enabled_vlans(self): + if self._values['vlans'] is None: + return None + if len(self._values['vlans']) == 0 and self._values['vlans_disabled'] is True: + return 'all' + elif len(self._values['vlans']) > 0 and self._values['vlans_enabled'] is True: + return self._values['vlans'] + + @property + def disabled_vlans(self): + if self._values['vlans'] is None: + return None + if len(self._values['vlans']) > 0 and self._values['vlans_disabled'] is True: + return self._values['vlans'] + + @property + def address_translation(self): + if self._values['address_translation'] is None: + return None + if self._values['address_translation'] == 'enabled': + return True + return False + + @property + def port_translation(self): + if self._values['port_translation'] is None: + return None + if self._values['port_translation'] == 'enabled': + return True + return False + + @property + def ip_protocol(self): + if self._values['ip_protocol'] is None: + return None + try: + int(self._values['ip_protocol']) + except ValueError: + return self._values['ip_protocol'] + + protocol = next((x[0] for x in self.ip_protocols_map if x[1] == self._values['ip_protocol']), None) + if protocol: + return protocol + return self._values['ip_protocol'] + + +class VirtualServerValidator(object): + def __init__(self, module=None, client=None, want=None, have=None): + self.have = have if have else ApiParameters() + self.want = want if want else ModuleParameters() + self.client = client + self.module = module + + def check_update(self): + # Regular checks + self._override_port_by_type() + self._override_protocol_by_type() + self._verify_type_has_correct_profiles() + self._verify_default_persistence_profile_for_type() + self._verify_fallback_persistence_profile_for_type() + self._update_persistence_profile() + self._ensure_server_type_supports_vlans() + self._verify_type_has_correct_ip_protocol() + + # For different server types + self._verify_dhcp_profile() + self._verify_fastl4_profile() + self._verify_stateless_profile() + + def check_create(self): + # Regular checks + self._set_default_ip_protocol() + self._set_default_profiles() + self._override_port_by_type() + self._override_protocol_by_type() + self._verify_type_has_correct_profiles() + 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() + self._verify_type_has_correct_ip_protocol() + self._verify_minimum_profile() + + # For different server types + self._verify_dhcp_profile() + self._verify_fastl4_profile() + self._verify_stateless_profile_on_create() + + def _ensure_server_type_supports_vlans(self): + """Verifies the specified server type supports VLANs + + A select number of server types do not support VLANs. This method + checks to see if the specified types were provided along with VLANs. + If they were, the module will raise an error informing the user that + they need to either remove the VLANs, or, change the ``type``. + + Returns: + None: Returned if no VLANs are specified. + Raises: + F5ModuleError: Raised if the server type conflicts with VLANs. + """ + if self.want.enabled_vlans is None: + return + if self.want.type == 'internal': + raise F5ModuleError( + "The 'internal' server type does not support VLANs." + ) + + def _override_vlans_if_all_specified(self): + """Overrides any specified VLANs if "all" VLANs are specified + + The special setting "all VLANs" in a BIG-IP requires that no other VLANs + be specified. If you specify any number of VLANs, AND include the "all" + VLAN, this method will erase all of the other VLANs and only return the + "all" VLAN. + """ + all_vlans = ['/common/all', 'all'] + if self.want.enabled_vlans is not None: + if any(x for x in self.want.enabled_vlans if x.lower() in all_vlans): + self.want.update( + dict( + enabled_vlans=[], + vlans_disabled=True, + vlans_enabled=False + ) + ) + + def _override_port_by_type(self): + if self.want.type == 'dhcp': + self.want.update({'port': 67}) + elif self.want.type == 'internal': + self.want.update({'port': 0}) + + def _override_protocol_by_type(self): + if self.want.type in ['stateless']: + self.want.update({'ip_protocol': 17}) + + def _check_source_and_destination_match(self): + """Verify that destination and source are of the same IP version + + BIG-IP does not allow for mixing of the IP versions for destination and + source addresses. For example, a destination IPv6 address cannot be + associated with a source IPv4 address. + + This method checks that you specified the same IP version for these + parameters. + + This method will not do this check if the virtual address name is used. + + Raises: + F5ModuleError: Raised when the IP versions of source and destination differ. + """ + if self.want.source and self.want.destination and not self.want.destination_tuple.not_ip: + want = ip_interface(u'{0}/{1}'.format(self.want.source_tuple.ip, self.want.source_tuple.cidr)) + have = ip_interface(u'{0}'.format(self.want.destination_tuple.ip)) + if want.version != have.version: + raise F5ModuleError( + "The source and destination addresses for the virtual server " + "must be be the same type (IPv4 or IPv6)." + ) + + def _verify_type_has_correct_ip_protocol(self): + if self.want.ip_protocol is None: + return + if self.want.type == 'standard': + # Standard supports + # - tcp + # - udp + # - sctp + # - ipsec-ah + # - ipsec esp + # - all protocols + if self.want.ip_protocol not in [6, 17, 132, 51, 50, 'any']: + raise F5ModuleError( + "The 'standard' server type does not support the specified 'ip_protocol'." + ) + elif self.want.type == 'performance-http': + # Perf HTTP supports + # + # - tcp + if self.want.ip_protocol not in [6]: + raise F5ModuleError( + "The 'performance-http' server type does not support the specified 'ip_protocol'." + ) + elif self.want.type == 'stateless': + # Stateless supports + # + # - udp + if self.want.ip_protocol not in [17]: + raise F5ModuleError( + "The 'stateless' server type does not support the specified 'ip_protocol'." + ) + elif self.want.type == 'dhcp': + # DHCP supports no IP protocols + if self.want.ip_protocol is not None: + raise F5ModuleError( + "The 'dhcp' server type does not support an 'ip_protocol'." + ) + elif self.want.type == 'internal': + # Internal supports + # + # - tcp + # - udp + if self.want.ip_protocol not in [6, 17]: + raise F5ModuleError( + "The 'internal' server type does not support the specified 'ip_protocol'." + ) + elif self.want.type == 'message-routing': + # Message Routing supports + # + # - tcp + # - udp + # - sctp + # - all protocols + if self.want.ip_protocol not in [6, 17, 132, 'all', 'any']: + raise F5ModuleError( + "The 'message-routing' server type does not support the specified 'ip_protocol'." + ) + + def _verify_virtual_has_required_parameters(self): + """Verify that the virtual has required parameters + + Virtual servers require several parameters that are not necessarily required + when updating the virtual. This method will check for the required params + upon creation. + + Ansible supports ``default`` variables in an Argument Spec, but those defaults + apply to all operations; including create, update, and delete. Since users are not + required to always specify these parameters, we cannot use Ansible's facility. + If we did, and then users would be required to provide them when, for example, + they attempted to delete a virtual (even though they are not required to delete + a virtual. + + Raises: + F5ModuleError: Raised when the user did not specify required parameters. + """ + required_resources = ['destination', 'port'] + if self.want.type == 'internal': + return + if all(getattr(self.want, v) is None for v in required_resources): + raise F5ModuleError( + "You must specify both of " + ', '.join(required_resources) + ) + + def _verify_default_persistence_profile_for_type(self): + """Verify that the server type supports default persistence profiles + + Verifies that the specified server type supports default persistence profiles. + Some virtual servers do not support these types of profiles. This method will + check that the type actually supports what you are sending it. + + Types that do not, at this time, support default persistence profiles include, + + * dhcp + * message-routing + * reject + * stateless + * forwarding-ip + * forwarding-l2 + + Raises: + F5ModuleError: Raised if server type does not support default persistence profiles. + """ + default_profile_not_allowed = [ + 'dhcp', 'message-routing', 'reject', 'stateless', 'forwarding-ip', 'forwarding-l2' + ] + if self.want.ip_protocol in default_profile_not_allowed: + raise F5ModuleError( + "The '{0}' server type does not support a 'default_persistence_profile'".format(self.want.type) + ) + + def _verify_fallback_persistence_profile_for_type(self): + """Verify that the server type supports fallback persistence profiles + + Verifies that the specified server type supports fallback persistence profiles. + Some virtual servers do not support these types of profiles. This method will + check that the type actually supports what you are sending it. + + Types that do not, at this time, support fallback persistence profiles include, + + * dhcp + * message-routing + * reject + * stateless + * forwarding-ip + * forwarding-l2 + * performance-http + + Raises: + F5ModuleError: Raised if server type does not support fallback persistence profiles. + """ + default_profile_not_allowed = [ + 'dhcp', 'message-routing', 'reject', 'stateless', 'forwarding-ip', 'forwarding-l2', + 'performance-http' + ] + if self.want.ip_protocol in default_profile_not_allowed: + raise F5ModuleError( + "The '{0}' server type does not support a 'fallback_persistence_profile'".format(self.want.type) + ) + + def _update_persistence_profile(self): + # This must be changed back to a list to make a valid REST API + # value. The module manipulates this as a normal dictionary + if self.want.default_persistence_profile is not None: + self.want.update({'default_persistence_profile': self.want.default_persistence_profile}) + + def _verify_type_has_correct_profiles(self): + """Verify that specified server type does not include forbidden profiles + + The type of the server determines the ``type``s of profiles that it accepts. This + method checks that the server ``type`` that you specified is indeed one that can + accept the profiles that you specified. + + The common situations are + + * ``standard`` types that include ``fasthttp``, ``fastl4``, or ``message routing`` profiles + * ``fasthttp`` types that are missing a ``fasthttp`` profile + * ``fastl4`` types that are missing a ``fastl4`` profile + * ``message-routing`` types that are missing ``diameter`` or ``sip`` profiles + + Raises: + F5ModuleError: Raised when a validation check fails. + """ + if self.want.type == 'standard': + if self.want.has_fasthttp_profiles: + raise F5ModuleError("A 'standard' type may not have 'fasthttp' profiles.") + if self.want.has_fastl4_profiles: + raise F5ModuleError("A 'standard' type may not have 'fastl4' profiles.") + # if self.want.has_message_routing_profiles: + # raise F5ModuleError("A 'standard' type may not have 'message-routing' profiles.") + elif self.want.type == 'performance-http': + if not self.want.has_fasthttp_profiles: + raise F5ModuleError("A 'fasthttp' type must have at least one 'fasthttp' profile.") + elif self.want.type == 'performance-l4': + if not self.want.has_fastl4_profiles: + raise F5ModuleError("A 'fastl4' type must have at least one 'fastl4' profile.") + elif self.want.type == 'message-routing': + if not self.want.has_message_routing_profiles: + raise F5ModuleError("A 'message-routing' type must have either a 'sip' or 'diameter' profile.") + + def _set_default_ip_protocol(self): + if self.want.type == 'dhcp': + return + if self.want.ip_protocol is None: + self.want.update({'ip_protocol': 6}) + + def _set_default_profiles(self): + if self.want.type == 'standard': + if not self.want.profiles: + # Sets a default profiles when creating a new standard virtual. + # + # It appears that if no profiles are deliberately specified, then under + # certain circumstances, the server type will default to ``performance-l4``. + # + # It's unclear what these circumstances are, but they are met in issue 00093. + # If this block of profile setting code is removed, the virtual server's + # type will change to performance-l4 for some reason. + # + if self.want.ip_protocol == 6: + self.want.update({'profiles': ['tcp']}) + if self.want.ip_protocol == 17: + self.want.update({'profiles': ['udp']}) + if self.want.ip_protocol == 132: + self.want.update({'profiles': ['sctp']}) + + def _verify_minimum_profile(self): + if self.want.profiles: + return None + if self.want.type == 'internal' and self.want.profiles == '': + raise F5ModuleError( + "An 'internal' server must have at least one profile relevant to its 'ip_protocol'. " + "For example, 'tcp', 'udp', or variations of those." + ) + + def _verify_dhcp_profile(self): + if self.want.type != 'dhcp': + return + if self.want.profiles is None: + return + have = set(self.read_dhcp_profiles_from_device()) + want = set([x['fullPath'] for x in self.want.profiles]) + if have.intersection(want): + return True + raise F5ModuleError( + "A dhcp profile, such as 'dhcpv4', or 'dhcpv6' must be specified when 'type' is 'dhcp'." + ) + + def _verify_fastl4_profile(self): + if self.want.type != 'performance-l4': + return + if self.want.profiles is None: + return + have = set(self.read_fastl4_profiles_from_device()) + want = set([x['fullPath'] for x in self.want.profiles]) + if have.intersection(want): + return True + raise F5ModuleError( + "A performance-l4 profile, such as 'fastL4', must be specified when 'type' is 'performance-l4'." + ) + + def _verify_fasthttp_profile(self): + if self.want.type != 'performance-http': + return + if self.want.profiles is None: + return + have = set(self.read_fasthttp_profiles_from_device()) + want = set([x['fullPath'] for x in self.want.profiles]) + if have.intersection(want): + return True + raise F5ModuleError( + "A performance-http profile, such as 'fasthttp', must be specified when 'type' is 'performance-http'." + ) + + def _verify_stateless_profile_on_create(self): + if self.want.type != 'stateless': + return + result = self._verify_stateless_profile() + if result is None: + raise F5ModuleError( + "A udp profile, must be specified when 'type' is 'stateless'." + ) + + def _verify_stateless_profile(self): + if self.want.type != 'stateless': + return + if self.want.profiles is None: + return + have = set(self.read_udp_profiles_from_device()) + want = set([x['fullPath'] for x in self.want.profiles]) + if have.intersection(want): + return True + raise F5ModuleError( + "A udp profile, must be specified when 'type' is 'stateless'." + ) + + def read_dhcp_profiles_from_device(self): + result = [] + result += self.read_dhcpv4_profiles_from_device() + result += self.read_dhcpv6_profiles_from_device() + return result + + def read_dhcpv4_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/dhcpv4/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [fq_name(self.want.partition, x['name']) for x in response['items']] + return result + + def read_dhcpv6_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/dhcpv6/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [fq_name(self.want.partition, x['name']) for x in response['items']] + return result + + def read_fastl4_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + # result = [fq_name(self.want.partition, x['name']) for x in response['items']] + result = [x['fullPath'] for x in response['items']] + return result + + def read_fasthttp_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fasthttp/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [fq_name(self.want.partition, x['name']) for x in response['items']] + return result + + def read_udp_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/udp/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = [fq_name(self.want.partition, x['name']) for x in response['items']] + return result + + +class Difference(object): + def __init__(self, want, have=None): + self.have = have + self.want = want + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + result = self.__default(param) + return result + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + def to_tuple(self, items): + result = [] + for x in items: + tmp = [(str(k), str(v)) for k, v in iteritems(x)] + result += tmp + return result + + def _diff_complex_items(self, want, have): + if want == [] and have is None: + return None + if want is None: + return None + w = self.to_tuple(want) + h = self.to_tuple(have) + if set(w).issubset(set(h)): + return None + else: + return want + + def _update_vlan_status(self, result): + if self.want.vlans_disabled is not None: + if self.want.vlans_disabled != self.have.vlans_disabled: + result['vlans_disabled'] = self.want.vlans_disabled + result['vlans_enabled'] = not self.want.vlans_disabled + elif self.want.vlans_enabled is not None: + if any(x.lower().endswith('/all') for x in self.want.vlans): + if self.have.vlans_enabled is True: + result['vlans_disabled'] = True + result['vlans_enabled'] = False + elif self.want.vlans_enabled != self.have.vlans_enabled: + result['vlans_disabled'] = not self.want.vlans_enabled + result['vlans_enabled'] = self.want.vlans_enabled + + @property + def destination(self): + # The internal type does not support the 'destination' parameter, so it is ignored. + if self.want.type == 'internal': + return + + addr_tuple = [self.want.destination, self.want.port, self.want.route_domain] + if all(x is None for x in addr_tuple): + return None + + have = self.have.destination_tuple + if self.want.port is None: + self.want.update({'port': have.port}) + if self.want.route_domain is None: + self.want.update({'route_domain': have.route_domain}) + if self.want.destination_tuple.ip is None: + address = have.ip + else: + address = self.want.destination_tuple.ip + want = self.want._format_destination(address, self.want.port, self.want.route_domain) + if want != self.have.destination: + return fq_name(self.want.partition, want) + + @property + def source(self): + if self.want.source is None: + return None + if self.want.source != self.have.source: + return self.want.source + + @property + def vlans(self): + if self.want.vlans is None: + return None + elif self.want.vlans == [] and self.have.vlans is None: + return None + elif self.want.vlans == self.have.vlans: + return None + + # Specifically looking for /all because the vlans return value will be + # an FQDN list. This means that "all" will be returned as "/partition/all", + # ex, /Common/all. + # + # We do not want to accidentally match values that would end with the word + # "all", like "vlansall". Therefore we look for the forward slash because this + # is a path delimiter. + elif any(x.lower().endswith('/all') for x in self.want.vlans): + if self.have.vlans is None: + return None + else: + return [] + else: + return self.want.vlans + + @property + def enabled_vlans(self): + return self.vlan_status + + @property + def disabled_vlans(self): + return self.vlan_status + + @property + def vlan_status(self): + result = dict() + vlans = self.vlans + if vlans is not None: + result['vlans'] = vlans + self._update_vlan_status(result) + return result + + @property + def port(self): + result = self.destination + if result is not None: + return dict( + destination=result + ) + + @property + def profiles(self): + if self.want.profiles is None: + return None + if self.want.profiles == '' and len(self.have.profiles) > 0: + have = set([(p['name'], p['context'], p['fullPath']) for p in self.have.profiles]) + if len(self.have.profiles) == 1: + if not any(x[0] in ['tcp', 'udp', 'sctp'] for x in have): + return [] + else: + return None + else: + return [] + if self.want.profiles == '' and len(self.have.profiles) == 0: + return None + want = set([(p['name'], p['context'], p['fullPath']) for p in self.want.profiles]) + have = set([(p['name'], p['context'], p['fullPath']) for p in self.have.profiles]) + if len(have) == 0: + return self.want.profiles + elif len(have) == 1: + if want != have: + return self.want.profiles + else: + if not any(x[0] == 'tcp' for x in want): + if self.want.type != 'stateless': + have = set([x for x in have if x[0] != 'tcp']) + if not any(x[0] == 'udp' for x in want): + have = set([x for x in have if x[0] != 'udp']) + if not any(x[0] == 'sctp' for x in want): + if self.want.type != 'stateless': + have = set([x for x in have if x[0] != 'sctp']) + want = set([(p[2], p[1]) for p in want]) + have = set([(p[2], p[1]) for p in have]) + if want != have: + return self.want.profiles + + @property + def ip_protocol(self): + if self.want.ip_protocol != self.have.ip_protocol: + return self.want.ip_protocol + + @property + def fallback_persistence_profile(self): + if self.want.fallback_persistence_profile is None: + return None + if self.want.fallback_persistence_profile == '' and self.have.fallback_persistence_profile is not None: + return "" + if self.want.fallback_persistence_profile == '' and self.have.fallback_persistence_profile is None: + return None + if self.want.fallback_persistence_profile != self.have.fallback_persistence_profile: + return self.want.fallback_persistence_profile + + @property + def default_persistence_profile(self): + if self.want.default_persistence_profile is None: + return None + if self.want.default_persistence_profile == '' and self.have.default_persistence_profile is not None: + return [] + if self.want.default_persistence_profile == '' and self.have.default_persistence_profile is None: + return None + if self.have.default_persistence_profile is None: + return dict( + default_persistence_profile=self.want.default_persistence_profile + ) + w_name = self.want.default_persistence_profile.get('name', None) + w_partition = self.want.default_persistence_profile.get('partition', None) + h_name = self.have.default_persistence_profile.get('name', None) + h_partition = self.have.default_persistence_profile.get('partition', None) + if w_name != h_name or w_partition != h_partition: + return dict( + default_persistence_profile=self.want.default_persistence_profile + ) + + @property + def ip_intelligence_policy(self): + if self.want.ip_intelligence_policy is None: + return None + if self.want.ip_intelligence_policy == '' and self.have.ip_intelligence_policy is not None: + return "" + if self.want.ip_intelligence_policy == '' and self.have.ip_intelligence_policy is None: + return None + if self.want.ip_intelligence_policy != self.have.ip_intelligence_policy: + return self.want.ip_intelligence_policy + + @property + def policies(self): + if self.want.policies is None: + return None + if self.want.policies in [[], ''] and self.have.policies is None: + return None + if self.want.policies == '' and len(self.have.policies) > 0: + return [] + if not self.have.policies: + return self.want.policies + want = set([(p['name'], p['partition']) for p in self.want.policies]) + have = set([(p['name'], p['partition']) for p in self.have.policies]) + if not want == have: + return self.want.policies + + @property + def snat(self): + if self.want.snat is None: + return None + if self.want.snat['type'] != self.have.snat['type']: + result = dict(snat=self.want.snat) + return result + + if self.want.snat.get('pool', None) is None: + return None + + if self.want.snat['pool'] != self.have.snat['pool']: + result = dict(snat=self.want.snat) + return result + + @property + def enabled(self): + if self.want.state == 'enabled' and self.have.disabled: + result = dict( + enabled=True, + disabled=False + ) + return result + elif self.want.state == 'disabled' and self.have.enabled: + result = dict( + enabled=False, + disabled=True + ) + return result + + @property + def irules(self): + if self.want.irules is None: + return None + if self.want.irules == '' and len(self.have.irules) > 0: + return [] + if self.want.irules in [[], ''] and len(self.have.irules) == 0: + return None + if self.want.irules != self.have.irules: + return self.want.irules + + @property + def pool(self): + if self.want.pool is None: + return None + if self.want.pool == '' and self.have.pool is not None: + return "" + if self.want.pool == '' and self.have.pool is None: + return None + if self.want.pool != self.have.pool: + return self.want.pool + + @property + def metadata(self): + if self.want.metadata is None: + return None + elif len(self.want.metadata) == 0 and self.have.metadata is None: + return None + elif len(self.want.metadata) == 0 and not self.want.insert_metadata: + return None + elif len(self.want.metadata) == 0 and self.want.insert_metadata: + return [] + elif self.have.metadata is None: + return self.want.metadata + result = self._diff_complex_items(self.want.metadata, self.have.metadata) + return result + + @property + def type(self): + if self.want.type != self.have.type: + raise F5ModuleError( + "Changing the 'type' parameter is not supported." + ) + + @property + def security_log_profiles(self): + result = cmp_simple_list(self.want.security_log_profiles, self.have.security_log_profiles) + return result + + @property + def security_nat_policy(self): + result = dict() + if self.want.sec_nat_use_device_policy is not None: + if self.want.sec_nat_use_device_policy != self.have.sec_nat_use_device_policy: + result['use_device_policy'] = self.want.sec_nat_use_device_policy + if self.want.sec_nat_use_rd_policy is not None: + if self.want.sec_nat_use_rd_policy != self.have.sec_nat_use_rd_policy: + result['use_route_domain_policy'] = self.want.sec_nat_use_rd_policy + if self.want.sec_nat_policy is not None: + if self.want.sec_nat_policy == '' and self.have.sec_nat_policy is None: + pass + elif self.want.sec_nat_policy != self.have.sec_nat_policy: + result['policy'] = self.want.sec_nat_policy + if result: + return dict(security_nat_policy=result) + + @property + def clone_pools(self): + if self.want.clone_pools == [] and self.have.clone_pools: + return self.want.clone_pools + result = self._diff_complex_items(self.want.clone_pools, self.have.clone_pools) + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = ApiParameters(client=self.client) + self.want = ModuleParameters(client=self.client, params=self.module.params) + self.changes = UsableChanges() + self.provisioned_modules = [] + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + self.provisioned_modules = modules_provisioned(self.client) + + if state in ['present', 'enabled', 'disabled']: + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.want.bypass_module_checks: + validator = VirtualServerValidator( + module=self.module, client=self.client, have=self.have, want=self.want + ) + validator.check_update() + + if self.want.ip_intelligence_policy is not None: + if not any(x for x in self.provisioned_modules if x in ['afm', 'asm']): + raise F5ModuleError( + "AFM must be provisioned to configure an IP Intelligence policy." + ) + + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource") + return True + + def get_reportable_changes(self): + result = ReportableChanges(params=self.changes.to_return()) + return result + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def create(self): + if not self.want.bypass_module_checks: + validator = VirtualServerValidator( + module=self.module, client=self.client, have=self.have, want=self.want + ) + validator.check_create() + + if self.want.ip_intelligence_policy is not None: + if not any(x for x in self.provisioned_modules if x in ['afm', 'asm']): + raise F5ModuleError( + "AFM must be provisioned to configure an IP Intelligence policy." + ) + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def update_on_device(self): + params = self.changes.api_params() + + 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) + + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/{2}?expandSubcollections=true".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response, client=self.client) + raise F5ModuleError(resp.content) + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + 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) + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/".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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + state=dict( + default='present', + choices=['present', 'absent', 'disabled', 'enabled'] + ), + name=dict( + required=True, + aliases=['vs'] + ), + destination=dict( + aliases=['address', 'ip'] + ), + port=dict(), + profiles=dict( + type='raw', + options=dict( + name=dict(), + context=dict(default='all', choices=['all', 'server-side', 'client-side']) + ), + aliases=['all_profiles'], + ), + policies=dict( + type='list', + elements='str', + aliases=['all_policies'] + ), + irules=dict( + type='list', + elements='str', + aliases=['all_rules'] + ), + enabled_vlans=dict( + type='list', + elements='str', + ), + disabled_vlans=dict( + type='list', + elements='str', + ), + pool=dict(), + description=dict(), + snat=dict(), + default_persistence_profile=dict(), + fallback_persistence_profile=dict(), + source=dict(), + metadata=dict(type='raw'), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + address_translation=dict(type='bool'), + port_translation=dict(type='bool'), + source_port=dict( + choices=[ + 'preserve', 'preserve-strict', 'change' + ] + ), + ip_protocol=dict( + choices=[ + 'ah', 'any', 'bna', 'esp', 'etherip', 'gre', 'icmp', 'ipencap', 'ipv6', + 'ipv6-auth', 'ipv6-crypt', 'ipv6-icmp', 'isp-ip', 'mux', 'ospf', + 'sctp', 'tcp', 'udp', 'udplite' + ] + ), + type=dict( + default='standard', + choices=[ + 'standard', 'forwarding-ip', 'forwarding-l2', 'internal', 'message-routing', + 'performance-http', 'performance-l4', 'reject', 'stateless', 'dhcp' + ] + ), + mirror=dict(type='bool'), + auto_last_hop=dict( + choices=['enabled', 'disabled', 'default'] + ), + mask=dict(), + firewall_staged_policy=dict(), + firewall_enforced_policy=dict(), + ip_intelligence_policy=dict(), + service_down_immediate_action=dict( + choices=['drop', 'none', 'reset'] + ), + security_log_profiles=dict( + type='list', + elements='str', + ), + security_nat_policy=dict( + type='dict', + options=dict( + policy=dict(), + use_device_policy=dict(type='bool'), + use_route_domain_policy=dict(type='bool') + ) + ), + insert_metadata=dict( + type='bool', + default='yes' + ), + rate_limit=dict(type='int'), + rate_limit_dst_mask=dict(type='int'), + rate_limit_src_mask=dict(type='int'), + rate_limit_mode=dict( + default='object', + choices=[ + 'destination', 'object-destination', 'object-source-destination', + 'source-destination', 'object', 'object-source', 'source' + ] + ), + clone_pools=dict( + type='list', + elements='dict', + options=dict( + pool_name=dict(required=True), + context=dict( + required=True, + choices=[ + 'clientside', 'serverside' + ] + ) + ) + ), + check_profiles=dict( + type='bool', + default='yes' + ), + bypass_module_checks=dict( + type='bool', + default='no' + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.mutually_exclusive = [ + ['enabled_vlans', 'disabled_vlans'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_vlan.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_vlan.py new file mode 100644 index 00000000..a8aec927 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_vlan.py @@ -0,0 +1,983 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_vlan +short_description: Manage VLANs on a BIG-IP system +description: + - Manage VLANs on a BIG-IP system +version_added: "1.0.0" +options: + description: + description: + - The description of the VLAN. + type: str + tagged_interfaces: + description: + - Specifies a list of tagged interfaces and trunks you want to + configure for the VLAN. Use tagged interfaces or trunks when + you want to assign a single interface or trunk to multiple VLANs. + - This parameter is mutually exclusive with the C(untagged_interfaces) + and C(interfaces) parameters. + type: list + elements: str + aliases: + - tagged_interface + untagged_interfaces: + description: + - Specifies a list of untagged interfaces and trunks you want to + configure for the VLAN. + - This parameter is mutually exclusive with the C(tagged_interfaces) + and C(interfaces) parameters. + type: list + elements: str + aliases: + - untagged_interface + name: + description: + - The VLAN to manage. If the special VLAN C(ALL) is specified with + the C(state) value of C(absent), all VLANs will be removed. + type: str + required: True + state: + description: + - The state of the VLAN on the system. When C(present), guarantees + the VLAN exists with the provided attributes. When C(absent), + removes the VLAN from the system. + type: str + choices: + - absent + - present + default: present + tag: + description: + - Tag number for the VLAN. The tag number can be any integer between 1 + and 4094. The system automatically assigns a tag number if you do not + specify a value. + type: int + mtu: + description: + - Specifies the maximum transmission unit (MTU) for traffic on this VLAN. + When creating a new VLAN, if this parameter is not specified, the default + value used is C(1500). + - This number must be between 576 to 9198. + type: int + cmp_hash: + description: + - Specifies how the traffic on the VLAN is disaggregated. The value + you select determines the traffic disaggregation method. You can choose to + disaggregate traffic based on C(source-address) (the source IP address), + C(destination-address) (destination IP address), or C(default), which + specifies the default CMP hash uses L4 ports. + - When creating a new VLAN, if this parameter is not specified, the default + is C(default). + type: str + choices: + - default + - destination-address + - source-address + - dst-ip + - src-ip + - dest + - destination + - source + - dst + - src + dag_tunnel: + description: + - Specifies how the disaggregator (DAG) distributes received tunnel-encapsulated + packets to TMM instances. Select C(inner) to distribute packets based on information + in inner headers. Select C(outer) to distribute packets based on information in + outer headers without inspecting inner headers. + - When creating a new VLAN, if this parameter is not specified, the default + is C(outer). + - This parameter is not supported on Virtual Editions (VEs) of BIG-IP. + type: str + choices: + - inner + - outer + dag_round_robin: + description: + - Specifies whether some of the stateless traffic on the VLAN should be + disaggregated in a round-robin order instead of using a static hash. The + stateless traffic includes non-IP L2 traffic, ICMP, some UDP protocols, + and so on. + - When creating a new VLAN, if this parameter is not specified, the default + is (no). + type: bool + partition: + description: + - Device partition to manage resources on. + type: str + default: Common + source_check: + description: + - When C(yes), specifies the system verifies the return route to an initial + packet is the same VLAN from which the packet originated. + - The system performs this verification only if the C(auto_last_hop) option is C(no). + type: bool + fail_safe: + description: + - When C(yes), specifies the VLAN takes the specified C(fail_safe_action) if the + system detects a loss of traffic on this VLAN's interfaces. + type: bool + fail_safe_timeout: + description: + - Specifies the number of seconds a system can run without detecting network + traffic on this VLAN before it takes the C(fail_safe_action). + type: int + fail_safe_action: + description: + - Specifies the action the system takes when it does not detect any traffic on + this VLAN, and the C(fail_safe_timeout) has expired. + type: str + choices: + - reboot + - restart-all + - failover + sflow_poll_interval: + description: + - Specifies the maximum interval in seconds between two pollings. + type: int + sflow_sampling_rate: + description: + - Specifies the ratio of packets observed to the samples generated. + type: int + interfaces: + description: + - Interfaces you want to add to the VLAN. This can include both tagged + and untagged interfaces, as the C(tagging) parameter specifies. + - This parameter is mutually exclusive with the C(untagged_interfaces) and + C(tagged_interfaces) parameters. + type: list + elements: dict + suboptions: + interface: + description: + - The name of the interface + type: str + tagging: + description: + - Whether the interface is C(tagged) or C(untagged). + type: str + choices: + - tagged + - untagged + hw_syn_cookie: + description: + - Enables hardware syncookie mode on a VLAN. + - When C(yes), the hardware per-VLAN SYN cookie protection is triggered when the certain traffic threshold + is reached on supported platforms. + type: bool + version_added: "1.3.0" +notes: + - Requires BIG-IP versions >= 12.0.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create VLAN + bigip_vlan: + name: net1 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Set VLAN tag + bigip_vlan: + name: net1 + tag: 2345 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Add VLAN 2345 as tagged to interface 1.1 + bigip_vlan: + tagged_interface: 1.1 + name: net1 + tag: 2345 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Add VLAN 1234 as tagged to interfaces 1.1 and 1.2 + bigip_vlan: + tagged_interfaces: + - 1.1 + - 1.2 + name: net1 + tag: 1234 + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The description set on the VLAN. + returned: changed + type: str + sample: foo VLAN +interfaces: + description: Interfaces the VLAN is assigned to. + returned: changed + type: list + sample: ['1.1','1.2'] +partition: + description: The partition the VLAN was created on. + returned: changed + type: str + sample: Common +tag: + description: The ID of the VLAN. + returned: changed + type: int + sample: 2345 +cmp_hash: + description: New traffic disaggregation method. + returned: changed + type: str + sample: source-address +dag_tunnel: + description: The new DAG tunnel setting. + returned: changed + type: str + sample: outer +source_check: + description: The new Source Check setting. + returned: changed + type: bool + sample: yes +fail_safe: + description: The new Fail Safe setting. + returned: changed + type: bool + sample: no +fail_safe_timeout: + description: The new Fail Safe Timeout setting. + returned: changed + type: int + sample: 90 +fail_safe_action: + description: The new Fail Safe Action setting. + returned: changed + type: str + sample: reboot +sflow_poll_interval: + description: The new sFlow Polling Interval setting. + returned: changed + type: int + sample: 10 +sflow_sampling_rate: + description: The new sFlow Sampling Rate setting. + returned: changed + type: int + sample: 20 +hw_syn_cookie: + description: Enables hardware syncookie mode on a VLAN. + returned: changed + type: bool + sample: no +''' +from datetime import datetime + +from ansible.module_utils.basic import ( + AnsibleModule, env_fallback +) +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean +) +from ..module_utils.compare import compare_complex_list +from ..module_utils.icontrol import tmos_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'cmpHash': 'cmp_hash', + 'dagTunnel': 'dag_tunnel', + 'dagRoundRobin': 'dag_round_robin', + 'interfacesReference': 'interfaces', + 'sourceChecking': 'source_check', + 'failsafe': 'fail_safe', + 'failsafeAction': 'fail_safe_action', + 'failsafeTimeout': 'fail_safe_timeout', + 'hardwareSyncookie': 'hw_syn_cookie', + } + + api_attributes = [ + 'description', + 'interfaces', + 'tag', + 'mtu', + 'cmpHash', + 'dagTunnel', + 'dagRoundRobin', + 'sourceChecking', + 'failsafe', + 'failsafeAction', + 'failsafeTimeout', + 'sflow', + 'hardwareSyncookie', + ] + + updatables = [ + 'interfaces', + 'tagged_interfaces', + 'untagged_interfaces', + 'tag', + 'description', + 'mtu', + 'cmp_hash', + 'dag_tunnel', + 'dag_round_robin', + 'source_check', + 'fail_safe', + 'fail_safe_action', + 'fail_safe_timeout', + 'sflow_poll_interval', + 'sflow_sampling_rate', + 'sflow', + 'hw_syn_cookie', + ] + + returnables = [ + 'description', + 'partition', + 'tag', + 'interfaces', + 'tagged_interfaces', + 'untagged_interfaces', + 'mtu', + 'cmp_hash', + 'dag_tunnel', + 'dag_round_robin', + 'source_check', + 'fail_safe', + 'fail_safe_action', + 'fail_safe_timeout', + 'sflow_poll_interval', + 'sflow_sampling_rate', + 'sflow', + 'hw_syn_cookie', + ] + + @property + def source_check(self): + return flatten_boolean(self._values['source_check']) + + @property + def fail_safe(self): + return flatten_boolean(self._values['fail_safe']) + + +class ApiParameters(Parameters): + @property + def interfaces(self): + if self._values['interfaces'] is None: + return None + if 'items' not in self._values['interfaces']: + return None + result = [] + for item in self._values['interfaces']['items']: + name = item['name'] + if 'tagged' in item: + tagged = item['tagged'] + result.append(dict(name=name, tagged=tagged)) + if 'untagged' in item: + untagged = item['untagged'] + result.append(dict(name=name, untagged=untagged)) + return result + + @property + def tagged_interfaces(self): + if self.interfaces is None: + return None + result = [str(x['name']) for x in self.interfaces if 'tagged' in x and x['tagged'] is True] + result = sorted(result) + return result + + @property + def untagged_interfaces(self): + if self.interfaces is None: + return None + result = [str(x['name']) for x in self.interfaces if 'untagged' in x and x['untagged'] is True] + result = sorted(result) + return result + + @property + def sflow_poll_interval(self): + try: + return self._values['sflow']['pollInterval'] + except (KeyError, TypeError): + return None + + @property + def sflow_sampling_rate(self): + try: + return self._values['sflow']['samplingRate'] + except (KeyError, TypeError): + return None + + +class ModuleParameters(Parameters): + @property + def interfaces(self): + if self._values['interfaces'] is None: + return None + elif len(self._values['interfaces']) == 1 and self._values['interfaces'][0] in ['', 'none']: + return '' + result = [] + for item in self._values['interfaces']: + if 'interface' not in item: + raise F5ModuleError( + "An 'interface' key must be provided when specifying a list of interfaces." + ) + if 'tagging' not in item: + raise F5ModuleError( + "A 'tagging' key must be provided when specifying a list of interfaces." + ) + name = str(item['interface']) + tagging = item['tagging'] + + if tagging == 'tagged': + result.append(dict(name=name, tagged=True)) + else: + result.append(dict(name=name, untagged=True)) + return result + + @property + def untagged_interfaces(self): + if self._values['untagged_interfaces'] is None: + return None + if self._values['untagged_interfaces'] is None: + return None + if len(self._values['untagged_interfaces']) == 1 and self._values['untagged_interfaces'][0] == '': + return '' + result = sorted([str(x) for x in self._values['untagged_interfaces']]) + return result + + @property + def tagged_interfaces(self): + if self._values['tagged_interfaces'] is None: + return None + if self._values['tagged_interfaces'] is None: + return None + if len(self._values['tagged_interfaces']) == 1 and self._values['tagged_interfaces'][0] == '': + return '' + result = sorted([str(x) for x in self._values['tagged_interfaces']]) + return result + + @property + def mtu(self): + if self._values['mtu'] is None: + return None + if int(self._values['mtu']) < 576 or int(self._values['mtu']) > 9198: + raise F5ModuleError( + "The mtu value must be between 576 - 9198" + ) + return int(self._values['mtu']) + + @property + def cmp_hash(self): + if self._values['cmp_hash'] is None: + return None + if self._values['cmp_hash'] in ['source-address', 'src', 'src-ip', 'source']: + return 'src-ip' + if self._values['cmp_hash'] in ['destination-address', 'dest', 'dst-ip', 'destination', 'dst']: + return 'dst-ip' + else: + return 'default' + + @property + def dag_round_robin(self): + if self._values['dag_round_robin'] is None: + return None + if self._values['dag_round_robin'] is True: + return 'enabled' + else: + return 'disabled' + + @property + def hw_syn_cookie(self): + result = flatten_boolean(self._values['hw_syn_cookie']) + if result == 'yes': + return 'enabled' + if result == 'no': + return 'disabled' + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + @property + def source_check(self): + if self._values['source_check'] is None: + return None + if self._values['source_check'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def fail_safe(self): + if self._values['fail_safe'] is None: + return None + if self._values['fail_safe'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def tagged_interfaces(self): + if self.interfaces is None: + return None + result = [str(x['name']) for x in self.interfaces if 'tagged' in x and x['tagged'] is True] + result = sorted(result) + return result + + @property + def untagged_interfaces(self): + if self.interfaces is None: + return None + result = [str(x['name']) for x in self.interfaces if 'untagged' in x and x['untagged'] is True] + result = sorted(result) + return result + + @property + def source_check(self): + return flatten_boolean(self._values['source_check']) + + @property + def fail_safe(self): + return flatten_boolean(self._values['fail_safe']) + + @property + def sflow(self): + return None + + @property + def sflow_poll_interval(self): + try: + return self._values['sflow']['pollInterval'] + except (KeyError, TypeError): + return None + + @property + def sflow_sampling_rate(self): + try: + return self._values['sflow']['samplingRate'] + except (KeyError, TypeError): + return None + + @property + def hw_syn_cookie(self): + return flatten_boolean(self._values['hw_syn_cookie']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def interfaces(self): + if self.want.interfaces is None: + return None + if self.have.interfaces is None and self.want.interfaces in ['', 'none']: + return None + if self.have.interfaces is not None and self.want.interfaces in ['', 'none']: + return [] + if self.have.interfaces is None: + return dict( + interfaces=self.want.interfaces + ) + return compare_complex_list(self.want.interfaces, self.have.interfaces) + + @property + def untagged_interfaces(self): + result = self.cmp_interfaces(self.want.untagged_interfaces, self.have.untagged_interfaces, False) + return result + + @property + def tagged_interfaces(self): + result = self.cmp_interfaces(self.want.tagged_interfaces, self.have.tagged_interfaces, True) + return result + + def cmp_interfaces(self, want, have, tagged): + result = [] + if tagged: + tag_key = 'tagged' + else: + tag_key = 'untagged' + if want is None: + return None + elif want == '' and have is None: + return None + elif want == '' and len(have) > 0: + pass + elif not have: + result = dict( + interfaces=[{'name': x, tag_key: True} for x in want] + ) + elif set(want) != set(have): + result = dict( + interfaces=[{'name': x, tag_key: True} for x in want] + ) + else: + return None + return result + + @property + def sflow(self): + result = {} + s = self.sflow_poll_interval + if s: + result.update(s) + s = self.sflow_sampling_rate + if s: + result.update(s) + if result: + return dict( + sflow=result + ) + + @property + def sflow_poll_interval(self): + if self.want.sflow_poll_interval is None: + return None + if self.want.sflow_poll_interval != self.have.sflow_poll_interval: + return dict( + pollInterval=self.want.sflow_poll_interval + ) + + @property + def sflow_sampling_rate(self): + if self.want.sflow_sampling_rate is None: + return None + if self.want.sflow_sampling_rate != self.have.sflow_sampling_rate: + return dict( + samplingRate=self.want.sflow_sampling_rate + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = tmos_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the VLAN") + return True + + def create(self): + self.have = ApiParameters() + if self.want.mtu is None: + self.want.update({'mtu': 1500}) + self._update_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + 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/net/vlan".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) + return response['selfLink'] + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/net/vlan/{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 resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + raise F5ModuleError(resp.content) + + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/net/vlan/{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 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 + + errors = [401, 403, 409, 500, 501, 502, 503, 504] + + if resp.status in errors or 'code' in response and response['code'] in errors: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/vlan/{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/net/vlan/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + query = '?expandSubcollections=true' + resp = self.client.api.get(uri + query) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return ApiParameters(params=response) + raise F5ModuleError(resp.content) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True, + ), + tagged_interfaces=dict( + type='list', + elements='str', + aliases=['tagged_interface'] + ), + untagged_interfaces=dict( + type='list', + elements='str', + aliases=['untagged_interface'] + ), + interfaces=dict( + type='list', + elements='dict', + options=dict( + interface=dict(), + tagging=dict( + choices=['tagged', 'untagged'] + ) + ) + ), + description=dict(), + tag=dict( + type='int' + ), + mtu=dict(type='int'), + cmp_hash=dict( + choices=[ + 'default', + 'destination-address', 'dest', 'dst-ip', 'destination', 'dst', + 'source-address', 'src', 'src-ip', 'source' + ] + ), + dag_tunnel=dict( + choices=['inner', 'outer'] + ), + dag_round_robin=dict(type='bool'), + source_check=dict(type='bool'), + fail_safe=dict(type='bool'), + fail_safe_timeout=dict(type='int'), + fail_safe_action=dict( + choices=['reboot', 'restart-all', 'failover'] + ), + sflow_poll_interval=dict(type='int'), + sflow_sampling_rate=dict(type='int'), + hw_syn_cookie=dict(type='bool'), + 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 = [ + ['tagged_interfaces', 'untagged_interfaces', 'interfaces'], + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive + ) + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_wait.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_wait.py new file mode 100644 index 00000000..1c14e598 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigip_wait.py @@ -0,0 +1,536 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigip_wait +short_description: Wait for a BIG-IP condition before continuing +description: + - With this module, you can wait for BIG-IP to be "ready", meaning the BIG-IP is ready + to accept configuration. + - This module can take into account situations where the device is in the middle + of rebooting due to a configuration change. +version_added: "1.0.0" +options: + type: + description: + - The type of the BIG-IP. + - Defaults to C(standard), the other choice is C(vcmp). + - This choice defines which module or service Ansible looks for to establish + that the device has recovered, so ensure to specify the correct choice, + especially when running this against VCMP. + type: str + default: standard + choices: + - standard + - vcmp + timeout: + description: + - Maximum number of seconds to wait. + - When used without other conditions, it is equivalent of just sleeping. + - The default timeout is deliberately set to 2 hours because there is no individual + REST API. + type: int + default: 7200 + delay: + description: + - Number of seconds to wait before starting to poll. + type: int + default: 0 + sleep: + description: + - Number of seconds to sleep between checks. Before version 2.3 this was hardcoded to 1 second. + type: int + default: 1 + msg: + description: + - This overrides the normal error message from a failure to meet the required conditions. + type: str +extends_documentation_fragment: f5networks.f5_modules.f5_rest_cli +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Wait for BIG-IP to be ready to take configuration + bigip_wait: + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Wait a maximum of 300 seconds for BIG-IP to be ready to take configuration + bigip_wait: + timeout: 300 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Wait for BIG-IP to be ready, don't start checking for 10 seconds + bigip_wait: + delay: 10 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import copy +import datetime +import signal +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import exec_command + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, is_cli, f5_argument_spec +) +from ..module_utils.teem import send_teem + +try: + from ..module_utils.common import run_commands + HAS_CLI_TRANSPORT = True +except ImportError: + HAS_CLI_TRANSPORT = False + + +def hard_timeout(module, want, start): + elapsed = datetime.datetime.utcnow() - start + module.fail_json( + msg=want.msg or "Timeout when waiting for BIG-IP", elapsed=elapsed.seconds + ) + + +class Parameters(AnsibleF5Parameters): + returnables = [ + 'elapsed' + ] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + @property + def delay(self): + if self._values['delay'] is None: + return None + return int(self._values['delay']) + + @property + def timeout(self): + if self._values['timeout'] is None: + return None + return int(self._values['timeout']) + + @property + def sleep(self): + if self._values['sleep'] is None: + return None + return int(self._values['sleep']) + + +class Changes(Parameters): + pass + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.have = None + self.want = Parameters(params=self.module.params) + self.changes = Parameters() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def execute(self): + if self.want.delay >= self.want.timeout: + raise F5ModuleError( + "The delay should not be greater than or equal to the timeout." + ) + if self.want.delay + self.want.sleep >= self.want.timeout: + raise F5ModuleError( + "The combined delay and sleep should not be greater than or equal to the timeout." + ) + signal.signal( + signal.SIGALRM, + lambda sig, frame: hard_timeout(self.module, self.want, start) + ) + + # setup handler before scheduling signal, to eliminate a race + signal.alarm(int(self.want.timeout)) + + start = datetime.datetime.utcnow() + if self.want.delay: + time.sleep(float(self.want.delay)) + end = start + datetime.timedelta(seconds=int(self.want.timeout)) + + self.wait_for_device(start, end) + + elapsed = datetime.datetime.utcnow() - start + self.changes.update({'elapsed': elapsed.seconds}) + return False + + def _wait_for_module_provisioning(self): + # To prevent things from running forever, the hack is to check + # for mprov's status twice. If mprov is finished, then in most + # cases (not ASM) the provisioning is probably ready. + nops = 0 + # Sleep a little to let provisioning settle and begin properly + time.sleep(5) + while nops < 4: + try: + if not self._is_mprov_running_on_device(): + nops += 1 + else: + nops = 0 + except Exception: + # This can be caused by restjavad restarting. + pass + time.sleep(10) + + def _wait_for_rest_interface(self): + nops = 0 + # Sleep a little to let daemons settle and begin checking if REST interface is available. + time.sleep(5) + while nops < 4: + if not self._rest_endpoints_ready(): + nops += 1 + else: + break + time.sleep(10) + + +class V1Manager(BaseManager): + def exec_module(self): + result = dict() + + changed = self.execute() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + return result + + def wait_for_device(self, start, end): + while datetime.datetime.utcnow() < end: + time.sleep(int(self.want.sleep)) + # First we check if SSH connection is ready by repeatedly attempting to run a simple command + rc, out, err = exec_command(self.module, 'date') + if rc != 0: + continue + if self._device_is_rebooting(): + # Wait for the reboot to happen and then start from the beginning + # of the waiting. + continue + if self.want.type == "standard": + if self._is_mprov_running_on_device(): + self._wait_for_module_provisioning() + elif self.want.type == "vcmp": + self._is_vcmpd_running_on_device() + if not self._rest_endpoints_ready(): + self._wait_for_rest_interface() + break + else: + elapsed = datetime.datetime.utcnow() - start + self.module.fail_json( + msg=self.want.msg or "Timeout when waiting for BIG-IP", elapsed=elapsed.seconds + ) + + def _is_mprov_running_on_device(self): + cmd = "ps aux | grep '[m]prov'" + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + raise F5ModuleError(err) + if out: + return True + return False + + def _is_vcmpd_running_on_device(self): + cmd = "ps aux | grep '[v]cmpd'" + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + raise F5ModuleError(err) + if out: + return True + return False + + def _rest_endpoints_ready(self): + cmd = "curl -o /dev/null -s -w\'%{http_code}\\n\' -u admin: http://localhost:8100/mgmt/tm/sys/available" + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + raise F5ModuleError(err) + if out == '200': + return True + return False + + def _device_is_rebooting(self): + cmd = 'runlevel' + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + raise F5ModuleError(err) + if out.split(' ')[1] == '6': + return True + return False + + +class V2Manager(BaseManager): + def exec_module(self): + start = datetime.datetime.now().isoformat() + + result = dict() + + changed = self.execute() + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, None) + return result + + def wait_for_device(self, start, end): + while datetime.datetime.utcnow() < end: + time.sleep(int(self.want.sleep)) + try: + # The first test verifies that the REST API is available; this is done + # by repeatedly trying to login to it. + self.client = self._get_client_connection() + if not self.client: + continue + + if self._device_is_rebooting(): + # Wait for the reboot to happen and then start from the beginning + # of the waiting. + continue + + if self.want.type == "standard": + if self._is_mprov_running_on_device(): + self._wait_for_module_provisioning() + elif self.want.type == "vcmp": + self._is_vcmpd_running_on_device() + if not self._rest_endpoints_ready(): + self._wait_for_rest_interface() + break + except Exception as ex: + if 'Failed to validate the SSL' in str(ex): + raise F5ModuleError(str(ex)) + + # The types of exception's we're handling here are "REST API is not + # ready" exceptions. + # + # For example, + # + # Typically caused by device starting up: + # + # icontrol.exceptions.iControlUnexpectedHTTPError: 404 Unexpected Error: + # Not Found for uri: https://localhost:10443/mgmt/tm/sys/ + # icontrol.exceptions.iControlUnexpectedHTTPError: 503 Unexpected Error: + # Service Temporarily Unavailable for uri: https://localhost:10443/mgmt/tm/sys/ + # + # + # Typically caused by a device being down + # + # requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=10443): + # Max retries exceeded with url: /mgmt/tm/sys/ (Caused by SSLError( + # SSLError("bad handshake: SysCallError(-1, 'Unexpected EOF')",),)) + # + # + # Typically caused by device still booting + # + # raise SSLError(e, request=request)\nrequests.exceptions.SSLError: + # HTTPSConnectionPool(host='localhost', port=10443): Max retries + # exceeded with url: /mgmt/shared/authn/login (Caused by + # SSLError(SSLError(\"bad handshake: SysCallError(-1, 'Unexpected EOF')\",),)), + continue + else: + elapsed = datetime.datetime.utcnow() - start + self.module.fail_json( + msg=self.want.msg or "Timeout when waiting for BIG-IP", elapsed=elapsed.seconds + ) + + def _get_client_connection(self): + return F5RestClient(**self.module.params) + + def _device_is_rebooting(self): + params = { + "command": "run", + "utilCmdArgs": '-c "runlevel"' + } + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'commandResult' in response and '6' in response['commandResult']: + return True + return False + + def _is_mprov_running_on_device(self): + params = { + "command": "run", + "utilCmdArgs": '-c "ps aux | grep \'[m]prov\'"' + } + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'commandResult' in response: + return True + return False + + def _is_vcmpd_running_on_device(self): + params = { + "command": "run", + "utilCmdArgs": '-c "ps aux | grep \'[v]cmpd\'"' + } + uri = "https://{0}:{1}/mgmt/tm/util/bash".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 resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]: + raise F5ModuleError(resp.content) + + if 'commandResult' in response: + return True + return False + + def _rest_endpoints_ready(self): + uri = "https://{0}:{1}/mgmt/tm/sys/available".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]: + return True + return False + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + self.module = kwargs.get('module', None) + + def exec_module(self): + if is_cli(self.module) and HAS_CLI_TRANSPORT: + manager = self.get_manager('v1') + else: + manager = self.get_manager('v2') + result = manager.exec_module() + return result + + def get_manager(self, type): + if type == 'v1': + return V1Manager(**self.kwargs) + elif type == 'v2': + return V2Manager(**self.kwargs) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + type=dict( + choices=['standard', 'vcmp'], + default='standard' + ), + timeout=dict(default=7200, type='int'), + delay=dict(default=0, type='int'), + sleep=dict(default=1, type='int'), + msg=dict(), + ) + # required to add CLI to choices and ssh_keyfile as per documentation + provider_update = dict( + transport=dict( + type='str', + default='rest', + choices=['cli', 'rest'] + ), + ssh_keyfile=dict( + type='path' + ), + + ) + new_spec = copy.deepcopy(f5_argument_spec) + self.argument_spec = {} + self.argument_spec.update(new_spec) + self.argument_spec['provider']['options'].update(provider_update) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fasthttp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fasthttp.py new file mode 100644 index 00000000..3a67cc4a --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fasthttp.py @@ -0,0 +1,762 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_application_fasthttp +short_description: Manages BIG-IQ FastHTTP applications +description: + - Manages BIG-IQ applications used for load balancing an HTTP-based application, speeding + up connections and reducing the number of connections to the back-end server. +version_added: "1.0.0" +options: + name: + description: + - Name of the new application. + type: str + required: True + description: + description: + - Description of the application. + type: str + servers: + description: + - A list of servers on which the application is hosted. + - If you are familiar with other BIG-IP settings, you might also refer to this + list as the list of pool members. + - When creating a new application, at least one server is required. + type: list + elements: dict + suboptions: + address: + description: + - The IP address of the server. + type: str + required: True + port: + description: + - The port of the server. + - When creating a new application and specifying a server, if this parameter + is not provided, the default is C(80). + type: str + default: 80 + inbound_virtual: + description: + - Settings to configure the virtual which receives the inbound connection. + - This virtual is used to host the HTTP endpoint of the application. + suboptions: + address: + description: + - Specifies destination IP address information to which the virtual server + sends traffic. + - This parameter is required when creating a new application. + type: str + required: True + netmask: + description: + - Specifies the netmask to associate with the given C(destination). + - This parameter is required when creating a new application. + type: str + required: True + port: + description: + - The port on which the virtual listens for connections. + - When creating a new application, if this parameter is not specified, the + default value is C(80). + type: str + default: 80 + type: dict + service_environment: + description: + - Specifies the name of the service environment to which the application is. + - When creating a new application, this parameter is required. + - The service environment type is automatically discovered by this module. + Therefore, it is crucial you maintain unique names for items in the + different service environment types (at this time, SSGs and BIGIPs). + type: str + add_analytics: + description: + - Collects statistics of the BIG-IP to which the application is deployed. + - This parameter is only relevant when specifying a C(service_environment) which + is a BIG-IP; not an SSG. + type: bool + default: no + state: + description: + - The state of the resource on the system. + - When C(present), guarantees the resource exists with the provided attributes. + - When C(absent), removes the resource from the system. + type: str + choices: + - absent + - present + default: present + wait: + description: + - If the module should wait for the application to be created, deleted, or updated. + type: bool + default: yes +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - This module does not support updating of your application (whether deployed or not). + If you need to update the application, we recommended removing and + re-creating it. + - This module does not work on BIG-IQ version 6.1.x or greater. +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Load balance an HTTP application on port 80 on BIG-IP + bigiq_application_fasthttp: + name: my-app + description: Fast HTTP + service_environment: my-ssg + servers: + - address: 1.2.3.4 + port: 8080 + - address: 5.6.7.8 + port: 8080 + inbound_virtual: + name: foo + address: 2.2.2.2 + netmask: 255.255.255.255 + port: 80 + provider: + password: secret + server: lb.mydomain.com + user: admin + state: present + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the application of the resource. + returned: changed + type: str + sample: My application +service_environment: + description: The environment to which the service was deployed. + returned: changed + type: str + sample: my-ssg1 +inbound_virtual_destination: + description: The destination of the virtual that was created. + returned: changed + type: str + sample: 6.7.8.9 +inbound_virtual_netmask: + description: The network mask of the provided inbound destination. + returned: changed + type: str + sample: 255.255.255.0 +inbound_virtual_port: + description: The port on which the inbound virtual address listens. + returned: changed + type: int + sample: 80 +servers: + description: List of servers and their ports that make up the application. + type: complex + returned: changed + contains: + address: + description: The IP address of the server. + returned: changed + type: str + sample: 2.3.4.5 + port: + description: The port the server listens on. + returned: changed + type: int + sample: 8080 + sample: hash/dictionary of values +''' + +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'templateReference': 'template_reference', + 'subPath': 'sub_path', + 'ssgReference': 'ssg_reference', + 'configSetName': 'config_set_name', + 'defaultDeviceReference': 'default_device_reference', + 'addAnalytics': 'add_analytics' + } + + api_attributes = [ + 'resources', 'description', 'configSetName', 'subPath', 'templateReference', + 'ssgReference', 'defaultDeviceReference', 'addAnalytics' + ] + + returnables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'ssg_reference', 'default_device_reference', 'servers', 'inbound_virtual', + 'add_analytics' + ] + + updatables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'ssg_reference', 'default_device_reference', 'servers', 'add_analytics' + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def http_profile(self): + return "profile_http" + + @property + def config_set_name(self): + return self.name + + @property + def sub_path(self): + return self.name + + @property + def template_reference(self): + filter = "name+eq+'Default-f5-fastHTTP-lb-template'" + uri = "https://{0}:{1}/mgmt/cm/global/templates/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No default HTTP LB template was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + @property + def default_device_reference(self): + if is_valid_ip(self.service_environment): + # An IP address was specified + filter = "address+eq+'{0}'".format(self.service_environment) + else: + # Assume a hostname was specified + filter = "hostname+eq+'{0}'".format(self.service_environment) + + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-adccore-allbigipDevices/devices/" \ + "?$filter={2}&$top=1&$select=selfLink".format(self.client.provider['server'], + self.client.provider['server_port'], filter) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + @property + def ssg_reference(self): + filter = "name+eq+'{0}'".format(self.service_environment) + uri = "https://{0}:{1}/mgmt/cm/cloud/service-scaling-groups/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def resources(self): + result = dict() + result.update(self.http_profile) + result.update(self.http_monitor) + result.update(self.virtual) + result.update(self.pool) + result.update(self.nodes) + return result + + @property + def virtual(self): + result = dict() + result['ltm:virtual:0257bb9bb997'] = [ + dict( + parameters=dict( + name='virtual', + destinationAddress=self.inbound_virtual['address'], + mask=self.inbound_virtual['netmask'], + destinationPort=self.inbound_virtual['port'] + ), + subcollectionResources=self.profiles + ) + ] + return result + + @property + def profiles(self): + result = { + 'profiles:53f9b3028d90': [ + dict( + parameters=dict() + ) + ], + 'profiles:b2f39bda63fd': [ + dict( + parameters=dict() + ) + ] + } + return result + + @property + def pool(self): + result = dict() + result['ltm:pool:f76ae78f1de6'] = [ + dict( + parameters=dict( + name='pool_0' + ), + subcollectionResources=self.pool_members + ) + ] + return result + + @property + def pool_members(self): + result = dict() + result['members:15ad51f7229e'] = [] + for x in self.servers: + member = dict( + parameters=dict( + port=x['port'], + nodeReference=dict( + link='#/resources/ltm:node:0783ce16685f/{0}'.format(x['address']), + fullPath='# {0}'.format(x['address']) + ) + ) + ) + result['members:15ad51f7229e'].append(member) + return result + + @property + def http_profile(self): + result = dict() + result['ltm:profile:http:b2f39bda63fd'] = [ + dict( + parameters=dict( + name='profile_http' + ) + ) + ] + return result + + @property + def http_monitor(self): + result = dict() + result['ltm:monitor:http:cf6f6e7ae758'] = [ + dict( + parameters=dict( + name='monitor-http' + ) + ) + ] + return result + + @property + def nodes(self): + result = dict() + result['ltm:node:0783ce16685f'] = [] + for x in self.servers: + tmp = dict( + parameters=dict( + name=x['address'], + address=x['address'] + ) + ) + result['ltm:node:0783ce16685f'].append(tmp) + return result + + @property + def node_addresses(self): + result = [x['address'] for x in self.servers] + return result + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.want.client = self.client + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def check_bigiq_version(self, version): + if Version(version) >= Version('6.1.0'): + raise F5ModuleError( + 'Module supports only BIGIQ version 6.0.x or lower.' + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + self.check_bigiq_version(version) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/ap/query/v1/tenants/default/reports/" \ + "AllApplicationsList?$filter=name+eq+'{2}'".format(self.client.provider['server'], + self.client.provider['server_port'], self.want.name) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if (resp.status == 200 and 'result' in response and + 'totalItems' in response['result'] and response['result']['totalItems'] == 0): + return False + return True + + def remove(self): + if self.module.check_mode: + return True + self_link = self.remove_from_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def has_no_service_environment(self): + if self.want.default_device_reference is None and self.want.ssg_reference is None: + return True + return False + + def create(self): + if self.want.service_environment is None: + raise F5ModuleError( + "A 'service_environment' must be specified when creating a new application." + ) + if self.want.servers is None: + raise F5ModuleError( + "At least one 'servers' item is needed when creating a new application." + ) + if self.want.inbound_virtual is None: + raise F5ModuleError( + "An 'inbound_virtual' must be specified when creating a new application." + ) + self._set_changed_options() + + if self.has_no_service_environment(): + raise F5ModuleError( + "The specified 'service_environment' ({0}) was not found.".format(self.want.service_environment) + ) + + if self.module.check_mode: + return True + self_link = self.create_on_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if not self.exists(): + raise F5ModuleError( + "Failed to deploy application." + ) + return True + + def create_on_device(self): + params = self.changes.api_params() + params['mode'] = 'CREATE' + + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + params = dict( + configSetName=self.want.name, + mode='DELETE' + ) + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def wait_for_apply_template_task(self, self_link): + host = 'https://{0}:{1}'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = self_link.replace('https://localhost', host) + + while True: + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if response['status'] == 'FINISHED' and response.get('currentStep', None) == 'DONE': + return True + elif 'errorMessage' in response: + raise F5ModuleError(response['errorMessage']) + time.sleep(5) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + servers=dict( + type='list', + elements='dict', + options=dict( + address=dict(required=True), + port=dict(default=80) + ) + ), + inbound_virtual=dict( + type='dict', + options=dict( + address=dict(required=True), + netmask=dict(required=True), + port=dict(default=80) + ) + ), + service_environment=dict(), + add_analytics=dict(type='bool', default='no'), + state=dict( + default='present', + choices=['present', 'absent'] + ), + wait=dict(type='bool', default='yes') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fastl4_tcp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fastl4_tcp.py new file mode 100644 index 00000000..9b2c28fc --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fastl4_tcp.py @@ -0,0 +1,710 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_application_fastl4_tcp +short_description: Manages BIG-IQ FastL4 TCP applications +description: + - Manages BIG-IQ applications used for load balancing a TCP-based application + with a FastL4 profile. +version_added: "1.0.0" +options: + name: + description: + - Name of the new application. + type: str + required: True + description: + description: + - Description of the application. + type: str + servers: + description: + - A list of servers on which the application is hosted. + - If you are familiar with other BIG-IP settings, you might also refer to this + list as the list of pool members. + - When creating a new application, at least one server is required. + type: list + elements: dict + suboptions: + address: + description: + - The IP address of the server. + type: str + required: True + port: + description: + - The port of the server. + - When creating a new application and specifying a server, if this parameter + is not provided, the default is C(8000). + type: str + default: 8000 + inbound_virtual: + description: + - Settings to configure the virtual which will receive the inbound connection. + type: dict + suboptions: + address: + description: + - Specifies destination IP address information to which the virtual server + sends traffic. + - This parameter is required when creating a new application. + type: str + required: True + netmask: + description: + - Specifies the netmask to associate with the given C(destination). + - This parameter is required when creating a new application. + type: str + required: True + port: + description: + - The port on which the virtual listens for connections. + - When creating a new application, if this parameter is not specified, the + default value is C(8080). + type: str + default: 8080 + service_environment: + description: + - Specifies the name of service environment to which the application is + deployed. + - When creating a new application, this parameter is required. + - The service environment type is automatically discovered by this module. + Therefore, it is crucial that you maintain unique names for items in the + different service environment types. + - SSGs are not supported for this type of application. + type: str + add_analytics: + description: + - Collects statistics of the BIG-IP to which the application is deployed. + - This parameter is only relevant when specifying a C(service_environment) which + is a BIG-IP; not an SSG. + type: bool + default: no + state: + description: + - The state of the resource on the system. + - When C(present), guarantees the resource exists with the provided attributes. + - When C(absent), removes the resource from the system. + type: str + choices: + - absent + - present + default: present + wait: + description: + - If the module should wait for the application to be created, deleted, or updated. + type: bool + default: yes +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - This module does not support updating of your application (whether deployed or not). + If you need to update the application, we recommend removing and + re-creating it. + - This module will not work on BIG-IQ version 6.1.x or greater. +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Load balance a TCP-based application with a FastL4 profile + bigiq_application_fastl4_tcp: + name: my-app + description: My description + service_environment: my-bigip-device + servers: + - address: 1.2.3.4 + port: 8080 + - address: 5.6.7.8 + port: 8080 + inbound_virtual: + name: foo + address: 2.2.2.2 + netmask: 255.255.255.255 + port: 443 + provider: + password: secret + server: lb.mydomain.com + user: admin + state: present + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the application of the resource. + returned: changed + type: str + sample: My application +service_environment: + description: The environment to which the service was deployed. + returned: changed + type: str + sample: my-ssg1 +inbound_virtual_destination: + description: The destination of the virtual that was created. + returned: changed + type: str + sample: 6.7.8.9 +inbound_virtual_netmask: + description: The network mask of the provided inbound destination. + returned: changed + type: str + sample: 255.255.255.0 +inbound_virtual_port: + description: The port on which the inbound virtual address listens. + returned: changed + type: int + sample: 80 +servers: + description: List of servers, and their ports, that make up the application. + type: complex + returned: changed + contains: + address: + description: The IP address of the server. + returned: changed + type: str + sample: 2.3.4.5 + port: + description: The port on which the server listens. + returned: changed + type: int + sample: 8080 + sample: hash/dictionary of values +''' + +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'templateReference': 'template_reference', + 'subPath': 'sub_path', + 'configSetName': 'config_set_name', + 'defaultDeviceReference': 'default_device_reference', + 'addAnalytics': 'add_analytics' + } + + api_attributes = [ + 'resources', 'description', 'configSetName', 'subPath', 'templateReference', + 'defaultDeviceReference', 'addAnalytics' + ] + + returnables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'default_device_reference', 'servers', 'inbound_virtual', 'add_analytics' + ] + + updatables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'default_device_reference', 'servers', 'add_analytics' + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def http_profile(self): + return "profile_http" + + @property + def config_set_name(self): + return self.name + + @property + def sub_path(self): + return self.name + + @property + def template_reference(self): + filter = "name+eq+'Default-f5-FastL4-TCP-lb-template'" + uri = "https://{0}:{1}/mgmt/cm/global/templates/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No default HTTP LB template was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + @property + def default_device_reference(self): + if is_valid_ip(self.service_environment): + # An IP address was specified + filter = "address+eq+'{0}'".format(self.service_environment) + else: + # Assume a hostname was specified + filter = "hostname+eq+'{0}'".format(self.service_environment) + + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-adccore-allbigipDevices/devices/" \ + "?$filter={2}&$top=1&$select=selfLink".format(self.client.provider['server'], + self.client.provider['server_port'], filter) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "The specified service_environment '{0}' was found.".format(self.service_environment) + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def resources(self): + result = dict() + result.update(self.tcp_monitor) + result.update(self.virtual) + result.update(self.pool) + result.update(self.nodes) + return result + + @property + def virtual(self): + result = dict() + result['ltm:virtual:20e0ce0ae107'] = [ + dict( + parameters=dict( + name='virtual', + destinationAddress=self.inbound_virtual['address'], + mask=self.inbound_virtual['netmask'], + destinationPort=self.inbound_virtual.get('port', 8080) + ), + subcollectionResources=self.profiles + ) + ] + return result + + @property + def profiles(self): + result = { + 'profiles:53f9b3028d90': [ + dict( + parameters=dict() + ) + ] + } + return result + + @property + def pool(self): + result = dict() + result['ltm:pool:9fa59a7bfc5c'] = [ + dict( + parameters=dict( + name='pool_0' + ), + subcollectionResources=self.pool_members + ) + ] + return result + + @property + def pool_members(self): + result = dict() + result['members:3e91bd30bbfb'] = [] + for x in self.servers: + member = dict( + parameters=dict( + port=x.get('port', 8000), + nodeReference=dict( + link='#/resources/ltm:node:3e91bd30bbfb/{0}'.format(x['address']), + fullPath='# {0}'.format(x['address']) + ) + ) + ) + result['members:3e91bd30bbfb'].append(member) + return result + + @property + def tcp_monitor(self): + result = dict() + result['ltm:monitor:tcp:f864a2efffea'] = [ + dict( + parameters=dict( + name='monitor-tcp' + ) + ) + ] + return result + + @property + def nodes(self): + result = dict() + result['ltm:node:3e91bd30bbfb'] = [] + for x in self.servers: + tmp = dict( + parameters=dict( + name=x['address'], + address=x['address'] + ) + ) + result['ltm:node:3e91bd30bbfb'].append(tmp) + return result + + @property + def node_addresses(self): + result = [x['address'] for x in self.servers] + return result + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.want.client = self.client + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def check_bigiq_version(self, version): + if Version(version) >= Version('6.1.0'): + raise F5ModuleError( + 'Module supports only BIGIQ version 6.0.x or lower.' + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + self.check_bigiq_version(version) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/ap/query/v1/tenants/default/reports/AllApplicationsList" \ + "?$filter=name+eq+'{2}'".format(self.client.provider['server'], + self.client.provider['server_port'], + self.want.name) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if (resp.status == 200 and 'result' in response and + 'totalItems' in response['result'] and response['result']['totalItems'] == 0): + return False + return True + + def remove(self): + if self.module.check_mode: + return True + self_link = self.remove_from_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.service_environment is None: + raise F5ModuleError( + "A 'service_environment' must be specified when creating a new application." + ) + if self.want.servers is None: + raise F5ModuleError( + "At least one 'servers' item is needed when creating a new application." + ) + if self.want.inbound_virtual is None: + raise F5ModuleError( + "An 'inbound_virtual' must be specified when creating a new application." + ) + self._set_changed_options() + if self.module.check_mode: + return True + self_link = self.create_on_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if not self.exists(): + raise F5ModuleError( + "Failed to deploy application." + ) + return True + + def create_on_device(self): + params = self.changes.api_params() + params['mode'] = 'CREATE' + + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + params = dict( + configSetName=self.want.name, + mode='DELETE' + ) + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def wait_for_apply_template_task(self, self_link): + host = 'https://{0}:{1}'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = self_link.replace('https://localhost', host) + + while True: + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if response['status'] == 'FINISHED' and response.get('currentStep', None) == 'DONE': + return True + elif 'errorMessage' in response: + raise F5ModuleError(response['errorMessage']) + time.sleep(5) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + servers=dict( + type='list', + elements='dict', + options=dict( + address=dict(required=True), + port=dict(default=8000) + ) + ), + inbound_virtual=dict( + type='dict', + options=dict( + address=dict(required=True), + netmask=dict(required=True), + port=dict(default=8080) + ) + ), + service_environment=dict(), + add_analytics=dict(type='bool', default='no'), + state=dict( + default='present', + choices=['present', 'absent'] + ), + wait=dict(type='bool', default='yes') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fastl4_udp.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fastl4_udp.py new file mode 100644 index 00000000..c3c90ccb --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_fastl4_udp.py @@ -0,0 +1,707 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_application_fastl4_udp +short_description: Manages BIG-IQ FastL4 UDP applications +description: + - Manages BIG-IQ applications used for load balancing a UDP-based application. + with a FastL4 profile. +version_added: "1.0.0" +options: + name: + description: + - Name of the new application. + type: str + required: True + description: + description: + - Description of the application. + type: str + servers: + description: + - A list of servers on which the application is hosted. + - If you are familiar with other BIG-IP settings, you might also refer to this + list as the list of pool members. + - When creating a new application, at least one server is required. + type: list + elements: dict + suboptions: + address: + description: + - The IP address of the server. + type: str + required: True + port: + description: + - The port of the server. + - When creating a new application and specifying a server, if this parameter + is not provided, the default is C(8000). + type: str + default: 8000 + inbound_virtual: + description: + - Settings to configure the virtual which receives the inbound connection. + type: dict + suboptions: + address: + description: + - Specifies destination IP address information to which the virtual server + sends traffic. + - This parameter is required when creating a new application. + type: str + required: True + netmask: + description: + - Specifies the netmask to associate with the given C(destination). + - This parameter is required when creating a new application. + type: str + required: True + port: + description: + - The port on which the virtual listens for connections. + - When creating a new application, if this parameter is not specified, the + default value is C(53). + type: str + default: 53 + service_environment: + description: + - Specifies the name of service environment to which the application is + deployed. + - When creating a new application, this parameter is required. + - The service environment type is automatically discovered by this module. + Therefore, it is crucial you maintain unique names for items in the + different service environment types. + - SSGs are not supported for this type of application. + type: str + add_analytics: + description: + - Collects statistics of the BIG-IP to which the application is deployed. + - This parameter is only relevant when specifying a C(service_environment) which + is a BIG-IP; not an SSG. + type: bool + default: no + state: + description: + - The state of the resource on the system. + - When C(present), guarantees the resource exists with the provided attributes. + - When C(absent), removes the resource from the system. + type: str + choices: + - absent + - present + default: present + wait: + description: + - If the module should wait for the application to be created, deleted, or updated. + type: bool + default: yes +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - This module does not support updating of your application (whether deployed or not). + If you need to update the application, we recommend removing and recreating it. + - This module will not work on BIG-IQ version 6.1.x or greater. +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Load balance a UDP-based application with a FastL4 profile + bigiq_application_fastl4_udp: + name: my-app + description: My description + service_environment: my-bigip-device + servers: + - address: 1.2.3.4 + port: 8080 + - address: 5.6.7.8 + port: 8080 + inbound_virtual: + name: foo + address: 2.2.2.2 + netmask: 255.255.255.255 + port: 53 + provider: + password: secret + server: lb.mydomain.com + user: admin + state: present + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the application of the resource. + returned: changed + type: str + sample: My application +service_environment: + description: The environment to which the service was deployed. + returned: changed + type: str + sample: my-ssg1 +inbound_virtual_destination: + description: The destination of the virtual that was created. + returned: changed + type: str + sample: 6.7.8.9 +inbound_virtual_netmask: + description: The network mask of the provided inbound destination. + returned: changed + type: str + sample: 255.255.255.0 +inbound_virtual_port: + description: The port on which the inbound virtual address listens. + returned: changed + type: int + sample: 80 +servers: + description: List of servers, and their ports, that make up the application. + type: complex + returned: changed + contains: + address: + description: The IP address of the server. + returned: changed + type: str + sample: 2.3.4.5 + port: + description: The port on which the server listens. + returned: changed + type: int + sample: 8080 + sample: hash/dictionary of values +''' + +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'templateReference': 'template_reference', + 'subPath': 'sub_path', + 'configSetName': 'config_set_name', + 'defaultDeviceReference': 'default_device_reference', + 'addAnalytics': 'add_analytics' + } + + api_attributes = [ + 'resources', 'description', 'configSetName', 'subPath', 'templateReference', + 'defaultDeviceReference', 'addAnalytics' + ] + + returnables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'default_device_reference', 'servers', 'inbound_virtual', 'add_analytics' + ] + + updatables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'default_device_reference', 'servers', 'add_analytics' + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def http_profile(self): + return "profile_http" + + @property + def config_set_name(self): + return self.name + + @property + def sub_path(self): + return self.name + + @property + def template_reference(self): + filter = "name+eq+'Default-f5-FastL4-UDP-lb-template'" + uri = "https://{0}:{1}/mgmt/cm/global/templates/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No default HTTP LB template was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + + result = dict(link=response['items'][0]['selfLink']) + return result + + @property + def default_device_reference(self): + if is_valid_ip(self.service_environment): + # An IP address was specified + filter = "address+eq+'{0}'".format(self.service_environment) + else: + # Assume a hostname was specified + filter = "hostname+eq+'{0}'".format(self.service_environment) + + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-adccore-allbigipDevices/devices/" \ + "?$filter={2}&$top=1&$select=selfLink".format(self.client.provider['server'], + self.client.provider['server_port'], + filter) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "The specified service_environment '{0}' was found.".format(self.service_environment) + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def resources(self): + result = dict() + result.update(self.udp_monitor) + result.update(self.virtual) + result.update(self.pool) + result.update(self.nodes) + return result + + @property + def virtual(self): + result = dict() + result['ltm:virtual:c2e739ba116f'] = [ + dict( + parameters=dict( + name='virtual', + destinationAddress=self.inbound_virtual['address'], + mask=self.inbound_virtual['netmask'], + destinationPort=self.inbound_virtual.get('port', 53) + ), + subcollectionResources=self.profiles + ) + ] + return result + + @property + def profiles(self): + result = { + 'profiles:53f9b3028d90': [ + dict( + parameters=dict() + ) + ] + } + return result + + @property + def pool(self): + result = dict() + result['ltm:pool:e6879775458c'] = [ + dict( + parameters=dict( + name='pool_0' + ), + subcollectionResources=self.pool_members + ) + ] + return result + + @property + def pool_members(self): + result = dict() + result['members:b19842fe713a'] = [] + for x in self.servers: + member = dict( + parameters=dict( + port=x.get('port', 8000), + nodeReference=dict( + link='#/resources/ltm:node:b19842fe713a/{0}'.format(x['address']), + fullPath='# {0}'.format(x['address']) + ) + ) + ) + result['members:b19842fe713a'].append(member) + return result + + @property + def udp_monitor(self): + result = dict() + result['ltm:monitor:udp:22cdcfda0a40'] = [ + dict( + parameters=dict( + name='monitor-udp' + ) + ) + ] + return result + + @property + def nodes(self): + result = dict() + result['ltm:node:b19842fe713a'] = [] + for x in self.servers: + tmp = dict( + parameters=dict( + name=x['address'], + address=x['address'] + ) + ) + result['ltm:node:b19842fe713a'].append(tmp) + return result + + @property + def node_addresses(self): + result = [x['address'] for x in self.servers] + return result + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.want.client = self.client + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def check_bigiq_version(self, version): + if Version(version) >= Version('6.1.0'): + raise F5ModuleError( + 'Module supports only BIGIQ version 6.0.x or lower.' + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + self.check_bigiq_version(version) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/ap/query/v1/tenants/default/reports/AllApplicationsList" \ + "?$filter=name+eq+'{2}'".format(self.client.provider['server'], + self.client.provider['server_port'], self.want.name) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if (resp.status == 200 and 'result' in response and + 'totalItems' in response['result'] and response['result']['totalItems'] == 0): + return False + return True + + def remove(self): + if self.module.check_mode: + return True + self_link = self.remove_from_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.service_environment is None: + raise F5ModuleError( + "A 'service_environment' must be specified when creating a new application." + ) + if self.want.servers is None: + raise F5ModuleError( + "At least one 'servers' item is needed when creating a new application." + ) + if self.want.inbound_virtual is None: + raise F5ModuleError( + "An 'inbound_virtual' must be specified when creating a new application." + ) + self._set_changed_options() + if self.module.check_mode: + return True + self_link = self.create_on_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if not self.exists(): + raise F5ModuleError( + "Failed to deploy application." + ) + return True + + def create_on_device(self): + params = self.changes.api_params() + params['mode'] = 'CREATE' + + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + params = dict( + configSetName=self.want.name, + mode='DELETE' + ) + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def wait_for_apply_template_task(self, self_link): + host = 'https://{0}:{1}'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = self_link.replace('https://localhost', host) + + while True: + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if response['status'] == 'FINISHED' and response.get('currentStep', None) == 'DONE': + return True + elif 'errorMessage' in response: + raise F5ModuleError(response['errorMessage']) + time.sleep(5) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + servers=dict( + type='list', + elements='dict', + options=dict( + address=dict(required=True), + port=dict(default=8000) + ) + ), + inbound_virtual=dict( + type='dict', + options=dict( + address=dict(required=True), + netmask=dict(required=True), + port=dict(default=53) + ) + ), + service_environment=dict(), + add_analytics=dict(type='bool', default='no'), + state=dict( + default='present', + choices=['present', 'absent'] + ), + wait=dict(type='bool', default='yes') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_http.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_http.py new file mode 100644 index 00000000..2189ffd2 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_http.py @@ -0,0 +1,760 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_application_http +short_description: Manages BIG-IQ HTTP applications +description: + - Manages BIG-IQ applications used for load balancing an HTTP application on + port 80 on BIG-IP systems. +version_added: "1.0.0" +options: + name: + description: + - Name of the new application. + type: str + required: True + description: + description: + - Description of the application. + type: str + servers: + description: + - A list of servers on which the application is hosted. + - If you are familiar with other BIG-IP settings, you might also refer to this + list as the list of pool members. + - When creating a new application, at least one server is required. + type: list + elements: dict + suboptions: + address: + description: + - The IP address of the server. + type: str + required: True + port: + description: + - The port of the server. + - When creating a new application and specifying a server, if this parameter + is not provided, the default is C(80). + type: str + default: 80 + inbound_virtual: + description: + - Settings to configure the virtual which receives the inbound connection. + - This virtual is used to host the HTTP endpoint of the application. + suboptions: + address: + description: + - Specifies destination IP address information to which the virtual server + sends traffic. + - This parameter is required when creating a new application. + type: str + required: True + netmask: + description: + - Specifies the netmask to associate with the given C(destination). + - This parameter is required when creating a new application. + type: str + required: True + port: + description: + - The port on which the virtual listens for connections. + - When creating a new application, if this parameter is not specified, the + default value is C(80). + type: str + default: 80 + type: dict + service_environment: + description: + - Specifies the name of service environment to which the application is + deployed. + - When creating a new application, this parameter is required. + - The service environment type is automatically discovered by this module. + Therefore, it is crucial that you maintain unique names for items in the + different service environment types (at this time, SSGs and BIG-IPs). + type: str + add_analytics: + description: + - Collects statistics of the BIG-IP to which the application is deployed. + - This parameter is only relevant when specifying a C(service_environment) which + is a BIG-IP; not an SSG. + type: bool + default: no + state: + description: + - The state of the resource on the system. + - When C(present), guarantees the resource exists with the provided attributes. + - When C(absent), removes the resource from the system. + type: str + choices: + - absent + - present + default: present + wait: + description: + - If the module should wait for the application to be created, deleted, or updated. + type: bool + default: yes +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - This module does not support updating of your application (whether deployed or not). + If you need to update the application, we recommend removing and recreating it. + - This module will not work on BIG-IQ version 6.1.x or greater. +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Load balance an HTTP application on port 80 on BIG-IP + bigiq_application_http: + name: my-app + description: Redirect HTTP to HTTPS + service_environment: my-ssg + servers: + - address: 1.2.3.4 + port: 8080 + - address: 5.6.7.8 + port: 8080 + inbound_virtual: + name: foo + address: 2.2.2.2 + netmask: 255.255.255.255 + port: 443 + provider: + password: secret + server: lb.mydomain.com + user: admin + state: present + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the application of the resource. + returned: changed + type: str + sample: My application +service_environment: + description: The environment to which the service was deployed. + returned: changed + type: str + sample: my-ssg1 +inbound_virtual_destination: + description: The destination of the virtual that was created. + returned: changed + type: str + sample: 6.7.8.9 +inbound_virtual_netmask: + description: The network mask of the provided inbound destination. + returned: changed + type: str + sample: 255.255.255.0 +inbound_virtual_port: + description: The port on which the inbound virtual address listens. + returned: changed + type: int + sample: 80 +servers: + description: List of servers, and their ports, that make up the application. + type: complex + returned: changed + contains: + address: + description: The IP address of the server. + returned: changed + type: str + sample: 2.3.4.5 + port: + description: The port on which the server listens. + returned: changed + type: int + sample: 8080 + sample: hash/dictionary of values +''' + +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'templateReference': 'template_reference', + 'subPath': 'sub_path', + 'ssgReference': 'ssg_reference', + 'configSetName': 'config_set_name', + 'defaultDeviceReference': 'default_device_reference', + 'addAnalytics': 'add_analytics' + } + + api_attributes = [ + 'resources', 'description', 'configSetName', 'subPath', 'templateReference', + 'ssgReference', 'defaultDeviceReference', 'addAnalytics' + ] + + returnables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'ssg_reference', 'default_device_reference', 'servers', 'inbound_virtual', + 'add_analytics' + ] + + updatables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'ssg_reference', 'default_device_reference', 'servers', 'add_analytics' + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def http_profile(self): + return "profile_http" + + @property + def config_set_name(self): + return self.name + + @property + def sub_path(self): + return self.name + + @property + def template_reference(self): + filter = "name+eq+'Default-f5-HTTP-lb-template'" + uri = "https://{0}:{1}/mgmt/cm/global/templates/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No default HTTP LB template was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + @property + def default_device_reference(self): + if is_valid_ip(self.service_environment): + filter = "address+eq+'{0}'".format(self.service_environment) + else: + # Assume a hostname was specified + filter = "hostname+eq+'{0}'".format(self.service_environment) + + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-adccore-allbigipDevices/devices/" \ + "?$filter={2}&$top=1&$select=selfLink".format(self.client.provider['server'], + self.client.provider['server_port'], filter) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict(link=response['items'][0]['selfLink']) + return result + + @property + def ssg_reference(self): + filter = "name+eq+'{0}'".format(self.service_environment) + uri = "https://{0}:{1}/mgmt/cm/cloud/service-scaling-groups/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def resources(self): + result = dict() + result.update(self.http_profile) + result.update(self.http_monitor) + result.update(self.virtual) + result.update(self.pool) + result.update(self.nodes) + return result + + @property + def virtual(self): + result = dict() + result['ltm:virtual::b487671f29ba'] = [ + dict( + parameters=dict( + name='virtual', + destinationAddress=self.inbound_virtual['address'], + mask=self.inbound_virtual['netmask'], + destinationPort=self.inbound_virtual.get('port', 80) + ), + subcollectionResources=self.profiles + ) + ] + return result + + @property + def profiles(self): + result = { + 'profiles:9448fe71611e': [ + dict( + parameters=dict() + ) + ], + 'profiles:03a4950ab656': [ + dict( + parameters=dict() + ) + ] + } + return result + + @property + def pool(self): + result = dict() + result['ltm:pool:9a593d17495b'] = [ + dict( + parameters=dict( + name='pool_0' + ), + subcollectionResources=self.pool_members + ) + ] + return result + + @property + def pool_members(self): + result = dict() + result['members:5109c66dfbac'] = [] + for x in self.servers: + member = dict( + parameters=dict( + port=x.get('port', 80), + nodeReference=dict( + link='#/resources/ltm:node:9e76a6323321/{0}'.format(x['address']), + fullPath='# {0}'.format(x['address']) + ) + ) + ) + result['members:5109c66dfbac'].append(member) + return result + + @property + def http_profile(self): + result = dict() + result['ltm:profile:http:03a4950ab656'] = [ + dict( + parameters=dict( + name='profile_http' + ) + ) + ] + return result + + @property + def http_monitor(self): + result = dict() + result['ltm:monitor:http:ea4346e49cdf'] = [ + dict( + parameters=dict( + name='monitor-http' + ) + ) + ] + return result + + @property + def nodes(self): + result = dict() + result['ltm:node:9e76a6323321'] = [] + for x in self.servers: + tmp = dict( + parameters=dict( + name=x['address'], + address=x['address'] + ) + ) + result['ltm:node:9e76a6323321'].append(tmp) + return result + + @property + def node_addresses(self): + result = [x['address'] for x in self.servers] + return result + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.want.client = self.client + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def check_bigiq_version(self, version): + if Version(version) >= Version('6.1.0'): + raise F5ModuleError( + 'Module supports only BIGIQ version 6.0.x or lower.' + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + self.check_bigiq_version(version) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/ap/query/v1/tenants/default/reports/AllApplicationsList" \ + "?$filter=name+eq+'{2}'".format(self.client.provider['server'], + self.client.provider['server_port'], + self.want.name) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if (resp.status == 200 and 'result' in response + and 'totalItems' in response['result'] and response['result']['totalItems'] == 0): + return False + return True + + def remove(self): + if self.module.check_mode: + return True + self_link = self.remove_from_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def has_no_service_environment(self): + if self.want.default_device_reference is None and self.want.ssg_reference is None: + return True + return False + + def create(self): + if self.want.service_environment is None: + raise F5ModuleError( + "A 'service_environment' must be specified when creating a new application." + ) + if self.want.servers is None: + raise F5ModuleError( + "At least one 'servers' item is needed when creating a new application." + ) + if self.want.inbound_virtual is None: + raise F5ModuleError( + "An 'inbound_virtual' must be specified when creating a new application." + ) + self._set_changed_options() + + if self.has_no_service_environment(): + raise F5ModuleError( + "The specified 'service_environment' ({0}) was not found.".format(self.want.service_environment) + ) + + if self.module.check_mode: + return True + self_link = self.create_on_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if not self.exists(): + raise F5ModuleError( + "Failed to deploy application." + ) + return True + + def create_on_device(self): + params = self.changes.api_params() + params['mode'] = 'CREATE' + + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + params = dict( + configSetName=self.want.name, + mode='DELETE' + ) + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def wait_for_apply_template_task(self, self_link): + host = 'https://{0}:{1}'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = self_link.replace('https://localhost', host) + + while True: + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if response['status'] == 'FINISHED' and response.get('currentStep', None) == 'DONE': + return True + elif 'errorMessage' in response: + raise F5ModuleError(response['errorMessage']) + time.sleep(5) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + servers=dict( + type='list', + elements='dict', + options=dict( + address=dict(required=True), + port=dict(default=80) + ) + ), + inbound_virtual=dict( + type='dict', + options=dict( + address=dict(required=True), + netmask=dict(required=True), + port=dict(default=80) + ) + ), + service_environment=dict(), + add_analytics=dict(type='bool', default='no'), + state=dict( + default='present', + choices=['present', 'absent'] + ), + wait=dict(type='bool', default='yes') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_https_offload.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_https_offload.py new file mode 100644 index 00000000..73ef0a66 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_https_offload.py @@ -0,0 +1,1020 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_application_https_offload +short_description: Manages BIG-IQ HTTPS offload applications +description: + - Manages BIG-IQ applications used for load balancing an HTTPS application on + port 443 with SSL offloading on a BIG-IP. +version_added: "1.0.0" +options: + name: + description: + - Name of the new application. + type: str + required: True + description: + description: + - Description of the application. + type: str + servers: + description: + - A list of servers on which the application is hosted. + - If you are familiar with other BIG-IP settings, you might also refer to this + list as the list of pool members. + - When creating a new application, at least one server is required. + type: list + elements: dict + suboptions: + address: + description: + - The IP address of the server. + type: str + required: True + port: + description: + - The port of the server. + type: str + default: 80 + inbound_virtual: + description: + - Settings to configure the virtual which receives the inbound connection. + - This virtual is used to host the HTTPS endpoint of the application. + - Traffic destined to the C(redirect_virtual) is offloaded to this + parameter to ensure proper redirection from insecure to secure occurs. + type: dict + suboptions: + address: + description: + - Specifies destination IP address information to which the virtual server + sends traffic. + - This parameter is required when creating a new application. + type: str + required: True + netmask: + description: + - Specifies the netmask to associate with the given C(address). + - This parameter is required when creating a new application. + type: str + required: True + port: + description: + - The port on which the virtual listens for connections. + - When creating a new application, if this parameter is not specified, the + default value is C(443). + type: str + default: 443 + redirect_virtual: + description: + - Settings to configure the virtual which receives the connection to be + redirected. + - This virtual is used to host the HTTP endpoint of the application. + - Traffic destined to this parameter is offloaded to the + C(inbound_virtual) parameter to ensure proper redirection from insecure + to secure occurs. + type: dict + suboptions: + address: + description: + - Specifies destination IP address information to which the virtual server + sends traffic. + - This parameter is required when creating a new application. + type: str + required: True + netmask: + description: + - Specifies the netmask to associate with the given C(address). + - This parameter is required when creating a new application. + type: str + required: True + port: + description: + - The port on which the virtual listens for connections. + - When creating a new application, if this parameter is not specified, the + default value is C(80). + type: str + default: 80 + client_ssl_profile: + description: + - Specifies the SSL profile for managing client-side SSL traffic. + type: dict + suboptions: + name: + description: + - The name of the client SSL profile to created and used. + - When creating a new application, if this value is not specified, the + default value is C(clientssl). + type: str + default: clientssl + cert_key_chain: + description: + - One or more certificates and keys to associate with the SSL profile. + - This option is always a list. The keys in the list dictate the details + of the client/key/chain/passphrase combination. + - BIG-IPs can only have one of each type of each certificate/key + type. This means you can only have one RSA, one DSA, and one ECDSA + per profile. + - If you attempt to assign two RSA, DSA, or ECDSA certificate/key combo, + the device rejects it. + - This list is a complex list that specifies a number of keys. + - When creating a new profile, if this parameter is not specified, the + default value is C(inherit). + type: raw + suboptions: + cert: + description: + - Specifies a cert name for use. + type: str + key: + description: + - Specifies a key name. + type: str + chain: + description: + - Specifies a certificate chain that is relevant to the specified certificate and + key. + - This key is optional. + type: str + passphrase: + description: + - Contains the passphrase of the key file, should it require one. + - Passphrases are encrypted on the remote BIG-IP device. + type: str + service_environment: + description: + - Specifies the name of service environment or the hostname of the BIG-IP to which + the application will be deployed. + - When creating a new application, this parameter is required. + type: str + add_analytics: + description: + - Collects statistics of the BIG-IP to which the application is deployed. + - This parameter is only relevant when specifying a C(service_environment) which + is a BIG-IP; not an SSG. + type: bool + default: no + state: + description: + - The state of the resource on the system. + - When C(present), guarantees the resource exists with the provided attributes. + - When C(absent), removes the resource from the system. + type: str + choices: + - absent + - present + default: present + wait: + description: + - If the module should wait for the application to be created, deleted or updated. + type: bool + default: yes +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - This module will not work on BIGIQ version 6.1.x or greater. +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Load balance an HTTPS application on port 443 with SSL offloading on BIG-IP + bigiq_application_https_offload: + name: my-app + description: Redirect HTTP to HTTPS + service_environment: my-ssg + servers: + - address: 1.2.3.4 + port: 8080 + - address: 5.6.7.8 + port: 8080 + inbound_virtual: + address: 2.2.2.2 + netmask: 255.255.255.255 + port: 443 + redirect_virtual: + address: 2.2.2.2 + netmask: 255.255.255.255 + port: 80 + provider: + password: secret + server: lb.mydomain.com + user: admin + state: present + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the application of the resource. + returned: changed + type: str + sample: My application +service_environment: + description: The environment to which the service was deployed. + returned: changed + type: str + sample: my-ssg1 +inbound_virtual_destination: + description: The destination of the virtual that was created. + returned: changed + type: str + sample: 6.7.8.9 +inbound_virtual_netmask: + description: The network mask of the provided inbound destination. + returned: changed + type: str + sample: 255.255.255.0 +inbound_virtual_port: + description: The port on which the inbound virtual address listens. + returned: changed + type: int + sample: 80 +servers: + description: List of servers, and their ports, that make up the application. + type: complex + returned: changed + contains: + address: + description: The IP address of the server. + returned: changed + type: str + sample: 2.3.4.5 + port: + description: The port on which the server listens. + returned: changed + type: int + sample: 8080 + sample: hash/dictionary of values +''' + +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) +from ansible.module_utils.six import string_types + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'templateReference': 'template_reference', + 'subPath': 'sub_path', + 'ssgReference': 'ssg_reference', + 'configSetName': 'config_set_name', + 'defaultDeviceReference': 'default_device_reference', + 'addAnalytics': 'add_analytics' + } + + api_attributes = [ + 'resources', 'description', 'configSetName', 'subPath', 'templateReference', + 'ssgReference', 'defaultDeviceReference', 'addAnalytics' + ] + + returnables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'ssg_reference', 'default_device_reference', 'servers', 'inbound_virtual', + 'redirect_virtual', 'client_ssl_profile', 'add_analytics' + ] + + updatables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'ssg_reference', 'default_device_reference', 'servers', 'add_analytics' + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def http_profile(self): + return "profile_http" + + @property + def config_set_name(self): + return self.name + + @property + def sub_path(self): + return self.name + + @property + def template_reference(self): + filter = "name+eq+'Default-f5-HTTPS-offload-lb-template'" + uri = "https://{0}:{1}/mgmt/cm/global/templates/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No default HTTP LB template was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + @property + def default_device_reference(self): + if is_valid_ip(self.service_environment): + # An IP address was specified + filter = "address+eq+'{0}'".format(self.service_environment) + else: + # Assume a hostname was specified + filter = "hostname+eq+'{0}'".format(self.service_environment) + + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-adccore-allbigipDevices/devices/" \ + "?$filter={2}&$top=1&$select=selfLink".format(self.client.provider['server'], + self.client.provider['server_port'], filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + @property + def ssg_reference(self): + filter = "name+eq+'{0}'".format(self.service_environment) + uri = "https://{0}:{1}/mgmt/cm/cloud/service-scaling-groups/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def resources(self): + result = dict() + result.update(self.http_profile) + result.update(self.http_monitor) + result.update(self.inbound_virtual_server) + result.update(self.redirect_virtual_server) + result.update(self.pool) + result.update(self.nodes) + result.update(self.ssl_profile) + return result + + @property + def inbound_virtual_server(self): + result = dict() + result['ltm:virtual:7a5f7da91996'] = [ + dict( + parameters=dict( + name='default_vs', + destinationAddress=self.inbound_virtual['address'], + mask=self.inbound_virtual['netmask'], + destinationPort=self.inbound_virtual['port'] + ), + subcollectionResources=self.inbound_profiles + ) + ] + return result + + @property + def inbound_profiles(self): + result = { + 'profiles:14c995c33411': [ + dict( + parameters=dict() + ) + ], + 'profiles:8ba4bb101701': [ + dict( + parameters=dict() + ) + ], + 'profiles:9448fe71611e': [ + dict( + parameters=dict() + ) + ] + } + return result + + @property + def redirect_virtual_server(self): + result = dict() + result['ltm:virtual:40e8c4a6f542'] = [ + dict( + parameters=dict( + name='default_redirect_vs', + destinationAddress=self.redirect_virtual['address'], + mask=self.redirect_virtual['netmask'], + destinationPort=self.redirect_virtual['port'] + ), + subcollectionResources=self.redirect_profiles + ) + ] + return result + + @property + def redirect_profiles(self): + result = { + 'profiles:8ba4bb101701': [ + dict( + parameters=dict() + ) + ], + 'profiles:9448fe71611e': [ + dict( + parameters=dict() + ) + ] + } + return result + + @property + def pool(self): + result = dict() + result['ltm:pool:be70d46c6d73'] = [ + dict( + parameters=dict( + name='pool_0' + ), + subcollectionResources=self.pool_members + ) + ] + return result + + @property + def pool_members(self): + result = dict() + result['members:dec6d24dc625'] = [] + for x in self.servers: + member = dict( + parameters=dict( + port=x['port'], + nodeReference=dict( + link='#/resources/ltm:node:45391b57b104/{0}'.format(x['address']), + fullPath='# {0}'.format(x['address']) + ) + ) + ) + result['members:dec6d24dc625'].append(member) + return result + + @property + def http_profile(self): + result = dict() + result['ltm:profile:http:8ba4bb101701'] = [ + dict( + parameters=dict( + name='profile_http' + ) + ) + ] + return result + + @property + def http_monitor(self): + result = dict() + result['ltm:monitor:http:fd07629373b0'] = [ + dict( + parameters=dict( + name='monitor-http' + ) + ) + ] + return result + + @property + def nodes(self): + result = dict() + result['ltm:node:45391b57b104'] = [] + for x in self.servers: + tmp = dict( + parameters=dict( + name=x['address'], + address=x['address'] + ) + ) + result['ltm:node:45391b57b104'].append(tmp) + return result + + @property + def node_addresses(self): + result = [x['address'] for x in self.servers] + return result + + @property + def ssl_profile(self): + result = dict() + result['ltm:profile:client-ssl:14c995c33411'] = [ + dict( + parameters=dict( + name='clientssl', + certKeyChain=self.cert_key_chains + ) + ) + ] + return result + + def _get_cert_references(self): + result = dict() + uri = "https://{0}:{1}/mgmt/cm/adc-core/working-config/sys/file/ssl-cert/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + for cert in response['items']: + key = fq_name(cert['partition'], cert['name']) + result[key] = cert['selfLink'] + return result + + def _get_key_references(self): + result = dict() + uri = "https://{0}:{1}/mgmt/cm/adc-core/working-config/sys/file/ssl-key/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + for cert in response['items']: + key = fq_name(cert['partition'], cert['name']) + result[key] = cert['selfLink'] + return result + + @property + def cert_key_chains(self): + result = [] + if self.client_ssl_profile is None: + return None + if 'cert_key_chain' not in self.client_ssl_profile: + return None + + kc = self.client_ssl_profile['cert_key_chain'] + if isinstance(kc, string_types) and kc != 'inherit': + raise F5ModuleError( + "Only the 'inherit' setting is available when 'cert_key_chain' is a string." + ) + + if not isinstance(kc, list): + raise F5ModuleError( + "The value of 'cert_key_chain' is not one of the supported types." + ) + + cert_references = self._get_cert_references() + key_references = self._get_key_references() + + for idx, x in enumerate(kc): + tmp = dict( + name='clientssl{0}'.format(idx) + ) + if 'cert' not in x: + raise F5ModuleError( + "A 'cert' option is required when specifying the 'cert_key_chain' parameter.." + ) + elif x['cert'] not in cert_references: + raise F5ModuleError( + "The specified 'cert' was not found. Did you specify its full path?" + ) + else: + key = x['cert'] + tmp['certReference'] = dict( + link=cert_references[key], + fullPath=key + ) + + if 'key' not in x: + raise F5ModuleError( + "A 'key' option is required when specifying the 'cert_key_chain' parameter.." + ) + elif x['key'] not in key_references: + raise F5ModuleError( + "The specified 'key' was not found. Did you specify its full path?" + ) + else: + key = x['key'] + tmp['keyReference'] = dict( + link=key_references[key], + fullPath=key + ) + + if 'chain' in x and x['chain'] not in cert_references: + raise F5ModuleError( + "The specified 'key' was not found. Did you specify its full path?" + ) + else: + key = x['chain'] + tmp['chainReference'] = dict( + link=cert_references[key], + fullPath=key + ) + + if 'passphrase' in x: + tmp['passphrase'] = x['passphrase'] + result.append(tmp) + return result + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.want.client = self.client + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + self.changes.client = self.client + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + self.changes.client = self.client + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def check_bigiq_version(self, version): + if Version(version) >= Version('6.1.0'): + raise F5ModuleError( + 'Module supports only BIGIQ version 6.0.x or lower.' + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + self.check_bigiq_version(version) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/ap/query/v1/tenants/default/reports/" \ + "AllApplicationsList?$filter=name+eq+'{2}'".format(self.client.provider['server'], + self.client.provider['server_port'], self.want.name) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if (resp.status == 200 and 'result' in response and + 'totalItems' in response['result'] and response['result']['totalItems'] == 0): + return False + return True + + def remove(self): + if self.module.check_mode: + return True + self_link = self.remove_from_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def has_no_service_environment(self): + if self.want.default_device_reference is None and self.want.ssg_reference is None: + return True + return False + + def create(self): + if self.want.service_environment is None: + raise F5ModuleError( + "A 'service_environment' must be specified when creating a new application." + ) + if self.want.servers is None: + raise F5ModuleError( + "At least one 'servers' item is needed when creating a new application." + ) + if self.want.inbound_virtual is None: + raise F5ModuleError( + "An 'inbound_virtual' must be specified when creating a new application." + ) + self._set_changed_options() + + if self.has_no_service_environment(): + raise F5ModuleError( + "The specified 'service_environment' ({0}) was not found.".format(self.want.service_environment) + ) + + if self.module.check_mode: + return True + self_link = self.create_on_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if not self.exists(): + raise F5ModuleError( + "Failed to deploy application." + ) + return True + + def create_on_device(self): + params = self.changes.api_params() + params['mode'] = 'CREATE' + + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + params = dict( + configSetName=self.want.name, + mode='DELETE' + ) + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def wait_for_apply_template_task(self, self_link): + host = 'https://{0}:{1}'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = self_link.replace('https://localhost', host) + + while True: + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if response['status'] == 'FINISHED' and response.get('currentStep', None) == 'DONE': + return True + elif 'errorMessage' in response: + raise F5ModuleError(response['errorMessage']) + time.sleep(5) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + servers=dict( + type='list', + elements='dict', + options=dict( + address=dict(required=True), + port=dict(default=80) + ) + ), + inbound_virtual=dict( + type='dict', + options=dict( + address=dict(required=True), + netmask=dict(required=True), + port=dict(default=443) + ) + ), + redirect_virtual=dict( + type='dict', + options=dict( + address=dict(required=True), + netmask=dict(required=True), + port=dict(default=80) + ) + ), + service_environment=dict(), + state=dict( + default='present', + choices=['present', 'absent'] + ), + client_ssl_profile=dict( + type='dict', + options=dict( + name=dict(default='clientssl'), + cert_key_chain=dict( + type='raw', + options=dict( + cert=dict(), + key=dict(), + chain=dict(), + passphrase=dict() + ) + ) + ) + ), + add_analytics=dict(type='bool', default='no'), + wait=dict(type='bool', default='yes') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_https_waf.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_https_waf.py new file mode 100644 index 00000000..89df0f63 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_application_https_waf.py @@ -0,0 +1,1049 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_application_https_waf +short_description: Manages BIG-IQ HTTPS WAF applications +description: + - Manages BIG-IQ applications used for load balancing an HTTPS application on port 443 + with a Web Application Firewall (WAF) using an ASM (Application Security Manager) Rapid Deployment policy. +version_added: "1.0.0" +options: + name: + description: + - Name of the new application. + type: str + required: True + description: + description: + - Description of the application. + type: str + servers: + description: + - A list of servers on which the application is hosted. + - If you are familiar with other BIG-IP settings, you might also refer to this + list as the list of pool members. + - When creating a new application, at least one server is required. + type: list + elements: dict + suboptions: + address: + description: + - The IP address of the server. + type: str + required: True + port: + description: + - The port of the server. + type: str + default: 80 + inbound_virtual: + description: + - Settings to configure the virtual which receives the inbound connection. + - This virtual is used to host the HTTPS endpoint of the application. + - Traffic destined to the C(redirect_virtual) is offloaded to this + parameter to ensure proper redirection from insecure to secure occurs. + type: dict + suboptions: + address: + description: + - Specifies destination IP address information to which the virtual server + sends traffic. + - This parameter is required when creating a new application. + type: str + required: True + netmask: + description: + - Specifies the netmask to associate with the given C(destination). + - This parameter is required when creating a new application. + type: str + required: True + port: + description: + - The port on which the virtual listens for connections. + - When creating a new application, if this parameter is not specified, the + default value is C(443). + type: str + default: 443 + redirect_virtual: + description: + - Settings to configure the virtual which receives the connection to be + redirected. + - This virtual is used to host the HTTP endpoint of the application. + - Traffic destined to this parameter is offloaded to the + C(inbound_virtual) parameter to ensure proper redirection from insecure + to secure occurs. + type: dict + suboptions: + address: + description: + - Specifies destination IP address information to which the virtual server + sends traffic. + - This parameter is required when creating a new application. + type: str + required: True + netmask: + description: + - Specifies the netmask to associate with the given C(destination). + - This parameter is required when creating a new application. + type: str + required: True + port: + description: + - The port on which the virtual listens for connections. + - When creating a new application, if this parameter is not specified, the + default value of C(80) will be used. + type: str + default: 80 + client_ssl_profile: + description: + - Specifies the SSL profile for managing client-side SSL traffic. + type: dict + suboptions: + name: + description: + - The name of the client SSL profile to created and used. + - When creating a new application, if this value is not specified, the + default value of C(clientssl) will be used. + type: str + default: clientssl + cert_key_chain: + description: + - One or more certificates and keys to associate with the SSL profile. + - This option is always a list. The keys in the list dictate the details + of the client/key/chain/passphrase combination. + - BIG-IPs can only have one of each type of each certificate/key + type. This means you can only have one RSA, one DSA, and one ECDSA + per profile. + - If you attempt to assign two RSA, DSA, or ECDSA certificate/key combo, + the device rejects it. + - This list is a complex list that specifies a number of keys. + - When creating a new profile, if this parameter is not specified, the + default value is C(inherit). + type: raw + suboptions: + cert: + description: + - Specifies a cert name for use. + type: str + key: + description: + - Specifies a key name. + type: str + chain: + description: + - Specifies a certificate chain that is relevant to the certificate and + key. + - This key is optional. + type: str + passphrase: + description: + - Contains the passphrase of the key file, should it require one. + - Passphrases are encrypted on the remote BIG-IP device. + type: str + service_environment: + description: + - Specifies the name of service environment the application will be + deployed to. + - When creating a new application, this parameter is required. + type: str + add_analytics: + description: + - Collects statistics of the BIG-IP that the application is deployed to. + - This parameter is only relevant when specifying a C(service_environment) which + is a BIG-IP; not an SSG. + type: bool + default: no + domain_names: + description: + - Specifies host names that are used to access the web application that this + security policy protects. + - When creating a new application, this parameter is required. + type: list + elements: str + state: + description: + - The state of the resource on the system. + - When C(present), guarantees the resource exists with the provided attributes. + - When C(absent), removes the resource from the system. + type: str + choices: + - absent + - present + default: present + wait: + description: + - If the module should wait for the application to be created, deleted, or updated. + type: bool + default: yes +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - This module does not work on BIG-IQ version 6.1.x or greater. +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Load balance an HTTPS application on port 443 with a WAF using ASM + bigiq_application_https_waf: + name: my-app + description: Redirect HTTP to HTTPS via WAF + service_environment: my-ssg + servers: + - address: 1.2.3.4 + port: 8080 + - address: 5.6.7.8 + port: 8080 + inbound_virtual: + address: 2.2.2.2 + netmask: 255.255.255.255 + port: 443 + redirect_virtual: + address: 2.2.2.2 + netmask: 255.255.255.255 + port: 80 + provider: + password: secret + server: lb.mydomain.com + user: admin + state: present + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the application of the resource. + returned: changed + type: str + sample: My application +service_environment: + description: The environment to which the service was deployed. + returned: changed + type: str + sample: my-ssg1 +inbound_virtual_destination: + description: The destination of the virtual that was created. + returned: changed + type: str + sample: 6.7.8.9 +inbound_virtual_netmask: + description: The network mask of the provided inbound destination. + returned: changed + type: str + sample: 255.255.255.0 +inbound_virtual_port: + description: The port on which the inbound virtual address listens. + returned: changed + type: int + sample: 80 +servers: + description: List of servers, and their ports, that make up the application. + type: complex + returned: changed + contains: + address: + description: The IP address of the server. + returned: changed + type: str + sample: 2.3.4.5 + port: + description: The port on which the server listens. + returned: changed + type: int + sample: 8080 + sample: hash/dictionary of values +''' + +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) +from ansible.module_utils.six import string_types + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, fq_name +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'templateReference': 'template_reference', + 'subPath': 'sub_path', + 'ssgReference': 'ssg_reference', + 'configSetName': 'config_set_name', + 'defaultDeviceReference': 'default_device_reference', + 'addAnalytics': 'add_analytics', + 'domains': 'domain_names' + } + + api_attributes = [ + 'resources', 'description', 'configSetName', 'subPath', 'templateReference', + 'ssgReference', 'defaultDeviceReference', 'addAnalytics', 'domains' + ] + + returnables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'ssg_reference', 'default_device_reference', 'servers', 'inbound_virtual', + 'redirect_virtual', 'client_ssl_profile', 'add_analytics', 'domain_names' + ] + + updatables = [ + 'resources', 'description', 'config_set_name', 'sub_path', 'template_reference', + 'ssg_reference', 'default_device_reference', 'servers', 'add_analytics', 'domain_names' + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def http_profile(self): + return "profile_http" + + @property + def config_set_name(self): + return self.name + + @property + def sub_path(self): + return self.name + + @property + def template_reference(self): + filter = "name+eq+'Default-f5-HTTPS-WAF-lb-template'" + uri = "https://{0}:{1}/mgmt/cm/global/templates/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No default HTTP LB template was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + @property + def default_device_reference(self): + if is_valid_ip(self.service_environment): + # An IP address was specified + filter = "address+eq+'{0}'".format(self.service_environment) + else: + # Assume a hostname was specified + filter = "hostname+eq+'{0}'".format(self.service_environment) + + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-adccore-allbigipDevices/devices/" \ + "?$filter={2}&$top=1&$select=selfLink".format(self.client.provider['server'], + self.client.provider['server_port'], filter) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + @property + def ssg_reference(self): + filter = "name+eq+'{0}'".format(self.service_environment) + uri = "https://{0}:{1}/mgmt/cm/cloud/service-scaling-groups/?$filter={2}&$top=1&$select=selfLink".format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = dict( + link=response['items'][0]['selfLink'] + ) + return result + + @property + def domain_names(self): + if self._values['domain_names'] is None: + return None + result = [] + for domain in self._values['domain_names']: + result.append( + dict( + domainName=domain + ) + ) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def resources(self): + result = dict() + result.update(self.http_profile) + result.update(self.http_monitor) + result.update(self.inbound_virtual_server) + result.update(self.redirect_virtual_server) + result.update(self.pool) + result.update(self.nodes) + result.update(self.ssl_profile) + return result + + @property + def inbound_virtual_server(self): + result = dict() + result['ltm:virtual:90735960bf4b'] = [ + dict( + parameters=dict( + name='default_vs', + destinationAddress=self.inbound_virtual['address'], + mask=self.inbound_virtual['netmask'], + destinationPort=self.inbound_virtual['port'] + ), + subcollectionResources=self.inbound_profiles + ) + ] + return result + + @property + def inbound_profiles(self): + result = { + 'profiles:78b1bcfdafad': [ + dict( + parameters=dict() + ) + ], + 'profiles:2f52acac9fde': [ + dict( + parameters=dict() + ) + ], + 'profiles:9448fe71611e': [ + dict( + parameters=dict() + ) + ] + } + return result + + @property + def redirect_virtual_server(self): + result = dict() + result['ltm:virtual:3341f412b980'] = [ + dict( + parameters=dict( + name='default_redirect_vs', + destinationAddress=self.redirect_virtual['address'], + mask=self.redirect_virtual['netmask'], + destinationPort=self.redirect_virtual['port'] + ), + subcollectionResources=self.redirect_profiles + ) + ] + return result + + @property + def redirect_profiles(self): + result = { + 'profiles:2f52acac9fde': [ + dict( + parameters=dict() + ) + ], + 'profiles:9448fe71611e': [ + dict( + parameters=dict() + ) + ] + } + return result + + @property + def pool(self): + result = dict() + result['ltm:pool:8bc5b256f9d1'] = [ + dict( + parameters=dict( + name='pool_0' + ), + subcollectionResources=self.pool_members + ) + ] + return result + + @property + def pool_members(self): + result = dict() + result['members:dec6d24dc625'] = [] + for x in self.servers: + member = dict( + parameters=dict( + port=x['port'], + nodeReference=dict( + link='#/resources/ltm:node:c072248f8e6a/{0}'.format(x['address']), + fullPath='# {0}'.format(x['address']) + ) + ) + ) + result['members:dec6d24dc625'].append(member) + return result + + @property + def http_profile(self): + result = dict() + result['ltm:profile:http:2f52acac9fde'] = [ + dict( + parameters=dict( + name='profile_http' + ) + ) + ] + return result + + @property + def http_monitor(self): + result = dict() + result['ltm:monitor:http:18765a198150'] = [ + dict( + parameters=dict( + name='monitor-http' + ) + ) + ] + return result + + @property + def nodes(self): + result = dict() + result['ltm:node:c072248f8e6a'] = [] + for x in self.servers: + tmp = dict( + parameters=dict( + name=x['address'], + address=x['address'] + ) + ) + result['ltm:node:c072248f8e6a'].append(tmp) + return result + + @property + def node_addresses(self): + result = [x['address'] for x in self.servers] + return result + + @property + def ssl_profile(self): + result = dict() + result['ltm:profile:client-ssl:78b1bcfdafad'] = [ + dict( + parameters=dict( + name='clientssl', + certKeyChain=self.cert_key_chains + ) + ) + ] + return result + + def _get_cert_references(self): + result = dict() + uri = "https://{0}:{1}/mgmt/cm/adc-core/working-config/sys/file/ssl-cert/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + for cert in response['items']: + key = fq_name(cert['partition'], cert['name']) + result[key] = cert['selfLink'] + return result + + def _get_key_references(self): + result = dict() + uri = "https://{0}:{1}/mgmt/cm/adc-core/working-config/sys/file/ssl-key/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + for cert in response['items']: + key = fq_name(cert['partition'], cert['name']) + result[key] = cert['selfLink'] + return result + + @property + def cert_key_chains(self): + result = [] + + if self.client_ssl_profile is None: + return None + if 'cert_key_chain' not in self.client_ssl_profile: + return None + + kc = self.client_ssl_profile['cert_key_chain'] + if isinstance(kc, string_types) and kc != 'inherit': + raise F5ModuleError( + "Only the 'inherit' setting is available when 'cert_key_chain' is a string." + ) + + if not isinstance(kc, list): + raise F5ModuleError( + "The value of 'cert_key_chain' is not one of the supported types." + ) + + cert_references = self._get_cert_references() + key_references = self._get_key_references() + + for idx, x in enumerate(kc): + tmp = dict( + name='clientssl{0}'.format(idx) + ) + if 'cert' not in x: + raise F5ModuleError( + "A 'cert' option is required when specifying the 'cert_key_chain' parameter.." + ) + elif x['cert'] not in cert_references: + raise F5ModuleError( + "The specified 'cert' was not found. Did you specify its full path?" + ) + else: + key = x['cert'] + tmp['certReference'] = dict( + link=cert_references[key], + fullPath=key + ) + + if 'key' not in x: + raise F5ModuleError( + "A 'key' option is required when specifying the 'cert_key_chain' parameter.." + ) + elif x['key'] not in key_references: + raise F5ModuleError( + "The specified 'key' was not found. Did you specify its full path?" + ) + else: + key = x['key'] + tmp['keyReference'] = dict( + link=key_references[key], + fullPath=key + ) + + if 'chain' in x and x['chain'] not in cert_references: + raise F5ModuleError( + "The specified 'key' was not found. Did you specify its full path?" + ) + else: + key = x['chain'] + tmp['chainReference'] = dict( + link=cert_references[key], + fullPath=key + ) + + if 'passphrase' in x: + tmp['passphrase'] = x['passphrase'] + result.append(tmp) + return result + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.want.client = self.client + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + self.changes.client = self.client + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + self.changes.client = self.client + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def check_bigiq_version(self, version): + if Version(version) >= Version('6.1.0'): + raise F5ModuleError( + 'Module supports only BIGIQ version 6.0.x or lower.' + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + self.check_bigiq_version(version) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/ap/query/v1/tenants/default/reports/AllApplicationsList?" \ + "$filter=name+eq+'{2}'".format(self.client.provider['server'], + self.client.provider['server_port'], self.want.name) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if (resp.status == 200 and 'result' in response and + 'totalItems' in response['result'] and response['result']['totalItems'] == 0): + return False + return True + + def remove(self): + if self.module.check_mode: + return True + self_link = self.remove_from_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def has_no_service_environment(self): + if self.want.default_device_reference is None and self.want.ssg_reference is None: + return True + return False + + def create(self): + if self.want.service_environment is None: + raise F5ModuleError( + "A 'service_environment' must be specified when creating a new application." + ) + if self.want.servers is None: + raise F5ModuleError( + "At least one 'servers' item is needed when creating a new application." + ) + if self.want.inbound_virtual is None: + raise F5ModuleError( + "An 'inbound_virtual' must be specified when creating a new application." + ) + if self.want.domain_names is None: + raise F5ModuleError( + "You must provide at least one value in the 'domain_names' parameter." + ) + self._set_changed_options() + + if self.has_no_service_environment(): + raise F5ModuleError( + "The specified 'service_environment' ({0}) was not found.".format(self.want.service_environment) + ) + + if self.module.check_mode: + return True + self_link = self.create_on_device() + if self.want.wait: + self.wait_for_apply_template_task(self_link) + if not self.exists(): + raise F5ModuleError( + "Failed to deploy application." + ) + return True + + def create_on_device(self): + params = self.changes.api_params() + params['mode'] = 'CREATE' + + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + params = dict( + configSetName=self.want.name, + mode='DELETE' + ) + uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['selfLink'] + + def wait_for_apply_template_task(self, self_link): + host = 'https://{0}:{1}'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + uri = self_link.replace('https://localhost', host) + + while True: + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if response['status'] == 'FINISHED' and response.get('currentStep', None) == 'DONE': + return True + elif 'errorMessage' in response: + raise F5ModuleError(response['errorMessage']) + time.sleep(5) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + servers=dict( + type='list', + elements='dict', + options=dict( + address=dict(required=True), + port=dict(default=80) + ) + ), + inbound_virtual=dict( + type='dict', + options=dict( + address=dict(required=True), + netmask=dict(required=True), + port=dict(default=443) + ) + ), + redirect_virtual=dict( + type='dict', + options=dict( + address=dict(required=True), + netmask=dict(required=True), + port=dict(default=80) + ) + ), + service_environment=dict(), + state=dict( + default='present', + choices=['present', 'absent'] + ), + client_ssl_profile=dict( + type='dict', + options=dict( + name=dict(default='clientssl'), + cert_key_chain=dict( + type='raw', + options=dict( + cert=dict(), + key=dict(), + chain=dict(), + passphrase=dict() + ) + ) + ) + ), + add_analytics=dict(type='bool', default='no'), + domain_names=dict( + type='list', + elements='str', + ), + wait=dict(type='bool', default='yes') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_device_discovery.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_device_discovery.py new file mode 100644 index 00000000..0795c8d3 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_device_discovery.py @@ -0,0 +1,1256 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_device_discovery +short_description: Manage BIG-IP devices through BIG-IQ +description: + - Discovers and imports BIG-IP device configuration on the BIG-IQ. +version_added: "1.0.0" +options: + device_address: + description: + - The IP address of the BIG-IP device to be imported/managed. + type: str + required: True + device_username: + description: + - The administrator username for the BIG-IP device. + - This parameter is only required when adding a new BIG-IP device to be managed. + type: str + device_password: + description: + - The administrator password for the BIG-IP device. + - This parameter is only required when adding a new BIG-IP device to be managed. + type: str + device_port: + description: + - The port on which a device trust setup between BIG-IQ and BIG-IP should happen. + type: int + default: 443 + ha_name: + description: + - DSC cluster name of the BIG-IP device to be managed. + - This is optional if the managed device is not a part of a cluster group. + - When C(use_bigiq_sync) is set to C(yes), this parameter is required. + type: str + use_bigiq_sync: + description: + - When set to C(no), initiate BIG-IP DSC sync when deploying configuration changes. + - When set to C(yes), ignore BIG-IP DSC sync when deploying configuration changes. + type: bool + default: no + conflict_policy: + description: + - Sets the conflict resolution policy for shared objects across BIG-IP devices, except LTM profiles and monitors. + type: str + choices: + - use_bigiq + - use_bigip + default: use_bigiq + versioned_conflict_policy: + description: + - Sets the conflict resolution policy for LTM profile and monitor objects that are specific to a BIG-IP software + version. + type: str + choices: + - use_bigiq + - use_bigip + - keep_version + device_conflict_policy: + description: + - Sets the conflict resolution policy for objects that are specific to a particular to a BIG-IP device + and not shared among BIG-IP devices. + type: str + choices: + - use_bigiq + - use_bigip + default: use_bigiq + access_conflict_policy: + description: + - Sets the conflict resolution policy for Access module C(apm) objects. Only used when the C(apm) module is specified. + type: str + choices: + - use_bigiq + - use_bigip + - keep_version + access_group_name: + description: + - Access group name to import Access configuration for devices. Once set it cannot be changed. + type: str + access_group_first_device: + description: + - Specifies if the imported device is the first device in the access group to import shared configuration for that + access group. + type: bool + default: yes + force: + description: + - Forces rediscovery and import of existing modules on the managed BIG-IP. + type: bool + default: no + modules: + description: + - List of modules to be discovered and imported into the device. + - These modules must be provisioned on the target device, otherwise operation will fail. + - The C(ltm) module must always be specified when performing discovery or re-discovery of the the device. + - When C(asm) or C(afm) are specified, the C(shared_security) module also needs to be declared. + type: list + elements: str + choices: + - ltm + - asm + - apm + - afm + - dns + - websafe + - security_shared + statistics: + description: + - Specify the statistics collection for discovered device. + type: dict + suboptions: + enable: + description: + - Enables statistics collection on a device. + type: bool + default: no + interval: + description: + - Specifies the interval the data is collected from the discovered device, in seconds. + type: int + default: 60 + choices: + - 30 + - 60 + - 120 + - 500 + zone: + description: + - Specifies in which DCD zone is collecting the data from device. + type: str + default: default + stat_modules: + description: + - Specifies for which modules the data is being collected. + type: list + elements: str + default: ['device', 'ltm'] + choices: + - device + - ltm + - dns + state: + description: + - The state of the managed device on the system. + - When C(present), enables new device addition as well as device rediscovery/import. + - When C(absent), completely removes the device from the system. + type: str + choices: + - absent + - present + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - BIG-IQ >= 6.1.0. + - This module does not support atomic removal of discovered modules on the device. +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Discover a new device and import config, use default conflict policy. + bigiq_device_discovery: + device_address: 192.168.1.1 + device_username: bigipadmin + device_password: bigipsecret + modules: + - ltm + - afm + - shared_security + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Discover a new device and import config, use non- default conflict policy. + bigiq_device_discovery: + device_address: 192.168.1.1 + modules: + - ltm + - dns + conflict_policy: use_bigip + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Force full device rediscovery + bigiq_device_discovery: + device_address: 192.168.1.1 + modules: + - ltm + - afm + - dns + - shared_security + force: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove discovered device and its config + bigiq_device_discovery: + device_address: 192.168.1.1 + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +device_address: + description: The IP address of the BIG-IP device to be imported/managed. + returned: changed + type: str + sample: 192.168.1.1 +device_port: + description: The port on which a device trust setup between BIG-IQ and BIG-IP should happen. + returned: changed + type: int + sample: 10443 +ha_name: + description: DSC cluster name of the BIG-IP device to be managed. + returned: changed + type: str + sample: GROUP_1 +use_bigiq_sync: + description: Indicates if BIG-IQ should manually synchronize DSC configuration. + returned: changed + type: bool + sample: yes +conflict_policy: + description: Sets the conflict resolution policy for shared objects across BIG-IP devices. + returned: changed + type: str + sample: use_bigip +device_conflict_policy: + description: Sets the conflict resolution policy for objects that are specific to a particular to a BIG-IP device. + returned: changed + type: str + sample: use_bigip +versioned_conflict_policy: + description: Sets the conflict resolution policy for LTM profile and monitor objects. + returned: changed + type: str + sample: keep_version +access_conflict_policy: + description: Sets the conflict resolution policy for Access module C(apm) objects. + returned: changed + type: str + sample: keep_version +access_group_name: + description: Access group name to import Access configuration for devices. + returned: changed + type: str + sample: foo_group +access_group_first_device: + description: First device in the access group to import shared configuration for that access group. + returned: changed + type: bool + sample: yes +modules: + description: List of modules to be discovered and imported into the device. + returned: changed + type: list + sample: ['ltm', 'dns'] + +''' + +import time +import traceback +from datetime import datetime + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'address': 'device_address', + 'userName': 'device_username', + 'password': 'device_password', + 'httpsPort': 'device_port', + 'clusterName': 'ha_name', + 'useBigiqSync': 'use_bigiq_sync', + } + + api_attributes = [ + 'address', + 'userName', + 'password', + 'httpsPort', + 'clusterName', + 'useBigiqSync', + ] + + returnables = [ + 'device_address', + 'device_username', + 'device_password', + 'device_port', + 'ha_name', + 'use_bigiq_sync', + 'modules', + 'conflict_policy', + 'versioned_conflict_policy', + 'device_conflict_policy', + 'access_group_name', + 'access_group_first_device', + 'access_conflict_policy', + 'module_list', + 'apm_properties', + ] + + updatables = [ + 'modules', + 'access_group_name', + 'apm_properties', + 'module_list', + ] + + +class ApiParameters(Parameters): + module_map = { + 'cm-security-shared-allSharedDevices': 'security_shared', + 'cm-asm-allAsmDevices': 'asm', + 'cm-firewall-allFirewallDevices': 'firewall', + 'cm-websafe-allFpsDevices': 'fps', + 'cm-dns-allBigIpDevices': 'dns', + 'cm-adccore-allbigipDevices': 'adc_core', + 'cm-access-allBigIpDevices': 'access', + } + + @property + def modules(self): + raw_data = self._values['properties'] + if raw_data is None: + return None + result = list() + for item in raw_data.keys(): + if item in self.module_map: + if raw_data[item]['discovered'] is True and raw_data[item]['imported'] is True: + result.append(self.module_map[item]) + return result + + @property + def access_group_name(self): + raw_data = self._values['properties'] + if raw_data is None: + return None + for item in raw_data.keys(): + if 'cm:access:access-group-name' in raw_data[item]: + return raw_data[item]['cm:access:access-group-name'] + return None + + +class ModuleParameters(Parameters): + module_map = { + 'ltm': 'adc_core', + 'afm': 'firewall', + 'websafe': 'fps', + 'apm': 'access', + } + + @property + def device_password(self): + if self._values['device_password'] is None: + return None + return self._values['device_password'] + + @property + def device_username(self): + if self._values['device_username'] is None: + return None + return self._values['device_username'] + + @property + def device_address(self): + if is_valid_ip(self._values['device_address']): + return self._values['device_address'] + raise F5ModuleError( + 'Provided device address: {0} is not a valid IP.'.format(self._values['device_address']) + ) + + @property + def device_port(self): + if self._values['device_port'] is None: + return None + return int(self._values['device_port']) + + @property + def conflict_policy(self): + return self._values['conflict_policy'].upper() + + @property + def device_conflict_policy(self): + return self._values['device_conflict_policy'].upper() + + @property + def versioned_conflict_policy(self): + if self._values['versioned_conflict_policy'] is None: + return None + return self._values['versioned_conflict_policy'].upper() + + @property + def access_conflict_policy(self): + if self._values['access_conflict_policy'] is None: + return None + return self._values['device_conflict_policy'].upper() + + @property + def modules(self): + if self._values['modules'] is None: + return None + result = list() + if 'security_shared' not in self._values['modules']: + if 'afm' in self._values['modules']: + raise F5ModuleError( + "Module 'shared_security' required for 'afm' module." + ) + if 'asm' in self._values['modules']: + raise F5ModuleError( + "Module 'shared_security' required for 'asm' module." + ) + if 'ltm' not in self._values['modules']: + raise F5ModuleError( + "LTM module must be specified for device discovery and import." + ) + if 'apm' in self._values['modules']: + if not self.access_group_name or not self.access_conflict_policy: + raise F5ModuleError( + "When importing APM 'access_group_name' and 'access_conflict_policy' must be specified." + ) + for item in self._values['modules']: + if item in self.module_map: + result.append(self.module_map[item]) + else: + result.append(item) + return result + + @property + def apm_properties(self): + if self._values['modules'] is None: + return None + if 'apm' in self._values['modules']: + result = { + 'cm:access:conflict-resolution': self.access_conflict_policy, + 'cm:access:access-group-name': self.access_group_name, + 'cm:access:import-shared': self.access_group_first_device + } + return result + + @property + def use_bigiq_sync(self): + result = flatten_boolean(self._values['use_bigiq_sync']) + if result: + if result == 'yes': + return True + return False + + @property + def access_group_first_device(self): + result = flatten_boolean(self._values['access_group_first_device']) + if result: + if result == 'yes': + return True + return False + + @property + def stats_enabled(self): + if self._values['statistics'] is None: + return None + result = flatten_boolean(self._values['statistics']['enable']) + if result: + if result == 'yes': + return True + return False + + @property + def interval(self): + if self._values['statistics'] is None: + return None + return self._values['statistics']['interval'] + + @property + def zone(self): + if self._values['statistics'] is None: + return None + return self._values['statistics']['zone'] + + @property + def stat_modules(self): + if self._values['statistics'] is None: + return None + modules = self._values['statistics']['stat_modules'] + result = list() + for module in modules: + result.append((dict(module=module.upper()))) + return result + + +class Changes(Parameters): + 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 UsableChanges(Changes): + @property + def modules(self): + if self._values['modules'] is None: + return None + result = list() + for item in self._values['modules']: + result.append(dict(module=item)) + return result + + @property + def module_list(self): + if self._values['modules'] is None: + return None + result = list() + for item in self._values['modules']: + if item == 'access': + result.append(dict(module=item, properties=self._values['apm_properties'])) + else: + result.append(dict(module=item)) + return result + + +class ReportableChanges(Changes): + @property + def module_list(self): + return None + + @property + def apm_properties(self): + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + @property + def modules(self): + if self.want.modules is None: + return None + if self.have.modules is None: + return self.want.modules + if set(self.want.modules).issubset(self.have.modules): + return None + if set(self.want.modules) != set(self.have.modules): + return self.want.modules + + @property + def access_group_name(self): + if self.want.access_group_name != self.have.access_group_name: + raise F5ModuleError( + 'Access group name cannot be modified once it is set.' + ) + + @property + def apm_properties(self): + # This is required for idempotency and updates as we do not compare these properties + return None + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + self.device_id = None + self.task_id = None + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + changed['apm_properties'] = self.want.apm_properties + self.changes = UsableChanges(params=changed) + return True + return False + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def check_bigiq_version(self, version): + if Version(version) < Version('6.1.0'): + raise F5ModuleError( + 'Module supports only BIGIQ version 6.1.x or higher.' + ) + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + self.check_bigiq_version(version) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update() and not self.want.force: + return False + if self.module.check_mode: + return True + if self.want.force: + self._set_changed_options() + self.discover_on_device() + self.import_modules_on_device() + if self.want.stats_enabled: + self.enable_stats_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_autority_from_device() + self.remove_trust_from_device() + return True + + def create(self): + if self.want.modules is None: + raise F5ModuleError( + 'List of modules cannot be empty if discovering a device.' + ) + self._set_changed_options() + if self.module.check_mode: + return True + self.set_trust_with_device() + self.discover_on_device() + self.import_modules_on_device() + if self.want.stats_enabled: + self.enable_stats_on_device() + return True + + def exists(self): + uri = "https://{0}:{1}/mgmt/cm/system/machineid-resolver".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + query = "?$filter=address%20eq%20'{0}'".format(self.want.device_address) + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError: + return False + + if resp.status == 404 or 'code' in response and response['code'] == 404: + raise F5ModuleError(response.message) + + if 'items' in response: + if not response['items']: + return False + self.device_id = response['items'][0]['machineId'] + return True + return False + + def set_trust_with_device(self): + params = self.changes.api_params() + params['name'] = 'trust_{0}'.format(self.want.device_address) + uri = "https://{0}:{1}/mgmt/cm/global/tasks/device-trust/".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + task = "https://{0}:{1}/mgmt/cm/global/tasks/device-trust/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + response['id'] + ) + query = "?$select=status,currentStep,errorMessage" + + if self._wait_for_task(task + query): + self._set_device_id(task) + return True + + def _set_device_id(self, uri): + 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) + + self.device_id = response['machineId'] + + def _wait_for_task(self, uri): + while True: + resp = self.client.api.get(uri) + + if resp.status == 401: + # handle expired tokens + self.client.reconnect() + 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) + + if response['status'] in ['FINISHED', 'FAILED', 'CANCELLED']: + break + + time.sleep(1) + + if response['status'] == 'FAILED': + raise F5ModuleError(response['errorMessage']) + if response['status'] == 'CANCELLED': + raise F5ModuleError( + 'The task process has been cancelled.' + ) + if response['status'] == 'FINISHED': + return True + + def discover_on_device(self): + tmp = self.changes.to_return() + if self.reuse_task_on_device('discovery'): + params = dict( + moduleList=tmp['modules'], + status='STARTED' + ) + uri = "https://{0}:{1}/mgmt/cm/global/tasks/device-discovery/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.task_id + ) + resp = self.client.api.patch(uri, json=params) + + else: + params = dict( + name='discovery_{0}'.format(self.want.device_address), + moduleList=tmp['modules'], + deviceReference=dict(link='https://localhost/mgmt/cm/system/machineid-resolver/{0}'.format( + self.device_id + ) + ), + status='STARTED' + ) + uri = "https://{0}:{1}/mgmt/cm/global/tasks/device-discovery".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + task = "https://{0}:{1}/mgmt/cm/global/tasks/device-discovery/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + response['id'] + ) + query = "?$select=status,currentStep,errorMessage" + + self._wait_for_task(task + query) + + return True + + def import_modules_on_device(self): + tmp = self.changes.to_return() + if self.reuse_task_on_device('import'): + params = dict( + moduleList=tmp['module_list'], + conflictPolicy=self.want.conflict_policy, + deviceConflictPolicy=self.want.device_conflict_policy, + status='STARTED' + ) + + if self.want.versioned_conflict_policy: + params['versionedConflictPolicy'] = self.want.versioned_conflict_policy + + uri = "https://{0}:{1}/mgmt/cm/global/tasks/device-import/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.task_id + ) + resp = self.client.api.patch(uri, json=params) + + else: + params = dict( + name='import_{0}'.format(self.want.device_address), + moduleList=tmp['module_list'], + conflictPolicy=self.want.conflict_policy, + deviceConflictPolicy=self.want.device_conflict_policy, + deviceReference=dict(link='https://localhost/mgmt/cm/system/machineid-resolver/{0}'.format( + self.device_id + ) + ), + status='STARTED' + ) + + if self.want.versioned_conflict_policy: + params['versionedConflictPolicy'] = self.want.versioned_conflict_policy + + uri = "https://{0}:{1}/mgmt/cm/global/tasks/device-import".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + task = "https://{0}:{1}/mgmt/cm/global/tasks/device-import/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + response['id'] + ) + query = "?$select=status,currentStep,errorMessage" + + self._wait_for_task(task + query) + + return True + + def enable_stats_on_device(self): + params = dict( + enabled=self.want.stats_enabled, + pushIntervalSecs=self.want.interval, + zone=self.want.zone, + modules=self.want.stat_modules, + targetDeviceReference=dict( + link='https://localhost/mgmt/cm/system/machineid-resolver/{0}'.format( + self.device_id + ) + ), + ) + + uri = "https://{0}:{1}/mgmt/cm/shared/stats-mgmt/agent-install-and-config-task".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + task = "https://{0}:{1}/mgmt/cm/shared/stats-mgmt/agent-install-and-config-task/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + response['id'] + ) + query = "?$select=status,currentStep,errorMessage" + + self._wait_for_task(task + query) + + return True + + def reuse_task_on_device(self, task): + if task == 'discovery': + uri = "https://{0}:{1}/mgmt/cm/global/tasks/device-discovery".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + else: + uri = "https://{0}:{1}/mgmt/cm/global/tasks/device-import".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + query = "?$filter=deviceReference/link%20eq%20'{0}'".format(self.device_id) + resp = self.client.api.get(uri + query) + + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + if 'items' in response: + if response['items']: + self.task_id = response['id'] + return True + return False + + def remove_autority_from_device(self): + # We can provide all of the modules for removal task, without ensuring they were discovered + modules = [ + {'module': 'adc_core'}, + {'module': 'access'}, + {'module': 'asm'}, + {'module': 'fps'}, + {'module': 'firewall'}, + {'module': 'security_shared'}, + {'module': 'dns'} + ] + params = dict( + moduleList=modules, + deviceReference=dict( + link="https://localhost/mgmt/cm/system/machineid-resolver/{0}".format(self.device_id) + ), + name='remove_auth_{0}'.format(self.want.device_address) + + ) + uri = "https://{0}:{1}/mgmt/cm/global/tasks/device-remove-mgmt-authority/".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + task = "https://{0}:{1}/mgmt/cm/global/tasks/device-remove-mgmt-authority/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + response['id'] + ) + query = "?$select=status,currentStep,errorMessage" + + self._wait_for_task(task + query) + + def remove_trust_from_device(self): + params = dict( + deviceReference=dict( + link="https://localhost/mgmt/cm/system/machineid-resolver/{0}".format(self.device_id) + ), + name='remove_auth_{0}'.format(self.want.device_address) + + ) + uri = "https://{0}:{1}/mgmt/cm/global/tasks/device-remove-trust/".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + task = "https://{0}:{1}/mgmt/cm/global/tasks/device-remove-trust/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + response['id'] + ) + query = "?$select=status,currentStep,errorMessage" + + self._wait_for_task(task + query) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/cm/system/machineid-resolver/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.device_id + ) + 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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.conflict = ['use_bigip', 'use_bigiq'] + argument_spec = dict( + device_address=dict( + required=True + ), + device_username=dict( + no_log=True + ), + device_password=dict( + no_log=True + ), + device_port=dict( + type='int', + default=443 + ), + ha_name=dict(), + use_bigiq_sync=dict( + type='bool', + default='no' + ), + conflict_policy=dict( + choices=self.conflict, + default='use_bigiq' + ), + versioned_conflict_policy=dict( + choices=self.conflict + ['keep_version'], + ), + device_conflict_policy=dict( + choices=self.conflict, + default='use_bigiq' + ), + force=dict( + type='bool', + default='no' + ), + modules=dict( + type='list', + elements='str', + choices=[ + 'ltm', 'asm', 'afm', 'dns', 'websafe', 'security_shared', 'apm' + ] + ), + access_conflict_policy=dict( + choices=self.conflict + ['keep_version'] + ), + access_group_name=dict(), + access_group_first_device=dict( + type='bool', + default='yes' + ), + statistics=dict( + type='dict', + options=dict( + enable=dict( + type='bool', + default='no' + ), + interval=dict( + type='int', + choices=[ + 30, 60, 120, 500 + ], + default=60 + ), + zone=dict( + type='str', + default='default' + ), + stat_modules=dict( + type='list', + elements='str', + choices=[ + 'device', 'ltm', 'dns' + ], + default=[ + 'device', 'ltm' + ] + ) + ) + + ), + state=dict(default='present', choices=['absent', 'present']), + ) + self.required_if = [ + ['use_bigiq_sync', True, ['ha_name']] + ] + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_device_info.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_device_info.py new file mode 100644 index 00000000..c24e38b1 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_device_info.py @@ -0,0 +1,2327 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018 F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_device_info +short_description: Collect information from F5 BIG-IQ devices +description: + - Collect information from F5 BIG-IQ devices. + - This module was called C(bigiq_device_facts) before Ansible 2.9. The usage did not change. +version_added: "1.0.0" +options: + gather_subset: + description: + - When supplied, this argument restricts the information returned to a given subset. + - You can specify a list of values to include a larger subset. + - Values can also be used with an initial C(!) to specify a specific subset + should not be collected. + type: list + elements: str + required: True + choices: + - all + - applications + - managed-devices + - purchased-pool-licenses + - regkey-pools + - system-info + - vlans + - "!all" + - "!applications" + - "!managed-devices" + - "!purchased-pool-licenses" + - "!regkey-pools" + - "!system-info" + - "!vlans" +extends_documentation_fragment: f5networks.f5_modules.f5 +notes: + - This module is supported with all BIG-IQ versions + - With BIGIQ 7.0 and later, a few metadata fields not included/supported (for example, uptime, product_changelist, product_jobid) +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Collect BIG-IQ information + bigiq_device_info: + gather_subset: + - system-info + - vlans + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Collect all BIG-IQ information + bigiq_device_info: + gather_subset: + - all + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Collect all BIG-IP information except trunks + bigiq_device_info: + gather_subset: + - all + - "!trunks" + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +applications: + description: Application related information + returned: When C(managed-devices) is specified in C(gather_subset). + type: complex + contains: + protection_mode: + description: + - The type of F5 Web Application Security Service protection on the application. + returned: changed + type: str + sample: Not Protected + id: + description: + - ID of the application as known to the BIG-IQ. + returned: changed + type: str + sample: 996baae8-5d1d-3662-8a2d-3612fa2aceae + name: + description: + - Name of the application. + returned: changed + type: str + sample: site12http.example.com + status: + description: + - Current state of the application. + returned: changed + type: str + sample: DEPLOYED + transactions_per_second: + description: + - Current measurement of Transactions Per second being handled by the application. + returned: changed + type: float + sample: 0.87 + connections: + description: + - Current number of connections established to the application. + returned: changed + type: float + sample: 3.06 + new_connections: + description: + - Number of new connections being established per second. + returned: changed + type: float + sample: 0.35 + response_time: + description: + - Measured response time of the application in milliseconds. + returned: changed + type: float + sample: 0.02 + health: + description: + - Health of the application. + returned: changed + type: str + sample: Good + active_alerts: + description: + - Number of alerts active on the application. + returned: changed + type: int + sample: 0 + bad_traffic: + description: + - Percent of traffic to application that is determined to be 'bad'. + - This value is dependent on C(protection_mode) being enabled. + returned: changed + type: float + sample: 1.7498 + enhanced_analytics: + description: + - Whether enhanced analytics is enabled for the application or not. + returned: changed + type: bool + sample: yes + bad_traffic_growth: + description: + - Whether or not Bad Traffic Growth alerts are configured to be triggered or not. + returned: changed + type: bool + sample: no + sample: hash/dictionary of values +managed_devices: + description: Managed device related information. + returned: When C(managed-devices) is specified in C(gather_subset). + type: complex + contains: + address: + description: + - Address where the device was discovered. + returned: changed + type: str + sample: 10.10.10.10 + build: + description: + - Build of the version. + returned: changed + type: str + sample: 0.0.4 + device_uri: + description: + - URI to reach the management interface of the device. + returned: changed + type: str + sample: "https://10.10.10.10:443" + edition: + description: + - Edition string of the product version. + returned: changed + type: str + sample: Final + group_name: + description: + - BIG-IQ group that the device is a member of. + returned: changed + type: str + sample: cm-bigip-allBigIpDevices + hostname: + description: + - Discovered hostname of the device. + returned: changed + type: str + sample: tier2labB1.lab.fp.foo.com + https_port: + description: + - HTTPS port available on the management interface of the device. + returned: changed + type: int + sample: 443 + is_clustered: + description: + - Whether the device is clustered or not. + returned: changed + type: bool + sample: no + is_license_expired: + description: + - Whether the license on the device is expired or not. + returned: changed + type: bool + sample: yes + is_virtual: + description: + - Whether the device is a virtual edition or not. + returned: changed + type: bool + sample: yes + machine_id: + description: + - Machine specific ID assigned to this device by BIG-IQ. + returned: changed + type: str + sample: c141bc88-f734-4434-be64-a3e9ea98356e + management_address: + description: + - IP address of the management interface on the device. + returned: changed + type: str + sample: 10.10.10.10 + mcp_device_name: + description: + - Device name as known by MCPD on the BIG-IP. + returned: changed + type: str + sample: /Common/tier2labB1.lab.fp.foo.com + product: + description: + - Product that the managed device is identified as. + returned: changed + type: str + sample: BIG-IP + rest_framework_version: + description: + - REST framework version running on the device + returned: changed + type: str + sample: 13.1.1-0.0.4 + self_link: + description: + - Internal reference to the managed device in BIG-IQ. + returned: changed + type: str + sample: "https://localhost/mgmt/shared/resolver/device-groups/cm-bigip-allBigIpDevices/devices/c141bc88-f734-4434-be64-a3e9ea98356e" + slots: + description: + - Volumes on the device and versions of software installed in those volumes. + returned: changed + type: dict + sample: {"volume": "HD1.1", "product": "BIG-IP", "version": "13.1.1", "build": "0.0.4", "isActive": "yes"} + state: + description: + - State of the device. + returned: changed + type: str + sample: ACTIVE + tags: + description: + - Misc tags that are assigned to the device. + returned: changed + type: dict + sample: {'BIGIQ_tier_2_device': '2018-08-22T13:30:47.693-07:00', 'BIGIQ_SSG_name': 'tim-ssg'} + trust_domain_guid: + description: + - GUID of the trust domain the device is part of. + returned: changed + type: str + sample: 40ddf541-e604-4905-bde3005056813e36 + uuid: + description: + - UUID of the device in BIG-IQ. + returned: changed + type: str + sample: c141bc88-f734-4434-be64-a3e9ea98356e + version: + description: + - Version of TMOS installed on the device. + returned: changed + type: str + sample: 13.1.1 + sample: hash/dictionary of values +purchased_pool_licenses: + description: Purchased Pool License related information. + returned: When C(purchased-pool-licenses) is specified in C(gather_subset). + type: complex + contains: + base_reg_key: + description: + - Base registration key of the purchased pool + returned: changed + type: str + sample: XXXXX-XXXXX-XXXXX-XXXXX-XXXXXXX + dossier: + description: + - Dossier of the purchased pool license + returned: changed + type: str + sample: d6bd4b8ba5...e9a1a1199b73af9932948a + free_device_licenses: + description: + - Number of free licenses remaining. + returned: changed + type: int + sample: 34 + name: + description: + - Name of the purchased pool + returned: changed + type: str + sample: my-pool1 + state: + description: + - State of the purchased pool license + returned: changed + type: str + sample: LICENSED + total_device_licenses: + description: + - Total number of licenses in the pool. + returned: changed + type: int + sample: 40 + uuid: + description: + - UUID of the purchased pool license + returned: changed + type: str + sample: b2112329-cba7-4f1f-9a26-fab9be416d60 + vendor: + description: + - Vendor who provided the license + returned: changed + type: str + sample: F5 Networks, Inc + licensed_date_time: + description: + - Timestamp that the pool was licensed. + returned: changed + type: str + sample: "2018-09-10T00:00:00-07:00" + licensed_version: + description: + - Version of BIG-IQ that is licensed. + returned: changed + type: str + sample: 6.0.1 + evaluation_start_date_time: + description: + - Date that evaluation license starts. + returned: changed + type: str + sample: "2018-09-09T00:00:00-07:00" + evaluation_end_date_time: + description: + - Date that evaluation license ends. + returned: changed + type: str + sample: "2018-10-11T00:00:00-07:00" + license_end_date_time: + description: + - Date that the license expires. + returned: changed + type: str + sample: "2018-10-11T00:00:00-07:00" + license_start_date_time: + description: + - Date that the license starts. + returned: changed + type: str + sample: "2018-09-09T00:00:00-07:00" + registration_key: + description: + - Purchased pool license key. + returned: changed + type: str + sample: XXXXX-XXXXX-XXXXX-XXXXX-XXXXXXX + sample: hash/dictionary of values +regkey_pools: + description: Regkey Pool related information. + returned: When C(regkey-pools) is specified in C(gather_subset). + type: complex + contains: + name: + description: + - Name of the regkey pool. + returned: changed + type: str + sample: pool1 + id: + description: + - ID of the regkey pool. + returned: changed + type: str + sample: 4f9b565c-0831-4657-b6c2-6dde6182a502 + total_offerings: + description: + - Total number of offerings in the pool + returned: changed + type: int + sample: 10 + offerings: + description: List of the offerings in the pool. + type: complex + contains: + dossier: + description: + - Dossier of the license. + returned: changed + type: str + sample: d6bd4b8ba5...e9a1a1199b73af9932948a + name: + description: + - Name of the regkey. + returned: changed + type: str + sample: regkey1 + state: + description: + - State of the regkey license + returned: changed + type: str + sample: LICENSED + licensed_date_time: + description: + - Timestamp that the regkey was licensed. + returned: changed + type: str + sample: "2018-09-10T00:00:00-07:00" + licensed_version: + description: + - Version of BIG-IQ that is licensed. + returned: changed + type: str + sample: 6.0.1 + evaluation_start_date_time: + description: + - Date that evaluation license starts. + returned: changed + type: str + sample: "2018-09-09T00:00:00-07:00" + evaluation_end_date_time: + description: + - Date that evaluation license ends. + returned: changed + type: str + sample: "2018-10-11T00:00:00-07:00" + license_end_date_time: + description: + - Date that the license expires. + returned: changed + type: str + sample: "2018-10-11T00:00:00-07:00" + license_start_date_time: + description: + - Date that the license starts. + returned: changed + type: str + sample: "2018-09-09T00:00:00-07:00" + registration_key: + description: + - Registration license key. + returned: changed + type: str + sample: XXXXX-XXXXX-XXXXX-XXXXX-XXXXXXX + sample: hash/dictionary of values + sample: hash/dictionary of values +system_info: + description: System info related information. + returned: When C(system-info) is specified in C(gather_subset). + type: complex + contains: + base_mac_address: + description: + - Media Access Control address (MAC address) of the device. + returned: changed + type: str + sample: "fa:16:3e:c3:42:6f" + marketing_name: + description: + - Marketing name of the device platform. + returned: changed + type: str + sample: BIG-IQ Virtual Edition + time: + description: + - Mapping of the current time information to specific time-named keys. + returned: changed + type: complex + contains: + day: + description: + - The current day of the month, in numeric form. + returned: changed + type: int + sample: 7 + hour: + description: + - The current hour of the day in 24-hour form. + returned: changed + type: int + sample: 18 + minute: + description: + - The current minute of the hour. + returned: changed + type: int + sample: 16 + month: + description: + - The current month, in numeric form. + returned: changed + type: int + sample: 6 + second: + description: + - The current second of the minute. + returned: changed + type: int + sample: 51 + year: + description: + - The current year in 4-digit form. + returned: changed + type: int + sample: 2018 + hardware_information: + description: + - Information related to the hardware (drives and CPUs) of the system. + type: complex + returned: changed + contains: + model: + description: + - The model of the hardware. + type: str + sample: Virtual Disk + name: + description: + - The name of the hardware. + type: str + sample: HD1 + type: + description: + - The type of hardware. + type: str + sample: physical-disk + versions: + description: + - Hardware specific properties + type: complex + contains: + name: + description: + - Name of the property + type: str + sample: Size + version: + description: + - Value of the property + type: str + sample: 154.00G + is_admin_password_changed: + description: + - Whether the admin password was changed from its default or not. + returned: changed + type: bool + sample: yes + is_root_password_changed: + description: + - Whether the root password was changed from its default or not. + returned: changed + type: bool + sample: no + is_system_setup: + description: + - Whether the system has been setup or not. + returned: changed + type: bool + sample: yes + package_edition: + description: + - Displays the software edition. + returned: changed + type: str + sample: Point Release 7 + package_version: + description: + - A string combining the C(product_build) and C(product_build_date). + type: str + sample: "Build 0.0.1 - Tue May 15 15:26:30 PDT 2018" + product_code: + description: + - Code identifying the product. + type: str + sample: BIG-IQ + product_build: + description: + - Build version of the release version. + type: str + sample: 0.0.1 + product_version: + description: + - Major product version of the running software. + type: str + sample: 6.0.0 + product_built: + description: + - Unix timestamp of when the product was built. + type: int + sample: 180515152630 + product_build_date: + description: + - Human readable build date. + type: str + sample: "Tue May 15 15:26:30 PDT 2018" + product_changelist: + description: + - Changelist that product branches from. + - Not supported with BIGIQ 7.0 and later versions + type: int + sample: 2557198 + product_jobid: + description: + - ID of the job that built the product version. + - Not supported with BIGIQ 7.0 and later versions + type: int + sample: 1012030 + chassis_serial: + description: + - Serial of the chassis + type: str + sample: 11111111-2222-3333-444444444444 + host_board_part_revision: + description: + - Revision of the host board. + type: str + host_board_serial: + description: + - Serial of the host board. + type: str + platform: + description: + - Platform identifier. + type: str + sample: Z100 + switch_board_part_revision: + description: + - Switch board revision. + type: str + switch_board_serial: + description: + - Serial of the switch board. + type: str + uptime: + description: + - Time, in seconds, since the system booted. + - Not supported with BIGIQ 7.0 and later versions + type: int + sample: 603202 + sample: hash/dictionary of values +vlans: + description: List of VLAN information. + returned: When C(vlans) is specified in C(gather_subset). + type: complex + contains: + auto_lasthop: + description: + - Allows the system to send return traffic to the MAC address that transmitted the + request, even if the routing table points to a different network or interface. + returned: changed + type: str + sample: enabled + cmp_hash_algorithm: + description: + - Specifies how the traffic on the VLAN will be disaggregated. + returned: changed + type: str + sample: default + description: + description: + - Description of the VLAN. + returned: changed + type: str + sample: My vlan + failsafe_action: + description: + - Action for the system to take when the fail-safe mechanism is triggered. + returned: changed + type: str + sample: reboot + failsafe_enabled: + description: + - Whether failsafe is enabled or not. + returned: changed + type: bool + sample: yes + failsafe_timeout: + description: + - Number of seconds that an active unit can run without detecting network traffic + on this VLAN before it starts a failover. + returned: changed + type: int + sample: 90 + if_index: + description: + - Index assigned to this VLAN. It is a unique identifier assigned for all objects + displayed in the SNMP IF-MIB. + returned: changed + type: int + sample: 176 + learning_mode: + description: + - Whether switch ports placed in the VLAN are configured for switch learning, + forwarding only, or dropped. + returned: changed + type: str + sample: enable-forward + interfaces: + description: + - List of tagged or untagged interfaces and trunks that you want to configure for the VLAN. + returned: changed + type: complex + contains: + full_path: + description: + - Full name of the resource as known to BIG-IP. + returned: changed + type: str + sample: 1.3 + name: + description: + - Relative name of the resource in BIG-IP. + returned: changed + type: str + sample: 1.3 + tagged: + description: + - Whether the interface is tagged or not. + returned: changed + type: bool + sample: no + mtu: + description: + - Specific maximum transition unit (MTU) for the VLAN. + returned: changed + type: int + sample: 1500 + sflow_poll_interval: + description: + - Maximum interval in seconds between two pollings. + returned: changed + type: int + sample: 0 + sflow_poll_interval_global: + description: + - Whether the global VLAN poll-interval setting, overrides the object-level + poll-interval setting. + returned: changed + type: bool + sample: no + sflow_sampling_rate: + description: + - Ratio of packets observed to the samples generated. + returned: changed + type: int + sample: 0 + sflow_sampling_rate_global: + description: + - Whether the global VLAN sampling-rate setting, overrides the object-level + sampling-rate setting. + returned: changed + type: bool + sample: yes + source_check_enabled: + description: + - Specifies that only connections that have a return route in the routing table are accepted. + returned: changed + type: bool + sample: yes + true_mac_address: + description: + - Media access control (MAC) address for the lowest-numbered interface assigned to this VLAN. + returned: changed + type: str + sample: "fa:16:3e:10:da:ff" + tag: + description: + - Tag number for the VLAN. + returned: changed + type: int + sample: 30 + sample: hash/dictionary of values +''' + +import copy +import datetime +import traceback +import math +import re + +try: + from packaging.version import Version +except ImportError: + HAS_PACKAGING = False + Version = None + PACKAGING_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PACKAGING = True + +from ansible.module_utils.basic import ( + AnsibleModule, missing_required_lib +) + +from ansible.module_utils.six import ( + iteritems, string_types +) + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean, transform_name +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.teem import send_teem + + +def parseStats(entry): + if 'description' in entry: + return entry['description'] + elif 'value' in entry: + return entry['value'] + elif 'entries' in entry or 'nestedStats' in entry and 'entries' in entry['nestedStats']: + if 'entries' in entry: + entries = entry['entries'] + else: + entries = entry['nestedStats']['entries'] + result = None + + for name in entries: + entry = entries[name] + if 'https://localhost' in name: + name = name.split('/') + name = name[-1] + if result and isinstance(result, list): + result.append(parseStats(entry)) + elif result and isinstance(result, dict): + result[name] = parseStats(entry) + else: + try: + int(name) + result = list() + result.append(parseStats(entry)) + except ValueError: + result = dict() + result[name] = parseStats(entry) + else: + if '.' in name: + names = name.split('.') + key = names[0] + value = names[1] + if not result[key]: + result[key] = {} + result[key][value] = parseStats(entry) + else: + if result and isinstance(result, list): + result.append(parseStats(entry)) + elif result and isinstance(result, dict): + result[name] = parseStats(entry) + else: + try: + int(name) + result = list() + result.append(parseStats(entry)) + except ValueError: + result = dict() + result[name] = parseStats(entry) + return result + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.kwargs = kwargs + + def exec_module(self): + start = datetime.datetime.now().isoformat() + version = bigiq_version(self.client) + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + send_teem(start, self.client, self.module, version) + return results + + +class Parameters(AnsibleF5Parameters): + @property + def gather_subset(self): + if isinstance(self._values['gather_subset'], string_types): + self._values['gather_subset'] = [self._values['gather_subset']] + elif not isinstance(self._values['gather_subset'], list): + raise F5ModuleError( + "The specified gather_subset must be a list." + ) + tmp = list(set(self._values['gather_subset'])) + tmp.sort() + self._values['gather_subset'] = tmp + + return self._values['gather_subset'] + + +class BaseParameters(Parameters): + @property + def enabled(self): + return flatten_boolean(self._values['enabled']) + + @property + def disabled(self): + return flatten_boolean(self._values['disabled']) + + def _remove_internal_keywords(self, resource): + resource.pop('kind', None) + resource.pop('generation', None) + resource.pop('selfLink', None) + resource.pop('isSubcollection', None) + resource.pop('fullPath', None) + + def to_return(self): + result = {} + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + + +class ApplicationsParameters(BaseParameters): + api_map = { + 'protectionMode': 'protection_mode', + 'transactionsPerSecond': 'transactions_per_second', + 'newConnections': 'new_connections', + 'responseTime': 'response_time', + 'activeAlerts': 'active_alerts', + 'badTraffic': 'bad_traffic', + 'enhancedAnalytics': 'enhanced_analytics', + 'badTrafficGrowth': 'bad_traffic_growth' + } + + returnables = [ + 'protection_mode', + 'id', + 'name', + 'status', + 'transactions_per_second', + 'connections', + 'new_connections', + 'response_time', + 'health', + 'active_alerts', + 'bad_traffic', + 'enhanced_analytics', + 'bad_traffic_growth', + ] + + @property + def enhanced_analytics(self): + return flatten_boolean(self._values['enhanced_analytics']) + + @property + def bad_traffic_growth(self): + return flatten_boolean(self._values['bad_traffic_growth']) + + +class ApplicationsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(ApplicationsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(applications=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['name']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = ApplicationsParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/ap/query/v1/tenants/default/reports/AllApplicationsList".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + try: + return response['result']['items'] + except KeyError: + return [] + + +class ManagedDevicesParameters(BaseParameters): + api_map = { + 'deviceUri': 'device_uri', + 'groupName': 'group_name', + 'httpsPort': 'https_port', + 'isClustered': 'is_clustered', + 'isLicenseExpired': 'is_license_expired', + 'isVirtual': 'is_virtual', + 'machineId': 'machine_id', + 'managementAddress': 'management_address', + 'mcpDeviceName': 'mcp_device_name', + 'restFrameworkVersion': 'rest_framework_version', + 'selfLink': 'self_link', + 'trustDomainGuid': 'trust_domain_guid', + } + + returnables = [ + 'address', + 'build', + 'device_uri', + 'edition', + 'group_name', + 'hostname', + 'https_port', + 'is_clustered', + 'is_license_expired', + 'is_virtual', + 'machine_id', + 'management_address', + 'mcp_device_name', + 'product', + 'rest_framework_version', + 'self_link', + 'slots', + 'state', + 'tags', + 'trust_domain_guid', + 'uuid', + 'version', + ] + + @property + def slots(self): + result = [] + if self._values['slots'] is None: + return None + for x in self._values['slots']: + x['is_active'] = flatten_boolean(x.pop('isActive', False)) + result.append(x) + return result + + @property + def tags(self): + if self._values['tags'] is None: + return None + result = dict((x['name'], x['value']) for x in self._values['tags']) + return result + + @property + def https_port(self): + return int(self._values['https_port']) + + @property + def is_clustered(self): + return flatten_boolean(self._values['is_clustered']) + + @property + def is_license_expired(self): + return flatten_boolean(self._values['is_license_expired']) + + @property + def is_virtual(self): + return flatten_boolean(self._values['is_virtual']) + + +class ManagedDevicesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(ManagedDevicesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(managed_devices=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['hostname']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = ManagedDevicesParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-bigip-allBigIpDevices/devices".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + if 'items' not in response: + return [] + result = response['items'] + return result + + +class PurchasedPoolLicensesParameters(BaseParameters): + api_map = { + 'baseRegKey': 'base_reg_key', + 'freeDeviceLicenses': 'free_device_licenses', + 'licenseState': 'license_state', + 'totalDeviceLicenses': 'total_device_licenses', + } + + returnables = [ + 'base_reg_key', + 'dossier', + 'free_device_licenses', + 'name', + 'state', + 'total_device_licenses', + 'uuid', + + # license_state facts + 'vendor', + 'licensed_date_time', + 'licensed_version', + 'evaluation_start_date_time', + 'evaluation_end_date_time', + 'license_end_date_time', + 'license_start_date_time', + 'registration_key', + ] + + @property + def registration_key(self): + try: + return self._values['license_state']['registrationKey'] + except KeyError: + return None + + @property + def license_start_date_time(self): + try: + return self._values['license_state']['licenseStartDateTime'] + except KeyError: + return None + + @property + def license_end_date_time(self): + try: + return self._values['license_state']['licenseEndDateTime'] + except KeyError: + return None + + @property + def evaluation_end_date_time(self): + try: + return self._values['license_state']['evaluationEndDateTime'] + except KeyError: + return None + + @property + def evaluation_start_date_time(self): + try: + return self._values['license_state']['evaluationStartDateTime'] + except KeyError: + return None + + @property + def licensed_version(self): + try: + return self._values['license_state']['licensedVersion'] + except KeyError: + return None + + @property + def licensed_date_time(self): + try: + return self._values['license_state']['licensedDateTime'] + except KeyError: + return None + + @property + def vendor(self): + try: + return self._values['license_state']['vendor'] + except KeyError: + return None + + +class PurchasedPoolLicensesFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(PurchasedPoolLicensesFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(purchased_pool_licenses=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['name']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = PurchasedPoolLicensesParameters(params=resource) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/purchased-pool/licenses".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + try: + return response['items'] + except KeyError: + return [] + + +class RegkeyPoolsParameters(BaseParameters): + api_map = { + + } + + returnables = [ + 'name', + 'id', + 'offerings', + 'total_offerings', + ] + + +class RegkeyPoolsOfferingParameters(BaseParameters): + api_map = { + 'regKey': 'registration_key', + 'licenseState': 'license_state', + 'status': 'state', + } + + returnables = [ + 'name', + 'dossier', + 'state', + + # license_state facts + 'licensed_date_time', + 'licensed_version', + 'evaluation_start_date_time', + 'evaluation_end_date_time', + 'license_end_date_time', + 'license_start_date_time', + 'registration_key', + ] + + @property + def registration_key(self): + try: + return self._values['license_state']['registrationKey'] + except KeyError: + return None + + @property + def license_start_date_time(self): + try: + return self._values['license_state']['licenseStartDateTime'] + except KeyError: + return None + + @property + def license_end_date_time(self): + try: + return self._values['license_state']['licenseEndDateTime'] + except KeyError: + return None + + @property + def evaluation_end_date_time(self): + try: + return self._values['license_state']['evaluationEndDateTime'] + except KeyError: + return None + + @property + def evaluation_start_date_time(self): + try: + return self._values['license_state']['evaluationStartDateTime'] + except KeyError: + return None + + @property + def licensed_version(self): + try: + return self._values['license_state']['licensedVersion'] + except KeyError: + return None + + @property + def licensed_date_time(self): + try: + return self._values['license_state']['licensedDateTime'] + except KeyError: + return None + + @property + def vendor(self): + try: + return self._values['license_state']['vendor'] + except KeyError: + return None + + +class RegkeyPoolsFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(RegkeyPoolsFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(regkey_pools=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['name']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + params = RegkeyPoolsParameters(params=resource) + offerings = self.read_offerings_from_device(resource['id']) + params.update({'total_offerings': len(offerings)}) + for offering in offerings: + params2 = RegkeyPoolsOfferingParameters(params=offering) + params.update({'offerings': params2.to_return()}) + results.append(params) + return results + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + try: + return response['items'] + except KeyError: + return [] + + def read_offerings_from_device(self, license): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings".format( + self.client.provider['server'], + self.client.provider['server_port'], + license, + ) + 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) + try: + return response['items'] + except KeyError: + return [] + + +class SystemInfoParameters(BaseParameters): + api_map = { + 'isSystemSetup': 'is_system_setup', + 'isAdminPasswordChanged': 'is_admin_password_changed', + 'isRootPasswordChanged': 'is_root_password_changed' + } + + returnables = [ + 'base_mac_address', + 'chassis_serial', + 'hardware_information', + 'host_board_part_revision', + 'host_board_serial', + 'is_admin_password_changed', + 'is_root_password_changed', + 'is_system_setup', + 'marketing_name', + 'package_edition', + 'package_version', + 'platform', + 'product_build', + 'product_build_date', + 'product_built', + 'product_changelist', + 'product_code', + 'product_information', + 'product_jobid', + 'product_version', + 'switch_board_part_revision', + 'switch_board_serial', + 'time', + 'uptime', + ] + + @property + def is_admin_password_changed(self): + return flatten_boolean(self._values['is_admin_password_changed']) + + @property + def is_root_password_changed(self): + return flatten_boolean(self._values['is_root_password_changed']) + + @property + def is_system_setup(self): + if self._values['is_system_setup'] is None: + return 'no' + return flatten_boolean(self._values['is_system_setup']) + + @property + def chassis_serial(self): + if self._values['system-info'] is None: + return None + + # Yes, this is still called "bigip" even though this is querying the BIG-IQ + # product. This is likely due to BIG-IQ inheriting TMOS. + if 'bigipChassisSerialNum' not in self._values['system-info'][0]: + return None + return self._values['system-info'][0]['bigipChassisSerialNum'] + + @property + def switch_board_serial(self): + if self._values['system-info'] is None: + return None + if 'switchBoardSerialNum' not in self._values['system-info'][0]: + return None + if self._values['system-info'][0]['switchBoardSerialNum'].strip() == '': + return None + return self._values['system-info'][0]['switchBoardSerialNum'] + + @property + def switch_board_part_revision(self): + if self._values['system-info'] is None: + return None + if 'switchBoardPartRevNum' not in self._values['system-info'][0]: + return None + if self._values['system-info'][0]['switchBoardPartRevNum'].strip() == '': + return None + return self._values['system-info'][0]['switchBoardPartRevNum'] + + @property + def platform(self): + if self._values['system-info'] is None: + return None + return self._values['system-info'][0]['platform'] + + @property + def host_board_serial(self): + if self._values['system-info'] is None: + return None + if 'hostBoardSerialNum' not in self._values['system-info'][0]: + return None + if self._values['system-info'][0]['hostBoardSerialNum'].strip() == '': + return None + return self._values['system-info'][0]['hostBoardSerialNum'] + + @property + def host_board_part_revision(self): + if self._values['system-info'] is None: + return None + if 'hostBoardPartRevNum' not in self._values['system-info'][0]: + return None + if self._values['system-info'][0]['hostBoardPartRevNum'].strip() == '': + return None + return self._values['system-info'][0]['hostBoardPartRevNum'] + + @property + def package_edition(self): + return self._values['Edition'] + + @property + def package_version(self): + return 'Build {0} - {1}'.format(self._values['Build'], self._values['Date']) + + @property + def product_build(self): + return self._values['Build'] + + @property + def product_build_date(self): + return self._values['Date'] + + @property + def product_built(self): + if 'version_info' not in self._values: + return None + if 'Built' in self._values['version_info']: + return int(self._values['version_info']['Built']) + + @property + def product_changelist(self): + if 'version_info' not in self._values: + return None + if 'Changelist' in self._values['version_info']: + return int(self._values['version_info']['Changelist']) + + @property + def product_jobid(self): + if 'version_info' not in self._values: + return None + if 'JobID' in self._values['version_info']: + return int(self._values['version_info']['JobID']) + + @property + def product_code(self): + return self._values['Product'] + + @property + def product_version(self): + return self._values['Version'] + + @property + def hardware_information(self): + if self._values['hardware-version'] is None: + return None + self._transform_name_attribute(self._values['hardware-version']) + result = [v for k, v in iteritems(self._values['hardware-version'])] + return result + + def _transform_name_attribute(self, entry): + if isinstance(entry, dict): + tmp = copy.deepcopy(entry) + for k, v in iteritems(tmp): + if k == 'tmName': + entry['name'] = entry.pop('tmName') + self._transform_name_attribute(v) + elif isinstance(entry, list): + for k in entry: + self._transform_name_attribute(k) + else: + return + + @property + def time(self): + if self._values['fullDate'] is None: + return None + date = datetime.datetime.strptime(self._values['fullDate'], "%Y-%m-%dT%H:%M:%SZ") + result = dict( + day=date.day, + hour=date.hour, + minute=date.minute, + month=date.month, + second=date.second, + year=date.year + ) + return result + + @property + def marketing_name(self): + if self._values['platform'] is None: + return None + return self._values['platform'][0]['marketingName'] + + @property + def base_mac_address(self): + if self._values['platform'] is None: + return None + return self._values['platform'][0]['baseMac'] + + +class SystemInfoFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(SystemInfoFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(system_info=facts) + return result + + def _exec_module(self): + facts = self.read_facts() + results = facts.to_return() + return results + + def read_facts(self): + collection = self.read_collection_from_device() + params = SystemInfoParameters(params=collection) + return params + + def read_collection_from_device(self): + result = dict() + tmp = self.read_hardware_info_from_device() + if tmp: + result.update(tmp) + + tmp = self.read_system_setup_from_device() + if tmp: + result.update(tmp) + + tmp = self.read_clock_info_from_device() + if tmp: + result.update(tmp) + + tmp = self.read_version_info_from_device() + if tmp: + result.update(tmp) + + if Version(bigiq_version(self.client)) < Version('7.0.0'): + tmp = self.read_uptime_info_from_device() + if tmp: + result.update(tmp) + + tmp = self.read_version_file_info_from_device() + if tmp: + result.update(tmp) + + return result + + def read_system_setup_from_device(self): + uri = "https://{0}:{1}/mgmt/shared/system/setup".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + + return response + + def read_version_file_info_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "cat /VERSION"' + ) + resp = self.client.api.post(uri, json=args) + 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) + try: + pattern = r'^(?P(Product|Build|Sequence|BaseBuild|Edition|Date|Built|Changelist|JobID))\:(?P.*)' + result = response['commandResult'].strip() + except KeyError: + return None + + if 'No such file or directory' in result: + return None + + lines = response['commandResult'].split("\n") + result = dict() + for line in lines: + if not line: + continue + matches = re.match(pattern, line) + if matches: + result[matches.group('key')] = matches.group('value').strip() + + if result: + return dict( + version_info=result + ) + + def read_uptime_info_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "cat /proc/uptime"' + ) + resp = self.client.api.post(uri, json=args) + 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) + try: + parts = response['commandResult'].strip().split(' ') + return dict( + uptime=math.floor(float(parts[0])) + ) + except KeyError: + pass + + def read_hardware_info_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/hardware".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = parseStats(response) + return result + + def read_clock_info_from_device(self): + """Parses clock info from the REST API + + The clock stat returned from the REST API (at the time of 13.1.0.7) + is similar to the following. + + { + "kind": "tm:sys:clock:clockstats", + "selfLink": "https://localhost/mgmt/tm/sys/clock?ver=13.1.0.4", + "entries": { + "https://localhost/mgmt/tm/sys/clock/0": { + "nestedStats": { + "entries": { + "fullDate": { + "description": "2018-06-05T13:38:33Z" + } + } + } + } + } + } + + Parsing this data using the ``parseStats`` method, yields a list of + the clock stats in a format resembling that below. + + [{'fullDate': '2018-06-05T13:41:05Z'}] + + Therefore, this method cherry-picks the first entry from this list + and returns it. There can be no other items in this list. + + Returns: + A dict mapping keys to the corresponding clock stats. For + example: + + {'fullDate': '2018-06-05T13:41:05Z'} + + There should never not be a clock stat, unless by chance it + is removed from the API in the future, or changed to a different + API endpoint. + + Raises: + F5ModuleError: A non-successful HTTP code was returned or a JSON + response was not found. + """ + uri = "https://{0}:{1}/mgmt/tm/sys/clock".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = parseStats(response) + if result is None: + return None + return result[0] + + def read_version_info_from_device(self): + """Parses version info from the REST API + + The version stat returned from the REST API (at the time of 13.1.0.7) + is similar to the following. + + { + "kind": "tm:sys:version:versionstats", + "selfLink": "https://localhost/mgmt/tm/sys/version?ver=13.1.0.4", + "entries": { + "https://localhost/mgmt/tm/sys/version/0": { + "nestedStats": { + "entries": { + "Build": { + "description": "0.0.6" + }, + "Date": { + "description": "Tue Mar 13 20:10:42 PDT 2018" + }, + "Edition": { + "description": "Point Release 4" + }, + "Product": { + "description": "BIG-IP" + }, + "Title": { + "description": "Main Package" + }, + "Version": { + "description": "13.1.0.4" + } + } + } + } + } + } + + Parsing this data using the ``parseStats`` method, yields a list of + the clock stats in a format resembling that below. + + [{'Build': '0.0.6', 'Date': 'Tue Mar 13 20:10:42 PDT 2018', + 'Edition': 'Point Release 4', 'Product': 'BIG-IP', 'Title': 'Main Package', + 'Version': '13.1.0.4'}] + + Therefore, this method cherry-picks the first entry from this list + and returns it. There can be no other items in this list. + + Returns: + A dict mapping keys to the corresponding clock stats. For + example: + + {'Build': '0.0.6', 'Date': 'Tue Mar 13 20:10:42 PDT 2018', + 'Edition': 'Point Release 4', 'Product': 'BIG-IP', 'Title': 'Main Package', + 'Version': '13.1.0.4'} + + There should never not be a version stat, unless by chance it + is removed from the API in the future, or changed to a different + API endpoint. + + Raises: + F5ModuleError: A non-successful HTTP code was returned or a JSON + response was not found. + """ + uri = "https://{0}:{1}/mgmt/tm/sys/version".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + result = parseStats(response) + if result is None: + return None + return result[0] + + +class VlansParameters(BaseParameters): + api_map = { + 'autoLasthop': 'auto_lasthop', + 'cmpHash': 'cmp_hash_algorithm', + 'failsafeAction': 'failsafe_action', + 'failsafe': 'failsafe_enabled', + 'failsafeTimeout': 'failsafe_timeout', + 'ifIndex': 'if_index', + 'learning': 'learning_mode', + 'interfacesReference': 'interfaces', + 'sourceChecking': 'source_check_enabled', + 'fullPath': 'full_path' + } + + returnables = [ + 'full_path', + 'name', + 'auto_lasthop', + 'cmp_hash_algorithm', + 'description', + 'failsafe_action', + 'failsafe_enabled', + 'failsafe_timeout', + 'if_index', + 'learning_mode', + 'interfaces', + 'mtu', + 'sflow_poll_interval', + 'sflow_poll_interval_global', + 'sflow_sampling_rate', + 'sflow_sampling_rate_global', + 'source_check_enabled', + 'true_mac_address', + 'tag', + ] + + @property + def interfaces(self): + if self._values['interfaces'] is None: + return None + if 'items' not in self._values['interfaces']: + return None + result = [] + for item in self._values['interfaces']['items']: + tmp = dict( + name=item['name'], + full_path=item['fullPath'] + ) + if 'tagged' in item: + tmp['tagged'] = 'yes' + else: + tmp['tagged'] = 'no' + result.append(tmp) + return result + + @property + def sflow_poll_interval(self): + return int(self._values['sflow']['pollInterval']) + + @property + def sflow_poll_interval_global(self): + return flatten_boolean(self._values['sflow']['pollIntervalGlobal']) + + @property + def sflow_sampling_rate(self): + return int(self._values['sflow']['samplingRate']) + + @property + def sflow_sampling_rate_global(self): + return flatten_boolean(self._values['sflow']['samplingRateGlobal']) + + @property + def source_check_state(self): + return flatten_boolean(self._values['source_check_state']) + + @property + def true_mac_address(self): + if self._values['stats']['macTrue'] in [None, 'none']: + return None + return self._values['stats']['macTrue'] + + @property + def tag(self): + return self._values['stats']['id'] + + @property + def failsafe_enabled(self): + return flatten_boolean(self._values['failsafe_enabled']) + + +class VlansFactManager(BaseManager): + def __init__(self, *args, **kwargs): + self.client = kwargs.get('client', None) + self.module = kwargs.get('module', None) + super(VlansFactManager, self).__init__(**kwargs) + + def exec_module(self): + facts = self._exec_module() + result = dict(vlans=facts) + return result + + def _exec_module(self): + results = [] + facts = self.read_facts() + for item in facts: + attrs = item.to_return() + results.append(attrs) + results = sorted(results, key=lambda k: k['full_path']) + return results + + def read_facts(self): + results = [] + collection = self.read_collection_from_device() + for resource in collection: + resource.update(self.read_stats(resource['fullPath'])) + params = VlansParameters(params=resource) + results.append(params) + return results + + def read_stats(self, resource): + uri = "https://{0}:{1}/mgmt/tm/net/vlan/{2}/stats".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(name=resource) + + ) + 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) + result = parseStats(response) + return result + + def read_collection_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/net/vlan/?expandSubcollections=true".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + if 'items' not in response: + return [] + result = response['items'] + return result + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.kwargs = kwargs + self.want = Parameters(params=self.module.params) + self.managers = { + 'applications': dict( + manager=ApplicationsFactManager, + client=F5RestClient, + ), + 'managed-devices': dict( + manager=ManagedDevicesFactManager, + client=F5RestClient, + ), + 'purchased-pool-licenses': dict( + manager=PurchasedPoolLicensesFactManager, + client=F5RestClient, + ), + 'regkey-pools': dict( + manager=RegkeyPoolsFactManager, + client=F5RestClient, + ), + 'system-info': dict( + manager=SystemInfoFactManager, + client=F5RestClient, + ), + 'vlans': dict( + manager=VlansFactManager, + client=F5RestClient, + ), + } + + def exec_module(self): + self.handle_all_keyword() + res = self.check_valid_gather_subset(self.want.gather_subset) + if res: + invalid = ','.join(res) + raise F5ModuleError( + "The specified 'gather_subset' options are invalid: {0}".format(invalid) + ) + result = self.filter_excluded_facts() + + managers = [] + for name in result: + manager = self.get_manager(name) + if manager: + managers.append(manager) + + if not managers: + result = dict( + changed=False + ) + return result + + result = self.execute_managers(managers) + if result: + result['changed'] = True + else: + result['changed'] = False + return result + + def filter_excluded_facts(self): + # Remove the excluded entries from the list of possible facts + exclude = [x[1:] for x in self.want.gather_subset if x[0] == '!'] + include = [x for x in self.want.gather_subset if x[0] != '!'] + result = [x for x in include if x not in exclude] + return result + + def handle_all_keyword(self): + if 'all' not in self.want.gather_subset: + return + managers = list(self.managers.keys()) + self.want.gather_subset + managers.remove('all') + self.want.update({'gather_subset': managers}) + + def check_valid_gather_subset(self, includes): + """Check that the specified subset is valid + + The ``gather_subset`` parameter is specified as a "raw" field which means that + any Python type could technically be provided + + :param includes: + :return: + """ + keys = self.managers.keys() + result = [] + for x in includes: + if x not in keys: + if x[0] == '!': + if x[1:] not in keys: + result.append(x) + else: + result.append(x) + return result + + def execute_managers(self, managers): + results = dict() + for manager in managers: + result = manager.exec_module() + results.update(result) + return results + + def get_manager(self, which): + result = {} + info = self.managers.get(which, None) + if not info: + return result + kwargs = dict() + kwargs.update(self.kwargs) + + manager = info.get('manager', None) + client = info.get('client', None) + kwargs['client'] = client(**self.module.params) + result = manager(**kwargs) + return result + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + gather_subset=dict( + type='list', + elements='str', + required=True, + choices=[ + # Meta choices + 'all', + + # Non-meta choices + 'applications', + 'managed-devices', + 'purchased-pool-licenses', + 'regkey-pools', + 'system-info', + 'vlans', + + # Negations of meta choices + '!all', + + # Negations of non-meta-choices + '!applications', + '!managed-devices', + '!purchased-pool-licenses', + '!regkey-pools', + '!system-info', + '!vlans', + ] + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode + ) + + if not HAS_PACKAGING: + module.fail_json( + msg=missing_required_lib('packaging'), + exception=PACKAGING_IMPORT_ERROR + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + + ansible_facts = dict() + + for key, value in iteritems(results): + key = 'ansible_net_%s' % key + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts, **results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_license.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_license.py new file mode 100644 index 00000000..5475afa9 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_license.py @@ -0,0 +1,500 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2022, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_regkey_license +short_description: Manages licenses in a BIG-IQ registration key pool +description: + - Manages licenses in a BIG-IQ registration key pool. +version_added: "1.0.0" +options: + regkey_pool: + description: + - The registration key pool in which you want to place the license. + - You must give your registration pools unique names. While + BIG-IQ does not require this, this module does. If you do not, + the behavior of the module is undefined and you may end up putting + licenses in the wrong registration key pool. + type: str + required: True + license_key: + description: + - The license key to put in the pool. + type: str + required: True + addon_keys: + description: + - The addon keys to put in the pool. + type: list + elements: str + version_added: "1.16.0" + description: + description: + - Description of the license. + type: str + accept_eula: + description: + - A key that signifies you accept the F5 EULA for this license. + - A copy of the EULA can be found here https://askf5.f5.com/csp/article/K12902 + - This is required when C(state) is C(present). + type: bool + state: + description: + - The state of the regkey license in the pool on the system. + - When C(present), guarantees the license exists in the pool. + - When C(absent), removes the license from the pool. + type: str + choices: + - absent + - present + default: present +requirements: + - BIG-IQ >= 5.3.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) + - Wojciech Wypior(@wojtek0806) +''' + +EXAMPLES = r''' +- name: Add a registration key license to a pool + bigiq_regkey_license: + regkey_pool: foo-pool + license_key: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX + accept_eula: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Add a registration key license with addon keys to a pool + bigiq_regkey_license: + regkey_pool: foo-pool + license_key: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX + addon_keys: + - YYYY-YYY-YYY + - ZZZZ-ZZZ-ZZZ + accept_eula: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Remove a registration key license from a pool + bigiq_regkey_license: + regkey_pool: foo-pool + license_key: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX + state: absent + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +description: + description: The new description of the license key. + returned: changed + type: str + sample: My license for BIG-IP 1 +''' + +import time +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'regKey': 'license_key', + 'addOnKeys': 'addon_keys' + } + + api_attributes = [ + 'regKey', 'description', 'addOnKeys' + ] + + returnables = [ + 'description' + ] + + updatables = [ + 'description' + ] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def regkey_pool_uuid(self): + if self._values['regkey_pool_uuid']: + return self._values['regkey_pool_uuid'] + collection = self.read_current_from_device() + resource = next((x for x in collection if x.name == self.regkey_pool), None) + if resource is None: + raise F5ModuleError("Could not find the specified regkey pool.") + self._values['regkey_pool_uuid'] = resource.id + return resource.id + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + if 'items' not in response: + return [] + result = [ApiParameters(params=r) for r in response['items']] + return result + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(client=self.client, params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.regkey_pool_uuid, + self.want.license_key + ) + 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 update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + if self.want.accept_eula is False: + raise F5ModuleError( + "To add a license, you must accept its EULA. Please see the module documentation for a link to this." + ) + self.create_on_device() + return True + + def create_on_device(self): + params = self.want.api_params() + params['name'] = self.want.name + params['status'] = 'ACTIVATING_AUTOMATIC' + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.regkey_pool_uuid, + ) + 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) + + for x in range(60): + resource = self.read_current_from_device() + if resource.status == 'READY': + break + elif resource.status == 'ACTIVATING_AUTOMATIC_NEED_EULA_ACCEPT': + params = dict( + status='ACTIVATING_AUTOMATIC_EULA_ACCEPTED', + eulaText=resource.eulaText + ) + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.regkey_pool_uuid, + self.want.license_key + ) + 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) + elif resource.status == 'ACTIVATION_FAILED': + raise F5ModuleError(str(resource.message)) + time.sleep(1) + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.regkey_pool_uuid, + self.want.license_key + ) + 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 absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.regkey_pool_uuid, + self.want.license_key + ) + resp = self.client.api.delete(uri) + if resp.status == 200: + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.regkey_pool_uuid, + self.want.license_key + ) + 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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + regkey_pool=dict(required=True), + license_key=dict(required=True, no_log=True), + addon_keys=dict(type='list', elements='str', no_log=True), + description=dict(), + accept_eula=dict(type='bool'), + state=dict( + default='present', + choices=['present', 'absent'] + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_if = [ + ['state', 'present', ['accept_eula']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_license_assignment.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_license_assignment.py new file mode 100644 index 00000000..e612212b --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_license_assignment.py @@ -0,0 +1,624 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_regkey_license_assignment +short_description: Manage regkey license assignment on BIG-IPs from a BIG-IQ +description: + - Manages the assignment of regkey licenses on a BIG-IQ. Assignment means + the license is assigned to a BIG-IP, or it needs to be assigned to a BIG-IP. + Additionally, this module supports revoking the assignments from BIG-IP devices. +version_added: "1.0.0" +options: + pool: + description: + - The registration key pool to use. + type: str + required: True + key: + description: + - The registration key you want to assign from the pool. + type: str + required: True + device: + description: + - When C(managed) is C(no), specifies the address, or hostname, where the BIG-IQ + can reach the remote device to register. + - When C(managed) is C(yes), specifies the managed device, or device UUID, that + you want to register. + - If C(managed) is C(yes), it is very important you do not have more than + one device with the same name. BIG-IQ internally recognizes devices by their ID, + and therefore, this module cannot guarantee the correct device will be + registered. The device returned is the device that is used. + type: str + required: True + managed: + description: + - Whether the specified device is a managed or un-managed device. + - When C(state) is C(present), this parameter is required. + type: bool + device_port: + description: + - Specifies the port of the remote device to connect to. + - If this parameter is not specified, the default is C(443). + type: int + default: 443 + device_username: + description: + - The username used to connect to the remote device. + - This username should be one that has sufficient privileges on the remote device + to do licensing. Usually this is the C(Administrator) role. + - When C(managed) is C(no), this parameter is required. + type: str + device_password: + description: + - The password of the C(device_username). + - When C(managed) is C(no), this parameter is required. + type: str + state: + description: + - When C(present), ensures the device is assigned the specified license. + - When C(absent), ensures the license is revoked from the remote device and freed + on the BIG-IQ. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Register an unmanaged device + bigiq_regkey_license_assignment: + pool: my-regkey-pool + key: XXXX-XXXX-XXXX-XXXX-XXXX + device: 1.1.1.1 + managed: no + device_username: admin + device_password: secret + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Register a managed device, by name + bigiq_regkey_license_assignment: + pool: my-regkey-pool + key: XXXX-XXXX-XXXX-XXXX-XXXX + device: bigi1.foo.com + managed: yes + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Register a managed device, by UUID + bigiq_regkey_license_assignment: + pool: my-regkey-pool + key: XXXX-XXXX-XXXX-XXXX-XXXX + device: 7141a063-7cf8-423f-9829-9d40599fa3e0 + managed: yes + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import re +import time +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'deviceReference': 'device_reference', + 'deviceAddress': 'device_address', + 'httpsPort': 'device_port' + } + + api_attributes = [ + 'deviceReference', 'deviceAddress', 'httpsPort', 'managed' + ] + + returnables = [ + 'device_address', 'device_reference', 'device_username', 'device_password', + 'device_port', 'managed' + ] + + updatables = [ + 'device_reference', 'device_address', 'device_username', 'device_password', + 'device_port', 'managed' + ] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def device_password(self): + if self._values['device_password'] is None: + return None + return self._values['device_password'] + + @property + def device_username(self): + if self._values['device_username'] is None: + return None + return self._values['device_username'] + + @property + def device_address(self): + if self.device_is_address: + return self._values['device'] + + @property + def device_port(self): + if self._values['device_port'] is None: + return None + return int(self._values['device_port']) + + @property + def device_is_address(self): + if is_valid_ip(self.device): + return True + return False + + @property + def device_is_id(self): + pattern = r'[A-Za-z0-9]{8}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{12}' + if re.match(pattern, self.device): + return True + return False + + @property + def device_is_name(self): + if not self.device_is_address and not self.device_is_id: + return True + return False + + @property + def device_reference(self): + if not self.managed: + return None + if self.device_is_address: + # This range lookup is how you do lookups for single IP addresses. Weird. + filter = "address+eq+'{0}...{0}'".format(self.device) + elif self.device_is_name: + filter = "hostname+eq+'{0}'".format(self.device) + elif self.device_is_id: + filter = "uuid+eq+'{0}'".format(self.device) + else: + raise F5ModuleError( + "Unknown device format '{0}'".format(self.device) + ) + + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-bigip-allBigIpDevices/devices/" \ + "?$filter={2}&$top=1".format(self.client.provider['server'], + self.client.provider['server_port'], filter) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No device with the specified address was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + id = response['items'][0]['uuid'] + result = dict( + link='https://localhost/mgmt/shared/resolver/device-groups/cm-bigip-allBigIpDevices/devices/{0}'.format(id) + ) + return result + + @property + def pool_id(self): + filter = "(name%20eq%20'{0}')".format(self.pool) + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses?$filter={2}&$top=1'.format( + self.client.provider['server'], + self.client.provider['server_port'], + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No pool with the specified name was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['items'][0]['id'] + + @property + def member_id(self): + if self.device_is_address: + # This range lookup is how you do lookups for single IP addresses. Weird. + filter = "deviceAddress+eq+'{0}...{0}'".format(self.device) + elif self.device_is_name: + filter = "deviceName+eq+'{0}'".format(self.device) + elif self.device_is_id: + filter = "deviceMachineId+eq+'{0}'".format(self.device) + else: + raise F5ModuleError( + "Unknown device format '{0}'".format(self.device) + ) + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}/members/' \ + '?$filter={4}'.format(self.client.provider['server'], self.client.provider['server_port'], + self.pool_id, self.key, filter) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = response['items'][0]['id'] + return result + + +class Changes(Parameters): + pass + + +class UsableChanges(Changes): + @property + def device_port(self): + if self._values['managed']: + return None + return self._values['device_port'] + + @property + def device_username(self): + if self._values['managed']: + return None + return self._values['device_username'] + + @property + def device_password(self): + if self._values['managed']: + return None + return self._values['device_password'] + + @property + def device_reference(self): + if not self._values['managed']: + return None + return self._values['device_reference'] + + @property + def device_address(self): + if self._values['managed']: + return None + return self._values['device_address'] + + @property + def managed(self): + return None + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params, client=self.client) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + return self.create() + + def exists(self): + if self.want.member_id is None: + return False + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}/members/{4}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.pool_id, + self.want.key, + self.want.member_id + ) + resp = self.client.api.get(uri) + if resp.status == 200: + return True + return False + + def remove(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + # Artificial sleeping to wait for remote licensing (on BIG-IP) to complete + # + # This should be something that BIG-IQ can do natively in 6.1-ish time. + time.sleep(60) + return True + + def create(self): + self._set_changed_options() + if not self.want.managed: + if self.want.device_username is None: + raise F5ModuleError( + "You must specify a 'device_username' when working with unmanaged devices." + ) + if self.want.device_password is None: + raise F5ModuleError( + "You must specify a 'device_password' when working with unmanaged devices." + ) + if self.module.check_mode: + return True + self.create_on_device() + if not self.exists(): + raise F5ModuleError( + "Failed to license the remote device." + ) + self.wait_for_device_to_be_licensed() + + # Artificial sleeping to wait for remote licensing (on BIG-IP) to complete + # + # This should be something that BIG-IQ can do natively in 6.1-ish time. + time.sleep(60) + return True + + def create_on_device(self): + params = self.changes.api_params() + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}/members/'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.pool_id, + self.want.key + ) + + if not self.want.managed: + params['username'] = self.want.device_username + params['password'] = self.want.device_password + + 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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def wait_for_device_to_be_licensed(self): + count = 0 + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}/members/{4}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.pool_id, + self.want.key, + self.want.member_id + ) + while count < 3: + 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) + if response['status'] == 'LICENSED': + count += 1 + else: + count = 0 + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}/offerings/{3}/members/{4}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.pool_id, + self.want.key, + self.want.member_id + ) + params = {} + if not self.want.managed: + params.update(self.changes.api_params()) + params['id'] = self.want.member_id + params['username'] = self.want.device_username + params['password'] = self.want.device_password + self.client.api.delete(uri, json=params) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + pool=dict(required=True), + key=dict(required=True, no_log=True), + device=dict(required=True), + managed=dict(type='bool'), + device_port=dict(type='int', default=443), + device_username=dict(no_log=True), + device_password=dict(no_log=True), + state=dict(default='present', choices=['absent', 'present']) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_if = [ + ['state', 'present', ['key', 'managed']], + ['managed', False, ['device', 'device_username', 'device_password']], + ['managed', True, ['device']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_pool.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_pool.py new file mode 100644 index 00000000..f1eb5578 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_regkey_pool.py @@ -0,0 +1,412 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_regkey_pool +short_description: Manages registration key pools on BIG-IQ +description: + - Manages registration key (regkey) pools on a BIG-IQ. These pools function as + a container in which you add lists of registration keys. To add registration + keys, use the C(bigiq_regkey_license) module. +version_added: "1.0.0" +options: + name: + description: + - Specifies the name of the registration key pool. + - You must name your registration pools unique names. While + BIG-IQ does not require this, this module does. If you do not, + the behavior of the module is undefined, and you may end up putting + licenses in the wrong registration key pool. + type: str + required: True + description: + description: + - A description to attach to the pool. + type: str + state: + description: + - The state of the regkey pool on the system. + - When C(present), guarantees the pool exists. + - When C(absent), removes the pool, and the licenses it contains, from the + system. + type: str + choices: + - absent + - present + default: present +requirements: + - BIG-IQ >= 5.3.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create a registration key (regkey) pool to hold individual device licenses + bigiq_regkey_pool: + name: foo-pool + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +description: + description: New description of the regkey pool. + returned: changed + type: str + sample: My description +''' +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + 'description' + ] + + returnables = [ + 'description' + ] + + updatables = [ + 'description' + ] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ModuleParameters(Parameters): + @property + def uuid(self): + """Returns UUID of a given name + + Will search for a given name and return the first one returned to us. If no name, + and therefore no ID, is found, will return the string "none". The string "none" + is returned because if we were to return the None value, it would cause the + license loading code to append a None string to the URI; essentially asking the + remote device for its collection (which we dont want and which would cause the SDK + to return an False error. + + :return: + """ + collection = self.read_current_from_device() + resource = next((x for x in collection if x.name == self._values['name']), None) + if resource: + return resource.id + else: + return "none" + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + 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) + if 'items' not in response: + return [] + result = [ApiParameters(params=r) for r in response['items']] + return result + + +class ApiParameters(Parameters): + @property + def uuid(self): + return self._values['id'] + + +class Changes(Parameters): + pass + + +class ReportableChanges(Changes): + pass + + +class UsableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(client=self.client, params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = Changes(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.uuid, + ) + 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 update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def create_on_device(self): + params = self.want.api_params() + params['name'] = self.want.name + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/".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/cm/device/licensing/pool/regkey/licenses/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.uuid + ) + 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 absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.uuid + ) + resp = self.client.api.delete(uri) + if resp.status == 200: + return True + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/regkey/licenses/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.uuid + ) + 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) + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + state=dict( + default='present', + choices=['absent', 'present'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_utility_license.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_utility_license.py new file mode 100644 index 00000000..d72defb6 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_utility_license.py @@ -0,0 +1,455 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_utility_license +short_description: Manage utility licenses on a BIG-IQ +description: + - Manages utility licenses on a BIG-IQ. Utility licenses are one form of license + that BIG-IQ can distribute. These licenses, unlike regkey licenses, do not require + a pool to be created before creation. Additionally, when assigning them, you assign + by offering instead of key. +version_added: "1.0.0" +options: + license_key: + description: + - The license key to install and activate. + type: str + required: True + accept_eula: + description: + - A key that signifies you accept the F5 EULA for this license. + - A copy of the EULA can be found here https://askf5.f5.com/csp/article/K12902 + - This is required when C(state) is C(present). + type: bool + state: + description: + - The state of the utility license on the system. + - When C(present), guarantees the license exists. + - When C(absent), removes the license from the system. + type: str + choices: + - absent + - present + default: present +requirements: + - BIG-IQ >= 5.3.0 +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Add a utility license to the system + bigiq_utility_license: + license_key: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX + accept_eula: yes + state: present + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost + +- name: Remove a utility license from the system + bigiq_utility_license: + license_key: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX + state: absent + provider: + user: admin + password: secret + server: lb.mydomain.com + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import time +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'regKey': 'license_key' + } + + api_attributes = [ + 'regKey' + ] + + returnables = [ + 'license_key' + ] + + updatables = [ + 'license_key' + ] + + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + raise + return result + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + 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 UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + @property + def license_key(self): + return None + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(client=self.client, params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + else: + return self.create() + + def exists(self): + uri = "https://{0}:{1}/mgmt/cm/device/licensing/pool/utility/licenses/?$filter=regKey+eq+'{2}'".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.license_key + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status == 200 and response['totalItems'] == 0: + return False + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + self.wait_for_removal() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + if self.want.accept_eula is False: + raise F5ModuleError( + "To add a license, you must accept its EULA. Please see the module documentation for a link to this." + ) + self.create_on_device() + self.wait_for_initial_license_activation() + self.wait_for_utility_license_activation() + if not self.exists(): + raise F5ModuleError( + "Failed to activate the license." + ) + return True + + def create_on_device(self): + params = self.changes.api_params() + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/initial-activation'.format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + + params['name'] = self.want.license_key + params['status'] = 'ACTIVATING_AUTOMATIC' + + 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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + + def wait_for_removal(self): + count = 0 + + while count < 3: + if not self.exists(): + count += 1 + else: + count = 0 + time.sleep(1) + + def wait_for_initial_license_activation(self): + count = 0 + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/initial-activation/{2}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.license_key + ) + + while count < 3: + 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) + + if response['status'] == 'READY': + count += 1 + elif response['status'] == 'ACTIVATING_AUTOMATIC_NEED_EULA_ACCEPT': + uri = response['selfLink'].replace( + 'https://localhost', + 'https://{0}:{1}'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + ) + self.client.api.patch(uri, json=dict( + status='ACTIVATING_AUTOMATIC_EULA_ACCEPTED', + eulaText=response['eulaText'] + )) + elif response['status'] == 'ACTIVATION_FAILED': + raise F5ModuleError(str(response['message'])) + else: + count = 0 + time.sleep(1) + + def wait_for_utility_license_activation(self): + count = 0 + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/utility/licenses/{2}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.license_key + ) + + while count < 3: + 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'] in [400, 401]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + + if response['status'] == 'READY': + count += 1 + elif response['status'] == 'ACTIVATION_FAILED': + raise F5ModuleError(str(response['message'])) + else: + count = 0 + time.sleep(1) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/utility/licenses/{2}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.license_key + ) + + resp = self.client.api.delete(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) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + license_key=dict(required=True, no_log=True), + accept_eula=dict(type='bool'), + state=dict( + default='present', + choices=['present', 'absent'] + ), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_if = [ + ['state', 'present', ['accept_eula']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_utility_license_assignment.py b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_utility_license_assignment.py new file mode 100644 index 00000000..f432867a --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/modules/bigiq_utility_license_assignment.py @@ -0,0 +1,639 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: bigiq_utility_license_assignment +short_description: Manage utility license assignment on BIG-IPs from a BIG-IQ +description: + - Manages the assignment of utility licenses on a BIG-IQ. Assignment means + the license is assigned to a BIG-IP, or it needs to be assigned to a BIG-IP. + Additionally, this module supports revoking the assignments from BIG-IP devices. +version_added: "1.0.0" +options: + unit_of_measure: + description: + - Sets the rate at which this license usage is billed. + - Depending on your license, you may have different units of measure + available to you. If a particular unit is not available to you, the module + notifies you at licensing time. + type: str + choices: + - hourly + - daily + - monthly + - yearly + default: hourly + key: + description: + - The registration key from which you want choose an offering. + type: str + required: True + offering: + description: + - Name of the license offering to assign to the device. + type: str + required: True + device: + description: + - When C(managed) is C(no), specifies the address, or hostname, where the BIG-IQ + can reach the remote device to register. + - When C(managed) is C(yes), specifies the managed device, or device UUID, + you want to register. + - If C(managed) is C(yes), it is very important you do not have more than + one device with the same name. BIG-IQ internally recognizes devices by their ID, + and therefore, this module cannot guarantee the correct device will be + registered. The device returned is the device that is used. + type: str + required: True + managed: + description: + - Whether the specified device is a managed or un-managed device. + - When C(state) is C(present), this parameter is required. + type: bool + device_port: + description: + - Specifies the port of the remote device to connect to. + - If this parameter is not specified, the default is C(443). + type: int + default: 443 + device_username: + description: + - The username used to connect to the remote device. + - This username should be one that has sufficient privileges on the remote device + to do licensing. Usually this is the C(Administrator) role. + - When C(managed) is C(no), this parameter is required. + type: str + device_password: + description: + - The password of the C(device_username). + - When C(managed) is C(no), this parameter is required. + type: str + state: + description: + - When C(present), ensures the device is assigned the specified license. + - When C(absent), ensures the license is revokes from the remote device and freed + on the BIG-IQ. + type: str + choices: + - present + - absent + default: present +extends_documentation_fragment: f5networks.f5_modules.f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Register an unmanaged device + bigiq_utility_license_assignment: + key: XXXX-XXXX-XXXX-XXXX-XXXX + offering: F5-BIG-MSP-AFM-10G-LIC + device: 1.1.1.1 + managed: no + device_username: admin + device_password: secret + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Register a managed device, by name + bigiq_utility_license_assignment: + key: XXXX-XXXX-XXXX-XXXX-XXXX + offering: F5-BIG-MSP-AFM-10G-LIC + device: bigi1.foo.com + managed: yes + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Register a managed device, by UUID + bigiq_utility_license_assignment: + key: XXXX-XXXX-XXXX-XXXX-XXXX + offering: F5-BIG-MSP-AFM-10G-LIC + device: 7141a063-7cf8-423f-9829-9d40599fa3e0 + managed: yes + state: present + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +# only common fields returned +''' + +import re +from datetime import datetime + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.bigip import F5RestClient +from ..module_utils.common import ( + F5ModuleError, AnsibleF5Parameters, f5_argument_spec +) +from ..module_utils.icontrol import bigiq_version +from ..module_utils.ipaddress import is_valid_ip +from ..module_utils.teem import send_teem + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'deviceReference': 'device_reference', + 'deviceAddress': 'device_address', + 'httpsPort': 'device_port', + 'unitOfMeasure': 'unit_of_measure' + } + + api_attributes = [ + 'deviceReference', 'deviceAddress', 'httpsPort', 'managed', 'unitOfMeasure' + ] + + returnables = [ + 'device_address', 'device_reference', 'device_username', 'device_password', + 'device_port', 'managed', 'unit_of_measure' + ] + + updatables = [ + 'device_reference', 'device_address', 'device_username', 'device_password', + 'device_port', 'managed', 'unit_of_measure' + ] + + 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 ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + @property + def device_password(self): + if self._values['device_password'] is None: + return None + return self._values['device_password'] + + @property + def device_username(self): + if self._values['device_username'] is None: + return None + return self._values['device_username'] + + @property + def device_address(self): + if self.device_is_address: + return self._values['device'] + + @property + def device_port(self): + if self._values['device_port'] is None: + return None + return int(self._values['device_port']) + + @property + def device_is_address(self): + if is_valid_ip(self.device): + return True + return False + + @property + def device_is_id(self): + pattern = r'[A-Za-z0-9]{8}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{12}' + if re.match(pattern, self.device): + return True + return False + + @property + def device_is_name(self): + if not self.device_is_address and not self.device_is_id: + return True + return False + + @property + def device_reference(self): + if not self.managed: + return None + if self.device_is_address: + # This range lookup is how you do lookups for single IP addresses. Weird. + filter = "address+eq+'{0}...{0}'".format(self.device) + elif self.device_is_name: + filter = "hostname+eq+'{0}'".format(self.device) + elif self.device_is_id: + filter = "uuid+eq+'{0}'".format(self.device) + else: + raise F5ModuleError( + "Unknown device format '{0}'".format(self.device) + ) + + uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-bigip-allBigIpDevices/devices/" \ + "?$filter={2}&$top=1".format(self.client.provider['server'], + self.client.provider['server_port'], filter) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No device with the specified address was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + id = response['items'][0]['uuid'] + result = dict( + link='https://localhost/mgmt/shared/resolver/device-groups/cm-bigip-allBigIpDevices/devices/{0}'.format(id) + ) + return result + + @property + def offering_id(self): + filter = "(name+eq+'{0}')".format(self.offering) + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/utility/licenses/{2}/offerings?$filter={3}&$top=1'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.key, + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if resp.status == 200 and response['totalItems'] == 0: + raise F5ModuleError( + "No offering with the specified name was found." + ) + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + return response['items'][0]['id'] + + @property + def member_id(self): + if self.device_is_address: + # This range lookup is how you do lookups for single IP addresses. Weird. + filter = "deviceAddress+eq+'{0}...{0}'".format(self.device) + elif self.device_is_name: + filter = "deviceName+eq+'{0}'".format(self.device) + elif self.device_is_id: + filter = "deviceMachineId+eq+'{0}'".format(self.device) + else: + raise F5ModuleError( + "Unknown device format '{0}'".format(self.device) + ) + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/utility/licenses/{2}/offerings/{3}/members/?$filter={4}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.key, + self.offering_id, + filter + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if resp.status == 200 and response['totalItems'] == 0: + return None + elif 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp._content) + result = response['items'][0]['id'] + return result + + +class Changes(Parameters): + pass + + +class UsableChanges(Changes): + @property + def device_port(self): + if self._values['managed']: + return None + return self._values['device_port'] + + @property + def device_username(self): + if self._values['managed']: + return None + return self._values['device_username'] + + @property + def device_password(self): + if self._values['managed']: + return None + return self._values['device_password'] + + @property + def device_reference(self): + if not self._values['managed']: + return None + return self._values['device_reference'] + + @property + def device_address(self): + if self._values['managed']: + return None + return self._values['device_address'] + + @property + def managed(self): + return None + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = F5RestClient(**self.module.params) + self.want = ModuleParameters(params=self.module.params, client=self.client) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Changes(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = Changes(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + start = datetime.now().isoformat() + version = bigiq_version(self.client) + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + send_teem(start, self.client, self.module, version) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return False + return self.create() + + def exists(self): + if self.want.member_id is None: + return False + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/utility/licenses/{2}/offerings/{3}/members/{4}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.key, + self.want.offering_id, + self.want.member_id + ) + resp = self.client.api.get(uri) + if resp.status == 200: + return True + return False + + def remove(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if not self.want.managed: + if self.want.device_username is None: + raise F5ModuleError( + "You must specify a 'device_username' when working with unmanaged devices." + ) + if self.want.device_password is None: + raise F5ModuleError( + "You must specify a 'device_password' when working with unmanaged devices." + ) + if self.module.check_mode: + return True + self.create_on_device() + if not self.exists(): + raise F5ModuleError( + "Failed to license the remote device." + ) + self.wait_for_device_to_be_licensed() + return True + + def create_on_device(self): + params = self.changes.api_params() + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/utility/licenses/{2}/offerings/{3}/members/'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.key, + self.want.offering_id, + ) + + if not self.want.managed: + params['username'] = self.want.device_username + params['password'] = self.want.device_password + + 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'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def wait_for_device_to_be_licensed(self): + count = 0 + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/utility/licenses/{2}/offerings/{3}/members/{4}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.key, + self.want.offering_id, + self.want.member_id, + ) + while count < 3: + 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) + if response['status'] == 'LICENSED': + count += 1 + else: + count = 0 + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove_from_device(self): + uri = 'https://{0}:{1}/mgmt/cm/device/licensing/pool/utility/licenses/{2}/offerings/{3}/members/{4}'.format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.key, + self.want.offering_id, + self.want.member_id + ) + params = {} + if not self.want.managed: + params.update(self.changes.api_params()) + params['id'] = self.want.member_id + params['username'] = self.want.device_username + params['password'] = self.want.device_password + self.client.api.delete(uri, json=params) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + offering=dict(required=True), + unit_of_measure=dict( + default='hourly', + choices=[ + 'hourly', 'daily', 'monthly', 'yearly' + ] + ), + key=dict(required=True, no_log=True), + device=dict(required=True), + managed=dict(type='bool'), + device_port=dict(type='int', default=443), + device_username=dict(no_log=True), + device_password=dict(no_log=True), + state=dict(default='present', choices=['absent', 'present']) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_if = [ + ['state', 'present', ['key', 'managed']], + ['managed', False, ['device', 'device_username', 'device_password']], + ['managed', True, ['device']] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + required_if=spec.required_if + ) + + try: + mm = ModuleManager(module=module) + results = mm.exec_module() + module.exit_json(**results) + except F5ModuleError as ex: + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/f5networks/f5_modules/plugins/terminal/bigip.py b/ansible_collections/f5networks/f5_modules/plugins/terminal/bigip.py new file mode 100644 index 00000000..33444631 --- /dev/null +++ b/ansible_collections/f5networks/f5_modules/plugins/terminal/bigip.py @@ -0,0 +1,62 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re + +from ansible.plugins.terminal import TerminalBase +from ansible.errors import AnsibleConnectionFailure + + +class TerminalModule(TerminalBase): + + terminal_stdout_re = [ + re.compile(br"[\r\n]?(?:\([^\)]+\)){,5}(?:>|#)\s*$"), + re.compile(br"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#)\s*$"), + re.compile(br"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$]\s*$"), + re.compile(br"(?:new|confirm) password:") + ] + + terminal_stderr_re = [ + re.compile(br"% ?Error"), + re.compile(br"Syntax Error", re.I), + re.compile(br"% User not present"), + re.compile(br"% ?Bad secret"), + re.compile(br"invalid input", re.I), + re.compile(br"(?:incomplete|ambiguous) command", re.I), + re.compile(br"connection timed out", re.I), + re.compile(br"the new password was not confirmed", re.I), + re.compile(br"[^\r\n]+ not found", re.I), + re.compile(br"'[^']' +returned error code: ?\d+"), + re.compile(br"[^\r\n]\/bin\/(?:ba)?sh") + ] + + def on_open_shell(self): + try: + self._exec_cli_command(b'modify cli preference display-threshold 0 pager disabled') + self._exec_cli_command(b'run /util bash -c "stty cols 1000000" 2> /dev/null') + except AnsibleConnectionFailure as ex: + output = str(ex) + if 'modify: command not found' in output: + try: + self._exec_cli_command(b'tmsh modify cli preference display-threshold 0 pager disabled') + self._exec_cli_command(b'stty cols 1000000 2> /dev/null') + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to set terminal parameters') -- cgit v1.2.3