From 7fec0b69a082aaeec72fee0612766aa42f6b1b4d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 18 Apr 2024 07:52:35 +0200 Subject: Merging upstream version 9.4.0+dfsg. Signed-off-by: Daniel Baumann --- .../general/plugins/modules/airbrake_deployment.py | 2 +- .../general/plugins/modules/aix_devices.py | 8 +- .../general/plugins/modules/aix_filesystem.py | 70 ++- .../general/plugins/modules/aix_inittab.py | 2 +- .../community/general/plugins/modules/aix_lvg.py | 6 +- .../community/general/plugins/modules/aix_lvol.py | 10 +- .../general/plugins/modules/alerta_customer.py | 2 +- .../general/plugins/modules/ali_instance.py | 30 +- .../general/plugins/modules/ali_instance_info.py | 9 +- .../general/plugins/modules/alternatives.py | 12 +- .../plugins/modules/ansible_galaxy_install.py | 139 ++---- .../general/plugins/modules/apache2_module.py | 4 +- .../community/general/plugins/modules/apk.py | 18 +- .../community/general/plugins/modules/apt_repo.py | 2 +- .../community/general/plugins/modules/apt_rpm.py | 76 ++- .../community/general/plugins/modules/archive.py | 25 +- .../general/plugins/modules/atomic_container.py | 1 - .../general/plugins/modules/atomic_host.py | 3 +- .../general/plugins/modules/atomic_image.py | 3 +- .../community/general/plugins/modules/awall.py | 6 +- .../community/general/plugins/modules/bearychat.py | 6 +- .../community/general/plugins/modules/bigpanda.py | 4 +- .../plugins/modules/bitbucket_access_key.py | 2 +- .../plugins/modules/bitbucket_pipeline_key_pair.py | 2 +- .../modules/bitbucket_pipeline_known_host.py | 4 +- .../plugins/modules/bitbucket_pipeline_variable.py | 4 +- .../general/plugins/modules/btrfs_subvolume.py | 14 +- .../community/general/plugins/modules/bundler.py | 26 +- .../community/general/plugins/modules/bzr.py | 7 +- .../general/plugins/modules/capabilities.py | 2 +- .../community/general/plugins/modules/cargo.py | 40 +- .../community/general/plugins/modules/catapult.py | 4 +- .../general/plugins/modules/circonus_annotation.py | 4 +- .../general/plugins/modules/cisco_webex.py | 4 +- .../general/plugins/modules/clc_firewall_policy.py | 2 +- .../general/plugins/modules/clc_server.py | 2 +- .../general/plugins/modules/cloudflare_dns.py | 114 +++-- .../general/plugins/modules/cobbler_sync.py | 8 +- .../general/plugins/modules/cobbler_system.py | 12 +- .../community/general/plugins/modules/composer.py | 19 +- .../community/general/plugins/modules/consul.py | 103 ++-- .../general/plugins/modules/consul_acl.py | 6 +- .../plugins/modules/consul_acl_bootstrap.py | 108 ++++ .../general/plugins/modules/consul_auth_method.py | 207 ++++++++ .../general/plugins/modules/consul_binding_rule.py | 183 +++++++ .../community/general/plugins/modules/consul_kv.py | 28 +- .../general/plugins/modules/consul_policy.py | 164 +++++++ .../general/plugins/modules/consul_role.py | 281 +++++++++++ .../general/plugins/modules/consul_session.py | 186 +++---- .../general/plugins/modules/consul_token.py | 331 +++++++++++++ .../community/general/plugins/modules/copr.py | 26 +- .../community/general/plugins/modules/cpanm.py | 51 +- .../community/general/plugins/modules/cronvar.py | 13 +- .../community/general/plugins/modules/crypttab.py | 20 +- .../general/plugins/modules/datadog_downtime.py | 9 +- .../general/plugins/modules/datadog_event.py | 2 +- .../general/plugins/modules/datadog_monitor.py | 50 +- .../community/general/plugins/modules/dconf.py | 10 +- .../general/plugins/modules/deploy_helper.py | 47 +- .../plugins/modules/dimensiondata_network.py | 2 +- .../general/plugins/modules/dimensiondata_vlan.py | 8 +- .../community/general/plugins/modules/discord.py | 6 +- .../general/plugins/modules/django_manage.py | 70 +-- .../general/plugins/modules/dnf_config_manager.py | 225 +++++++++ .../general/plugins/modules/dnf_versionlock.py | 16 +- .../community/general/plugins/modules/dnsimple.py | 16 +- .../general/plugins/modules/dnsimple_info.py | 36 +- .../general/plugins/modules/dnsmadeeasy.py | 14 +- .../general/plugins/modules/dpkg_divert.py | 26 +- .../general/plugins/modules/easy_install.py | 18 +- .../general/plugins/modules/ejabberd_user.py | 41 +- .../plugins/modules/elasticsearch_plugin.py | 1 - .../general/plugins/modules/emc_vnx_sg_member.py | 4 +- .../community/general/plugins/modules/etcd3.py | 8 +- .../community/general/plugins/modules/facter.py | 2 +- .../general/plugins/modules/facter_facts.py | 90 ++++ .../community/general/plugins/modules/filesize.py | 38 +- .../general/plugins/modules/filesystem.py | 132 ++++- .../community/general/plugins/modules/flatpak.py | 14 +- .../general/plugins/modules/flatpak_remote.py | 22 +- .../community/general/plugins/modules/flowdock.py | 8 +- .../general/plugins/modules/gandi_livedns.py | 4 +- .../general/plugins/modules/gconftool2.py | 52 +- .../community/general/plugins/modules/gem.py | 4 +- .../community/general/plugins/modules/gio_mime.py | 108 ++++ .../general/plugins/modules/git_config.py | 172 ++++--- .../general/plugins/modules/git_config_info.py | 187 +++++++ .../general/plugins/modules/github_deploy_key.py | 12 +- .../general/plugins/modules/github_key.py | 16 +- .../general/plugins/modules/github_release.py | 8 +- .../general/plugins/modules/github_repo.py | 28 +- .../general/plugins/modules/github_webhook.py | 2 +- .../general/plugins/modules/github_webhook_info.py | 1 - .../general/plugins/modules/gitlab_branch.py | 10 +- .../general/plugins/modules/gitlab_deploy_key.py | 16 +- .../general/plugins/modules/gitlab_group.py | 35 +- .../plugins/modules/gitlab_group_access_token.py | 320 ++++++++++++ .../plugins/modules/gitlab_group_members.py | 57 ++- .../plugins/modules/gitlab_group_variable.py | 130 +++-- .../general/plugins/modules/gitlab_hook.py | 27 +- .../plugins/modules/gitlab_instance_variable.py | 360 ++++++++++++++ .../general/plugins/modules/gitlab_issue.py | 408 ++++++++++++++++ .../general/plugins/modules/gitlab_label.py | 500 +++++++++++++++++++ .../plugins/modules/gitlab_merge_request.py | 416 ++++++++++++++++ .../general/plugins/modules/gitlab_milestone.py | 496 +++++++++++++++++++ .../general/plugins/modules/gitlab_project.py | 91 ++-- .../plugins/modules/gitlab_project_access_token.py | 318 ++++++++++++ .../plugins/modules/gitlab_project_badge.py | 17 +- .../plugins/modules/gitlab_project_members.py | 36 +- .../plugins/modules/gitlab_project_variable.py | 149 +++--- .../plugins/modules/gitlab_protected_branch.py | 16 +- .../general/plugins/modules/gitlab_runner.py | 137 +++--- .../general/plugins/modules/gitlab_user.py | 56 +-- .../community/general/plugins/modules/grove.py | 4 +- .../general/plugins/modules/hana_query.py | 219 --------- .../community/general/plugins/modules/haproxy.py | 12 +- .../general/plugins/modules/heroku_collaborator.py | 10 +- .../community/general/plugins/modules/hg.py | 7 +- .../community/general/plugins/modules/hipchat.py | 2 +- .../community/general/plugins/modules/homebrew.py | 31 +- .../general/plugins/modules/homebrew_tap.py | 4 +- .../community/general/plugins/modules/homectl.py | 30 +- .../plugins/modules/honeybadger_deployment.py | 2 +- .../general/plugins/modules/hpilo_info.py | 4 +- .../community/general/plugins/modules/htpasswd.py | 115 ++--- .../general/plugins/modules/hwc_ecs_instance.py | 8 +- .../general/plugins/modules/hwc_smn_topic.py | 4 +- .../general/plugins/modules/hwc_vpc_eip.py | 4 +- .../general/plugins/modules/hwc_vpc_private_ip.py | 4 +- .../general/plugins/modules/hwc_vpc_route.py | 4 +- .../plugins/modules/hwc_vpc_security_group.py | 14 +- .../plugins/modules/hwc_vpc_security_group_rule.py | 6 +- .../general/plugins/modules/hwc_vpc_subnet.py | 8 +- .../general/plugins/modules/icinga2_feature.py | 8 +- .../general/plugins/modules/icinga2_host.py | 20 +- .../plugins/modules/idrac_redfish_config.py | 6 +- .../general/plugins/modules/idrac_redfish_info.py | 4 +- .../general/plugins/modules/ilo_redfish_command.py | 2 +- .../community/general/plugins/modules/imc_rest.py | 26 +- .../community/general/plugins/modules/imgadm.py | 15 +- .../general/plugins/modules/influxdb_database.py | 1 - .../general/plugins/modules/influxdb_query.py | 1 - .../plugins/modules/influxdb_retention_policy.py | 9 +- .../general/plugins/modules/influxdb_user.py | 1 - .../general/plugins/modules/influxdb_write.py | 1 - .../community/general/plugins/modules/ini_file.py | 172 ++++--- .../community/general/plugins/modules/installp.py | 4 +- .../general/plugins/modules/interfaces_file.py | 54 +- .../general/plugins/modules/ipa_config.py | 42 +- .../general/plugins/modules/ipa_dnsrecord.py | 31 +- .../general/plugins/modules/ipa_dnszone.py | 3 +- .../community/general/plugins/modules/ipa_group.py | 18 +- .../general/plugins/modules/ipa_hbacrule.py | 12 +- .../community/general/plugins/modules/ipa_host.py | 3 +- .../general/plugins/modules/ipa_hostgroup.py | 4 +- .../general/plugins/modules/ipa_otptoken.py | 4 +- .../general/plugins/modules/ipa_pwpolicy.py | 89 +++- .../general/plugins/modules/ipa_sudorule.py | 58 ++- .../community/general/plugins/modules/ipa_user.py | 16 +- .../community/general/plugins/modules/ipa_vault.py | 1 - .../general/plugins/modules/ipbase_info.py | 304 ++++++++++++ .../general/plugins/modules/ipify_facts.py | 2 +- .../community/general/plugins/modules/ipmi_boot.py | 1 - .../general/plugins/modules/ipmi_power.py | 17 +- .../general/plugins/modules/iptables_state.py | 68 +-- .../general/plugins/modules/ipwcli_dns.py | 16 +- .../community/general/plugins/modules/irc.py | 54 +- .../general/plugins/modules/iso_create.py | 21 +- .../general/plugins/modules/iso_customize.py | 7 +- .../general/plugins/modules/iso_extract.py | 11 +- .../community/general/plugins/modules/java_cert.py | 94 ++-- .../general/plugins/modules/java_keystore.py | 32 +- .../community/general/plugins/modules/jboss.py | 6 +- .../general/plugins/modules/jenkins_build.py | 31 +- .../general/plugins/modules/jenkins_build_info.py | 210 ++++++++ .../general/plugins/modules/jenkins_job.py | 14 +- .../general/plugins/modules/jenkins_job_info.py | 10 +- .../general/plugins/modules/jenkins_plugin.py | 51 +- .../general/plugins/modules/jenkins_script.py | 6 +- .../community/general/plugins/modules/jira.py | 30 +- .../community/general/plugins/modules/kdeconfig.py | 8 +- .../general/plugins/modules/kernel_blacklist.py | 15 +- .../plugins/modules/keycloak_authentication.py | 149 +++--- .../keycloak_authentication_required_actions.py | 457 +++++++++++++++++ .../modules/keycloak_authz_authorization_scope.py | 12 +- .../modules/keycloak_authz_custom_policy.py | 211 ++++++++ .../plugins/modules/keycloak_authz_permission.py | 433 ++++++++++++++++ .../modules/keycloak_authz_permission_info.py | 173 +++++++ .../general/plugins/modules/keycloak_client.py | 117 ++--- .../plugins/modules/keycloak_client_rolemapping.py | 61 ++- .../plugins/modules/keycloak_clientscope.py | 52 +- .../plugins/modules/keycloak_clientscope_type.py | 6 +- .../plugins/modules/keycloak_clientsecret_info.py | 6 +- .../plugins/modules/keycloak_clienttemplate.py | 67 ++- .../plugins/modules/keycloak_component_info.py | 169 +++++++ .../general/plugins/modules/keycloak_group.py | 10 +- .../plugins/modules/keycloak_identity_provider.py | 36 +- .../general/plugins/modules/keycloak_realm.py | 4 +- .../general/plugins/modules/keycloak_realm_key.py | 475 ++++++++++++++++++ .../plugins/modules/keycloak_realm_rolemapping.py | 391 +++++++++++++++ .../general/plugins/modules/keycloak_role.py | 81 ++- .../general/plugins/modules/keycloak_user.py | 542 +++++++++++++++++++++ .../plugins/modules/keycloak_user_federation.py | 69 +-- .../plugins/modules/keycloak_user_rolemapping.py | 8 +- .../community/general/plugins/modules/keyring.py | 4 +- .../general/plugins/modules/kibana_plugin.py | 2 +- .../community/general/plugins/modules/launchd.py | 10 +- .../community/general/plugins/modules/layman.py | 15 +- .../general/plugins/modules/ldap_attrs.py | 67 ++- .../general/plugins/modules/ldap_entry.py | 17 +- .../general/plugins/modules/ldap_passwd.py | 11 +- .../general/plugins/modules/ldap_search.py | 117 ++++- .../community/general/plugins/modules/linode.py | 7 +- .../community/general/plugins/modules/linode_v4.py | 5 +- .../general/plugins/modules/listen_ports_facts.py | 13 +- .../general/plugins/modules/locale_gen.py | 334 ++++++------- .../community/general/plugins/modules/lvg.py | 367 +++++++++++--- .../general/plugins/modules/lvg_rename.py | 170 +++++++ .../community/general/plugins/modules/lvol.py | 43 +- .../general/plugins/modules/lxc_container.py | 24 +- .../general/plugins/modules/lxd_container.py | 78 +-- .../general/plugins/modules/lxd_profile.py | 15 +- .../general/plugins/modules/lxd_project.py | 17 +- .../community/general/plugins/modules/macports.py | 2 +- .../community/general/plugins/modules/mail.py | 36 +- .../community/general/plugins/modules/make.py | 54 +- .../plugins/modules/manageiq_alert_profiles.py | 4 +- .../general/plugins/modules/manageiq_alerts.py | 6 +- .../general/plugins/modules/manageiq_group.py | 22 +- .../general/plugins/modules/manageiq_policies.py | 53 +- .../plugins/modules/manageiq_policies_info.py | 4 +- .../general/plugins/modules/manageiq_provider.py | 101 +++- .../general/plugins/modules/manageiq_tags.py | 45 +- .../general/plugins/modules/manageiq_tags_info.py | 4 +- .../general/plugins/modules/manageiq_tenant.py | 14 +- .../general/plugins/modules/manageiq_user.py | 14 +- .../community/general/plugins/modules/mas.py | 20 +- .../general/plugins/modules/mattermost.py | 10 +- .../general/plugins/modules/maven_artifact.py | 34 +- .../general/plugins/modules/memset_dns_reload.py | 4 +- .../plugins/modules/memset_memstore_info.py | 5 +- .../general/plugins/modules/memset_server_info.py | 5 +- .../general/plugins/modules/memset_zone.py | 2 +- .../general/plugins/modules/memset_zone_domain.py | 4 +- .../general/plugins/modules/memset_zone_record.py | 4 +- .../community/general/plugins/modules/modprobe.py | 12 +- .../community/general/plugins/modules/monit.py | 4 +- .../community/general/plugins/modules/mqtt.py | 6 +- .../community/general/plugins/modules/mssql_db.py | 1 - .../general/plugins/modules/mssql_script.py | 95 +++- .../community/general/plugins/modules/nagios.py | 33 +- .../general/plugins/modules/netcup_dns.py | 17 +- .../general/plugins/modules/newrelic_deployment.py | 46 +- .../community/general/plugins/modules/nexmo.py | 2 +- .../community/general/plugins/modules/nictagadm.py | 14 +- .../community/general/plugins/modules/nmcli.py | 462 +++++++++++------- .../community/general/plugins/modules/nomad_job.py | 14 +- .../general/plugins/modules/nomad_job_info.py | 4 +- .../general/plugins/modules/nomad_token.py | 301 ++++++++++++ .../community/general/plugins/modules/nosh.py | 20 +- .../community/general/plugins/modules/npm.py | 95 ++-- .../community/general/plugins/modules/nsupdate.py | 17 +- .../general/plugins/modules/ocapi_command.py | 6 +- .../general/plugins/modules/ocapi_info.py | 16 +- .../community/general/plugins/modules/oci_vcn.py | 12 +- .../community/general/plugins/modules/odbc.py | 3 +- .../community/general/plugins/modules/one_host.py | 10 +- .../community/general/plugins/modules/one_image.py | 20 +- .../general/plugins/modules/one_image_info.py | 13 +- .../general/plugins/modules/one_service.py | 16 +- .../general/plugins/modules/one_template.py | 24 +- .../community/general/plugins/modules/one_vm.py | 96 ++-- .../plugins/modules/oneandone_firewall_policy.py | 1 - .../plugins/modules/oneandone_load_balancer.py | 5 +- .../plugins/modules/oneandone_monitoring_policy.py | 1 - .../plugins/modules/oneandone_private_network.py | 1 - .../general/plugins/modules/oneandone_public_ip.py | 1 - .../general/plugins/modules/oneandone_server.py | 3 +- .../general/plugins/modules/onepassword_info.py | 9 +- .../plugins/modules/oneview_datacenter_info.py | 2 - .../plugins/modules/oneview_enclosure_info.py | 6 +- .../plugins/modules/oneview_ethernet_network.py | 6 +- .../modules/oneview_ethernet_network_info.py | 4 +- .../general/plugins/modules/oneview_fc_network.py | 4 +- .../plugins/modules/oneview_fc_network_info.py | 2 - .../plugins/modules/oneview_fcoe_network.py | 6 +- .../plugins/modules/oneview_fcoe_network_info.py | 2 - .../modules/oneview_logical_interconnect_group.py | 4 +- .../oneview_logical_interconnect_group_info.py | 2 - .../general/plugins/modules/oneview_network_set.py | 4 +- .../plugins/modules/oneview_network_set_info.py | 6 +- .../general/plugins/modules/oneview_san_manager.py | 6 +- .../plugins/modules/oneview_san_manager_info.py | 10 +- .../general/plugins/modules/open_iscsi.py | 4 +- .../general/plugins/modules/openbsd_pkg.py | 20 +- .../general/plugins/modules/openwrt_init.py | 7 +- .../community/general/plugins/modules/opkg.py | 21 +- .../general/plugins/modules/osx_defaults.py | 7 +- .../general/plugins/modules/ovh_ip_failover.py | 2 +- .../general/plugins/modules/ovh_monthly_billing.py | 2 +- .../general/plugins/modules/pacemaker_cluster.py | 8 + .../general/plugins/modules/packet_device.py | 16 +- .../general/plugins/modules/packet_ip_subnet.py | 9 +- .../general/plugins/modules/packet_project.py | 3 +- .../general/plugins/modules/packet_sshkey.py | 3 +- .../general/plugins/modules/packet_volume.py | 3 +- .../plugins/modules/packet_volume_attachment.py | 3 +- .../community/general/plugins/modules/pacman.py | 86 ++-- .../general/plugins/modules/pacman_key.py | 10 +- .../community/general/plugins/modules/pagerduty.py | 4 +- .../general/plugins/modules/pagerduty_alert.py | 286 ++++++++--- .../general/plugins/modules/pagerduty_change.py | 6 +- .../general/plugins/modules/pagerduty_user.py | 8 +- .../general/plugins/modules/pam_limits.py | 39 +- .../community/general/plugins/modules/pamd.py | 24 +- .../community/general/plugins/modules/parted.py | 44 +- .../community/general/plugins/modules/pear.py | 8 +- .../community/general/plugins/modules/pids.py | 2 +- .../general/plugins/modules/pip_package_info.py | 2 +- .../community/general/plugins/modules/pipx.py | 26 +- .../community/general/plugins/modules/pipx_info.py | 10 +- .../community/general/plugins/modules/pkg5.py | 4 +- .../community/general/plugins/modules/pkgin.py | 11 +- .../community/general/plugins/modules/pkgng.py | 23 +- .../community/general/plugins/modules/pkgutil.py | 10 +- .../community/general/plugins/modules/pmem.py | 28 +- .../community/general/plugins/modules/pnpm.py | 462 ++++++++++++++++++ .../community/general/plugins/modules/portage.py | 6 +- .../general/plugins/modules/pritunl_org.py | 10 +- .../general/plugins/modules/pritunl_user.py | 18 +- .../general/plugins/modules/pritunl_user_info.py | 2 +- .../general/plugins/modules/profitbricks.py | 3 +- .../plugins/modules/profitbricks_datacenter.py | 2 +- .../general/plugins/modules/profitbricks_nic.py | 2 +- .../general/plugins/modules/profitbricks_volume.py | 4 +- .../modules/profitbricks_volume_attachments.py | 2 +- .../community/general/plugins/modules/proxmox.py | 412 ++++++++++++---- .../general/plugins/modules/proxmox_disk.py | 188 ++++--- .../general/plugins/modules/proxmox_kvm.py | 497 +++++++++++++------ .../general/plugins/modules/proxmox_nic.py | 10 +- .../general/plugins/modules/proxmox_node_info.py | 140 ++++++ .../general/plugins/modules/proxmox_pool.py | 180 +++++++ .../general/plugins/modules/proxmox_pool_member.py | 238 +++++++++ .../general/plugins/modules/proxmox_snap.py | 47 +- .../modules/proxmox_storage_contents_info.py | 144 ++++++ .../plugins/modules/proxmox_storage_info.py | 4 +- .../general/plugins/modules/proxmox_tasks_info.py | 4 +- .../general/plugins/modules/proxmox_template.py | 98 ++-- .../general/plugins/modules/proxmox_user_info.py | 6 +- .../general/plugins/modules/proxmox_vm_info.py | 267 ++++++++++ .../general/plugins/modules/pubnub_blocks.py | 21 +- .../community/general/plugins/modules/pulp_repo.py | 16 +- .../community/general/plugins/modules/puppet.py | 18 +- .../general/plugins/modules/pushbullet.py | 2 +- .../plugins/modules/python_requirements_info.py | 7 +- .../community/general/plugins/modules/rax.py | 30 +- .../community/general/plugins/modules/rax_cbs.py | 4 +- .../general/plugins/modules/rax_cbs_attachments.py | 4 +- .../community/general/plugins/modules/rax_cdb.py | 4 +- .../general/plugins/modules/rax_cdb_database.py | 2 - .../general/plugins/modules/rax_cdb_user.py | 2 - .../community/general/plugins/modules/rax_clb.py | 4 +- .../general/plugins/modules/rax_clb_nodes.py | 4 +- .../general/plugins/modules/rax_clb_ssl.py | 4 +- .../community/general/plugins/modules/rax_dns.py | 4 +- .../general/plugins/modules/rax_dns_record.py | 10 +- .../community/general/plugins/modules/rax_facts.py | 2 - .../community/general/plugins/modules/rax_files.py | 4 +- .../general/plugins/modules/rax_files_objects.py | 14 +- .../general/plugins/modules/rax_identity.py | 4 +- .../general/plugins/modules/rax_keypair.py | 4 +- .../community/general/plugins/modules/rax_meta.py | 4 +- .../general/plugins/modules/rax_mon_alarm.py | 16 +- .../general/plugins/modules/rax_mon_check.py | 66 ++- .../general/plugins/modules/rax_mon_entity.py | 12 +- .../plugins/modules/rax_mon_notification.py | 2 +- .../plugins/modules/rax_mon_notification_plan.py | 12 +- .../general/plugins/modules/rax_scaling_group.py | 6 +- .../general/plugins/modules/rax_scaling_policy.py | 8 +- .../community/general/plugins/modules/read_csv.py | 18 +- .../general/plugins/modules/redfish_command.py | 113 ++++- .../general/plugins/modules/redfish_config.py | 124 ++++- .../general/plugins/modules/redfish_info.py | 51 +- .../general/plugins/modules/redhat_subscription.py | 180 ++++--- .../community/general/plugins/modules/redis.py | 10 +- .../general/plugins/modules/redis_data.py | 4 +- .../general/plugins/modules/redis_data_incr.py | 6 +- .../general/plugins/modules/redis_info.py | 48 +- .../community/general/plugins/modules/rhevm.py | 8 +- .../general/plugins/modules/rhn_channel.py | 14 +- .../general/plugins/modules/rhn_register.py | 18 +- .../general/plugins/modules/rhsm_release.py | 8 +- .../general/plugins/modules/rhsm_repository.py | 184 +++---- .../community/general/plugins/modules/riak.py | 2 +- .../general/plugins/modules/rocketchat.py | 16 +- .../general/plugins/modules/rollbar_deployment.py | 2 +- .../general/plugins/modules/rpm_ostree_pkg.py | 4 +- .../general/plugins/modules/rundeck_acl_policy.py | 4 +- .../general/plugins/modules/rundeck_job_run.py | 6 +- .../community/general/plugins/modules/runit.py | 10 +- .../plugins/modules/sap_task_list_execute.py | 348 ------------- .../general/plugins/modules/sapcar_extract.py | 228 --------- .../general/plugins/modules/scaleway_compute.py | 8 +- .../modules/scaleway_compute_private_network.py | 4 +- .../general/plugins/modules/scaleway_container.py | 8 +- .../modules/scaleway_container_namespace.py | 6 +- .../plugins/modules/scaleway_container_registry.py | 8 +- .../plugins/modules/scaleway_database_backup.py | 28 +- .../general/plugins/modules/scaleway_function.py | 8 +- .../plugins/modules/scaleway_function_namespace.py | 6 +- .../general/plugins/modules/scaleway_ip.py | 4 +- .../plugins/modules/scaleway_private_network.py | 4 +- .../plugins/modules/scaleway_security_group.py | 6 +- .../modules/scaleway_security_group_rule.py | 23 +- .../general/plugins/modules/scaleway_sshkey.py | 4 +- .../general/plugins/modules/scaleway_volume.py | 4 +- .../general/plugins/modules/sefcontext.py | 36 +- .../general/plugins/modules/selinux_permissive.py | 2 +- .../community/general/plugins/modules/sendgrid.py | 5 +- .../general/plugins/modules/sensu_check.py | 18 +- .../general/plugins/modules/sensu_client.py | 2 +- .../general/plugins/modules/serverless.py | 4 +- .../community/general/plugins/modules/shutdown.py | 8 +- .../general/plugins/modules/simpleinit_msb.py | 322 ++++++++++++ .../community/general/plugins/modules/sl_vm.py | 3 +- .../community/general/plugins/modules/slack.py | 41 +- .../community/general/plugins/modules/slackpkg.py | 2 +- .../general/plugins/modules/smartos_image_info.py | 8 +- .../community/general/plugins/modules/snap.py | 254 +++++++--- .../general/plugins/modules/snap_alias.py | 33 +- .../general/plugins/modules/snmp_facts.py | 18 +- .../general/plugins/modules/solaris_zone.py | 24 +- .../community/general/plugins/modules/sorcery.py | 255 +++++++--- .../general/plugins/modules/spectrum_device.py | 14 +- .../plugins/modules/spectrum_model_attrs.py | 8 +- .../plugins/modules/spotinst_aws_elastigroup.py | 21 +- .../general/plugins/modules/ssh_config.py | 82 +++- .../general/plugins/modules/stackdriver.py | 5 + .../general/plugins/modules/stacki_host.py | 14 +- .../community/general/plugins/modules/statsd.py | 12 +- .../plugins/modules/statusio_maintenance.py | 13 +- .../community/general/plugins/modules/sudoers.py | 36 +- .../general/plugins/modules/supervisorctl.py | 33 +- .../community/general/plugins/modules/svc.py | 10 +- .../community/general/plugins/modules/svr4pkg.py | 14 +- .../community/general/plugins/modules/swdepot.py | 2 +- .../community/general/plugins/modules/swupd.py | 6 +- .../community/general/plugins/modules/sysrc.py | 24 +- .../general/plugins/modules/sysupgrade.py | 2 +- .../community/general/plugins/modules/telegram.py | 4 +- .../community/general/plugins/modules/terraform.py | 103 +++- .../community/general/plugins/modules/timezone.py | 12 +- .../general/plugins/modules/udm_dns_record.py | 16 +- .../general/plugins/modules/udm_dns_zone.py | 8 +- .../community/general/plugins/modules/udm_share.py | 7 +- .../community/general/plugins/modules/udm_user.py | 36 +- .../community/general/plugins/modules/ufw.py | 44 +- .../community/general/plugins/modules/urpmi.py | 8 +- .../community/general/plugins/modules/usb_facts.py | 113 +++++ .../general/plugins/modules/utm_proxy_location.py | 2 +- .../plugins/modules/utm_proxy_location_info.py | 2 +- .../community/general/plugins/modules/vdo.py | 2 +- .../general/plugins/modules/vertica_info.py | 2 - .../general/plugins/modules/vertica_role.py | 2 +- .../general/plugins/modules/vertica_schema.py | 2 +- .../general/plugins/modules/vertica_user.py | 8 +- .../community/general/plugins/modules/vmadm.py | 28 +- .../general/plugins/modules/wdc_redfish_command.py | 6 +- .../general/plugins/modules/wdc_redfish_info.py | 4 +- .../general/plugins/modules/webfaction_app.py | 12 +- .../general/plugins/modules/webfaction_db.py | 12 +- .../general/plugins/modules/webfaction_domain.py | 16 +- .../general/plugins/modules/webfaction_mailbox.py | 12 +- .../general/plugins/modules/webfaction_site.py | 14 +- .../community/general/plugins/modules/xattr.py | 17 +- .../community/general/plugins/modules/xbps.py | 2 +- .../general/plugins/modules/xcc_redfish_command.py | 10 +- .../general/plugins/modules/xenserver_facts.py | 2 +- .../general/plugins/modules/xenserver_guest.py | 63 ++- .../plugins/modules/xenserver_guest_info.py | 10 +- .../plugins/modules/xenserver_guest_powerstate.py | 15 +- .../community/general/plugins/modules/xfconf.py | 44 +- .../general/plugins/modules/xfconf_info.py | 6 +- .../community/general/plugins/modules/xml.py | 54 +- .../general/plugins/modules/yum_versionlock.py | 30 +- .../community/general/plugins/modules/zfs.py | 8 +- .../general/plugins/modules/zfs_delegate_admin.py | 18 +- .../community/general/plugins/modules/znode.py | 7 +- .../community/general/plugins/modules/zypper.py | 26 +- .../general/plugins/modules/zypper_repository.py | 5 +- 490 files changed, 18094 insertions(+), 5691 deletions(-) create mode 100644 ansible_collections/community/general/plugins/modules/consul_acl_bootstrap.py create mode 100644 ansible_collections/community/general/plugins/modules/consul_auth_method.py create mode 100644 ansible_collections/community/general/plugins/modules/consul_binding_rule.py create mode 100644 ansible_collections/community/general/plugins/modules/consul_policy.py create mode 100644 ansible_collections/community/general/plugins/modules/consul_role.py create mode 100644 ansible_collections/community/general/plugins/modules/consul_token.py create mode 100644 ansible_collections/community/general/plugins/modules/dnf_config_manager.py create mode 100644 ansible_collections/community/general/plugins/modules/facter_facts.py create mode 100644 ansible_collections/community/general/plugins/modules/gio_mime.py create mode 100644 ansible_collections/community/general/plugins/modules/git_config_info.py create mode 100644 ansible_collections/community/general/plugins/modules/gitlab_group_access_token.py create mode 100644 ansible_collections/community/general/plugins/modules/gitlab_instance_variable.py create mode 100644 ansible_collections/community/general/plugins/modules/gitlab_issue.py create mode 100644 ansible_collections/community/general/plugins/modules/gitlab_label.py create mode 100644 ansible_collections/community/general/plugins/modules/gitlab_merge_request.py create mode 100644 ansible_collections/community/general/plugins/modules/gitlab_milestone.py create mode 100644 ansible_collections/community/general/plugins/modules/gitlab_project_access_token.py delete mode 100644 ansible_collections/community/general/plugins/modules/hana_query.py create mode 100644 ansible_collections/community/general/plugins/modules/ipbase_info.py create mode 100644 ansible_collections/community/general/plugins/modules/jenkins_build_info.py create mode 100644 ansible_collections/community/general/plugins/modules/keycloak_authentication_required_actions.py create mode 100644 ansible_collections/community/general/plugins/modules/keycloak_authz_custom_policy.py create mode 100644 ansible_collections/community/general/plugins/modules/keycloak_authz_permission.py create mode 100644 ansible_collections/community/general/plugins/modules/keycloak_authz_permission_info.py create mode 100644 ansible_collections/community/general/plugins/modules/keycloak_component_info.py create mode 100644 ansible_collections/community/general/plugins/modules/keycloak_realm_key.py create mode 100644 ansible_collections/community/general/plugins/modules/keycloak_realm_rolemapping.py create mode 100644 ansible_collections/community/general/plugins/modules/keycloak_user.py create mode 100644 ansible_collections/community/general/plugins/modules/lvg_rename.py create mode 100644 ansible_collections/community/general/plugins/modules/nomad_token.py create mode 100644 ansible_collections/community/general/plugins/modules/pnpm.py create mode 100644 ansible_collections/community/general/plugins/modules/proxmox_node_info.py create mode 100644 ansible_collections/community/general/plugins/modules/proxmox_pool.py create mode 100644 ansible_collections/community/general/plugins/modules/proxmox_pool_member.py create mode 100644 ansible_collections/community/general/plugins/modules/proxmox_storage_contents_info.py create mode 100644 ansible_collections/community/general/plugins/modules/proxmox_vm_info.py delete mode 100644 ansible_collections/community/general/plugins/modules/sap_task_list_execute.py delete mode 100644 ansible_collections/community/general/plugins/modules/sapcar_extract.py create mode 100644 ansible_collections/community/general/plugins/modules/simpleinit_msb.py create mode 100644 ansible_collections/community/general/plugins/modules/usb_facts.py (limited to 'ansible_collections/community/general/plugins/modules') diff --git a/ansible_collections/community/general/plugins/modules/airbrake_deployment.py b/ansible_collections/community/general/plugins/modules/airbrake_deployment.py index 42ac037e1..bad1b2c9d 100644 --- a/ansible_collections/community/general/plugins/modules/airbrake_deployment.py +++ b/ansible_collections/community/general/plugins/modules/airbrake_deployment.py @@ -72,7 +72,7 @@ options: type: str validate_certs: description: - - If C(false), SSL certificates for the target url will not be validated. This should only be used + - If V(false), SSL certificates for the target url will not be validated. This should only be used on personally controlled sites using self-signed certificates. required: false default: true diff --git a/ansible_collections/community/general/plugins/modules/aix_devices.py b/ansible_collections/community/general/plugins/modules/aix_devices.py index ef4ed4961..a0f3cf48d 100644 --- a/ansible_collections/community/general/plugins/modules/aix_devices.py +++ b/ansible_collections/community/general/plugins/modules/aix_devices.py @@ -31,7 +31,7 @@ options: device: description: - The name of the device. - - C(all) is valid to rescan C(available) all devices (AIX cfgmgr command). + - V(all) is valid to rescan C(available) all devices (AIX cfgmgr command). type: str force: description: @@ -46,9 +46,9 @@ options: state: description: - Controls the device state. - - C(available) (alias C(present)) rescan a specific device or all devices (when C(device) is not specified). - - C(removed) (alias C(absent) removes a device. - - C(defined) changes device to Defined state. + - V(available) (alias V(present)) rescan a specific device or all devices (when O(device) is not specified). + - V(removed) (alias V(absent) removes a device. + - V(defined) changes device to Defined state. type: str choices: [ available, defined, removed ] default: available diff --git a/ansible_collections/community/general/plugins/modules/aix_filesystem.py b/ansible_collections/community/general/plugins/modules/aix_filesystem.py index b1f363a93..6abf6317f 100644 --- a/ansible_collections/community/general/plugins/modules/aix_filesystem.py +++ b/ansible_collections/community/general/plugins/modules/aix_filesystem.py @@ -38,8 +38,8 @@ options: type: list elements: str default: - - agblksize='4096' - - isnapshot='no' + - agblksize=4096 + - isnapshot=no auto_mount: description: - File system is automatically mounted at system restart. @@ -58,7 +58,7 @@ options: default: jfs2 permissions: description: - - Set file system permissions. C(rw) (read-write) or C(ro) (read-only). + - Set file system permissions. V(rw) (read-write) or V(ro) (read-only). type: str choices: [ ro, rw ] default: rw @@ -77,13 +77,13 @@ options: type: str rm_mount_point: description: - - Removes the mount point directory when used with state C(absent). + - Removes the mount point directory when used with state V(absent). type: bool default: false size: description: - Specifies the file system size. - - For already C(present) it will be resized. + - For already V(present) it will be resized. - 512-byte blocks, Megabytes or Gigabytes. If the value has M specified it will be in Megabytes. If the value has G specified it will be in Gigabytes. @@ -96,10 +96,10 @@ options: state: description: - Controls the file system state. - - C(present) check if file system exists, creates or resize. - - C(absent) removes existing file system if already C(unmounted). - - C(mounted) checks if the file system is mounted or mount the file system. - - C(unmounted) check if the file system is unmounted or unmount the file system. + - V(present) check if file system exists, creates or resize. + - V(absent) removes existing file system if already V(unmounted). + - V(mounted) checks if the file system is mounted or mount the file system. + - V(unmounted) check if the file system is unmounted or unmount the file system. type: str choices: [ absent, mounted, present, unmounted ] default: present @@ -108,7 +108,7 @@ options: - Specifies an existing volume group (VG). type: str notes: - - For more C(attributes), please check "crfs" AIX manual. + - For more O(attributes), please check "crfs" AIX manual. ''' EXAMPLES = r''' @@ -365,7 +365,53 @@ def create_fs( # Creates a LVM file system. crfs_cmd = module.get_bin_path('crfs', True) if not module.check_mode: - cmd = [crfs_cmd, "-v", fs_type, "-m", filesystem, vg, device, mount_group, auto_mount, account_subsystem, "-p", permissions, size, "-a", attributes] + cmd = [crfs_cmd] + + cmd.append("-v") + cmd.append(fs_type) + + if vg: + (flag, value) = vg.split() + cmd.append(flag) + cmd.append(value) + + if device: + (flag, value) = device.split() + cmd.append(flag) + cmd.append(value) + + cmd.append("-m") + cmd.append(filesystem) + + if mount_group: + (flag, value) = mount_group.split() + cmd.append(flag) + cmd.append(value) + + if auto_mount: + (flag, value) = auto_mount.split() + cmd.append(flag) + cmd.append(value) + + if account_subsystem: + (flag, value) = account_subsystem.split() + cmd.append(flag) + cmd.append(value) + + cmd.append("-p") + cmd.append(permissions) + + if size: + (flag, value) = size.split() + cmd.append(flag) + cmd.append(value) + + if attributes: + splitted_attributes = attributes.split() + cmd.append("-a") + for value in splitted_attributes: + cmd.append(value) + rc, crfs_out, err = module.run_command(cmd) if rc == 10: @@ -461,7 +507,7 @@ def main(): module = AnsibleModule( argument_spec=dict( account_subsystem=dict(type='bool', default=False), - attributes=dict(type='list', elements='str', default=["agblksize='4096'", "isnapshot='no'"]), + attributes=dict(type='list', elements='str', default=["agblksize=4096", "isnapshot=no"]), auto_mount=dict(type='bool', default=True), device=dict(type='str'), filesystem=dict(type='str', required=True), diff --git a/ansible_collections/community/general/plugins/modules/aix_inittab.py b/ansible_collections/community/general/plugins/modules/aix_inittab.py index c2c968189..d4c9aa0b5 100644 --- a/ansible_collections/community/general/plugins/modules/aix_inittab.py +++ b/ansible_collections/community/general/plugins/modules/aix_inittab.py @@ -204,7 +204,7 @@ def main(): ":" + module.params['action'] + ":" + module.params['command'] # If current entry exists or fields are different(if the entry does not - # exists, then the entry wil be created + # exists, then the entry will be created if (not current_entry['exist']) or ( module.params['runlevel'] != current_entry['runlevel'] or module.params['action'] != current_entry['action'] or diff --git a/ansible_collections/community/general/plugins/modules/aix_lvg.py b/ansible_collections/community/general/plugins/modules/aix_lvg.py index d89c43de4..2892a68ad 100644 --- a/ansible_collections/community/general/plugins/modules/aix_lvg.py +++ b/ansible_collections/community/general/plugins/modules/aix_lvg.py @@ -36,13 +36,13 @@ options: pvs: description: - List of comma-separated devices to use as physical devices in this volume group. - - Required when creating or extending (C(present) state) the volume group. - - If not informed reducing (C(absent) state) the volume group will be removed. + - Required when creating or extending (V(present) state) the volume group. + - If not informed reducing (V(absent) state) the volume group will be removed. type: list elements: str state: description: - - Control if the volume group exists and volume group AIX state varyonvg C(varyon) or varyoffvg C(varyoff). + - Control if the volume group exists and volume group AIX state varyonvg V(varyon) or varyoffvg V(varyoff). type: str choices: [ absent, present, varyoff, varyon ] default: present diff --git a/ansible_collections/community/general/plugins/modules/aix_lvol.py b/ansible_collections/community/general/plugins/modules/aix_lvol.py index 0a4a6eff5..1e7b42568 100644 --- a/ansible_collections/community/general/plugins/modules/aix_lvol.py +++ b/ansible_collections/community/general/plugins/modules/aix_lvol.py @@ -53,15 +53,15 @@ options: policy: description: - Sets the interphysical volume allocation policy. - - C(maximum) allocates logical partitions across the maximum number of physical volumes. - - C(minimum) allocates logical partitions across the minimum number of physical volumes. + - V(maximum) allocates logical partitions across the maximum number of physical volumes. + - V(minimum) allocates logical partitions across the minimum number of physical volumes. type: str choices: [ maximum, minimum ] default: maximum state: description: - - Control if the logical volume exists. If C(present) and the - volume does not already exist then the C(size) option is required. + - Control if the logical volume exists. If V(present) and the + volume does not already exist then the O(size) option is required. type: str choices: [ absent, present ] default: present @@ -72,7 +72,7 @@ options: default: '' pvs: description: - - A list of physical volumes e.g. C(hdisk1,hdisk2). + - A list of physical volumes, for example V(hdisk1,hdisk2). type: list elements: str default: [] diff --git a/ansible_collections/community/general/plugins/modules/alerta_customer.py b/ansible_collections/community/general/plugins/modules/alerta_customer.py index 120d98932..5e1a5f86c 100644 --- a/ansible_collections/community/general/plugins/modules/alerta_customer.py +++ b/ansible_collections/community/general/plugins/modules/alerta_customer.py @@ -58,7 +58,7 @@ options: state: description: - Whether the customer should exist or not. - - Both I(customer) and I(match) identify a customer that should be added or removed. + - Both O(customer) and O(match) identify a customer that should be added or removed. type: str choices: [ absent, present ] default: present diff --git a/ansible_collections/community/general/plugins/modules/ali_instance.py b/ansible_collections/community/general/plugins/modules/ali_instance.py index 232c21ee0..087dc64b6 100644 --- a/ansible_collections/community/general/plugins/modules/ali_instance.py +++ b/ansible_collections/community/general/plugins/modules/ali_instance.py @@ -51,12 +51,12 @@ options: type: str image_id: description: - - Image ID used to launch instances. Required when I(state=present) and creating new ECS instances. + - Image ID used to launch instances. Required when O(state=present) and creating new ECS instances. aliases: ['image'] type: str instance_type: description: - - Instance type used to launch instances. Required when I(state=present) and creating new ECS instances. + - Instance type used to launch instances. Required when O(state=present) and creating new ECS instances. aliases: ['type'] type: str security_groups: @@ -95,7 +95,7 @@ options: max_bandwidth_out: description: - Maximum outgoing bandwidth to the public network, measured in Mbps (Megabits per second). - Required when I(allocate_public_ip=true). Ignored when I(allocate_public_ip=false). + Required when O(allocate_public_ip=true). Ignored when O(allocate_public_ip=false). default: 0 type: int host_name: @@ -134,16 +134,16 @@ options: type: str count: description: - - The number of the new instance. An integer value which indicates how many instances that match I(count_tag) + - The number of the new instance. An integer value which indicates how many instances that match O(count_tag) should be running. Instances are either created or terminated based on this value. default: 1 type: int count_tag: description: - - I(count) determines how many instances based on a specific tag criteria should be present. + - O(count) determines how many instances based on a specific tag criteria should be present. This can be expressed in multiple ways and is shown in the EXAMPLES section. - The specified count_tag must already exist or be passed in as the I(tags) option. - If it is not specified, it will be replaced by I(instance_name). + The specified count_tag must already exist or be passed in as the O(tags) option. + If it is not specified, it will be replaced by O(instance_name). type: str allocate_public_ip: description: @@ -159,7 +159,7 @@ options: type: str period: description: - - The charge duration of the instance, in months. Required when I(instance_charge_type=PrePaid). + - The charge duration of the instance, in months. Required when O(instance_charge_type=PrePaid). - The valid value are [1-9, 12, 24, 36]. default: 1 type: int @@ -170,13 +170,13 @@ options: default: false auto_renew_period: description: - - The duration of the automatic renew the charge of the instance. Required when I(auto_renew=true). + - The duration of the automatic renew the charge of the instance. Required when O(auto_renew=true). choices: [1, 2, 3, 6, 12] type: int instance_ids: description: - A list of instance ids. It is required when need to operate existing instances. - If it is specified, I(count) will lose efficacy. + If it is specified, O(count) will lose efficacy. type: list elements: str force: @@ -186,7 +186,7 @@ options: type: bool tags: description: - - A hash/dictionaries of instance tags, to add to the new instance or for starting/stopping instance by tag. C({"key":"value"}) + - A hash/dictionaries of instance tags, to add to the new instance or for starting/stopping instance by tag. V({"key":"value"}) aliases: ["instance_tags"] type: dict version_added: '0.2.0' @@ -229,7 +229,7 @@ options: version_added: '0.2.0' period_unit: description: - - The duration unit that you will buy the resource. It is valid when I(instance_charge_type=PrePaid). + - The duration unit that you will buy the resource. It is valid when O(instance_charge_type=PrePaid). choices: ['Month', 'Week'] default: 'Month' type: str @@ -237,10 +237,10 @@ options: dry_run: description: - Specifies whether to send a dry-run request. - - If I(dry_run=true), Only a dry-run request is sent and no instance is created. The system checks whether the + - If O(dry_run=true), Only a dry-run request is sent and no instance is created. The system checks whether the required parameters are set, and validates the request format, service permissions, and available ECS instances. If the validation fails, the corresponding error code is returned. If the validation succeeds, the DryRunOperation error code is returned. - - If I(dry_run=false), A request is sent. If the validation succeeds, the instance is created. + - If O(dry_run=false), A request is sent. If the validation succeeds, the instance is created. default: false type: bool version_added: '0.2.0' @@ -253,7 +253,7 @@ options: author: - "He Guimin (@xiaozhu36)" requirements: - - "python >= 3.6" + - "Python >= 3.6" - "footmark >= 1.19.0" extends_documentation_fragment: - community.general.alicloud diff --git a/ansible_collections/community/general/plugins/modules/ali_instance_info.py b/ansible_collections/community/general/plugins/modules/ali_instance_info.py index e7ec7f395..d6a787374 100644 --- a/ansible_collections/community/general/plugins/modules/ali_instance_info.py +++ b/ansible_collections/community/general/plugins/modules/ali_instance_info.py @@ -31,7 +31,6 @@ short_description: Gather information on instances of Alibaba Cloud ECS description: - This module fetches data from the Open API in Alicloud. The module must be called from within the ECS instance itself. - - This module was called C(ali_instance_facts) before Ansible 2.9. The usage did not change. attributes: check_mode: @@ -53,15 +52,15 @@ options: description: - A dict of filters to apply. Each dict item consists of a filter key and a filter value. The filter keys can be all of request parameters. See U(https://www.alibabacloud.com/help/doc-detail/25506.htm) for parameter details. - Filter keys can be same as request parameter name or be lower case and use underscore ("_") or dash ("-") to - connect different words in one parameter. 'InstanceIds' should be a list. - 'Tag.n.Key' and 'Tag.n.Value' should be a dict and using I(tags) instead. + Filter keys can be same as request parameter name or be lower case and use underscore (V("_")) or dash (V("-")) to + connect different words in one parameter. C(InstanceIds) should be a list. + C(Tag.n.Key) and C(Tag.n.Value) should be a dict and using O(tags) instead. type: dict version_added: '0.2.0' author: - "He Guimin (@xiaozhu36)" requirements: - - "python >= 3.6" + - "Python >= 3.6" - "footmark >= 1.13.0" extends_documentation_fragment: - community.general.alicloud diff --git a/ansible_collections/community/general/plugins/modules/alternatives.py b/ansible_collections/community/general/plugins/modules/alternatives.py index 97d4f51fb..0d1b1e8cb 100644 --- a/ansible_collections/community/general/plugins/modules/alternatives.py +++ b/ansible_collections/community/general/plugins/modules/alternatives.py @@ -44,21 +44,21 @@ options: description: - The path to the symbolic link that should point to the real executable. - This option is always required on RHEL-based distributions. On Debian-based distributions this option is - required when the alternative I(name) is unknown to the system. + required when the alternative O(name) is unknown to the system. type: path priority: description: - - The priority of the alternative. If no priority is given for creation C(50) is used as a fallback. + - The priority of the alternative. If no priority is given for creation V(50) is used as a fallback. type: int state: description: - - C(present) - install the alternative (if not already installed), but do + - V(present) - install the alternative (if not already installed), but do not set it as the currently selected alternative for the group. - - C(selected) - install the alternative (if not already installed), and + - V(selected) - install the alternative (if not already installed), and set it as the currently selected alternative for the group. - - C(auto) - install the alternative (if not already installed), and + - V(auto) - install the alternative (if not already installed), and set the group to auto mode. Added in community.general 5.1.0. - - C(absent) - removes the alternative. Added in community.general 5.1.0. + - V(absent) - removes the alternative. Added in community.general 5.1.0. choices: [ present, selected, auto, absent ] default: selected type: str diff --git a/ansible_collections/community/general/plugins/modules/ansible_galaxy_install.py b/ansible_collections/community/general/plugins/modules/ansible_galaxy_install.py index 0f38eabdf..3b0a8fd47 100644 --- a/ansible_collections/community/general/plugins/modules/ansible_galaxy_install.py +++ b/ansible_collections/community/general/plugins/modules/ansible_galaxy_install.py @@ -17,15 +17,13 @@ version_added: 3.5.0 description: - This module allows the installation of Ansible collections or roles using C(ansible-galaxy). notes: - - > - B(Ansible 2.9/2.10): The C(ansible-galaxy) command changed significantly between Ansible 2.9 and - ansible-base 2.10 (later ansible-core 2.11). See comments in the parameters. + - Support for B(Ansible 2.9/2.10) was removed in community.general 8.0.0. - > The module will try and run using the C(C.UTF-8) locale. If that fails, it will try C(en_US.UTF-8). If that one also fails, the module will fail. requirements: - - Ansible 2.9, ansible-base 2.10, or ansible-core 2.11 or newer + - ansible-core 2.11 or newer extends_documentation_fragment: - community.general.attributes attributes: @@ -37,9 +35,8 @@ options: type: description: - The type of installation performed by C(ansible-galaxy). - - If I(type) is C(both), then I(requirements_file) must be passed and it may contain both roles and collections. - - "Note however that the opposite is not true: if using a I(requirements_file), then I(type) can be any of the three choices." - - "B(Ansible 2.9): The option C(both) will have the same effect as C(role)." + - If O(type=both), then O(requirements_file) must be passed and it may contain both roles and collections. + - "Note however that the opposite is not true: if using a O(requirements_file), then O(type) can be any of the three choices." type: str choices: [collection, role, both] required: true @@ -48,22 +45,21 @@ options: - Name of the collection or role being installed. - > Versions can be specified with C(ansible-galaxy) usual formats. - For example, the collection C(community.docker:1.6.1) or the role C(ansistrano.deploy,3.8.0). - - I(name) and I(requirements_file) are mutually exclusive. + For example, the collection V(community.docker:1.6.1) or the role V(ansistrano.deploy,3.8.0). + - O(name) and O(requirements_file) are mutually exclusive. type: str requirements_file: description: - Path to a file containing a list of requirements to be installed. - - It works for I(type) equals to C(collection) and C(role). - - I(name) and I(requirements_file) are mutually exclusive. - - "B(Ansible 2.9): It can only be used to install either I(type=role) or I(type=collection), but not both at the same run." + - It works for O(type) equals to V(collection) and V(role). + - O(name) and O(requirements_file) are mutually exclusive. type: path dest: description: - - The path to the directory containing your collections or roles, according to the value of I(type). + - The path to the directory containing your collections or roles, according to the value of O(type). - > - Please notice that C(ansible-galaxy) will not install collections with I(type=both), when I(requirements_file) - contains both roles and collections and I(dest) is specified. + Please notice that C(ansible-galaxy) will not install collections with O(type=both), when O(requirements_file) + contains both roles and collections and O(dest) is specified. type: path no_deps: description: @@ -74,25 +70,17 @@ options: force: description: - Force overwriting an existing role or collection. - - Using I(force=true) is mandatory when downgrading. - - "B(Ansible 2.9 and 2.10): Must be C(true) to upgrade roles and collections." + - Using O(force=true) is mandatory when downgrading. type: bool default: false ack_ansible29: description: - - Acknowledge using Ansible 2.9 with its limitations, and prevents the module from generating warnings about them. - - This option is completely ignored if using a version of Ansible greater than C(2.9.x). - - Note that this option will be removed without any further deprecation warning once support - for Ansible 2.9 is removed from this module. + - This option has no longer any effect and will be removed in community.general 9.0.0. type: bool default: false ack_min_ansiblecore211: description: - - Acknowledge the module is deprecating support for Ansible 2.9 and ansible-base 2.10. - - Support for those versions will be removed in community.general 8.0.0. - At the same time, this option will be removed without any deprecation warning! - - This option is completely ignored if using a version of ansible-core/ansible-base/Ansible greater than C(2.11). - - For the sake of conciseness, setting this parameter to C(true) implies I(ack_ansible29=true). + - This option has no longer any effect and will be removed in community.general 9.0.0. type: bool default: false """ @@ -124,30 +112,29 @@ EXAMPLES = """ RETURN = """ type: - description: The value of the I(type) parameter. + description: The value of the O(type) parameter. type: str returned: always name: - description: The value of the I(name) parameter. + description: The value of the O(name) parameter. type: str returned: always dest: - description: The value of the I(dest) parameter. + description: The value of the O(dest) parameter. type: str returned: always requirements_file: - description: The value of the I(requirements_file) parameter. + description: The value of the O(requirements_file) parameter. type: str returned: always force: - description: The value of the I(force) parameter. + description: The value of the O(force) parameter. type: bool returned: always installed_roles: description: - - If I(requirements_file) is specified instead, returns dictionary with all the roles installed per path. - - If I(name) is specified, returns that role name and the version installed per path. - - "B(Ansible 2.9): Returns empty because C(ansible-galaxy) has no C(list) subcommand." + - If O(requirements_file) is specified instead, returns dictionary with all the roles installed per path. + - If O(name) is specified, returns that role name and the version installed per path. type: dict returned: always when installing roles contains: @@ -162,9 +149,8 @@ RETURN = """ ansistrano.deploy: 3.8.0 installed_collections: description: - - If I(requirements_file) is specified instead, returns dictionary with all the collections installed per path. - - If I(name) is specified, returns that collection name and the version installed per path. - - "B(Ansible 2.9): Returns empty because C(ansible-galaxy) has no C(list) subcommand." + - If O(requirements_file) is specified instead, returns dictionary with all the collections installed per path. + - If O(name) is specified, returns that collection name and the version installed per path. type: dict returned: always when installing collections contains: @@ -206,7 +192,6 @@ class AnsibleGalaxyInstall(ModuleHelper): _RE_LIST_ROLE = re.compile(r'^- (?P\w+\.\w+),\s+(?P[\d\.]+)\s*$') _RE_INSTALL_OUTPUT = None # Set after determining ansible version, see __init_module__() ansible_version = None - is_ansible29 = None output_params = ('type', 'name', 'dest', 'requirements_file', 'force', 'no_deps') module = dict( @@ -217,8 +202,18 @@ class AnsibleGalaxyInstall(ModuleHelper): dest=dict(type='path'), force=dict(type='bool', default=False), no_deps=dict(type='bool', default=False), - ack_ansible29=dict(type='bool', default=False), - ack_min_ansiblecore211=dict(type='bool', default=False), + ack_ansible29=dict( + type='bool', + default=False, + removed_in_version='9.0.0', + removed_from_collection='community.general', + ), + ack_min_ansiblecore211=dict( + type='bool', + default=False, + removed_in_version='9.0.0', + removed_from_collection='community.general', + ), ), mutually_exclusive=[('name', 'requirements_file')], required_one_of=[('name', 'requirements_file')], @@ -268,26 +263,22 @@ class AnsibleGalaxyInstall(ModuleHelper): def __init_module__(self): # self.runner = CmdRunner(self.module, command=self.command, arg_formats=self.command_args_formats, force_lang=self.force_lang) self.runner, self.ansible_version = self._get_ansible_galaxy_version() - if self.ansible_version < (2, 11) and not self.vars.ack_min_ansiblecore211: - self.module.deprecate( - "Support for Ansible 2.9 and ansible-base 2.10 is being deprecated. " - "At the same time support for them is ended, also the ack_ansible29 option will be removed. " - "Upgrading is strongly recommended, or set 'ack_min_ansiblecore211' to suppress this message.", - version="8.0.0", - collection_name="community.general", + if self.ansible_version < (2, 11): + self.module.fail_json( + msg="Support for Ansible 2.9 and ansible-base 2.10 has ben removed." ) - self.is_ansible29 = self.ansible_version < (2, 10) - if self.is_ansible29: - self._RE_INSTALL_OUTPUT = re.compile(r"^(?:.*Installing '(?P\w+\.\w+):(?P[\d\.]+)'.*" - r'|- (?P\w+\.\w+) \((?P[\d\.]+)\)' - r' was installed successfully)$') - else: - # Collection install output changed: - # ansible-base 2.10: "coll.name (x.y.z)" - # ansible-core 2.11+: "coll.name:x.y.z" - self._RE_INSTALL_OUTPUT = re.compile(r'^(?:(?P\w+\.\w+)(?: \(|:)(?P[\d\.]+)\)?' - r'|- (?P\w+\.\w+) \((?P[\d\.]+)\))' - r' was installed successfully$') + # Collection install output changed: + # ansible-base 2.10: "coll.name (x.y.z)" + # ansible-core 2.11+: "coll.name:x.y.z" + self._RE_INSTALL_OUTPUT = re.compile(r'^(?:(?P\w+\.\w+)(?: \(|:)(?P[\d\.]+)\)?' + r'|- (?P\w+\.\w+) \((?P[\d\.]+)\))' + r' was installed successfully$') + self.vars.set("new_collections", {}, change=True) + self.vars.set("new_roles", {}, change=True) + if self.vars.type != "collection": + self.vars.installed_roles = self._list_roles() + if self.vars.type != "roles": + self.vars.installed_collections = self._list_collections() def _list_element(self, _type, path_re, elem_re): def process(rc, out, err): @@ -322,24 +313,8 @@ class AnsibleGalaxyInstall(ModuleHelper): def _list_roles(self): return self._list_element('role', self._RE_LIST_PATH, self._RE_LIST_ROLE) - def _setup29(self): - self.vars.set("new_collections", {}) - self.vars.set("new_roles", {}) - self.vars.set("ansible29_change", False, change=True, output=False) - if not (self.vars.ack_ansible29 or self.vars.ack_min_ansiblecore211): - self.warn("Ansible 2.9 or older: unable to retrieve lists of roles and collections already installed") - if self.vars.requirements_file is not None and self.vars.type == 'both': - self.warn("Ansible 2.9 or older: will install only roles from requirement files") - - def _setup210plus(self): - self.vars.set("new_collections", {}, change=True) - self.vars.set("new_roles", {}, change=True) - if self.vars.type != "collection": - self.vars.installed_roles = self._list_roles() - if self.vars.type != "roles": - self.vars.installed_collections = self._list_collections() - def __run__(self): + def process(rc, out, err): for line in out.splitlines(): match = self._RE_INSTALL_OUTPUT.match(line) @@ -347,19 +322,9 @@ class AnsibleGalaxyInstall(ModuleHelper): continue if match.group("collection"): self.vars.new_collections[match.group("collection")] = match.group("cversion") - if self.is_ansible29: - self.vars.ansible29_change = True elif match.group("role"): self.vars.new_roles[match.group("role")] = match.group("rversion") - if self.is_ansible29: - self.vars.ansible29_change = True - - if self.is_ansible29: - if self.vars.type == 'both': - raise ValueError("Type 'both' not supported in Ansible 2.9") - self._setup29() - else: - self._setup210plus() + with self.runner("type galaxy_cmd force no_deps dest requirements_file name", output_process=process) as ctx: ctx.run(galaxy_cmd="install") if self.verbosity > 2: diff --git a/ansible_collections/community/general/plugins/modules/apache2_module.py b/ansible_collections/community/general/plugins/modules/apache2_module.py index 2e2456d74..a9fd72b24 100644 --- a/ansible_collections/community/general/plugins/modules/apache2_module.py +++ b/ansible_collections/community/general/plugins/modules/apache2_module.py @@ -37,7 +37,7 @@ options: description: - Identifier of the module as listed by C(apache2ctl -M). This is optional and usually determined automatically by the common convention of - appending C(_module) to I(name) as well as custom exception for popular modules. + appending V(_module) to O(name) as well as custom exception for popular modules. required: false force: description: @@ -154,7 +154,7 @@ def _get_ctl_binary(module): if ctl_binary is not None: return ctl_binary - module.fail_json(msg="Neither of apache2ctl nor apachctl found. At least one apache control binary is necessary.") + module.fail_json(msg="Neither of apache2ctl nor apachectl found. At least one apache control binary is necessary.") def _module_is_enabled(module): diff --git a/ansible_collections/community/general/plugins/modules/apk.py b/ansible_collections/community/general/plugins/modules/apk.py index e56b2165d..a6b058b93 100644 --- a/ansible_collections/community/general/plugins/modules/apk.py +++ b/ansible_collections/community/general/plugins/modules/apk.py @@ -17,7 +17,7 @@ DOCUMENTATION = ''' module: apk short_description: Manages apk packages description: - - Manages I(apk) packages for Alpine Linux. + - Manages C(apk) packages for Alpine Linux. author: "Kevin Brebanov (@kbrebanov)" extends_documentation_fragment: - community.general.attributes @@ -35,7 +35,9 @@ options: default: false name: description: - - A package name, like C(foo), or multiple packages, like C(foo, bar). + - A package name, like V(foo), or multiple packages, like V(foo,bar). + - Do not include additional whitespace when specifying multiple packages as a string. + Prefer YAML lists over comma-separating multiple package names. type: list elements: str no_cache: @@ -53,15 +55,15 @@ options: state: description: - Indicates the desired package(s) state. - - C(present) ensures the package(s) is/are present. C(installed) can be used as an alias. - - C(absent) ensures the package(s) is/are absent. C(removed) can be used as an alias. - - C(latest) ensures the package(s) is/are present and the latest version(s). + - V(present) ensures the package(s) is/are present. V(installed) can be used as an alias. + - V(absent) ensures the package(s) is/are absent. V(removed) can be used as an alias. + - V(latest) ensures the package(s) is/are present and the latest version(s). default: present choices: [ "present", "absent", "latest", "installed", "removed" ] type: str update_cache: description: - - Update repository indexes. Can be run with other steps or on it's own. + - Update repository indexes. Can be run with other steps or on its own. type: bool default: false upgrade: @@ -76,8 +78,8 @@ options: default: /etc/apk/world version_added: 5.4.0 notes: - - 'I(name) and I(upgrade) are mutually exclusive.' - - When used with a C(loop:) each package will be processed individually, it is much more efficient to pass the list directly to the I(name) option. + - 'O(name) and O(upgrade) are mutually exclusive.' + - When used with a C(loop:) each package will be processed individually, it is much more efficient to pass the list directly to the O(name) option. ''' EXAMPLES = ''' diff --git a/ansible_collections/community/general/plugins/modules/apt_repo.py b/ansible_collections/community/general/plugins/modules/apt_repo.py index 556039027..4c82587d0 100644 --- a/ansible_collections/community/general/plugins/modules/apt_repo.py +++ b/ansible_collections/community/general/plugins/modules/apt_repo.py @@ -41,7 +41,7 @@ options: remove_others: description: - Remove other then added repositories - - Used if I(state=present) + - Used if O(state=present) type: bool default: false update: diff --git a/ansible_collections/community/general/plugins/modules/apt_rpm.py b/ansible_collections/community/general/plugins/modules/apt_rpm.py index 8749086bb..de1b57411 100644 --- a/ansible_collections/community/general/plugins/modules/apt_rpm.py +++ b/ansible_collections/community/general/plugins/modules/apt_rpm.py @@ -16,7 +16,7 @@ DOCUMENTATION = ''' module: apt_rpm short_description: APT-RPM package manager description: - - Manages packages with I(apt-rpm). Both low-level (I(rpm)) and high-level (I(apt-get)) package manager binaries required. + - Manages packages with C(apt-rpm). Both low-level (C(rpm)) and high-level (C(apt-get)) package manager binaries required. extends_documentation_fragment: - community.general.attributes attributes: @@ -28,6 +28,9 @@ options: package: description: - List of packages to install, upgrade, or remove. + - Since community.general 8.0.0, may include paths to local C(.rpm) files + if O(state=installed) or O(state=present), requires C(rpm) python + module. aliases: [ name, pkg ] type: list elements: str @@ -63,6 +66,9 @@ options: type: bool default: false version_added: 6.5.0 +requirements: + - C(rpm) python package (rpm bindings), optional. Required if O(package) + option includes local files. author: - Evgenii Terechkov (@evgkrsk) ''' @@ -109,15 +115,48 @@ EXAMPLES = ''' ''' import os - -from ansible.module_utils.basic import AnsibleModule - +import re +import traceback + +from ansible.module_utils.basic import ( + AnsibleModule, + missing_required_lib, +) +from ansible.module_utils.common.text.converters import to_native + +try: + import rpm +except ImportError: + HAS_RPM_PYTHON = False + RPM_PYTHON_IMPORT_ERROR = traceback.format_exc() +else: + HAS_RPM_PYTHON = True + RPM_PYTHON_IMPORT_ERROR = None + +APT_CACHE = "/usr/bin/apt-cache" APT_PATH = "/usr/bin/apt-get" RPM_PATH = "/usr/bin/rpm" APT_GET_ZERO = "\n0 upgraded, 0 newly installed" UPDATE_KERNEL_ZERO = "\nTry to install new kernel " +def local_rpm_package_name(path): + """return package name of a local rpm passed in. + Inspired by ansible.builtin.yum""" + + ts = rpm.TransactionSet() + ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) + fd = os.open(path, os.O_RDONLY) + try: + header = ts.hdrFromFdno(fd) + except rpm.error as e: + return None + finally: + os.close(fd) + + return to_native(header[rpm.RPMTAG_NAME]) + + def query_package(module, name): # rpm -q returns 0 if the package is installed, # 1 if it is not installed @@ -128,11 +167,38 @@ def query_package(module, name): return False +def check_package_version(module, name): + # compare installed and candidate version + # if newest version already installed return True + # otherwise return False + + rc, out, err = module.run_command([APT_CACHE, "policy", name], environ_update={"LANG": "C"}) + installed = re.split("\n |: ", out)[2] + candidate = re.split("\n |: ", out)[4] + if installed >= candidate: + return True + return False + + def query_package_provides(module, name): # rpm -q returns 0 if the package is installed, # 1 if it is not installed + if name.endswith('.rpm'): + # Likely a local RPM file + if not HAS_RPM_PYTHON: + module.fail_json( + msg=missing_required_lib('rpm'), + exception=RPM_PYTHON_IMPORT_ERROR, + ) + + name = local_rpm_package_name(name) + rc, out, err = module.run_command("%s -q --provides %s" % (RPM_PATH, name)) - return rc == 0 + if rc == 0: + if check_package_version(module, name): + return True + else: + return False def update_package_db(module): diff --git a/ansible_collections/community/general/plugins/modules/archive.py b/ansible_collections/community/general/plugins/modules/archive.py index 8748fb8a3..6784aa1ac 100644 --- a/ansible_collections/community/general/plugins/modules/archive.py +++ b/ansible_collections/community/general/plugins/modules/archive.py @@ -20,7 +20,7 @@ extends_documentation_fragment: description: - Creates or extends an archive. - The source and archive are on the remote host, and the archive I(is not) copied to the local host. - - Source files can be deleted after archival by specifying I(remove=True). + - Source files can be deleted after archival by specifying O(remove=True). attributes: check_mode: support: full @@ -36,27 +36,26 @@ options: format: description: - The type of compression to use. - - Support for xz was added in Ansible 2.5. type: str choices: [ bz2, gz, tar, xz, zip ] default: gz dest: description: - The file name of the destination archive. The parent directory must exists on the remote host. - - This is required when C(path) refers to multiple files by either specifying a glob, a directory or multiple paths in a list. + - This is required when O(path) refers to multiple files by either specifying a glob, a directory or multiple paths in a list. - If the destination archive already exists, it will be truncated and overwritten. type: path exclude_path: description: - - Remote absolute path, glob, or list of paths or globs for the file or files to exclude from I(path) list and glob expansion. - - Use I(exclusion_patterns) to instead exclude files or subdirectories below any of the paths from the I(path) list. + - Remote absolute path, glob, or list of paths or globs for the file or files to exclude from O(path) list and glob expansion. + - Use O(exclusion_patterns) to instead exclude files or subdirectories below any of the paths from the O(path) list. type: list elements: path default: [] exclusion_patterns: description: - Glob style patterns to exclude files or directories from the resulting archive. - - This differs from I(exclude_path) which applies only to the source paths from I(path). + - This differs from O(exclude_path) which applies only to the source paths from O(path). type: list elements: path version_added: 3.2.0 @@ -73,7 +72,7 @@ options: type: bool default: false notes: - - Can produce I(gzip), I(bzip2), I(lzma), and I(zip) compressed files or archives. + - Can produce C(gzip), C(bzip2), C(lzma), and C(zip) compressed files or archives. - This module uses C(tarfile), C(zipfile), C(gzip), and C(bz2) packages on the target host to create archives. These are part of the Python standard library for Python 2 and 3. requirements: @@ -144,16 +143,16 @@ EXAMPLES = r''' RETURN = r''' state: description: - The state of the input C(path). + The state of the input O(path). type: str returned: always dest_state: description: - - The state of the I(dest) file. - - C(absent) when the file does not exist. - - C(archive) when the file is an archive. - - C(compress) when the file is compressed, but not an archive. - - C(incomplete) when the file is an archive, but some files under I(path) were not found. + - The state of the O(dest) file. + - V(absent) when the file does not exist. + - V(archive) when the file is an archive. + - V(compress) when the file is compressed, but not an archive. + - V(incomplete) when the file is an archive, but some files under O(path) were not found. type: str returned: success version_added: 3.4.0 diff --git a/ansible_collections/community/general/plugins/modules/atomic_container.py b/ansible_collections/community/general/plugins/modules/atomic_container.py index c26510296..d1567c892 100644 --- a/ansible_collections/community/general/plugins/modules/atomic_container.py +++ b/ansible_collections/community/general/plugins/modules/atomic_container.py @@ -21,7 +21,6 @@ notes: - Host should support C(atomic) command requirements: - atomic - - "python >= 2.6" extends_documentation_fragment: - community.general.attributes attributes: diff --git a/ansible_collections/community/general/plugins/modules/atomic_host.py b/ansible_collections/community/general/plugins/modules/atomic_host.py index bb44c4489..ebb74caf1 100644 --- a/ansible_collections/community/general/plugins/modules/atomic_host.py +++ b/ansible_collections/community/general/plugins/modules/atomic_host.py @@ -21,7 +21,6 @@ notes: - Host should be an atomic platform (verified by existence of '/run/ostree-booted' file). requirements: - atomic - - python >= 2.6 extends_documentation_fragment: - community.general.attributes attributes: @@ -33,7 +32,7 @@ options: revision: description: - The version number of the atomic host to be deployed. - - Providing C(latest) will upgrade to the latest available version. + - Providing V(latest) will upgrade to the latest available version. default: 'latest' aliases: [ version ] type: str diff --git a/ansible_collections/community/general/plugins/modules/atomic_image.py b/ansible_collections/community/general/plugins/modules/atomic_image.py index 65aec1e9d..4bd15e27a 100644 --- a/ansible_collections/community/general/plugins/modules/atomic_image.py +++ b/ansible_collections/community/general/plugins/modules/atomic_image.py @@ -21,7 +21,6 @@ notes: - Host should support C(atomic) command. requirements: - atomic - - python >= 2.6 extends_documentation_fragment: - community.general.attributes attributes: @@ -43,7 +42,7 @@ options: state: description: - The state of the container image. - - The state C(latest) will ensure container image is upgraded to the latest version and forcefully restart container, if running. + - The state V(latest) will ensure container image is upgraded to the latest version and forcefully restart container, if running. choices: [ 'absent', 'latest', 'present' ] default: 'latest' type: str diff --git a/ansible_collections/community/general/plugins/modules/awall.py b/ansible_collections/community/general/plugins/modules/awall.py index da1b29f70..f3c2384b5 100644 --- a/ansible_collections/community/general/plugins/modules/awall.py +++ b/ansible_collections/community/general/plugins/modules/awall.py @@ -16,7 +16,7 @@ short_description: Manage awall policies author: Ted Trask (@tdtrask) description: - This modules allows for enable/disable/activate of C(awall) policies. - - Alpine Wall (I(awall)) generates a firewall configuration from the enabled policy files + - Alpine Wall (C(awall)) generates a firewall configuration from the enabled policy files and activates the configuration on the system. extends_documentation_fragment: - community.general.attributes @@ -41,11 +41,11 @@ options: description: - Activate the new firewall rules. - Can be run with other steps or on its own. - - Idempotency is affected if I(activate=true), as the module will always report a changed state. + - Idempotency is affected if O(activate=true), as the module will always report a changed state. type: bool default: false notes: - - At least one of I(name) and I(activate) is required. + - At least one of O(name) and O(activate) is required. ''' EXAMPLES = r''' diff --git a/ansible_collections/community/general/plugins/modules/bearychat.py b/ansible_collections/community/general/plugins/modules/bearychat.py index 28f1f8fcd..f52737fac 100644 --- a/ansible_collections/community/general/plugins/modules/bearychat.py +++ b/ansible_collections/community/general/plugins/modules/bearychat.py @@ -27,7 +27,7 @@ options: description: - BearyChat WebHook URL. This authenticates you to the bearychat service. It looks like - C(https://hook.bearychat.com/=ae2CF/incoming/e61bd5c57b164e04b11ac02e66f47f60). + V(https://hook.bearychat.com/=ae2CF/incoming/e61bd5c57b164e04b11ac02e66f47f60). required: true text: type: str @@ -35,14 +35,14 @@ options: - Message to send. markdown: description: - - If C(true), text will be parsed as markdown. + - If V(true), text will be parsed as markdown. default: true type: bool channel: type: str description: - Channel to send the message to. If absent, the message goes to the - default channel selected by the I(url). + default channel selected by the O(url). attachments: type: list elements: dict diff --git a/ansible_collections/community/general/plugins/modules/bigpanda.py b/ansible_collections/community/general/plugins/modules/bigpanda.py index bab200bc4..7bde5fc1d 100644 --- a/ansible_collections/community/general/plugins/modules/bigpanda.py +++ b/ansible_collections/community/general/plugins/modules/bigpanda.py @@ -72,10 +72,10 @@ options: description: - Base URL of the API server. required: false - default: https://api.bigpanda.io + default: "https://api.bigpanda.io" validate_certs: description: - - If C(false), SSL certificates for the target url will not be validated. This should only be used + - If V(false), SSL certificates for the target url will not be validated. This should only be used on personally controlled sites using self-signed certificates. required: false default: true diff --git a/ansible_collections/community/general/plugins/modules/bitbucket_access_key.py b/ansible_collections/community/general/plugins/modules/bitbucket_access_key.py index 5ef199f7a..29c19b8b3 100644 --- a/ansible_collections/community/general/plugins/modules/bitbucket_access_key.py +++ b/ansible_collections/community/general/plugins/modules/bitbucket_access_key.py @@ -33,7 +33,7 @@ options: workspace: description: - The repository owner. - - I(username) used to be an alias of this option. Since community.general 6.0.0 it is an alias of I(user). + - "B(Note:) O(ignore:username) used to be an alias of this option. Since community.general 6.0.0 it is an alias of O(user)." type: str required: true key: diff --git a/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_key_pair.py b/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_key_pair.py index d39c054b1..3bc41c298 100644 --- a/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_key_pair.py +++ b/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_key_pair.py @@ -33,7 +33,7 @@ options: workspace: description: - The repository owner. - - I(username) used to be an alias of this option. Since community.general 6.0.0 it is an alias of I(user). + - "B(Note:) O(ignore:username) used to be an alias of this option. Since community.general 6.0.0 it is an alias of O(user)." type: str required: true public_key: diff --git a/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_known_host.py b/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_known_host.py index 28ff48739..3e6c4bfbf 100644 --- a/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_known_host.py +++ b/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_known_host.py @@ -14,7 +14,7 @@ module: bitbucket_pipeline_known_host short_description: Manages Bitbucket pipeline known hosts description: - Manages Bitbucket pipeline known hosts under the "SSH Keys" menu. - - The host fingerprint will be retrieved automatically, but in case of an error, one can use I(key) field to specify it manually. + - The host fingerprint will be retrieved automatically, but in case of an error, one can use O(key) field to specify it manually. author: - Evgeniy Krysanov (@catcombo) extends_documentation_fragment: @@ -36,7 +36,7 @@ options: workspace: description: - The repository owner. - - I(username) used to be an alias of this option. Since community.general 6.0.0 it is an alias of I(user). + - "B(Note:) O(ignore:username) used to be an alias of this option. Since community.general 6.0.0 it is an alias of O(user)." type: str required: true name: diff --git a/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_variable.py b/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_variable.py index eac0d18dd..1ff8e4375 100644 --- a/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_variable.py +++ b/ansible_collections/community/general/plugins/modules/bitbucket_pipeline_variable.py @@ -33,7 +33,7 @@ options: workspace: description: - The repository owner. - - I(username) used to be an alias of this option. Since community.general 6.0.0 it is an alias of I(user). + - "B(Note:) O(ignore:username) used to be an alias of this option. Since community.general 6.0.0 it is an alias of O(user)." type: str required: true name: @@ -58,7 +58,7 @@ options: choices: [ absent, present ] notes: - Check mode is supported. - - For secured values return parameter C(changed) is always C(True). + - For secured values return parameter C(changed) is always V(true). ''' EXAMPLES = r''' diff --git a/ansible_collections/community/general/plugins/modules/btrfs_subvolume.py b/ansible_collections/community/general/plugins/modules/btrfs_subvolume.py index cd2ac6f97..864bb65a6 100644 --- a/ansible_collections/community/general/plugins/modules/btrfs_subvolume.py +++ b/ansible_collections/community/general/plugins/modules/btrfs_subvolume.py @@ -23,7 +23,7 @@ options: default: false default: description: - - Make the subvolume specified by I(name) the filesystem's default subvolume. + - Make the subvolume specified by O(name) the filesystem's default subvolume. type: bool default: false filesystem_device: @@ -49,7 +49,7 @@ options: recursive: description: - When true, indicates that parent/child subvolumes should be created/removedas necessary - to complete the operation (for I(state=present) and I(state=absent) respectively). + to complete the operation (for O(state=present) and O(state=absent) respectively). type: bool default: false snapshot_source: @@ -60,11 +60,11 @@ options: snapshot_conflict: description: - Policy defining behavior when a subvolume already exists at the path of the requested snapshot. - - C(skip) - Create a snapshot only if a subvolume does not yet exist at the target location, otherwise indicate that no change is required. + - V(skip) - Create a snapshot only if a subvolume does not yet exist at the target location, otherwise indicate that no change is required. Warning, this option does not yet verify that the target subvolume was generated from a snapshot of the requested source. - - C(clobber) - If a subvolume already exists at the requested location, delete it first. + - V(clobber) - If a subvolume already exists at the requested location, delete it first. This option is not idempotent and will result in a new snapshot being generated on every execution. - - C(error) - If a subvolume already exists at the requested location, return an error. + - V(error) - If a subvolume already exists at the requested location, return an error. This option is not idempotent and will result in an error on replay of the module. type: str choices: [ skip, clobber, error ] @@ -77,7 +77,7 @@ options: default: present notes: - - If any or all of the options I(filesystem_device), I(filesystem_label) or I(filesystem_uuid) parameters are provided, there is expected + - If any or all of the options O(filesystem_device), O(filesystem_label) or O(filesystem_uuid) parameters are provided, there is expected to be a matching btrfs filesystem. If none are provided and only a single btrfs filesystem exists or only a single btrfs filesystem is mounted, that filesystem will be used; otherwise, the module will take no action and return an error. @@ -201,7 +201,7 @@ modifications: target_subvolume_id: description: - - The ID of the subvolume specified with the I(name) parameter, either pre-existing or created as part of module execution. + - The ID of the subvolume specified with the O(name) parameter, either pre-existing or created as part of module execution. type: int sample: 257 returned: Success and subvolume exists after module execution diff --git a/ansible_collections/community/general/plugins/modules/bundler.py b/ansible_collections/community/general/plugins/modules/bundler.py index 682dd334a..59f10800c 100644 --- a/ansible_collections/community/general/plugins/modules/bundler.py +++ b/ansible_collections/community/general/plugins/modules/bundler.py @@ -30,7 +30,7 @@ options: state: type: str description: - - The desired state of the Gem bundle. C(latest) updates gems to the most recent, acceptable version + - The desired state of the Gem bundle. V(latest) updates gems to the most recent, acceptable version choices: [present, latest] default: present chdir: @@ -44,19 +44,19 @@ options: elements: str description: - A list of Gemfile groups to exclude during operations. This only - applies when state is C(present). Bundler considers this + applies when O(state=present). Bundler considers this a 'remembered' property for the Gemfile and will automatically exclude - groups in future operations even if C(exclude_groups) is not set + groups in future operations even if O(exclude_groups) is not set clean: description: - - Only applies if state is C(present). If set removes any gems on the + - Only applies if O(state=present). If set removes any gems on the target host that are not in the gemfile type: bool default: false gemfile: type: path description: - - Only applies if state is C(present). The path to the gemfile to use to install gems. + - Only applies if O(state=present). The path to the gemfile to use to install gems. - If not specified it will default to the Gemfile in current directory local: description: @@ -65,31 +65,31 @@ options: default: false deployment_mode: description: - - Only applies if state is C(present). If set it will install gems in + - Only applies if O(state=present). If set it will install gems in ./vendor/bundle instead of the default location. Requires a Gemfile.lock file to have been created prior type: bool default: false user_install: description: - - Only applies if state is C(present). Installs gems in the local user's cache or for all users + - Only applies if O(state=present). Installs gems in the local user's cache or for all users type: bool default: true gem_path: type: path description: - - Only applies if state is C(present). Specifies the directory to - install the gems into. If C(chdir) is set then this path is relative to - C(chdir) + - Only applies if O(state=present). Specifies the directory to + install the gems into. If O(chdir) is set then this path is relative to + O(chdir) - If not specified the default RubyGems gem paths will be used. binstub_directory: type: path description: - - Only applies if state is C(present). Specifies the directory to + - Only applies if O(state=present). Specifies the directory to install any gem bins files to. When executed the bin files will run within the context of the Gemfile and fail if any required gem - dependencies are not installed. If C(chdir) is set then this path is - relative to C(chdir) + dependencies are not installed. If O(chdir) is set then this path is + relative to O(chdir) extra_args: type: str description: diff --git a/ansible_collections/community/general/plugins/modules/bzr.py b/ansible_collections/community/general/plugins/modules/bzr.py index e7aca7c6b..5a60d765c 100644 --- a/ansible_collections/community/general/plugins/modules/bzr.py +++ b/ansible_collections/community/general/plugins/modules/bzr.py @@ -16,7 +16,7 @@ author: - André Paramés (@andreparames) short_description: Deploy software (or files) from bzr branches description: - - Manage I(bzr) branches to deploy files or software. + - Manage C(bzr) branches to deploy files or software. extends_documentation_fragment: - community.general.attributes attributes: @@ -44,9 +44,8 @@ options: type: str force: description: - - If C(true), any modified files in the working - tree will be discarded. Before 1.9 the default - value was C(true). + - If V(true), any modified files in the working + tree will be discarded. type: bool default: false executable: diff --git a/ansible_collections/community/general/plugins/modules/capabilities.py b/ansible_collections/community/general/plugins/modules/capabilities.py index 9b72ac6ea..a0b6d5222 100644 --- a/ansible_collections/community/general/plugins/modules/capabilities.py +++ b/ansible_collections/community/general/plugins/modules/capabilities.py @@ -30,7 +30,7 @@ options: aliases: [ key ] capability: description: - - Desired capability to set (with operator and flags, if state is C(present)) or remove (if state is C(absent)) + - Desired capability to set (with operator and flags, if O(state=present)) or remove (if O(state=absent)) type: str required: true aliases: [ cap ] diff --git a/ansible_collections/community/general/plugins/modules/cargo.py b/ansible_collections/community/general/plugins/modules/cargo.py index 24be43741..ba9c05ed7 100644 --- a/ansible_collections/community/general/plugins/modules/cargo.py +++ b/ansible_collections/community/general/plugins/modules/cargo.py @@ -25,6 +25,12 @@ attributes: diff_mode: support: none options: + executable: + description: + - Path to the C(cargo) installed in the system. + - If not specified, the module will look C(cargo) in E(PATH). + type: path + version_added: 7.5.0 name: description: - The name of a Rust package to install. @@ -35,15 +41,23 @@ options: description: -> The base path where to install the Rust packages. Cargo automatically appends - C(/bin). In other words, C(/usr/local) will become C(/usr/local/bin). + V(/bin). In other words, V(/usr/local) will become V(/usr/local/bin). type: path version: description: -> - The version to install. If I(name) contains multiple values, the module will + The version to install. If O(name) contains multiple values, the module will try to install all of them in this version. type: str required: false + locked: + description: + - Install with locked dependencies. + - This is only used when installing packages. + required: false + type: bool + default: false + version_added: 7.5.0 state: description: - The state of the Rust package. @@ -52,7 +66,7 @@ options: default: present choices: [ "present", "absent", "latest" ] requirements: - - cargo installed in bin path (recommended /usr/local/bin) + - cargo installed """ EXAMPLES = r""" @@ -60,6 +74,11 @@ EXAMPLES = r""" community.general.cargo: name: ludusavi +- name: Install "ludusavi" Rust package with locked dependencies + community.general.cargo: + name: ludusavi + locked: true + - name: Install "ludusavi" Rust package in version 0.10.0 community.general.cargo: name: ludusavi @@ -90,12 +109,12 @@ from ansible.module_utils.basic import AnsibleModule class Cargo(object): def __init__(self, module, **kwargs): self.module = module + self.executable = [kwargs["executable"] or module.get_bin_path("cargo", True)] self.name = kwargs["name"] self.path = kwargs["path"] self.state = kwargs["state"] self.version = kwargs["version"] - - self.executable = [module.get_bin_path("cargo", True)] + self.locked = kwargs["locked"] @property def path(self): @@ -118,6 +137,10 @@ class Cargo(object): def get_installed(self): cmd = ["install", "--list"] + if self.path: + cmd.append("--root") + cmd.append(self.path) + data, dummy = self._exec(cmd, True, False, False) package_regex = re.compile(r"^([\w\-]+) v(.+):$") @@ -132,6 +155,8 @@ class Cargo(object): def install(self, packages=None): cmd = ["install"] cmd.extend(packages or self.name) + if self.locked: + cmd.append("--locked") if self.path: cmd.append("--root") cmd.append(self.path) @@ -160,15 +185,16 @@ class Cargo(object): def main(): arg_spec = dict( + executable=dict(default=None, type="path"), name=dict(required=True, type="list", elements="str"), path=dict(default=None, type="path"), state=dict(default="present", choices=["present", "absent", "latest"]), version=dict(default=None, type="str"), + locked=dict(default=False, type="bool"), ) module = AnsibleModule(argument_spec=arg_spec, supports_check_mode=True) name = module.params["name"] - path = module.params["path"] state = module.params["state"] version = module.params["version"] @@ -180,7 +206,7 @@ def main(): LANG="C", LC_ALL="C", LC_MESSAGES="C", LC_CTYPE="C" ) - cargo = Cargo(module, name=name, path=path, state=state, version=version) + cargo = Cargo(module, **module.params) changed, out, err = False, None, None installed_packages = cargo.get_installed() if state == "present": diff --git a/ansible_collections/community/general/plugins/modules/catapult.py b/ansible_collections/community/general/plugins/modules/catapult.py index a3bbef6c4..acd839851 100644 --- a/ansible_collections/community/general/plugins/modules/catapult.py +++ b/ansible_collections/community/general/plugins/modules/catapult.py @@ -28,13 +28,13 @@ options: src: type: str description: - - One of your catapult telephone numbers the message should come from (must be in E.164 format, like C(+19195551212)). + - One of your catapult telephone numbers the message should come from (must be in E.164 format, like V(+19195551212)). required: true dest: type: list elements: str description: - - The phone number or numbers the message should be sent to (must be in E.164 format, like C(+19195551212)). + - The phone number or numbers the message should be sent to (must be in E.164 format, like V(+19195551212)). required: true msg: type: str diff --git a/ansible_collections/community/general/plugins/modules/circonus_annotation.py b/ansible_collections/community/general/plugins/modules/circonus_annotation.py index 937610776..f3b94a052 100644 --- a/ansible_collections/community/general/plugins/modules/circonus_annotation.py +++ b/ansible_collections/community/general/plugins/modules/circonus_annotation.py @@ -52,12 +52,12 @@ options: type: int description: - Unix timestamp of event start - - If not specified, it defaults to I(now). + - If not specified, it defaults to "now". stop: type: int description: - Unix timestamp of event end - - If not specified, it defaults to I(now) + I(duration). + - If not specified, it defaults to "now" + O(duration). duration: type: int description: diff --git a/ansible_collections/community/general/plugins/modules/cisco_webex.py b/ansible_collections/community/general/plugins/modules/cisco_webex.py index 2e5cb50ea..caa77f576 100644 --- a/ansible_collections/community/general/plugins/modules/cisco_webex.py +++ b/ansible_collections/community/general/plugins/modules/cisco_webex.py @@ -17,7 +17,7 @@ description: - Send a message to a Cisco Webex Teams Room or Individual with options to control the formatting. author: Drew Rusell (@drew-russell) notes: - - The C(recipient_id) type must be valid for the supplied C(recipient_id). + - The O(recipient_type) must be valid for the supplied O(recipient_id). - Full API documentation can be found at U(https://developer.webex.com/docs/api/basics). extends_documentation_fragment: @@ -40,7 +40,7 @@ options: recipient_id: description: - - The unique identifier associated with the supplied C(recipient_type). + - The unique identifier associated with the supplied O(recipient_type). required: true type: str diff --git a/ansible_collections/community/general/plugins/modules/clc_firewall_policy.py b/ansible_collections/community/general/plugins/modules/clc_firewall_policy.py index c832571d3..b30037c6f 100644 --- a/ansible_collections/community/general/plugins/modules/clc_firewall_policy.py +++ b/ansible_collections/community/general/plugins/modules/clc_firewall_policy.py @@ -49,7 +49,7 @@ options: description: - The list of ports associated with the policy. TCP and UDP can take in single ports or port ranges. - - "Example: C(['any', 'icmp', 'TCP/123', 'UDP/123', 'TCP/123-456', 'UDP/123-456'])." + - "Example: V(['any', 'icmp', 'TCP/123', 'UDP/123', 'TCP/123-456', 'UDP/123-456'])." type: list elements: str firewall_policy_id: diff --git a/ansible_collections/community/general/plugins/modules/clc_server.py b/ansible_collections/community/general/plugins/modules/clc_server.py index d2d019ff0..6bfe5a9b9 100644 --- a/ansible_collections/community/general/plugins/modules/clc_server.py +++ b/ansible_collections/community/general/plugins/modules/clc_server.py @@ -1501,7 +1501,7 @@ class ClcServer: return aa_policy_id # - # This is the function that gets patched to the Request.server object using a lamda closure + # This is the function that gets patched to the Request.server object using a lambda closure # @staticmethod diff --git a/ansible_collections/community/general/plugins/modules/cloudflare_dns.py b/ansible_collections/community/general/plugins/modules/cloudflare_dns.py index 8f45fcef3..d2bea4266 100644 --- a/ansible_collections/community/general/plugins/modules/cloudflare_dns.py +++ b/ansible_collections/community/general/plugins/modules/cloudflare_dns.py @@ -13,8 +13,6 @@ DOCUMENTATION = r''' module: cloudflare_dns author: - Michael Gruener (@mgruener) -requirements: - - python >= 2.6 short_description: Manage Cloudflare DNS records description: - "Manages dns records via the Cloudflare API, see the docs: U(https://api.cloudflare.com/)." @@ -31,7 +29,7 @@ options: - API token. - Required for api token authentication. - "You can obtain your API token from the bottom of the Cloudflare 'My Account' page, found here: U(https://dash.cloudflare.com/)." - - Can be specified in C(CLOUDFLARE_TOKEN) environment variable since community.general 2.0.0. + - Can be specified in E(CLOUDFLARE_TOKEN) environment variable since community.general 2.0.0. type: str required: false version_added: '0.2.0' @@ -51,41 +49,54 @@ options: algorithm: description: - Algorithm number. - - Required for I(type=DS) and I(type=SSHFP) when I(state=present). + - Required for O(type=DS) and O(type=SSHFP) when O(state=present). type: int cert_usage: description: - Certificate usage number. - - Required for I(type=TLSA) when I(state=present). + - Required for O(type=TLSA) when O(state=present). type: int choices: [ 0, 1, 2, 3 ] + flag: + description: + - Issuer Critical Flag. + - Required for O(type=CAA) when O(state=present). + type: int + choices: [ 0, 1 ] + version_added: 8.0.0 + tag: + description: + - CAA issue restriction. + - Required for O(type=CAA) when O(state=present). + type: str + choices: [ issue, issuewild, iodef ] + version_added: 8.0.0 hash_type: description: - Hash type number. - - Required for I(type=DS), I(type=SSHFP) and I(type=TLSA) when I(state=present). + - Required for O(type=DS), O(type=SSHFP) and O(type=TLSA) when O(state=present). type: int choices: [ 1, 2 ] key_tag: description: - DNSSEC key tag. - - Needed for I(type=DS) when I(state=present). + - Needed for O(type=DS) when O(state=present). type: int port: description: - Service port. - - Required for I(type=SRV) and I(type=TLSA). + - Required for O(type=SRV) and O(type=TLSA). type: int priority: description: - Record priority. - - Required for I(type=MX) and I(type=SRV) + - Required for O(type=MX) and O(type=SRV) default: 1 type: int proto: description: - - Service protocol. Required for I(type=SRV) and I(type=TLSA). + - Service protocol. Required for O(type=SRV) and O(type=TLSA). - Common values are TCP and UDP. - - Before Ansible 2.6 only TCP and UDP were available. type: str proxied: description: @@ -95,26 +106,26 @@ options: record: description: - Record to add. - - Required if I(state=present). - - Default is C(@) (e.g. the zone name). + - Required if O(state=present). + - Default is V(@) (that is, the zone name). type: str default: '@' aliases: [ name ] selector: description: - Selector number. - - Required for I(type=TLSA) when I(state=present). + - Required for O(type=TLSA) when O(state=present). choices: [ 0, 1 ] type: int service: description: - Record service. - - Required for I(type=SRV). + - Required for O(type=SRV). type: str solo: description: - Whether the record should be the only one for that record type and record name. - - Only use with I(state=present). + - Only use with O(state=present). - This will delete all other records with the same record name and type. type: bool state: @@ -136,20 +147,20 @@ options: default: 1 type: description: - - The type of DNS record to create. Required if I(state=present). - - I(type=DS), I(type=SSHFP) and I(type=TLSA) added in Ansible 2.7. + - The type of DNS record to create. Required if O(state=present). + - Note that V(SPF) is no longer supported by CloudFlare. Support for it will be removed from community.general 9.0.0. type: str - choices: [ A, AAAA, CNAME, DS, MX, NS, SPF, SRV, SSHFP, TLSA, TXT ] + choices: [ A, AAAA, CNAME, DS, MX, NS, SPF, SRV, SSHFP, TLSA, CAA, TXT ] value: description: - The record value. - - Required for I(state=present). + - Required for O(state=present). type: str aliases: [ content ] weight: description: - Service weight. - - Required for I(type=SRV). + - Required for O(type=SRV). type: int default: 1 zone: @@ -262,6 +273,15 @@ EXAMPLES = r''' hash_type: 1 value: 6b76d034492b493e15a7376fccd08e63befdad0edab8e442562f532338364bf3 +- name: Create a CAA record subdomain.example.com + community.general.cloudflare_dns: + zone: example.com + record: subdomain + type: CAA + flag: 0 + tag: issue + value: ca.example.com + - name: Create a DS record for subdomain.example.com community.general.cloudflare_dns: zone: example.com @@ -291,7 +311,7 @@ record: sample: "2016-03-25T19:09:42.516553Z" data: description: Additional record data. - returned: success, if type is SRV, DS, SSHFP or TLSA + returned: success, if type is SRV, DS, SSHFP TLSA or CAA type: dict sample: { name: "jabber", @@ -391,6 +411,8 @@ class CloudflareAPI(object): self.algorithm = module.params['algorithm'] self.cert_usage = module.params['cert_usage'] self.hash_type = module.params['hash_type'] + self.flag = module.params['flag'] + self.tag = module.params['tag'] self.key_tag = module.params['key_tag'] self.port = module.params['port'] self.priority = module.params['priority'] @@ -595,7 +617,7 @@ class CloudflareAPI(object): def delete_dns_records(self, **kwargs): params = {} for param in ['port', 'proto', 'service', 'solo', 'type', 'record', 'value', 'weight', 'zone', - 'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag']: + 'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag', 'flag', 'tag']: if param in kwargs: params[param] = kwargs[param] else: @@ -613,7 +635,7 @@ class CloudflareAPI(object): content = str(params['key_tag']) + '\t' + str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value'] elif params['type'] == 'SSHFP': if not (params['value'] is None or params['value'] == ''): - content = str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value'] + content = str(params['algorithm']) + ' ' + str(params['hash_type']) + ' ' + params['value'].upper() elif params['type'] == 'TLSA': if not (params['value'] is None or params['value'] == ''): content = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value'] @@ -640,7 +662,7 @@ class CloudflareAPI(object): def ensure_dns_record(self, **kwargs): params = {} for param in ['port', 'priority', 'proto', 'proxied', 'service', 'ttl', 'type', 'record', 'value', 'weight', 'zone', - 'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag']: + 'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag', 'flag', 'tag']: if param in kwargs: params[param] = kwargs[param] else: @@ -726,7 +748,7 @@ class CloudflareAPI(object): if (attr is None) or (attr == ''): self.module.fail_json(msg="You must provide algorithm, hash_type and a value to create this record type") sshfp_data = { - "fingerprint": params['value'], + "fingerprint": params['value'].upper(), "type": params['hash_type'], "algorithm": params['algorithm'], } @@ -736,7 +758,7 @@ class CloudflareAPI(object): 'data': sshfp_data, "ttl": params['ttl'], } - search_value = str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value'] + search_value = str(params['algorithm']) + ' ' + str(params['hash_type']) + ' ' + params['value'] if params['type'] == 'TLSA': for attr in [params['port'], params['proto'], params['cert_usage'], params['selector'], params['hash_type'], params['value']]: @@ -757,12 +779,36 @@ class CloudflareAPI(object): } search_value = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value'] + if params['type'] == 'CAA': + for attr in [params['flag'], params['tag'], params['value']]: + if (attr is None) or (attr == ''): + self.module.fail_json(msg="You must provide flag, tag and a value to create this record type") + caa_data = { + "flags": params['flag'], + "tag": params['tag'], + "value": params['value'], + } + new_record = { + "type": params['type'], + "name": params['record'], + 'data': caa_data, + "ttl": params['ttl'], + } + search_value = None + zone_id = self._get_zone_id(params['zone']) records = self.get_dns_records(params['zone'], params['type'], search_record, search_value) # in theory this should be impossible as cloudflare does not allow # the creation of duplicate records but lets cover it anyways if len(records) > 1: - self.module.fail_json(msg="More than one record already exists for the given attributes. That should be impossible, please open an issue!") + # As Cloudflare API cannot filter record containing quotes + # CAA records must be compared locally + if params['type'] == 'CAA': + for rr in records: + if rr['data']['flags'] == caa_data['flags'] and rr['data']['tag'] == caa_data['tag'] and rr['data']['value'] == caa_data['value']: + return rr, self.changed + else: + self.module.fail_json(msg="More than one record already exists for the given attributes. That should be impossible, please open an issue!") # record already exists, check if it must be updated if len(records) == 1: cur_record = records[0] @@ -811,6 +857,8 @@ def main(): hash_type=dict(type='int', choices=[1, 2]), key_tag=dict(type='int', no_log=False), port=dict(type='int'), + flag=dict(type='int', choices=[0, 1]), + tag=dict(type='str', choices=['issue', 'issuewild', 'iodef']), priority=dict(type='int', default=1), proto=dict(type='str'), proxied=dict(type='bool', default=False), @@ -821,7 +869,7 @@ def main(): state=dict(type='str', default='present', choices=['absent', 'present']), timeout=dict(type='int', default=30), ttl=dict(type='int', default=1), - type=dict(type='str', choices=['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'SPF', 'SRV', 'SSHFP', 'TLSA', 'TXT']), + type=dict(type='str', choices=['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'SPF', 'SRV', 'SSHFP', 'TLSA', 'CAA', 'TXT']), value=dict(type='str', aliases=['content']), weight=dict(type='int', default=1), zone=dict(type='str', required=True, aliases=['domain']), @@ -832,6 +880,7 @@ def main(): ('state', 'absent', ['record']), ('type', 'SRV', ['proto', 'service']), ('type', 'TLSA', ['proto', 'port']), + ('type', 'CAA', ['flag', 'tag']), ], ) @@ -858,6 +907,13 @@ def main(): and (module.params['value'] is None or module.params['value'] == ''))): module.fail_json(msg="For TLSA records the params cert_usage, selector, hash_type and value all need to be defined, or not at all.") + if module.params['type'] == 'CAA': + if not ((module.params['flag'] is not None and module.params['tag'] is not None + and not (module.params['value'] is None or module.params['value'] == '')) + or (module.params['flag'] is None and module.params['tag'] is None + and (module.params['value'] is None or module.params['value'] == ''))): + module.fail_json(msg="For CAA records the params flag, tag and value all need to be defined, or not at all.") + if module.params['type'] == 'DS': if not ((module.params['key_tag'] is not None and module.params['algorithm'] is not None and module.params['hash_type'] is not None and not (module.params['value'] is None or module.params['value'] == '')) diff --git a/ansible_collections/community/general/plugins/modules/cobbler_sync.py b/ansible_collections/community/general/plugins/modules/cobbler_sync.py index d7acf4be6..4ec87c96c 100644 --- a/ansible_collections/community/general/plugins/modules/cobbler_sync.py +++ b/ansible_collections/community/general/plugins/modules/cobbler_sync.py @@ -30,7 +30,7 @@ options: port: description: - Port number to be used for REST connection. - - The default value depends on parameter C(use_ssl). + - The default value depends on parameter O(use_ssl). type: int username: description: @@ -43,13 +43,13 @@ options: type: str use_ssl: description: - - If C(false), an HTTP connection will be used instead of the default HTTPS connection. + - If V(false), an HTTP connection will be used instead of the default HTTPS connection. type: bool default: true validate_certs: description: - - If C(false), SSL certificates will not be validated. - - This should only set to C(false) when used on personally controlled sites using self-signed certificates. + - If V(false), SSL certificates will not be validated. + - This should only set to V(false) when used on personally controlled sites using self-signed certificates. type: bool default: true author: diff --git a/ansible_collections/community/general/plugins/modules/cobbler_system.py b/ansible_collections/community/general/plugins/modules/cobbler_system.py index c30b4f1c1..cecc02f71 100644 --- a/ansible_collections/community/general/plugins/modules/cobbler_system.py +++ b/ansible_collections/community/general/plugins/modules/cobbler_system.py @@ -30,7 +30,7 @@ options: port: description: - Port number to be used for REST connection. - - The default value depends on parameter C(use_ssl). + - The default value depends on parameter O(use_ssl). type: int username: description: @@ -43,13 +43,13 @@ options: type: str use_ssl: description: - - If C(false), an HTTP connection will be used instead of the default HTTPS connection. + - If V(false), an HTTP connection will be used instead of the default HTTPS connection. type: bool default: true validate_certs: description: - - If C(false), SSL certificates will not be validated. - - This should only set to C(false) when used on personally controlled sites using self-signed certificates. + - If V(false), SSL certificates will not be validated. + - This should only set to V(false) when used on personally controlled sites using self-signed certificates. type: bool default: true name: @@ -144,11 +144,11 @@ EXAMPLES = r''' RETURN = r''' systems: description: List of systems - returned: I(state=query) and I(name) is not provided + returned: O(state=query) and O(name) is not provided type: list system: description: (Resulting) information about the system we are working with - returned: when I(name) is provided + returned: when O(name) is provided type: dict ''' diff --git a/ansible_collections/community/general/plugins/modules/composer.py b/ansible_collections/community/general/plugins/modules/composer.py index 793abcda1..3d1c4a346 100644 --- a/ansible_collections/community/general/plugins/modules/composer.py +++ b/ansible_collections/community/general/plugins/modules/composer.py @@ -49,7 +49,7 @@ options: description: - Directory of your project (see --working-dir). This is required when the command is not run globally. - - Will be ignored if I(global_command=true). + - Will be ignored if O(global_command=true). global_command: description: - Runs the specified command globally. @@ -107,11 +107,11 @@ options: composer_executable: type: path description: - - Path to composer executable on the remote host, if composer is not in C(PATH) or a custom composer is needed. + - Path to composer executable on the remote host, if composer is not in E(PATH) or a custom composer is needed. version_added: 3.2.0 requirements: - php - - composer installed in bin path (recommended /usr/local/bin) or specified in I(composer_executable) + - composer installed in bin path (recommended /usr/local/bin) or specified in O(composer_executable) notes: - Default options that are always appended in each execution are --no-ansi, --no-interaction and --no-progress if available. - We received reports about issues on macOS if composer was installed by Homebrew. Please use the official install method to avoid issues. @@ -170,10 +170,15 @@ def get_available_options(module, command='install'): return command_help_json['definition']['options'] -def composer_command(module, command, arguments="", options=None, global_command=False): +def composer_command(module, command, arguments="", options=None): if options is None: options = [] + global_command = module.params['global_command'] + + if not global_command: + options.extend(['--working-dir', "'%s'" % module.params['working_dir']]) + if module.params['executable'] is None: php_path = module.get_bin_path("php", True, ["/usr/local/bin"]) else: @@ -217,7 +222,6 @@ def main(): module.fail_json(msg="Use the 'arguments' param for passing arguments with the 'command'") arguments = module.params['arguments'] - global_command = module.params['global_command'] available_options = get_available_options(module=module, command=command) options = [] @@ -234,9 +238,6 @@ def main(): option = "--%s" % option options.append(option) - if not global_command: - options.extend(['--working-dir', "'%s'" % module.params['working_dir']]) - option_params = { 'prefer_source': 'prefer-source', 'prefer_dist': 'prefer-dist', @@ -260,7 +261,7 @@ def main(): else: module.exit_json(skipped=True, msg="command '%s' does not support check mode, skipping" % command) - rc, out, err = composer_command(module, command, arguments, options, global_command) + rc, out, err = composer_command(module, command, arguments, options) if rc != 0: output = parse_out(err) diff --git a/ansible_collections/community/general/plugins/modules/consul.py b/ansible_collections/community/general/plugins/modules/consul.py index cc599be36..fe1a89883 100644 --- a/ansible_collections/community/general/plugins/modules/consul.py +++ b/ansible_collections/community/general/plugins/modules/consul.py @@ -21,8 +21,8 @@ description: notify the health of the entire node to the cluster. Service level checks do not require a check name or id as these are derived by Consul from the Service name and id respectively by appending 'service:' - Node level checks require a I(check_name) and optionally a I(check_id)." - - Currently, there is no complete way to retrieve the script, interval or ttl + Node level checks require a O(check_name) and optionally a O(check_id)." + - Currently, there is no complete way to retrieve the script, interval or TTL metadata for a registered check. Without this metadata it is not possible to tell if the data supplied with ansible represents a change to a check. As a result this does not attempt to determine changes and will always report a @@ -56,7 +56,7 @@ options: service_id: type: str description: - - The ID for the service, must be unique per node. If I(state=absent), + - The ID for the service, must be unique per node. If O(state=absent), defaults to the service name if supplied. host: type: str @@ -86,12 +86,12 @@ options: type: int description: - The port on which the service is listening. Can optionally be supplied for - registration of a service, i.e. if I(service_name) or I(service_id) is set. + registration of a service, that is if O(service_name) or O(service_id) is set. service_address: type: str description: - The address to advertise that the service will be listening on. - This value will be passed as the I(address) parameter to Consul's + This value will be passed as the C(address) parameter to Consul's C(/v1/agent/service/register) API method, so refer to the Consul API documentation for further details. tags: @@ -103,55 +103,69 @@ options: type: str description: - The script/command that will be run periodically to check the health of the service. - - Requires I(interval) to be provided. + - Requires O(interval) to be provided. + - Mutually exclusive with O(ttl), O(tcp) and O(http). interval: type: str description: - The interval at which the service check will be run. - This is a number with a C(s) or C(m) suffix to signify the units of seconds or minutes e.g C(15s) or C(1m). - If no suffix is supplied C(s) will be used by default, e.g. C(10) will be C(10s). - - Required if one of the parameters I(script), I(http), or I(tcp) is specified. + This is a number with a V(s) or V(m) suffix to signify the units of seconds or minutes, for example V(15s) or V(1m). + If no suffix is supplied V(s) will be used by default, for example V(10) will be V(10s). + - Required if one of the parameters O(script), O(http), or O(tcp) is specified. check_id: type: str description: - - An ID for the service check. If I(state=absent), defaults to - I(check_name). Ignored if part of a service definition. + - An ID for the service check. If O(state=absent), defaults to + O(check_name). Ignored if part of a service definition. check_name: type: str description: - Name for the service check. Required if standalone, ignored if part of service definition. + check_node: + description: + - Node name. + # TODO: properly document! + type: str + check_host: + description: + - Host name. + # TODO: properly document! + type: str ttl: type: str description: - - Checks can be registered with a ttl instead of a I(script) and I(interval) + - Checks can be registered with a TTL instead of a O(script) and O(interval) this means that the service will check in with the agent before the - ttl expires. If it doesn't the check will be considered failed. + TTL expires. If it doesn't the check will be considered failed. Required if registering a check and the script an interval are missing - Similar to the interval this is a number with a C(s) or C(m) suffix to - signify the units of seconds or minutes e.g C(15s) or C(1m). - If no suffix is supplied C(s) will be used by default, e.g. C(10) will be C(10s). + Similar to the interval this is a number with a V(s) or V(m) suffix to + signify the units of seconds or minutes, for example V(15s) or V(1m). + If no suffix is supplied V(s) will be used by default, for example V(10) will be V(10s). + - Mutually exclusive with O(script), O(tcp) and O(http). tcp: type: str description: - Checks can be registered with a TCP port. This means that consul will check if the connection attempt to that port is successful (that is, the port is currently accepting connections). - The format is C(host:port), for example C(localhost:80). - - Requires I(interval) to be provided. + The format is V(host:port), for example V(localhost:80). + - Requires O(interval) to be provided. + - Mutually exclusive with O(script), O(ttl) and O(http). version_added: '1.3.0' http: type: str description: - Checks can be registered with an HTTP endpoint. This means that consul will check that the http endpoint returns a successful HTTP status. - - Requires I(interval) to be provided. + - Requires O(interval) to be provided. + - Mutually exclusive with O(script), O(ttl) and O(tcp). timeout: type: str description: - A custom HTTP check timeout. The consul default is 10 seconds. - Similar to the interval this is a number with a C(s) or C(m) suffix to - signify the units of seconds or minutes, e.g. C(15s) or C(1m). - If no suffix is supplied C(s) will be used by default, e.g. C(10) will be C(10s). + Similar to the interval this is a number with a V(s) or V(m) suffix to + signify the units of seconds or minutes, for example V(15s) or V(1m). + If no suffix is supplied V(s) will be used by default, for example V(10) will be V(10s). token: type: str description: @@ -159,7 +173,7 @@ options: ack_params_state_absent: type: bool description: - - Disable deprecation warning when using parameters incompatible with I(state=absent). + - This parameter has no more effect and is deprecated. It will be removed in community.general 10.0.0. ''' EXAMPLES = ''' @@ -377,13 +391,7 @@ def get_service_by_id_or_name(consul_api, service_id_or_name): def parse_check(module): - _checks = [module.params[p] for p in ('script', 'ttl', 'tcp', 'http') if module.params[p]] - - if len(_checks) > 1: - module.fail_json( - msg='checks are either script, tcp, http or ttl driven, supplying more than one does not make sense') - - if module.params['check_id'] or _checks: + if module.params['check_id'] or any(module.params[p] is not None for p in ('script', 'ttl', 'tcp', 'http')): return ConsulCheck( module.params['check_id'], module.params['check_name'], @@ -501,15 +509,9 @@ class ConsulCheck(object): self.check = consul.Check.ttl(self.ttl) if http: - if interval is None: - raise Exception('http check must specify interval') - self.check = consul.Check.http(http, self.interval, self.timeout) if tcp: - if interval is None: - raise Exception('tcp check must specify interval') - regex = r"(?P.*):(?P(?:[0-9]+))$" match = re.match(regex, tcp) @@ -596,30 +598,33 @@ def main(): timeout=dict(type='str'), tags=dict(type='list', elements='str'), token=dict(no_log=True), - ack_params_state_absent=dict(type='bool'), + ack_params_state_absent=dict( + type='bool', + removed_in_version='10.0.0', + removed_from_collection='community.general', + ), ), + mutually_exclusive=[ + ('script', 'ttl', 'tcp', 'http'), + ], required_if=[ ('state', 'present', ['service_name']), ('state', 'absent', ['service_id', 'service_name', 'check_id', 'check_name'], True), ], + required_by={ + 'script': 'interval', + 'http': 'interval', + 'tcp': 'interval', + }, supports_check_mode=False, ) p = module.params test_dependencies(module) - if p['state'] == 'absent' and any(p[x] for x in ['script', 'ttl', 'tcp', 'http', 'interval']) and not p['ack_params_state_absent']: - module.deprecate( - "The use of parameters 'script', 'ttl', 'tcp', 'http', 'interval' along with 'state=absent' is deprecated. " - "In community.general 8.0.0 their use will become an error. " - "To suppress this deprecation notice, set parameter ack_params_state_absent=true.", - version="8.0.0", - collection_name="community.general", + if p['state'] == 'absent' and any(p[x] for x in ['script', 'ttl', 'tcp', 'http', 'interval']): + module.fail_json( + msg="The use of parameters 'script', 'ttl', 'tcp', 'http', 'interval' along with 'state=absent' is no longer allowed." ) - # When reaching c.g 8.0.0: - # - Replace the deprecation with a fail_json(), remove the "ack_params_state_absent" condition from the "if" - # - Add mutually_exclusive for ('script', 'ttl', 'tcp', 'http'), then remove that validation from parse_check() - # - Add required_by {'script': 'interval', 'http': 'interval', 'tcp': 'interval'}, then remove checks for 'interval' in ConsulCheck.__init__() - # - Deprecate the parameter ack_params_state_absent try: register_with_consul(module) diff --git a/ansible_collections/community/general/plugins/modules/consul_acl.py b/ansible_collections/community/general/plugins/modules/consul_acl.py index 91f955228..4617090fd 100644 --- a/ansible_collections/community/general/plugins/modules/consul_acl.py +++ b/ansible_collections/community/general/plugins/modules/consul_acl.py @@ -26,6 +26,10 @@ attributes: support: none diff_mode: support: none +deprecated: + removed_in: 10.0.0 + why: The legacy ACL system was removed from Consul. + alternative: Use M(community.general.consul_token) and/or M(community.general.consul_policy) instead. options: mgmt_token: description: @@ -156,7 +160,7 @@ token: rules: description: the HCL JSON representation of the rules associated to the ACL, in the format described in the Consul documentation (https://www.consul.io/docs/guides/acl.html#rule-specification). - returned: I(status) == "present" + returned: when O(state=present) type: dict sample: { "key": { diff --git a/ansible_collections/community/general/plugins/modules/consul_acl_bootstrap.py b/ansible_collections/community/general/plugins/modules/consul_acl_bootstrap.py new file mode 100644 index 000000000..bf1da110b --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/consul_acl_bootstrap.py @@ -0,0 +1,108 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2024, Florian Apolloner (@apollo13) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +module: consul_acl_bootstrap +short_description: Bootstrap ACLs in Consul +version_added: 8.3.0 +description: + - Allows bootstrapping of ACLs in a Consul cluster, see + U(https://developer.hashicorp.com/consul/api-docs/acl#bootstrap-acls) for details. +author: + - Florian Apolloner (@apollo13) +extends_documentation_fragment: + - community.general.consul + - community.general.attributes +attributes: + check_mode: + support: none + diff_mode: + support: none +options: + state: + description: + - Whether the token should be present or absent. + choices: ['present', 'bootstrapped'] + default: present + type: str + bootstrap_secret: + description: + - The secret to be used as secret ID for the initial token. + - Needs to be an UUID. + type: str +""" + +EXAMPLES = """ +- name: Bootstrap the ACL system + community.general.consul_acl_bootstrap: + bootstrap_secret: 22eaeed1-bdbd-4651-724e-42ae6c43e387 +""" + +RETURN = """ +result: + description: + - The bootstrap result as returned by the consul HTTP API. + - "B(Note:) If O(bootstrap_secret) has been specified the C(SecretID) and + C(ID) will not contain the secret but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER). + If you pass O(bootstrap_secret), make sure your playbook/role does not depend + on this return value!" + returned: changed + type: dict + sample: + AccessorID: 834a5881-10a9-a45b-f63c-490e28743557 + CreateIndex: 25 + CreateTime: '2024-01-21T20:26:27.114612038+01:00' + Description: Bootstrap Token (Global Management) + Hash: X2AgaFhnQGRhSSF/h0m6qpX1wj/HJWbyXcxkEM/5GrY= + ID: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER + Local: false + ModifyIndex: 25 + Policies: + - ID: 00000000-0000-0000-0000-000000000001 + Name: global-management + SecretID: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + RequestError, + _ConsulModule, +) + +_ARGUMENT_SPEC = { + "state": dict(type="str", choices=["present", "bootstrapped"], default="present"), + "bootstrap_secret": dict(type="str", no_log=True), +} +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) +_ARGUMENT_SPEC.pop("token") + + +def main(): + module = AnsibleModule(_ARGUMENT_SPEC) + consul_module = _ConsulModule(module) + + data = {} + if "bootstrap_secret" in module.params: + data["BootstrapSecret"] = module.params["bootstrap_secret"] + + try: + response = consul_module.put("acl/bootstrap", data=data) + except RequestError as e: + if e.status == 403 and b"ACL bootstrap no longer allowed" in e.response_data: + return module.exit_json(changed=False) + raise + else: + return module.exit_json(changed=True, result=response) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/consul_auth_method.py b/ansible_collections/community/general/plugins/modules/consul_auth_method.py new file mode 100644 index 000000000..afe549f6e --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/consul_auth_method.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2024, Florian Apolloner (@apollo13) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +module: consul_auth_method +short_description: Manipulate Consul auth methods +version_added: 8.3.0 +description: + - Allows the addition, modification and deletion of auth methods in a consul + cluster via the agent. For more details on using and configuring ACLs, + see U(https://www.consul.io/docs/guides/acl.html). +author: + - Florian Apolloner (@apollo13) +extends_documentation_fragment: + - community.general.consul + - community.general.consul.actiongroup_consul + - community.general.consul.token + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: partial + details: + - In check mode the diff will miss operational attributes. +options: + state: + description: + - Whether the token should be present or absent. + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Specifies a name for the ACL auth method. + - The name can contain alphanumeric characters, dashes C(-), and underscores C(_). + type: str + required: true + type: + description: + - The type of auth method being configured. + - This field is immutable. + - Required when the auth method is created. + type: str + choices: ['kubernetes', 'jwt', 'oidc', 'aws-iam'] + description: + description: + - Free form human readable description of the auth method. + type: str + display_name: + description: + - An optional name to use instead of O(name) when displaying information about this auth method. + type: str + max_token_ttl: + description: + - This specifies the maximum life of any token created by this auth method. + - Can be specified in the form of V(60s) or V(5m) (that is, 60 seconds or 5 minutes, respectively). + type: str + token_locality: + description: + - Defines the kind of token that this auth method should produce. + type: str + choices: ['local', 'global'] + config: + description: + - The raw configuration to use for the chosen auth method. + - Contents will vary depending upon the type chosen. + - Required when the auth method is created. + type: dict +""" + +EXAMPLES = """ +- name: Create an auth method + community.general.consul_auth_method: + name: test + type: jwt + config: + jwt_validation_pubkeys: + - | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo + 4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u + +qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh + kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ + 0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg + cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc + mwIDAQAB + -----END PUBLIC KEY----- + token: "{{ consul_management_token }}" + +- name: Delete auth method + community.general.consul_auth_method: + name: test + state: absent + token: "{{ consul_management_token }}" +""" + +RETURN = """ +auth_method: + description: The auth method as returned by the consul HTTP API. + returned: always + type: dict + sample: + Config: + JWTValidationPubkeys: + - |- + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo + 4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u + +qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh + kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ + 0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg + cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc + mwIDAQAB + -----END PUBLIC KEY----- + CreateIndex: 416 + ModifyIndex: 487 + Name: test + Type: jwt +operation: + description: The operation performed. + returned: changed + type: str + sample: update +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + _ConsulModule, + camel_case_key, +) + + +def normalize_ttl(ttl): + matches = re.findall(r"(\d+)(:h|m|s)", ttl) + ttl = 0 + for value, unit in matches: + value = int(value) + if unit == "m": + value *= 60 + elif unit == "h": + value *= 60 * 60 + ttl += value + + new_ttl = "" + hours, remainder = divmod(ttl, 3600) + if hours: + new_ttl += "{0}h".format(hours) + minutes, seconds = divmod(remainder, 60) + if minutes: + new_ttl += "{0}m".format(minutes) + if seconds: + new_ttl += "{0}s".format(seconds) + return new_ttl + + +class ConsulAuthMethodModule(_ConsulModule): + api_endpoint = "acl/auth-method" + result_key = "auth_method" + unique_identifier = "name" + + def map_param(self, k, v, is_update): + if k == "config" and v: + v = {camel_case_key(k2): v2 for k2, v2 in v.items()} + return super(ConsulAuthMethodModule, self).map_param(k, v, is_update) + + def needs_update(self, api_obj, module_obj): + if "MaxTokenTTL" in module_obj: + module_obj["MaxTokenTTL"] = normalize_ttl(module_obj["MaxTokenTTL"]) + return super(ConsulAuthMethodModule, self).needs_update(api_obj, module_obj) + + +_ARGUMENT_SPEC = { + "name": dict(type="str", required=True), + "type": dict(type="str", choices=["kubernetes", "jwt", "oidc", "aws-iam"]), + "description": dict(type="str"), + "display_name": dict(type="str"), + "max_token_ttl": dict(type="str", no_log=False), + "token_locality": dict(type="str", choices=["local", "global"]), + "config": dict(type="dict"), + "state": dict(default="present", choices=["present", "absent"]), +} +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) + + +def main(): + module = AnsibleModule( + _ARGUMENT_SPEC, + supports_check_mode=True, + ) + consul_module = ConsulAuthMethodModule(module) + consul_module.execute() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/consul_binding_rule.py b/ansible_collections/community/general/plugins/modules/consul_binding_rule.py new file mode 100644 index 000000000..88496f867 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/consul_binding_rule.py @@ -0,0 +1,183 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2024, Florian Apolloner (@apollo13) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +module: consul_binding_rule +short_description: Manipulate Consul binding rules +version_added: 8.3.0 +description: + - Allows the addition, modification and deletion of binding rules in a consul + cluster via the agent. For more details on using and configuring binding rules, + see U(https://developer.hashicorp.com/consul/api-docs/acl/binding-rules). +author: + - Florian Apolloner (@apollo13) +extends_documentation_fragment: + - community.general.consul + - community.general.consul.actiongroup_consul + - community.general.consul.token + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: partial + details: + - In check mode the diff will miss operational attributes. +options: + state: + description: + - Whether the binding rule should be present or absent. + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Specifies a name for the binding rule. + - 'Note: This is used to identify the binding rule. But since the API does not support a name, it is prefixed to the description.' + type: str + required: true + description: + description: + - Free form human readable description of the binding rule. + type: str + auth_method: + description: + - The name of the auth method that this rule applies to. + type: str + required: true + selector: + description: + - Specifies the expression used to match this rule against valid identities returned from an auth method validation. + - If empty this binding rule matches all valid identities returned from the auth method. + type: str + bind_type: + description: + - Specifies the way the binding rule affects a token created at login. + type: str + choices: [service, node, role, templated-policy] + bind_name: + description: + - The name to bind to a token at login-time. + - What it binds to can be adjusted with different values of the O(bind_type) parameter. + type: str + bind_vars: + description: + - Specifies the templated policy variables when O(bind_type) is set to V(templated-policy). + type: dict +""" + +EXAMPLES = """ +- name: Create a binding rule + community.general.consul_binding_rule: + name: my_name + description: example rule + auth_method: minikube + bind_type: service + bind_name: "{{ serviceaccount.name }}" + token: "{{ consul_management_token }}" + +- name: Remove a binding rule + community.general.consul_binding_rule: + name: my_name + auth_method: minikube + state: absent +""" + +RETURN = """ +binding_rule: + description: The binding rule as returned by the consul HTTP API. + returned: always + type: dict + sample: + Description: "my_name: example rule" + AuthMethod: minikube + Selector: serviceaccount.namespace==default + BindType: service + BindName: "{{ serviceaccount.name }}" + CreateIndex: 30 + ID: 59c8a237-e481-4239-9202-45f117950c5f + ModifyIndex: 33 +operation: + description: The operation performed. + returned: changed + type: str + sample: update +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + RequestError, + _ConsulModule, +) + + +class ConsulBindingRuleModule(_ConsulModule): + api_endpoint = "acl/binding-rule" + result_key = "binding_rule" + unique_identifier = "id" + + def read_object(self): + url = "acl/binding-rules?authmethod={0}".format(self.params["auth_method"]) + try: + results = self.get(url) + for result in results: + if result.get("Description").startswith( + "{0}: ".format(self.params["name"]) + ): + return result + except RequestError as e: + if e.status == 404: + return + elif e.status == 403 and b"ACL not found" in e.response_data: + return + raise + + def module_to_obj(self, is_update): + obj = super(ConsulBindingRuleModule, self).module_to_obj(is_update) + del obj["Name"] + return obj + + def prepare_object(self, existing, obj): + final = super(ConsulBindingRuleModule, self).prepare_object(existing, obj) + name = self.params["name"] + description = final.pop("Description", "").split(": ", 1)[-1] + final["Description"] = "{0}: {1}".format(name, description) + return final + + +_ARGUMENT_SPEC = { + "name": dict(type="str", required=True), + "description": dict(type="str"), + "auth_method": dict(type="str", required=True), + "selector": dict(type="str"), + "bind_type": dict( + type="str", choices=["service", "node", "role", "templated-policy"] + ), + "bind_name": dict(type="str"), + "bind_vars": dict(type="dict"), + "state": dict(default="present", choices=["present", "absent"]), +} +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) + + +def main(): + module = AnsibleModule( + _ARGUMENT_SPEC, + supports_check_mode=True, + ) + consul_module = ConsulBindingRuleModule(module) + consul_module.execute() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/consul_kv.py b/ansible_collections/community/general/plugins/modules/consul_kv.py index a4457f244..84169fc6b 100644 --- a/ansible_collections/community/general/plugins/modules/consul_kv.py +++ b/ansible_collections/community/general/plugins/modules/consul_kv.py @@ -17,7 +17,7 @@ description: - Allows the retrieval, addition, modification and deletion of key/value entries in a consul cluster via the agent. The entire contents of the record, including the indices, flags and session are returned as C(value). - - If the C(key) represents a prefix then note that when a value is removed, the existing + - If the O(key) represents a prefix then note that when a value is removed, the existing value if any is returned as part of the results. - See http://www.consul.io/docs/agent/http.html#kv for more details. requirements: @@ -36,14 +36,14 @@ attributes: options: state: description: - - The action to take with the supplied key and value. If the state is C(present) and I(value) is set, the key - contents will be set to the value supplied and C(changed) will be set to C(true) only if the value was - different to the current contents. If the state is C(present) and I(value) is not set, the existing value - associated to the key will be returned. The state C(absent) will remove the key/value pair, - again C(changed) will be set to true only if the key actually existed + - The action to take with the supplied key and value. If the state is V(present) and O(value) is set, the key + contents will be set to the value supplied and C(changed) will be set to V(true) only if the value was + different to the current contents. If the state is V(present) and O(value) is not set, the existing value + associated to the key will be returned. The state V(absent) will remove the key/value pair, + again C(changed) will be set to V(true) only if the key actually existed prior to the removal. An attempt can be made to obtain or free the - lock associated with a key/value pair with the states C(acquire) or - C(release) respectively. a valid session must be supplied to make the + lock associated with a key/value pair with the states V(acquire) or + V(release) respectively. a valid session must be supplied to make the attempt changed will be true if the attempt is successful, false otherwise. type: str @@ -56,17 +56,17 @@ options: required: true value: description: - - The value should be associated with the given key, required if C(state) - is C(present). + - The value should be associated with the given key, required if O(state) + is V(present). type: str recurse: description: - If the key represents a prefix, each entry with the prefix can be - retrieved by setting this to C(true). + retrieved by setting this to V(true). type: bool retrieve: description: - - If the I(state) is C(present) and I(value) is set, perform a + - If the O(state) is V(present) and O(value) is set, perform a read after setting the value and return this value. default: true type: bool @@ -82,9 +82,9 @@ options: type: str cas: description: - - Used when acquiring a lock with a session. If the C(cas) is C(0), then + - Used when acquiring a lock with a session. If the O(cas) is V(0), then Consul will only put the key if it does not already exist. If the - C(cas) value is non-zero, then the key is only set if the index matches + O(cas) value is non-zero, then the key is only set if the index matches the ModifyIndex of that key. type: str flags: diff --git a/ansible_collections/community/general/plugins/modules/consul_policy.py b/ansible_collections/community/general/plugins/modules/consul_policy.py new file mode 100644 index 000000000..f020622a0 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/consul_policy.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Håkon Lerring +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +module: consul_policy +short_description: Manipulate Consul policies +version_added: 7.2.0 +description: + - Allows the addition, modification and deletion of policies in a consul + cluster via the agent. For more details on using and configuring ACLs, + see U(https://www.consul.io/docs/guides/acl.html). +author: + - Håkon Lerring (@Hakon) +extends_documentation_fragment: + - community.general.consul + - community.general.consul.actiongroup_consul + - community.general.consul.token + - community.general.attributes +attributes: + check_mode: + support: full + version_added: 8.3.0 + diff_mode: + support: partial + version_added: 8.3.0 + details: + - In check mode the diff will miss operational attributes. +options: + state: + description: + - Whether the policy should be present or absent. + choices: ['present', 'absent'] + default: present + type: str + valid_datacenters: + description: + - Valid datacenters for the policy. All if list is empty. + type: list + elements: str + name: + description: + - The name that should be associated with the policy, this is opaque + to Consul. + required: true + type: str + description: + description: + - Description of the policy. + type: str + rules: + type: str + description: + - Rule document that should be associated with the current policy. +""" + +EXAMPLES = """ +- name: Create a policy with rules + community.general.consul_policy: + host: consul1.example.com + token: some_management_acl + name: foo-access + rules: | + key "foo" { + policy = "read" + } + key "private/foo" { + policy = "deny" + } + +- name: Update the rules associated to a policy + community.general.consul_policy: + host: consul1.example.com + token: some_management_acl + name: foo-access + rules: | + key "foo" { + policy = "read" + } + key "private/foo" { + policy = "deny" + } + event "bbq" { + policy = "write" + } + +- name: Remove a policy + community.general.consul_policy: + host: consul1.example.com + token: some_management_acl + name: foo-access + state: absent +""" + +RETURN = """ +policy: + description: The policy as returned by the consul HTTP API. + returned: always + type: dict + sample: + CreateIndex: 632 + Description: Testing + Hash: rj5PeDHddHslkpW7Ij4OD6N4bbSXiecXFmiw2SYXg2A= + Name: foo-access + Rules: |- + key "foo" { + policy = "read" + } + key "private/foo" { + policy = "deny" + } +operation: + description: The operation performed. + returned: changed + type: str + sample: update +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + OPERATION_READ, + _ConsulModule, +) + +_ARGUMENT_SPEC = { + "name": dict(required=True), + "description": dict(required=False, type="str"), + "rules": dict(type="str"), + "valid_datacenters": dict(type="list", elements="str"), + "state": dict(default="present", choices=["present", "absent"]), +} +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) + + +class ConsulPolicyModule(_ConsulModule): + api_endpoint = "acl/policy" + result_key = "policy" + unique_identifier = "id" + + def endpoint_url(self, operation, identifier=None): + if operation == OPERATION_READ: + return [self.api_endpoint, "name", self.params["name"]] + return super(ConsulPolicyModule, self).endpoint_url(operation, identifier) + + +def main(): + module = AnsibleModule( + _ARGUMENT_SPEC, + supports_check_mode=True, + ) + consul_module = ConsulPolicyModule(module) + consul_module.execute() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/consul_role.py b/ansible_collections/community/general/plugins/modules/consul_role.py new file mode 100644 index 000000000..0da71507a --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/consul_role.py @@ -0,0 +1,281 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Håkon Lerring +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +module: consul_role +short_description: Manipulate Consul roles +version_added: 7.5.0 +description: + - Allows the addition, modification and deletion of roles in a consul + cluster via the agent. For more details on using and configuring ACLs, + see U(https://www.consul.io/docs/guides/acl.html). +author: + - Håkon Lerring (@Hakon) +extends_documentation_fragment: + - community.general.consul + - community.general.consul.token + - community.general.consul.actiongroup_consul + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: partial + details: + - In check mode the diff will miss operational attributes. + version_added: 8.3.0 +options: + name: + description: + - A name used to identify the role. + required: true + type: str + state: + description: + - whether the role should be present or absent. + choices: ['present', 'absent'] + default: present + type: str + description: + description: + - Description of the role. + - If not specified, the assigned description will not be changed. + type: str + policies: + type: list + elements: dict + description: + - List of policies to attach to the role. Each policy is a dict. + - If the parameter is left blank, any policies currently assigned will not be changed. + - Any empty array (V([])) will clear any policies previously set. + suboptions: + name: + description: + - The name of the policy to attach to this role; see M(community.general.consul_policy) for more info. + - Either this or O(policies[].id) must be specified. + type: str + id: + description: + - The ID of the policy to attach to this role; see M(community.general.consul_policy) for more info. + - Either this or O(policies[].name) must be specified. + type: str + templated_policies: + description: + - The list of templated policies that should be applied to the role. + type: list + elements: dict + version_added: 8.3.0 + suboptions: + template_name: + description: + - The templated policy name. + type: str + required: true + template_variables: + description: + - The templated policy variables. + - Not all templated policies require variables. + type: dict + service_identities: + type: list + elements: dict + description: + - List of service identities to attach to the role. + - If not specified, any service identities currently assigned will not be changed. + - If the parameter is an empty array (V([])), any node identities assigned will be unassigned. + suboptions: + service_name: + description: + - The name of the node. + - Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character. + - May only contain lowercase alphanumeric characters as well as - and _. + - This suboption has been renamed from O(service_identities[].name) to O(service_identities[].service_name) + in community.general 8.3.0. The old name can still be used. + type: str + required: true + aliases: + - name + datacenters: + description: + - The datacenters the policies will be effective. + - This will result in effective policy only being valid in this datacenter. + - If an empty array (V([])) is specified, the policies will valid in all datacenters. + - including those which do not yet exist but may in the future. + type: list + elements: str + node_identities: + type: list + elements: dict + description: + - List of node identities to attach to the role. + - If not specified, any node identities currently assigned will not be changed. + - If the parameter is an empty array (V([])), any node identities assigned will be unassigned. + suboptions: + node_name: + description: + - The name of the node. + - Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character. + - May only contain lowercase alphanumeric characters as well as - and _. + - This suboption has been renamed from O(node_identities[].name) to O(node_identities[].node_name) + in community.general 8.3.0. The old name can still be used. + type: str + required: true + aliases: + - name + datacenter: + description: + - The nodes datacenter. + - This will result in effective policy only being valid in this datacenter. + type: str + required: true +""" + +EXAMPLES = """ +- name: Create a role with 2 policies + community.general.consul_role: + host: consul1.example.com + token: some_management_acl + name: foo-role + policies: + - id: 783beef3-783f-f41f-7422-7087dc272765 + - name: "policy-1" + +- name: Create a role with service identity + community.general.consul_role: + host: consul1.example.com + token: some_management_acl + name: foo-role-2 + service_identities: + - name: web + datacenters: + - dc1 + +- name: Create a role with node identity + community.general.consul_role: + host: consul1.example.com + token: some_management_acl + name: foo-role-3 + node_identities: + - name: node-1 + datacenter: dc2 + +- name: Remove a role + community.general.consul_role: + host: consul1.example.com + token: some_management_acl + name: foo-role-3 + state: absent +""" + +RETURN = """ +role: + description: The role object. + returned: success + type: dict + sample: + { + "CreateIndex": 39, + "Description": "", + "Hash": "Trt0QJtxVEfvTTIcdTUbIJRr6Dsi6E4EcwSFxx9tCYM=", + "ID": "9a300b8d-48db-b720-8544-a37c0f5dafb5", + "ModifyIndex": 39, + "Name": "foo-role", + "Policies": [ + {"ID": "b1a00172-d7a1-0e66-a12e-7a4045c4b774", "Name": "foo-access"} + ] + } +operation: + description: The operation performed on the role. + returned: changed + type: str + sample: update +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + OPERATION_READ, + _ConsulModule, +) + + +class ConsulRoleModule(_ConsulModule): + api_endpoint = "acl/role" + result_key = "role" + unique_identifier = "id" + + def endpoint_url(self, operation, identifier=None): + if operation == OPERATION_READ: + return [self.api_endpoint, "name", self.params["name"]] + return super(ConsulRoleModule, self).endpoint_url(operation, identifier) + + +NAME_ID_SPEC = dict( + name=dict(type="str"), + id=dict(type="str"), +) + +NODE_ID_SPEC = dict( + node_name=dict(type="str", required=True, aliases=["name"]), + datacenter=dict(type="str", required=True), +) + +SERVICE_ID_SPEC = dict( + service_name=dict(type="str", required=True, aliases=["name"]), + datacenters=dict(type="list", elements="str"), +) + +TEMPLATE_POLICY_SPEC = dict( + template_name=dict(type="str", required=True), + template_variables=dict(type="dict"), +) + +_ARGUMENT_SPEC = { + "name": dict(type="str", required=True), + "description": dict(type="str"), + "policies": dict( + type="list", + elements="dict", + options=NAME_ID_SPEC, + mutually_exclusive=[("name", "id")], + required_one_of=[("name", "id")], + ), + "templated_policies": dict( + type="list", + elements="dict", + options=TEMPLATE_POLICY_SPEC, + ), + "node_identities": dict( + type="list", + elements="dict", + options=NODE_ID_SPEC, + ), + "service_identities": dict( + type="list", + elements="dict", + options=SERVICE_ID_SPEC, + ), + "state": dict(default="present", choices=["present", "absent"]), +} +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) + + +def main(): + module = AnsibleModule( + _ARGUMENT_SPEC, + supports_check_mode=True, + ) + consul_module = ConsulRoleModule(module) + consul_module.execute() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/consul_session.py b/ansible_collections/community/general/plugins/modules/consul_session.py index 246d13846..bd03b561a 100644 --- a/ansible_collections/community/general/plugins/modules/consul_session.py +++ b/ansible_collections/community/general/plugins/modules/consul_session.py @@ -16,12 +16,13 @@ description: cluster. These sessions can then be used in conjunction with key value pairs to implement distributed locks. In depth documentation for working with sessions can be found at http://www.consul.io/docs/internals/sessions.html -requirements: - - python-consul - - requests author: - Steve Gargan (@sgargan) + - Håkon Lerring (@Hakon) extends_documentation_fragment: + - community.general.consul + - community.general.consul.actiongroup_consul + - community.general.consul.token - community.general.attributes attributes: check_mode: @@ -31,25 +32,25 @@ attributes: options: id: description: - - ID of the session, required when I(state) is either C(info) or - C(remove). + - ID of the session, required when O(state) is either V(info) or + V(remove). type: str state: description: - Whether the session should be present i.e. created if it doesn't - exist, or absent, removed if present. If created, the I(id) for the - session is returned in the output. If C(absent), I(id) is + exist, or absent, removed if present. If created, the O(id) for the + session is returned in the output. If V(absent), O(id) is required to remove the session. Info for a single session, all the sessions for a node or all available sessions can be retrieved by - specifying C(info), C(node) or C(list) for the I(state); for C(node) - or C(info), the node I(name) or session I(id) is required as parameter. + specifying V(info), V(node) or V(list) for the O(state); for V(node) + or V(info), the node O(name) or session O(id) is required as parameter. choices: [ absent, info, list, node, present ] type: str default: present name: description: - The name that should be associated with the session. Required when - I(state=node) is used. + O(state=node) is used. type: str delay: description: @@ -76,26 +77,6 @@ options: the associated lock delay has expired. type: list elements: str - host: - description: - - The host of the consul agent defaults to localhost. - type: str - default: localhost - port: - description: - - The port on which the consul agent is running. - type: int - default: 8500 - scheme: - description: - - The protocol scheme on which the consul agent is running. - type: str - default: http - validate_certs: - description: - - Whether to verify the TLS certificate of the consul agent. - type: bool - default: true behavior: description: - The optional behavior that can be attached to the session when it @@ -109,10 +90,6 @@ options: type: int version_added: 5.4.0 token: - description: - - The token key identifying an ACL rule set that controls access to - the key value pair. - type: str version_added: 5.6.0 ''' @@ -147,37 +124,50 @@ EXAMPLES = ''' ttl: 600 # sec ''' -try: - import consul - from requests.exceptions import ConnectionError - python_consul_installed = True -except ImportError: - python_consul_installed = False - from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, _ConsulModule +) -def execute(module): +def execute(module, consul_module): state = module.params.get('state') if state in ['info', 'list', 'node']: - lookup_sessions(module) + lookup_sessions(module, consul_module) elif state == 'present': - update_session(module) + update_session(module, consul_module) else: - remove_session(module) + remove_session(module, consul_module) + + +def list_sessions(consul_module, datacenter): + return consul_module.get( + 'session/list', + params={'dc': datacenter}) + + +def list_sessions_for_node(consul_module, node, datacenter): + return consul_module.get( + ('session', 'node', node), + params={'dc': datacenter}) -def lookup_sessions(module): +def get_session_info(consul_module, session_id, datacenter): + return consul_module.get( + ('session', 'info', session_id), + params={'dc': datacenter}) + + +def lookup_sessions(module, consul_module): datacenter = module.params.get('datacenter') state = module.params.get('state') - consul_client = get_consul_api(module) try: if state == 'list': - sessions_list = consul_client.session.list(dc=datacenter) + sessions_list = list_sessions(consul_module, datacenter) # Ditch the index, this can be grabbed from the results if sessions_list and len(sessions_list) >= 2: sessions_list = sessions_list[1] @@ -185,14 +175,14 @@ def lookup_sessions(module): sessions=sessions_list) elif state == 'node': node = module.params.get('node') - sessions = consul_client.session.node(node, dc=datacenter) + sessions = list_sessions_for_node(consul_module, node, datacenter) module.exit_json(changed=True, node=node, sessions=sessions) elif state == 'info': session_id = module.params.get('id') - session_by_id = consul_client.session.info(session_id, dc=datacenter) + session_by_id = get_session_info(consul_module, session_id, datacenter) module.exit_json(changed=True, session_id=session_id, sessions=session_by_id) @@ -201,7 +191,26 @@ def lookup_sessions(module): module.fail_json(msg="Could not retrieve session info %s" % e) -def update_session(module): +def create_session(consul_module, name, behavior, ttl, node, + lock_delay, datacenter, checks): + create_data = { + "LockDelay": lock_delay, + "Node": node, + "Name": name, + "Checks": checks, + "Behavior": behavior, + } + if ttl is not None: + create_data["TTL"] = "%ss" % str(ttl) # TTL is in seconds + create_session_response_dict = consul_module.put( + 'session/create', + params={ + 'dc': datacenter}, + data=create_data) + return create_session_response_dict["ID"] + + +def update_session(module, consul_module): name = module.params.get('name') delay = module.params.get('delay') @@ -211,18 +220,16 @@ def update_session(module): behavior = module.params.get('behavior') ttl = module.params.get('ttl') - consul_client = get_consul_api(module) - try: - session = consul_client.session.create( - name=name, - behavior=behavior, - ttl=ttl, - node=node, - lock_delay=delay, - dc=datacenter, - checks=checks - ) + session = create_session(consul_module, + name=name, + behavior=behavior, + ttl=ttl, + node=node, + lock_delay=delay, + datacenter=datacenter, + checks=checks + ) module.exit_json(changed=True, session_id=session, name=name, @@ -235,13 +242,15 @@ def update_session(module): module.fail_json(msg="Could not create/update session %s" % e) -def remove_session(module): - session_id = module.params.get('id') +def destroy_session(consul_module, session_id): + return consul_module.put(('session', 'destroy', session_id)) - consul_client = get_consul_api(module) + +def remove_session(module, consul_module): + session_id = module.params.get('id') try: - consul_client.session.destroy(session_id) + destroy_session(consul_module, session_id) module.exit_json(changed=True, session_id=session_id) @@ -250,36 +259,31 @@ def remove_session(module): session_id, e)) -def get_consul_api(module): - return consul.Consul(host=module.params.get('host'), - port=module.params.get('port'), - scheme=module.params.get('scheme'), - verify=module.params.get('validate_certs'), - token=module.params.get('token')) - - -def test_dependencies(module): - if not python_consul_installed: - module.fail_json(msg="python-consul required for this module. " - "see https://python-consul.readthedocs.io/en/latest/#installation") - - def main(): argument_spec = dict( checks=dict(type='list', elements='str'), delay=dict(type='int', default='15'), - behavior=dict(type='str', default='release', choices=['release', 'delete']), + behavior=dict( + type='str', + default='release', + choices=[ + 'release', + 'delete']), ttl=dict(type='int'), - host=dict(type='str', default='localhost'), - port=dict(type='int', default=8500), - scheme=dict(type='str', default='http'), - validate_certs=dict(type='bool', default=True), id=dict(type='str'), name=dict(type='str'), node=dict(type='str'), - state=dict(type='str', default='present', choices=['absent', 'info', 'list', 'node', 'present']), + state=dict( + type='str', + default='present', + choices=[ + 'absent', + 'info', + 'list', + 'node', + 'present']), datacenter=dict(type='str'), - token=dict(type='str', no_log=True), + **AUTH_ARGUMENTS_SPEC ) module = AnsibleModule( @@ -291,14 +295,10 @@ def main(): ], supports_check_mode=False ) - - test_dependencies(module) + consul_module = _ConsulModule(module) try: - execute(module) - except ConnectionError as e: - module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % ( - module.params.get('host'), module.params.get('port'), e)) + execute(module, consul_module) except Exception as e: module.fail_json(msg=str(e)) diff --git a/ansible_collections/community/general/plugins/modules/consul_token.py b/ansible_collections/community/general/plugins/modules/consul_token.py new file mode 100644 index 000000000..eee419863 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/consul_token.py @@ -0,0 +1,331 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2024, Florian Apolloner (@apollo13) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +module: consul_token +short_description: Manipulate Consul tokens +version_added: 8.3.0 +description: + - Allows the addition, modification and deletion of tokens in a consul + cluster via the agent. For more details on using and configuring ACLs, + see U(https://www.consul.io/docs/guides/acl.html). +author: + - Florian Apolloner (@apollo13) +extends_documentation_fragment: + - community.general.consul + - community.general.consul.token + - community.general.consul.actiongroup_consul + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: partial + details: + - In check mode the diff will miss operational attributes. +options: + state: + description: + - Whether the token should be present or absent. + choices: ['present', 'absent'] + default: present + type: str + accessor_id: + description: + - Specifies a UUID to use as the token's Accessor ID. + If not specified a UUID will be generated for this field. + type: str + secret_id: + description: + - Specifies a UUID to use as the token's Secret ID. + If not specified a UUID will be generated for this field. + type: str + description: + description: + - Free form human readable description of the token. + type: str + policies: + type: list + elements: dict + description: + - List of policies to attach to the token. Each policy is a dict. + - If the parameter is left blank, any policies currently assigned will not be changed. + - Any empty array (V([])) will clear any policies previously set. + suboptions: + name: + description: + - The name of the policy to attach to this token; see M(community.general.consul_policy) for more info. + - Either this or O(policies[].id) must be specified. + type: str + id: + description: + - The ID of the policy to attach to this token; see M(community.general.consul_policy) for more info. + - Either this or O(policies[].name) must be specified. + type: str + roles: + type: list + elements: dict + description: + - List of roles to attach to the token. Each role is a dict. + - If the parameter is left blank, any roles currently assigned will not be changed. + - Any empty array (V([])) will clear any roles previously set. + suboptions: + name: + description: + - The name of the role to attach to this token; see M(community.general.consul_role) for more info. + - Either this or O(roles[].id) must be specified. + type: str + id: + description: + - The ID of the role to attach to this token; see M(community.general.consul_role) for more info. + - Either this or O(roles[].name) must be specified. + type: str + templated_policies: + description: + - The list of templated policies that should be applied to the role. + type: list + elements: dict + suboptions: + template_name: + description: + - The templated policy name. + type: str + required: true + template_variables: + description: + - The templated policy variables. + - Not all templated policies require variables. + type: dict + service_identities: + type: list + elements: dict + description: + - List of service identities to attach to the token. + - If not specified, any service identities currently assigned will not be changed. + - If the parameter is an empty array (V([])), any node identities assigned will be unassigned. + suboptions: + service_name: + description: + - The name of the service. + - Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character. + - May only contain lowercase alphanumeric characters as well as V(-) and V(_). + type: str + required: true + datacenters: + description: + - The datacenters the token will be effective. + - If an empty array (V([])) is specified, the token will valid in all datacenters. + - including those which do not yet exist but may in the future. + type: list + elements: str + node_identities: + type: list + elements: dict + description: + - List of node identities to attach to the token. + - If not specified, any node identities currently assigned will not be changed. + - If the parameter is an empty array (V([])), any node identities assigned will be unassigned. + suboptions: + node_name: + description: + - The name of the node. + - Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character. + - May only contain lowercase alphanumeric characters as well as V(-) and V(_). + type: str + required: true + datacenter: + description: + - The nodes datacenter. + - This will result in effective token only being valid in this datacenter. + type: str + required: true + local: + description: + - If true, indicates that the token should not be replicated globally + and instead be local to the current datacenter. + type: bool + expiration_ttl: + description: + - This is a convenience field and if set will initialize the C(expiration_time). + Can be specified in the form of V(60s) or V(5m) (that is, 60 seconds or 5 minutes, + respectively). Ingored when the token is updated! + type: str +""" + +EXAMPLES = """ +- name: Create / Update a token by accessor_id + community.general.consul_token: + state: present + accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21 + token: 8adddd91-0bd6-d41d-ae1a-3b49cfa9a0e8 + roles: + - name: role1 + - name: role2 + service_identities: + - service_name: service1 + datacenters: [dc1, dc2] + node_identities: + - node_name: node1 + datacenter: dc1 + expiration_ttl: 50m + +- name: Delete a token + community.general.consul_token: + state: absent + accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21 + token: 8adddd91-0bd6-d41d-ae1a-3b49cfa9a0e8 +""" + +RETURN = """ +token: + description: The token as returned by the consul HTTP API. + returned: always + type: dict + sample: + AccessorID: 07a7de84-c9c7-448a-99cc-beaf682efd21 + CreateIndex: 632 + CreateTime: "2024-01-14T21:53:01.402749174+01:00" + Description: Testing + Hash: rj5PeDHddHslkpW7Ij4OD6N4bbSXiecXFmiw2SYXg2A= + Local: false + ModifyIndex: 633 + SecretID: bd380fba-da17-7cee-8576-8d6427c6c930 + ServiceIdentities: [{"ServiceName": "test"}] +operation: + description: The operation performed. + returned: changed + type: str + sample: update +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + _ConsulModule, +) + + +def normalize_link_obj(api_obj, module_obj, key): + api_objs = api_obj.get(key) + module_objs = module_obj.get(key) + if api_objs is None or module_objs is None: + return + name_to_id = {i["Name"]: i["ID"] for i in api_objs} + id_to_name = {i["ID"]: i["Name"] for i in api_objs} + + for obj in module_objs: + identifier = obj.get("ID") + name = obj.get("Name)") + if identifier and not name and identifier in id_to_name: + obj["Name"] = id_to_name[identifier] + if not identifier and name and name in name_to_id: + obj["ID"] = name_to_id[name] + + +class ConsulTokenModule(_ConsulModule): + api_endpoint = "acl/token" + result_key = "token" + unique_identifier = "accessor_id" + + create_only_fields = {"expiration_ttl"} + + def read_object(self): + # if `accessor_id` is not supplied we can only create objects and are not idempotent + if not self.params.get(self.unique_identifier): + return None + return super(ConsulTokenModule, self).read_object() + + def needs_update(self, api_obj, module_obj): + # SecretID is usually not supplied + if "SecretID" not in module_obj and "SecretID" in api_obj: + del api_obj["SecretID"] + normalize_link_obj(api_obj, module_obj, "Roles") + normalize_link_obj(api_obj, module_obj, "Policies") + # ExpirationTTL is only supported on create, not for update + # it writes to ExpirationTime, so we need to remove that as well + if "ExpirationTTL" in module_obj: + del module_obj["ExpirationTTL"] + return super(ConsulTokenModule, self).needs_update(api_obj, module_obj) + + +NAME_ID_SPEC = dict( + name=dict(type="str"), + id=dict(type="str"), +) + +NODE_ID_SPEC = dict( + node_name=dict(type="str", required=True), + datacenter=dict(type="str", required=True), +) + +SERVICE_ID_SPEC = dict( + service_name=dict(type="str", required=True), + datacenters=dict(type="list", elements="str"), +) + +TEMPLATE_POLICY_SPEC = dict( + template_name=dict(type="str", required=True), + template_variables=dict(type="dict"), +) + + +_ARGUMENT_SPEC = { + "description": dict(), + "accessor_id": dict(), + "secret_id": dict(no_log=True), + "roles": dict( + type="list", + elements="dict", + options=NAME_ID_SPEC, + mutually_exclusive=[("name", "id")], + required_one_of=[("name", "id")], + ), + "policies": dict( + type="list", + elements="dict", + options=NAME_ID_SPEC, + mutually_exclusive=[("name", "id")], + required_one_of=[("name", "id")], + ), + "templated_policies": dict( + type="list", + elements="dict", + options=TEMPLATE_POLICY_SPEC, + ), + "node_identities": dict( + type="list", + elements="dict", + options=NODE_ID_SPEC, + ), + "service_identities": dict( + type="list", + elements="dict", + options=SERVICE_ID_SPEC, + ), + "local": dict(type="bool"), + "expiration_ttl": dict(type="str"), + "state": dict(default="present", choices=["present", "absent"]), +} +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) + + +def main(): + module = AnsibleModule( + _ARGUMENT_SPEC, + required_if=[("state", "absent", ["accessor_id"])], + supports_check_mode=True, + ) + consul_module = ConsulTokenModule(module) + consul_module.execute() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/copr.py b/ansible_collections/community/general/plugins/modules/copr.py index 965c2a935..157a6c160 100644 --- a/ansible_collections/community/general/plugins/modules/copr.py +++ b/ansible_collections/community/general/plugins/modules/copr.py @@ -42,14 +42,14 @@ options: type: str state: description: - - Whether to set this project as C(enabled), C(disabled) or C(absent). + - Whether to set this project as V(enabled), V(disabled), or V(absent). default: enabled type: str choices: [absent, enabled, disabled] chroot: description: - The name of the chroot that you want to enable/disable/remove in the project, - for example C(epel-7-x86_64). Default chroot is determined by the operating system, + for example V(epel-7-x86_64). Default chroot is determined by the operating system, version of the operating system, and architecture on which the module is run. type: str """ @@ -97,11 +97,26 @@ except ImportError: DNF_IMP_ERR = traceback.format_exc() HAS_DNF_PACKAGES = False +from ansible.module_utils.common import respawn from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.basic import missing_required_lib -from ansible.module_utils import distro # pylint: disable=import-error -from ansible.module_utils.basic import AnsibleModule # pylint: disable=import-error -from ansible.module_utils.urls import open_url # pylint: disable=import-error +from ansible.module_utils import distro +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import open_url + + +def _respawn_dnf(): + if respawn.has_respawned(): + return + system_interpreters = ( + "/usr/libexec/platform-python", + "/usr/bin/python3", + "/usr/bin/python2", + "/usr/bin/python", + ) + interpreter = respawn.probe_interpreters_for_module(system_interpreters, "dnf") + if interpreter: + respawn.respawn_module(interpreter) class CoprModule(object): @@ -460,6 +475,7 @@ def run_module(): params = module.params if not HAS_DNF_PACKAGES: + _respawn_dnf() module.fail_json(msg=missing_required_lib("dnf"), exception=DNF_IMP_ERR) CoprModule.ansible_module = module diff --git a/ansible_collections/community/general/plugins/modules/cpanm.py b/ansible_collections/community/general/plugins/modules/cpanm.py index 6260992df..20ac3e714 100644 --- a/ansible_collections/community/general/plugins/modules/cpanm.py +++ b/ansible_collections/community/general/plugins/modules/cpanm.py @@ -27,8 +27,8 @@ options: name: type: str description: - - The Perl library to install. Valid values change according to the I(mode), see notes for more details. - - Note that for installing from a local path the parameter I(from_path) should be used. + - The Perl library to install. Valid values change according to the O(mode), see notes for more details. + - Note that for installing from a local path the parameter O(from_path) should be used. aliases: [pkg] from_path: type: path @@ -59,7 +59,7 @@ options: default: false version: description: - - Version specification for the perl module. When I(mode) is C(new), C(cpanm) version operators are accepted. + - Version specification for the perl module. When O(mode) is V(new), C(cpanm) version operators are accepted. type: str executable: description: @@ -68,32 +68,24 @@ options: mode: description: - Controls the module behavior. See notes below for more details. + - Default is V(compatibility) but that behavior is deprecated and will be changed to V(new) in community.general 9.0.0. type: str choices: [compatibility, new] - default: compatibility version_added: 3.0.0 name_check: description: - - When in C(new) mode, this parameter can be used to check if there is a module I(name) installed (at I(version), when specified). + - When O(mode=new), this parameter can be used to check if there is a module O(name) installed (at O(version), when specified). type: str version_added: 3.0.0 notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. - - "This module now comes with a choice of execution I(mode): C(compatibility) or C(new)." - - "C(compatibility) mode:" - - When using C(compatibility) mode, the module will keep backward compatibility. This is the default mode. - - I(name) must be either a module name or a distribution file. - - > - If the perl module given by I(name) is installed (at the exact I(version) when specified), then nothing happens. - Otherwise, it will be installed using the C(cpanm) executable. - - I(name) cannot be an URL, or a git URL. - - C(cpanm) version specifiers do not work in this mode. - - "C(new) mode:" - - "When using C(new) mode, the module will behave differently" - - > - The I(name) parameter may refer to a module name, a distribution file, - a HTTP URL or a git repository URL as described in C(cpanminus) documentation. - - C(cpanm) version specifiers are recognized. + - "This module now comes with a choice of execution O(mode): V(compatibility) or V(new)." + - "O(mode=compatibility): When using V(compatibility) mode, the module will keep backward compatibility. This is the default mode. + O(name) must be either a module name or a distribution file. If the perl module given by O(name) is installed (at the exact O(version) + when specified), then nothing happens. Otherwise, it will be installed using the C(cpanm) executable. O(name) cannot be an URL, or a git URL. + C(cpanm) version specifiers do not work in this mode." + - "O(mode=new): When using V(new) mode, the module will behave differently. The O(name) parameter may refer to a module name, a distribution file, + a HTTP URL or a git repository URL as described in C(cpanminus) documentation. C(cpanm) version specifiers are recognized." author: - "Franck Cuny (@fcuny)" - "Alexei Znamensky (@russoz)" @@ -158,7 +150,7 @@ class CPANMinus(ModuleHelper): mirror_only=dict(type='bool', default=False), installdeps=dict(type='bool', default=False), executable=dict(type='path'), - mode=dict(type='str', choices=['compatibility', 'new'], default='compatibility'), + mode=dict(type='str', choices=['compatibility', 'new']), name_check=dict(type='str') ), required_one_of=[('name', 'from_path')], @@ -176,6 +168,14 @@ class CPANMinus(ModuleHelper): def __init_module__(self): v = self.vars + if v.mode is None: + self.deprecate( + "The default value 'compatibility' for parameter 'mode' is being deprecated " + "and it will be replaced by 'new'", + version="9.0.0", + collection_name="community.general" + ) + v.mode = "compatibility" if v.mode == "compatibility": if v.name_check: self.do_raise("Parameter name_check can only be used with mode=new") @@ -183,8 +183,9 @@ class CPANMinus(ModuleHelper): if v.name and v.from_path: self.do_raise("Parameters 'name' and 'from_path' are mutually exclusive when 'mode=new'") - self.command = self.get_bin_path(v.executable if v.executable else self.command) - self.vars.set("binary", self.command) + self.command = v.executable if v.executable else self.command + self.runner = CmdRunner(self.module, self.command, self.command_args_formats, check_rc=True) + self.vars.binary = self.runner.binary def _is_package_installed(self, name, locallib, version): def process(rc, out, err): @@ -220,8 +221,6 @@ class CPANMinus(ModuleHelper): self.do_raise(msg=err, cmd=self.vars.cmd_args) return 'is up to date' not in err and 'is up to date' not in out - runner = CmdRunner(self.module, self.command, self.command_args_formats, check_rc=True) - v = self.vars pkg_param = 'from_path' if v.from_path else 'name' @@ -235,7 +234,7 @@ class CPANMinus(ModuleHelper): return pkg_spec = self.sanitize_pkg_spec_version(v[pkg_param], v.version) - with runner(['notest', 'locallib', 'mirror', 'mirror_only', 'installdeps', 'pkg_spec'], output_process=process) as ctx: + with self.runner(['notest', 'locallib', 'mirror', 'mirror_only', 'installdeps', 'pkg_spec'], output_process=process) as ctx: self.changed = ctx.run(pkg_spec=pkg_spec) diff --git a/ansible_collections/community/general/plugins/modules/cronvar.py b/ansible_collections/community/general/plugins/modules/cronvar.py index 7effed2ae..fdcbc7d24 100644 --- a/ansible_collections/community/general/plugins/modules/cronvar.py +++ b/ansible_collections/community/general/plugins/modules/cronvar.py @@ -40,16 +40,16 @@ options: value: description: - The value to set this variable to. - - Required if I(state=present). + - Required if O(state=present). type: str insertafter: description: - If specified, the variable will be inserted after the variable specified. - - Used with I(state=present). + - Used with O(state=present). type: str insertbefore: description: - - Used with I(state=present). If specified, the variable will be inserted + - Used with O(state=present). If specified, the variable will be inserted just before the variable specified. type: str state: @@ -61,18 +61,19 @@ options: user: description: - The specific user whose crontab should be modified. - - This parameter defaults to C(root) when unset. + - This parameter defaults to V(root) when unset. type: str cron_file: description: - If specified, uses this file instead of an individual user's crontab. - - Without a leading C(/), this is assumed to be in I(/etc/cron.d). - - With a leading C(/), this is taken as absolute. + - Without a leading V(/), this is assumed to be in C(/etc/cron.d). + - With a leading V(/), this is taken as absolute. type: str backup: description: - If set, create a backup of the crontab before it is modified. The location of the backup is returned in the C(backup) variable by this module. + # TODO: C() above should be RV(), but return values have not been documented! type: bool default: false requirements: diff --git a/ansible_collections/community/general/plugins/modules/crypttab.py b/ansible_collections/community/general/plugins/modules/crypttab.py index 6aea362e7..931a0c930 100644 --- a/ansible_collections/community/general/plugins/modules/crypttab.py +++ b/ansible_collections/community/general/plugins/modules/crypttab.py @@ -25,38 +25,38 @@ options: name: description: - Name of the encrypted block device as it appears in the C(/etc/crypttab) file, or - optionally prefixed with C(/dev/mapper/), as it appears in the filesystem. I(/dev/mapper/) - will be stripped from I(name). + optionally prefixed with V(/dev/mapper/), as it appears in the filesystem. V(/dev/mapper/) + will be stripped from O(name). type: str required: true state: description: - - Use I(present) to add a line to C(/etc/crypttab) or update its definition + - Use V(present) to add a line to C(/etc/crypttab) or update its definition if already present. - - Use I(absent) to remove a line with matching I(name). - - Use I(opts_present) to add options to those already present; options with + - Use V(absent) to remove a line with matching O(name). + - Use V(opts_present) to add options to those already present; options with different values will be updated. - - Use I(opts_absent) to remove options from the existing set. + - Use V(opts_absent) to remove options from the existing set. type: str required: true choices: [ absent, opts_absent, opts_present, present ] backing_device: description: - Path to the underlying block device or file, or the UUID of a block-device - prefixed with I(UUID=). + prefixed with V(UUID=). type: str password: description: - Encryption password, the path to a file containing the password, or - C(-) or unset if the password should be entered at boot. + V(-) or unset if the password should be entered at boot. type: path opts: description: - - A comma-delimited list of options. See C(crypttab(5) ) for details. + - A comma-delimited list of options. See V(crypttab(5\)) for details. type: str path: description: - - Path to file to use instead of C(/etc/crypttab). + - Path to file to use instead of V(/etc/crypttab). - This might be useful in a chroot environment. type: path default: /etc/crypttab diff --git a/ansible_collections/community/general/plugins/modules/datadog_downtime.py b/ansible_collections/community/general/plugins/modules/datadog_downtime.py index 6e506eb85..a3a6a660f 100644 --- a/ansible_collections/community/general/plugins/modules/datadog_downtime.py +++ b/ansible_collections/community/general/plugins/modules/datadog_downtime.py @@ -38,7 +38,7 @@ options: api_host: description: - The URL to the Datadog API. - - This value can also be set with the C(DATADOG_HOST) environment variable. + - This value can also be set with the E(DATADOG_HOST) environment variable. required: false default: https://api.datadoghq.com type: str @@ -57,7 +57,7 @@ options: id: description: - The identifier of the downtime. - - If empty, a new downtime gets created, otherwise it is either updated or deleted depending of the C(state). + - If empty, a new downtime gets created, otherwise it is either updated or deleted depending of the O(state). - To keep your playbook idempotent, you should save the identifier in a file and read it in a lookup. type: int monitor_tags: @@ -99,7 +99,7 @@ options: - For example, to have a recurring event on the first day of each month, select a type of rrule and set the C(FREQ) to C(MONTHLY) and C(BYMONTHDAY) to C(1). - Most common rrule options from the iCalendar Spec are supported. - - Attributes specifying the duration in C(RRULE) are not supported (e.g. C(DTSTART), C(DTEND), C(DURATION)). + - Attributes specifying the duration in C(RRULE) are not supported (for example C(DTSTART), C(DTEND), C(DURATION)). type: str """ @@ -248,7 +248,8 @@ def build_downtime(module): downtime.timezone = module.params["timezone"] if module.params["rrule"]: downtime.recurrence = DowntimeRecurrence( - rrule=module.params["rrule"] + rrule=module.params["rrule"], + type="rrule", ) return downtime diff --git a/ansible_collections/community/general/plugins/modules/datadog_event.py b/ansible_collections/community/general/plugins/modules/datadog_event.py index b8161eca6..6008b565b 100644 --- a/ansible_collections/community/general/plugins/modules/datadog_event.py +++ b/ansible_collections/community/general/plugins/modules/datadog_event.py @@ -82,7 +82,7 @@ options: description: ["An arbitrary string to use for aggregation."] validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be used + - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. type: bool default: true diff --git a/ansible_collections/community/general/plugins/modules/datadog_monitor.py b/ansible_collections/community/general/plugins/modules/datadog_monitor.py index f58df358b..75ae8c233 100644 --- a/ansible_collections/community/general/plugins/modules/datadog_monitor.py +++ b/ansible_collections/community/general/plugins/modules/datadog_monitor.py @@ -16,7 +16,6 @@ short_description: Manages Datadog monitors description: - Manages monitors within Datadog. - Options as described on https://docs.datadoghq.com/api/. - - The type C(event-v2) was added in community.general 4.8.0. author: Sebastian Kornehl (@skornehl) requirements: [datadog] extends_documentation_fragment: @@ -34,8 +33,8 @@ options: type: str api_host: description: - - The URL to the Datadog API. Default value is C(https://api.datadoghq.com). - - This value can also be set with the C(DATADOG_HOST) environment variable. + - The URL to the Datadog API. Default value is V(https://api.datadoghq.com). + - This value can also be set with the E(DATADOG_HOST) environment variable. required: false type: str version_added: '0.2.0' @@ -59,8 +58,9 @@ options: type: description: - The type of the monitor. - - The types C(query alert), C(trace-analytics alert) and C(rum alert) were added in community.general 2.1.0. - - The type C(composite) was added in community.general 3.4.0. + - The types V(query alert), V(trace-analytics alert) and V(rum alert) were added in community.general 2.1.0. + - The type V(composite) was added in community.general 3.4.0. + - The type V(event-v2 alert) was added in community.general 4.8.0. choices: - metric alert - service check @@ -117,7 +117,7 @@ options: escalation_message: description: - A message to include with a re-notification. Supports the '@username' notification we allow elsewhere. - - Not applicable if I(renotify_interval=None). + - Not applicable if O(renotify_interval=none). type: str notify_audit: description: @@ -130,7 +130,7 @@ options: - A dictionary of thresholds by status. - Only available for service checks and metric alerts. - Because each of them can have multiple thresholds, we do not define them directly in the query. - - "If not specified, it defaults to: C({'ok': 1, 'critical': 1, 'warning': 1})." + - "If not specified, it defaults to: V({'ok': 1, 'critical': 1, 'warning': 1})." locked: description: - Whether changes to this monitor should be restricted to the creator or admins. @@ -167,6 +167,32 @@ options: - Integer from 1 (high) to 5 (low) indicating alert severity. type: int version_added: 4.6.0 + notification_preset_name: + description: + - Toggles the display of additional content sent in the monitor notification. + choices: + - show_all + - hide_query + - hide_handles + - hide_all + type: str + version_added: 7.1.0 + renotify_occurrences: + description: + - The number of times re-notification messages should be sent on the current status at the provided re-notification interval. + type: int + version_added: 7.1.0 + renotify_statuses: + description: + - The types of monitor statuses for which re-notification messages are sent. + choices: + - alert + - warn + - no data + type: list + elements: str + version_added: 7.1.0 + ''' EXAMPLES = ''' @@ -175,6 +201,10 @@ EXAMPLES = ''' type: "metric alert" name: "Test monitor" state: "present" + renotify_interval: 30 + renotify_occurrences: 1 + renotify_statuses: ["warn"] + notification_preset_name: "show_all" query: "datadog.agent.up.over('host:host1').last(2).count_by_status()" notification_message: "Host [[host.name]] with IP [[host.ip]] is failing to report to datadog." api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" @@ -254,6 +284,9 @@ def main(): id=dict(), include_tags=dict(required=False, default=True, type='bool'), priority=dict(type='int'), + notification_preset_name=dict(choices=['show_all', 'hide_query', 'hide_handles', 'hide_all']), + renotify_occurrences=dict(type='int'), + renotify_statuses=dict(type='list', elements='str', choices=['alert', 'warn', 'no data']), ) ) @@ -368,6 +401,9 @@ def install_monitor(module): "new_host_delay": module.params['new_host_delay'], "evaluation_delay": module.params['evaluation_delay'], "include_tags": module.params['include_tags'], + "notification_preset_name": module.params['notification_preset_name'], + "renotify_occurrences": module.params['renotify_occurrences'], + "renotify_statuses": module.params['renotify_statuses'], } if module.params['type'] == "service check": diff --git a/ansible_collections/community/general/plugins/modules/dconf.py b/ansible_collections/community/general/plugins/modules/dconf.py index 8c325486c..065cf1a6a 100644 --- a/ansible_collections/community/general/plugins/modules/dconf.py +++ b/ansible_collections/community/general/plugins/modules/dconf.py @@ -46,11 +46,11 @@ notes: - Keep in mind that the C(dconf) CLI tool, which this module wraps around, utilises an unusual syntax for the values (GVariant). For example, if you wanted to provide a string value, the correct syntax would be - I(value="'myvalue'") - with single quotes as part of the Ansible parameter + O(value="'myvalue'") - with single quotes as part of the Ansible parameter value. - When using loops in combination with a value like - "[('xkb', 'us'), ('xkb', 'se')]", you need to be aware of possible - type conversions. Applying a filter C({{ item.value | string }}) + V("[('xkb', 'us'\), ('xkb', 'se'\)]"), you need to be aware of possible + type conversions. Applying a filter V({{ item.value | string }}) to the parameter variable can avoid potential conversion problems. - The easiest way to figure out exact syntax/value you need to provide for a key is by making the configuration change in application affected by the @@ -76,7 +76,7 @@ options: - Value to set for the specified dconf key. Value should be specified in GVariant format. Due to complexity of this format, it is best to have a look at existing values in the dconf database. - - Required for I(state=present). + - Required for O(state=present). - Although the type is specified as "raw", it should typically be specified as a string. However, boolean values in particular are handled properly even when specified as booleans rather than strings @@ -400,7 +400,7 @@ class DconfPreference(object): rc, out, err = dbus_wrapper.run_command(command) if rc != 0: - self.module.fail_json(msg='dconf failed while reseting the value with error: %s' % err, + self.module.fail_json(msg='dconf failed while resetting the value with error: %s' % err, out=out, err=err) diff --git a/ansible_collections/community/general/plugins/modules/deploy_helper.py b/ansible_collections/community/general/plugins/modules/deploy_helper.py index f0246cae6..b47ed8254 100644 --- a/ansible_collections/community/general/plugins/modules/deploy_helper.py +++ b/ansible_collections/community/general/plugins/modules/deploy_helper.py @@ -20,8 +20,9 @@ description: - The Deploy Helper manages some of the steps common in deploying software. It creates a folder structure, manages a symlink for the current release and cleans up old releases. - - "Running it with the I(state=query) or I(state=present) will return the C(deploy_helper) fact. - C(project_path), whatever you set in the I(path) parameter, + # TODO: convert below to RETURN documentation! + - "Running it with the O(state=query) or O(state=present) will return the C(deploy_helper) fact. + C(project_path), whatever you set in the O(path) parameter, C(current_path), the path to the symlink that points to the active release, C(releases_path), the path to the folder to keep releases in, C(shared_path), the path to the folder to keep shared resources in, @@ -50,33 +51,33 @@ options: type: str description: - The state of the project. - C(query) will only gather facts, - C(present) will create the project I(root) folder, and in it the I(releases) and I(shared) folders, - C(finalize) will remove the unfinished_filename file, create a symlink to the newly - deployed release and optionally clean old releases, - C(clean) will remove failed & old releases, - C(absent) will remove the project folder (synonymous to the M(ansible.builtin.file) module with I(state=absent)). + - V(query) will only gather facts. + - V(present) will create the project C(root) folder, and in it the C(releases) and C(shared) folders. + - V(finalize) will remove the unfinished_filename file, create a symlink to the newly + deployed release and optionally clean old releases. + - V(clean) will remove failed & old releases. + - V(absent) will remove the project folder (synonymous to the M(ansible.builtin.file) module with O(state=absent)). choices: [ present, finalize, absent, clean, query ] default: present release: type: str description: - - The release version that is being deployed. Defaults to a timestamp format %Y%m%d%H%M%S (i.e. '20141119223359'). - This parameter is optional during I(state=present), but needs to be set explicitly for I(state=finalize). - You can use the generated fact I(release={{ deploy_helper.new_release }}). + - The release version that is being deployed. Defaults to a timestamp format C(%Y%m%d%H%M%S) (for example V(20141119223359)). + This parameter is optional during O(state=present), but needs to be set explicitly for O(state=finalize). + You can use the generated fact C(release={{ deploy_helper.new_release }}). releases_path: type: str description: - - The name of the folder that will hold the releases. This can be relative to I(path) or absolute. + - The name of the folder that will hold the releases. This can be relative to O(path) or absolute. Returned in the C(deploy_helper.releases_path) fact. default: releases shared_path: type: path description: - - The name of the folder that will hold the shared resources. This can be relative to I(path) or absolute. + - The name of the folder that will hold the shared resources. This can be relative to O(path) or absolute. If this is set to an empty string, no shared folder will be created. Returned in the C(deploy_helper.shared_path) fact. default: shared @@ -84,38 +85,38 @@ options: current_path: type: path description: - - The name of the symlink that is created when the deploy is finalized. Used in I(finalize) and I(clean). + - The name of the symlink that is created when the deploy is finalized. Used in O(state=finalize) and O(state=clean). Returned in the C(deploy_helper.current_path) fact. default: current unfinished_filename: type: str description: - - The name of the file that indicates a deploy has not finished. All folders in the I(releases_path) that - contain this file will be deleted on I(state=finalize) with I(clean=True), or I(state=clean). This file is - automatically deleted from the I(new_release_path) during I(state=finalize). + - The name of the file that indicates a deploy has not finished. All folders in the O(releases_path) that + contain this file will be deleted on O(state=finalize) with O(clean=true), or O(state=clean). This file is + automatically deleted from the C(new_release_path) during O(state=finalize). default: DEPLOY_UNFINISHED clean: description: - - Whether to run the clean procedure in case of I(state=finalize). + - Whether to run the clean procedure in case of O(state=finalize). type: bool default: true keep_releases: type: int description: - - The number of old releases to keep when cleaning. Used in I(finalize) and I(clean). Any unfinished builds + - The number of old releases to keep when cleaning. Used in O(state=finalize) and O(state=clean). Any unfinished builds will be deleted first, so only correct releases will count. The current version will not count. default: 5 notes: - - Facts are only returned for I(state=query) and I(state=present). If you use both, you should pass any overridden + - Facts are only returned for O(state=query) and O(state=present). If you use both, you should pass any overridden parameters to both calls, otherwise the second call will overwrite the facts of the first one. - - When using I(state=clean), the releases are ordered by I(creation date). You should be able to switch to a + - When using O(state=clean), the releases are ordered by I(creation date). You should be able to switch to a new naming strategy without problems. - - Because of the default behaviour of generating the I(new_release) fact, this module will not be idempotent - unless you pass your own release name with I(release). Due to the nature of deploying software, this should not + - Because of the default behaviour of generating the C(new_release) fact, this module will not be idempotent + unless you pass your own release name with O(release). Due to the nature of deploying software, this should not be much of a problem. extends_documentation_fragment: - ansible.builtin.files diff --git a/ansible_collections/community/general/plugins/modules/dimensiondata_network.py b/ansible_collections/community/general/plugins/modules/dimensiondata_network.py index 8c1469063..cfb7d61cd 100644 --- a/ansible_collections/community/general/plugins/modules/dimensiondata_network.py +++ b/ansible_collections/community/general/plugins/modules/dimensiondata_network.py @@ -84,7 +84,7 @@ EXAMPLES = ''' RETURN = ''' network: description: Dictionary describing the network. - returned: On success when I(state=present). + returned: On success when O(state=present). type: complex contains: id: diff --git a/ansible_collections/community/general/plugins/modules/dimensiondata_vlan.py b/ansible_collections/community/general/plugins/modules/dimensiondata_vlan.py index 7d83ddc69..9d129f3de 100644 --- a/ansible_collections/community/general/plugins/modules/dimensiondata_vlan.py +++ b/ansible_collections/community/general/plugins/modules/dimensiondata_vlan.py @@ -51,20 +51,20 @@ options: private_ipv4_prefix_size: description: - The size of the IPv4 address space, e.g 24. - - Required, if C(private_ipv4_base_address) is specified. + - Required, if O(private_ipv4_base_address) is specified. type: int default: 0 state: description: - The desired state for the target VLAN. - - C(readonly) ensures that the state is only ever read, not modified (the module will fail if the resource does not exist). + - V(readonly) ensures that the state is only ever read, not modified (the module will fail if the resource does not exist). choices: [present, absent, readonly] default: present type: str allow_expand: description: - Permit expansion of the target VLAN's network if the module parameters specify a larger network than the VLAN currently possesses. - - If C(False), the module will fail under these conditions. + - If V(false), the module will fail under these conditions. - This is intended to prevent accidental expansion of a VLAN's network (since this operation is not reversible). type: bool default: false @@ -105,7 +105,7 @@ EXAMPLES = ''' RETURN = ''' vlan: description: Dictionary describing the VLAN. - returned: On success when I(state) is 'present' + returned: On success when O(state=present) type: complex contains: id: diff --git a/ansible_collections/community/general/plugins/modules/discord.py b/ansible_collections/community/general/plugins/modules/discord.py index 8b5391d44..130649f07 100644 --- a/ansible_collections/community/general/plugins/modules/discord.py +++ b/ansible_collections/community/general/plugins/modules/discord.py @@ -43,7 +43,7 @@ options: content: description: - Content of the message to the Discord channel. - - At least one of I(content) and I(embeds) must be specified. + - At least one of O(content) and O(embeds) must be specified. type: str username: description: @@ -55,7 +55,7 @@ options: type: str tts: description: - - Set this to C(true) if this is a TTS (Text to Speech) message. + - Set this to V(true) if this is a TTS (Text to Speech) message. type: bool default: false embeds: @@ -63,7 +63,7 @@ options: - Send messages as Embeds to the Discord channel. - Embeds can have a colored border, embedded images, text fields and more. - "Allowed parameters are described in the Discord Docs: U(https://discord.com/developers/docs/resources/channel#embed-object)" - - At least one of I(content) and I(embeds) must be specified. + - At least one of O(content) and O(embeds) must be specified. type: list elements: dict ''' diff --git a/ansible_collections/community/general/plugins/modules/django_manage.py b/ansible_collections/community/general/plugins/modules/django_manage.py index 537cf0fa7..114ec0353 100644 --- a/ansible_collections/community/general/plugins/modules/django_manage.py +++ b/ansible_collections/community/general/plugins/modules/django_manage.py @@ -16,7 +16,7 @@ module: django_manage short_description: Manages a Django application description: - Manages a Django application using the C(manage.py) application frontend to C(django-admin). With the - I(virtualenv) parameter, all management commands will be executed by the given C(virtualenv) installation. + O(virtualenv) parameter, all management commands will be executed by the given C(virtualenv) installation. extends_documentation_fragment: - community.general.attributes attributes: @@ -29,20 +29,20 @@ options: description: - The name of the Django management command to run. The commands listed below are built in this module and have some basic parameter validation. - > - C(cleanup) - clean up old data from the database (deprecated in Django 1.5). This parameter will be - removed in community.general 9.0.0. Use C(clearsessions) instead. - - C(collectstatic) - Collects the static files into C(STATIC_ROOT). - - C(createcachetable) - Creates the cache tables for use with the database cache backend. - - C(flush) - Removes all data from the database. - - C(loaddata) - Searches for and loads the contents of the named I(fixtures) into the database. - - C(migrate) - Synchronizes the database state with models and migrations. + V(cleanup) - clean up old data from the database (deprecated in Django 1.5). This parameter will be + removed in community.general 9.0.0. Use V(clearsessions) instead. + - V(collectstatic) - Collects the static files into C(STATIC_ROOT). + - V(createcachetable) - Creates the cache tables for use with the database cache backend. + - V(flush) - Removes all data from the database. + - V(loaddata) - Searches for and loads the contents of the named O(fixtures) into the database. + - V(migrate) - Synchronizes the database state with models and migrations. - > - C(syncdb) - Synchronizes the database state with models and migrations (deprecated in Django 1.7). - This parameter will be removed in community.general 9.0.0. Use C(migrate) instead. - - C(test) - Runs tests for all installed apps. + V(syncdb) - Synchronizes the database state with models and migrations (deprecated in Django 1.7). + This parameter will be removed in community.general 9.0.0. Use V(migrate) instead. + - V(test) - Runs tests for all installed apps. - > - C(validate) - Validates all installed models (deprecated in Django 1.7). This parameter will be - removed in community.general 9.0.0. Use C(check) instead. + V(validate) - Validates all installed models (deprecated in Django 1.7). This parameter will be + removed in community.general 9.0.0. Use V(check) instead. - Other commands can be entered, but will fail if they are unknown to Django. Other commands that may prompt for user input should be run with the C(--noinput) flag. type: str @@ -55,14 +55,14 @@ options: aliases: [app_path, chdir] settings: description: - - The Python path to the application's settings module, such as C(myapp.settings). + - The Python path to the application's settings module, such as V(myapp.settings). type: path required: false pythonpath: description: - A directory to add to the Python path. Typically used to include the settings module if it is located external to the application directory. - - This would be equivalent to adding I(pythonpath)'s value to the C(PYTHONPATH) environment variable. + - This would be equivalent to adding O(pythonpath)'s value to the E(PYTHONPATH) environment variable. type: path required: false aliases: [python_path] @@ -73,54 +73,54 @@ options: aliases: [virtual_env] apps: description: - - A list of space-delimited apps to target. Used by the C(test) command. + - A list of space-delimited apps to target. Used by the V(test) command. type: str required: false cache_table: description: - - The name of the table used for database-backed caching. Used by the C(createcachetable) command. + - The name of the table used for database-backed caching. Used by the V(createcachetable) command. type: str required: false clear: description: - Clear the existing files before trying to copy or link the original file. - - Used only with the C(collectstatic) command. The C(--noinput) argument will be added automatically. + - Used only with the V(collectstatic) command. The C(--noinput) argument will be added automatically. required: false default: false type: bool database: description: - - The database to target. Used by the C(createcachetable), C(flush), C(loaddata), C(syncdb), - and C(migrate) commands. + - The database to target. Used by the V(createcachetable), V(flush), V(loaddata), V(syncdb), + and V(migrate) commands. type: str required: false failfast: description: - - Fail the command immediately if a test fails. Used by the C(test) command. + - Fail the command immediately if a test fails. Used by the V(test) command. required: false default: false type: bool aliases: [fail_fast] fixtures: description: - - A space-delimited list of fixture file names to load in the database. B(Required) by the C(loaddata) command. + - A space-delimited list of fixture file names to load in the database. B(Required) by the V(loaddata) command. type: str required: false skip: description: - - Will skip over out-of-order missing migrations, you can only use this parameter with C(migrate) command. + - Will skip over out-of-order missing migrations, you can only use this parameter with V(migrate) command. required: false type: bool merge: description: - Will run out-of-order or missing migrations as they are not rollback migrations, you can only use this - parameter with C(migrate) command. + parameter with V(migrate) command. required: false type: bool link: description: - Will create links to the files instead of copying them, you can only use this parameter with - C(collectstatic) command. + V(collectstatic) command. required: false type: bool testrunner: @@ -133,9 +133,9 @@ options: ack_venv_creation_deprecation: description: - >- - When a I(virtualenv) is set but the virtual environment does not exist, the current behavior is + When a O(virtualenv) is set but the virtual environment does not exist, the current behavior is to create a new virtual environment. That behavior is deprecated and if that case happens it will - generate a deprecation warning. Set this flag to C(true) to suppress the deprecation warning. + generate a deprecation warning. Set this flag to V(true) to suppress the deprecation warning. - Please note that you will receive no further warning about this being removed until the module will start failing in such cases from community.general 9.0.0 on. type: bool @@ -146,19 +146,19 @@ notes: B(ATTENTION - DEPRECATION): Support for Django releases older than 4.1 will be removed in community.general version 9.0.0 (estimated to be released in May 2024). Please notice that Django 4.1 requires Python 3.8 or greater. - - C(virtualenv) (U(http://www.virtualenv.org)) must be installed on the remote host if the I(virtualenv) parameter + - C(virtualenv) (U(http://www.virtualenv.org)) must be installed on the remote host if the O(virtualenv) parameter is specified. This requirement is deprecated and will be removed in community.general version 9.0.0. - - This module will create a virtualenv if the I(virtualenv) parameter is specified and a virtual environment does not already + - This module will create a virtualenv if the O(virtualenv) parameter is specified and a virtual environment does not already exist at the given location. This behavior is deprecated and will be removed in community.general version 9.0.0. - - The parameter I(virtualenv) will remain in use, but it will require the specified virtualenv to exist. + - The parameter O(virtualenv) will remain in use, but it will require the specified virtualenv to exist. The recommended way to create one in Ansible is by using M(ansible.builtin.pip). - - This module assumes English error messages for the C(createcachetable) command to detect table existence, + - This module assumes English error messages for the V(createcachetable) command to detect table existence, unfortunately. - - To be able to use the C(migrate) command with django versions < 1.7, you must have C(south) installed and added + - To be able to use the V(migrate) command with django versions < 1.7, you must have C(south) installed and added as an app in your settings. - - To be able to use the C(collectstatic) command, you must have enabled staticfiles in your settings. - - Your C(manage.py) application must be executable (rwxr-xr-x), and must have a valid shebang, - i.e. C(#!/usr/bin/env python), for invoking the appropriate Python interpreter. + - To be able to use the V(collectstatic) command, you must have enabled staticfiles in your settings. + - Your C(manage.py) application must be executable (C(rwxr-xr-x)), and must have a valid shebang, + for example C(#!/usr/bin/env python), for invoking the appropriate Python interpreter. seealso: - name: django-admin and manage.py Reference description: Reference for C(django-admin) or C(manage.py) commands. diff --git a/ansible_collections/community/general/plugins/modules/dnf_config_manager.py b/ansible_collections/community/general/plugins/modules/dnf_config_manager.py new file mode 100644 index 000000000..069fd0ddc --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/dnf_config_manager.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Andrew Hyatt +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: dnf_config_manager +short_description: Enable or disable dnf repositories using config-manager +version_added: 8.2.0 +description: + - This module enables or disables repositories using the C(dnf config-manager) sub-command. +author: Andrew Hyatt (@ahyattdev) +requirements: + - dnf + - dnf-plugins-core +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + name: + description: + - Repository ID, for example V(crb). + default: [] + required: false + type: list + elements: str + state: + description: + - Whether the repositories should be V(enabled) or V(disabled). + default: enabled + required: false + type: str + choices: [enabled, disabled] +seealso: + - module: ansible.builtin.dnf + - module: ansible.builtin.yum_repository +''' + +EXAMPLES = r''' +- name: Ensure the crb repository is enabled + community.general.dnf_config_manager: + name: crb + state: enabled + +- name: Ensure the appstream and zfs repositories are disabled + community.general.dnf_config_manager: + name: + - appstream + - zfs + state: disabled +''' + +RETURN = r''' +repo_states_pre: + description: Repo IDs before action taken. + returned: success + type: dict + contains: + enabled: + description: Enabled repository IDs. + returned: success + type: list + elements: str + disabled: + description: Disabled repository IDs. + returned: success + type: list + elements: str + sample: + enabled: + - appstream + - baseos + - crb + disabled: + - appstream-debuginfo + - appstream-source + - baseos-debuginfo + - baseos-source + - crb-debug + - crb-source +repo_states_post: + description: Repository states after action taken. + returned: success + type: dict + contains: + enabled: + description: Enabled repository IDs. + returned: success + type: list + elements: str + disabled: + description: Disabled repository IDs. + returned: success + type: list + elements: str + sample: + enabled: + - appstream + - baseos + - crb + disabled: + - appstream-debuginfo + - appstream-source + - baseos-debuginfo + - baseos-source + - crb-debug + - crb-source +changed_repos: + description: Repositories changed. + returned: success + type: list + elements: str + sample: [ 'crb' ] +''' + +from ansible.module_utils.basic import AnsibleModule +import os +import re + +DNF_BIN = "/usr/bin/dnf" +REPO_ID_RE = re.compile(r'^Repo-id\s*:\s*(\S+)$') +REPO_STATUS_RE = re.compile(r'^Repo-status\s*:\s*(disabled|enabled)$') + + +def get_repo_states(module): + rc, out, err = module.run_command([DNF_BIN, 'repolist', '--all', '--verbose'], check_rc=True) + + repos = dict() + last_repo = '' + for i, line in enumerate(out.split('\n')): + m = REPO_ID_RE.match(line) + if m: + if len(last_repo) > 0: + module.fail_json(msg='dnf repolist parse failure: parsed another repo id before next status') + last_repo = m.group(1) + continue + m = REPO_STATUS_RE.match(line) + if m: + if len(last_repo) == 0: + module.fail_json(msg='dnf repolist parse failure: parsed status before repo id') + repos[last_repo] = m.group(1) + last_repo = '' + return repos + + +def set_repo_states(module, repo_ids, state): + module.run_command([DNF_BIN, 'config-manager', '--set-{0}'.format(state)] + repo_ids, check_rc=True) + + +def pack_repo_states_for_return(states): + enabled = [] + disabled = [] + for repo_id in states: + if states[repo_id] == 'enabled': + enabled.append(repo_id) + else: + disabled.append(repo_id) + + # Sort for consistent results + enabled.sort() + disabled.sort() + + return {'enabled': enabled, 'disabled': disabled} + + +def main(): + module_args = dict( + name=dict(type='list', elements='str', required=False, default=[]), + state=dict(type='str', required=False, choices=['enabled', 'disabled'], default='enabled') + ) + + result = dict( + changed=False + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + if not os.path.exists(DNF_BIN): + module.fail_json(msg="%s was not found" % DNF_BIN) + + repo_states = get_repo_states(module) + result['repo_states_pre'] = pack_repo_states_for_return(repo_states) + + desired_repo_state = module.params['state'] + names = module.params['name'] + + to_change = [] + for repo_id in names: + if repo_id not in repo_states: + module.fail_json(msg="did not find repo with ID '{0}' in dnf repolist --all --verbose".format(repo_id)) + if repo_states[repo_id] != desired_repo_state: + to_change.append(repo_id) + result['changed'] = len(to_change) > 0 + result['changed_repos'] = to_change + + if module.check_mode: + module.exit_json(**result) + + if len(to_change) > 0: + set_repo_states(module, to_change, desired_repo_state) + + repo_states_post = get_repo_states(module) + result['repo_states_post'] = pack_repo_states_for_return(repo_states_post) + + for repo_id in to_change: + if repo_states_post[repo_id] != desired_repo_state: + module.fail_json(msg="dnf config-manager failed to make '{0}' {1}".format(repo_id, desired_repo_state)) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/dnf_versionlock.py b/ansible_collections/community/general/plugins/modules/dnf_versionlock.py index fac3ad78d..3fcf132ea 100644 --- a/ansible_collections/community/general/plugins/modules/dnf_versionlock.py +++ b/ansible_collections/community/general/plugins/modules/dnf_versionlock.py @@ -38,7 +38,7 @@ options: description: - Package name spec to add or exclude to or delete from the C(locklist) using the format expected by the C(dnf repoquery) command. - - This parameter is mutually exclusive with I(state=clean). + - This parameter is mutually exclusive with O(state=clean). type: list required: false elements: str @@ -52,19 +52,19 @@ options: default: false state: description: - - Whether to add (C(present) or C(excluded)) to or remove (C(absent) or - C(clean)) from the C(locklist). - - C(present) will add a package name spec to the C(locklist). If there is a + - Whether to add (V(present) or V(excluded)) to or remove (V(absent) or + V(clean)) from the C(locklist). + - V(present) will add a package name spec to the C(locklist). If there is a installed package that matches, then only that version will be added. Otherwise, all available package versions will be added. - - C(excluded) will add a package name spec as excluded to the + - V(excluded) will add a package name spec as excluded to the C(locklist). It means that packages represented by the package name spec will be excluded from transaction operations. All available package versions will be added. - - C(absent) will delete entries in the C(locklist) that match the + - V(absent) will delete entries in the C(locklist) that match the package name spec. - - C(clean) will delete all entries in the C(locklist). This option is - mutually exclusive with C(name). + - V(clean) will delete all entries in the C(locklist). This option is + mutually exclusive with O(name). choices: [ 'absent', 'clean', 'excluded', 'present' ] type: str default: present diff --git a/ansible_collections/community/general/plugins/modules/dnsimple.py b/ansible_collections/community/general/plugins/modules/dnsimple.py index df41f73a6..c5829e36e 100644 --- a/ansible_collections/community/general/plugins/modules/dnsimple.py +++ b/ansible_collections/community/general/plugins/modules/dnsimple.py @@ -26,13 +26,13 @@ attributes: options: account_email: description: - - Account email. If omitted, the environment variables C(DNSIMPLE_EMAIL) and C(DNSIMPLE_API_TOKEN) will be looked for. + - Account email. If omitted, the environment variables E(DNSIMPLE_EMAIL) and E(DNSIMPLE_API_TOKEN) will be looked for. - "If those aren't found, a C(.dnsimple) file will be looked for, see: U(https://github.com/mikemaccana/dnsimple-python#getting-started)." - "C(.dnsimple) config files are only supported in dnsimple-python<2.0.0" type: str account_api_token: description: - - Account API token. See I(account_email) for more information. + - Account API token. See O(account_email) for more information. type: str domain: description: @@ -77,7 +77,7 @@ options: solo: description: - Whether the record should be the only one for that record type and record name. - - Only use with C(state) is set to C(present) on a record. + - Only use with O(state) is set to V(present) on a record. type: 'bool' default: false sandbox: @@ -178,7 +178,7 @@ class DNSimpleV2(): client = Client(sandbox=self.sandbox, email=self.account_email, access_token=self.account_api_token, user_agent="ansible/community.general") else: msg = "Option account_email or account_api_token not provided. " \ - "Dnsimple authentiction with a .dnsimple config file is not " \ + "Dnsimple authentication with a .dnsimple config file is not " \ "supported with dnsimple-python>=2.0.0" raise DNSimpleException(msg) client.identity.whoami() @@ -225,24 +225,24 @@ class DNSimpleV2(): self.client.domains.delete_domain(self.account.id, domain) def get_records(self, zone, dnsimple_filter=None): - """return dns ressource records which match a specified filter""" + """return dns resource records which match a specified filter""" records_list = self._get_paginated_result(self.client.zones.list_records, account_id=self.account.id, zone=zone, filter=dnsimple_filter) return [d.__dict__ for d in records_list] def delete_record(self, domain, rid): - """delete a single dns ressource record""" + """delete a single dns resource record""" self.client.zones.delete_record(self.account.id, domain, rid) def update_record(self, domain, rid, ttl=None, priority=None): - """update a single dns ressource record""" + """update a single dns resource record""" zr = ZoneRecordUpdateInput(ttl=ttl, priority=priority) result = self.client.zones.update_record(self.account.id, str(domain), str(rid), zr).data.__dict__ return result def create_record(self, domain, name, record_type, content, ttl=None, priority=None): - """create a single dns ressource record""" + """create a single dns resource record""" zr = ZoneRecordInput(name=name, type=record_type, content=content, ttl=ttl, priority=priority) return self.client.zones.create_record(self.account.id, str(domain), zr).data.__dict__ diff --git a/ansible_collections/community/general/plugins/modules/dnsimple_info.py b/ansible_collections/community/general/plugins/modules/dnsimple_info.py index 52fd53303..46c2877f7 100644 --- a/ansible_collections/community/general/plugins/modules/dnsimple_info.py +++ b/ansible_collections/community/general/plugins/modules/dnsimple_info.py @@ -83,7 +83,7 @@ dnsimple_domain_info: description: Returns a list of dictionaries of all domains associated with the supplied account ID. type: list elements: dict - returned: success when I(name) is not specified + returned: success when O(name) is not specified sample: - account_id: 1234 created_at: '2021-10-16T21:25:42Z' @@ -120,7 +120,7 @@ dnsimple_records_info: description: Returns a list of dictionaries with all records for the domain supplied. type: list elements: dict - returned: success when I(name) is specified, but I(record) is not + returned: success when O(name) is specified, but O(record) is not sample: - content: ns1.dnsimple.com admin.dnsimple.com created_at: '2021-10-16T19:07:34Z' @@ -174,7 +174,7 @@ dnsimple_records_info: type: str dnsimple_record_info: description: Returns a list of dictionaries that match the record supplied. - returned: success when I(name) and I(record) are specified + returned: success when O(name) and O(record) are specified type: list elements: dict sample: @@ -239,9 +239,9 @@ with deps.declare("requests"): def build_url(account, key, is_sandbox): headers = {'Accept': 'application/json', - 'Authorization': 'Bearer ' + key} - url = 'https://api{sandbox}.dnsimple.com/'.format( - sandbox=".sandbox" if is_sandbox else "") + 'v2/' + account + 'Authorization': 'Bearer {0}'.format(key)} + sandbox = '.sandbox' if is_sandbox else '' + url = 'https://api{sandbox}.dnsimple.com/v2/{account}'.format(sandbox=sandbox, account=account) req = Request(url=url, headers=headers) prepped_request = req.prepare() return prepped_request @@ -250,19 +250,21 @@ def build_url(account, key, is_sandbox): def iterate_data(module, request_object): base_url = request_object.url response = Session().send(request_object) - if 'pagination' in response.json(): - data = response.json()["data"] - pages = response.json()["pagination"]["total_pages"] - if int(pages) > 1: - for page in range(1, pages): - page = page + 1 - request_object.url = base_url + '&page=' + str(page) - new_results = Session().send(request_object) - data = data + new_results.json()["data"] - return data - else: + if 'pagination' not in response.json(): module.fail_json('API Call failed, check ID, key and sandbox values') + data = response.json()["data"] + total_pages = response.json()["pagination"]["total_pages"] + page = 1 + + while page < total_pages: + page = page + 1 + request_object.url = '{url}&page={page}'.format(url=base_url, page=page) + new_results = Session().send(request_object) + data = data + new_results.json()['data'] + + return data + def record_info(dnsimple_mod, req_obj): req_obj.url, req_obj.method = req_obj.url + '/zones/' + dnsimple_mod.params["name"] + '/records?name=' + dnsimple_mod.params["record"], 'GET' diff --git a/ansible_collections/community/general/plugins/modules/dnsmadeeasy.py b/ansible_collections/community/general/plugins/modules/dnsmadeeasy.py index 44587ca39..47d9430e7 100644 --- a/ansible_collections/community/general/plugins/modules/dnsmadeeasy.py +++ b/ansible_collections/community/general/plugins/modules/dnsmadeeasy.py @@ -87,14 +87,14 @@ options: validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be used + - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. type: bool default: true monitor: description: - - If C(true), add or change the monitor. This is applicable only for A records. + - If V(true), add or change the monitor. This is applicable only for A records. type: bool default: false @@ -133,7 +133,7 @@ options: contactList: description: - Name or id of the contact list that the monitor will notify. - - The default C('') means the Account Owner. + - The default V('') means the Account Owner. type: str httpFqdn: @@ -153,7 +153,7 @@ options: failover: description: - - If C(true), add or change the failover. This is applicable only for A records. + - If V(true), add or change the failover. This is applicable only for A records. type: bool default: false @@ -509,15 +509,15 @@ class DME2(object): return json.dumps(data, separators=(',', ':')) def createRecord(self, data): - # @TODO update the cache w/ resultant record + id when impleneted + # @TODO update the cache w/ resultant record + id when implemented return self.query(self.record_url, 'POST', data) def updateRecord(self, record_id, data): - # @TODO update the cache w/ resultant record + id when impleneted + # @TODO update the cache w/ resultant record + id when implemented return self.query(self.record_url + '/' + str(record_id), 'PUT', data) def deleteRecord(self, record_id): - # @TODO remove record from the cache when impleneted + # @TODO remove record from the cache when implemented return self.query(self.record_url + '/' + str(record_id), 'DELETE') def getMonitor(self, record_id): diff --git a/ansible_collections/community/general/plugins/modules/dpkg_divert.py b/ansible_collections/community/general/plugins/modules/dpkg_divert.py index 4a1651f51..5f0d924fe 100644 --- a/ansible_collections/community/general/plugins/modules/dpkg_divert.py +++ b/ansible_collections/community/general/plugins/modules/dpkg_divert.py @@ -20,13 +20,13 @@ description: - A diversion is for C(dpkg) the knowledge that only a given package (or the local administrator) is allowed to install a file at a given location. Other packages shipping their own version of this file will - be forced to I(divert) it, i.e. to install it at another location. It + be forced to O(divert) it, that is to install it at another location. It allows one to keep changes in a file provided by a debian package by preventing its overwrite at package upgrade. - This module manages diversions of debian packages files using the C(dpkg-divert) commandline tool. It can either create or remove a diversion for a given file, but also update an existing diversion - to modify its I(holder) and/or its I(divert) location. + to modify its O(holder) and/or its O(divert) location. extends_documentation_fragment: - community.general.attributes attributes: @@ -39,14 +39,14 @@ options: description: - The original and absolute path of the file to be diverted or undiverted. This path is unique, i.e. it is not possible to get - two diversions for the same I(path). + two diversions for the same O(path). required: true type: path state: description: - - When I(state=absent), remove the diversion of the specified - I(path); when I(state=present), create the diversion if it does - not exist, or update its package I(holder) or I(divert) location, + - When O(state=absent), remove the diversion of the specified + O(path); when O(state=present), create the diversion if it does + not exist, or update its package O(holder) or O(divert) location, if it already exists. type: str default: present @@ -59,31 +59,31 @@ options: - The actual package does not have to be installed or even to exist for its name to be valid. If not specified, the diversion is hold by 'LOCAL', that is reserved by/for dpkg for local diversions. - - This parameter is ignored when I(state=absent). + - This parameter is ignored when O(state=absent). type: str divert: description: - The location where the versions of file will be diverted. - Default is to add suffix C(.distrib) to the file path. - - This parameter is ignored when I(state=absent). + - This parameter is ignored when O(state=absent). type: path rename: description: - - Actually move the file aside (when I(state=present)) or back (when - I(state=absent)), but only when changing the state of the diversion. + - Actually move the file aside (when O(state=present)) or back (when + O(state=absent)), but only when changing the state of the diversion. This parameter has no effect when attempting to add a diversion that already exists or when removing an unexisting one. - - Unless I(force=true), renaming fails if the destination file already + - Unless O(force=true), renaming fails if the destination file already exists (this lock being a dpkg-divert feature, and bypassing it being a module feature). type: bool default: false force: description: - - When I(rename=true) and I(force=true), renaming is performed even if + - When O(rename=true) and O(force=true), renaming is performed even if the target of the renaming exists, i.e. the existing contents of the file at this location will be lost. - - This parameter is ignored when I(rename=false). + - This parameter is ignored when O(rename=false). type: bool default: false requirements: diff --git a/ansible_collections/community/general/plugins/modules/easy_install.py b/ansible_collections/community/general/plugins/modules/easy_install.py index 564493180..2e8fc2f4f 100644 --- a/ansible_collections/community/general/plugins/modules/easy_install.py +++ b/ansible_collections/community/general/plugins/modules/easy_install.py @@ -14,7 +14,7 @@ DOCUMENTATION = ''' module: easy_install short_description: Installs Python libraries description: - - Installs Python libraries, optionally in a I(virtualenv) + - Installs Python libraries, optionally in a C(virtualenv) extends_documentation_fragment: - community.general.attributes attributes: @@ -26,13 +26,13 @@ options: name: type: str description: - - A Python library name + - A Python library name. required: true virtualenv: type: str description: - - an optional I(virtualenv) directory path to install into. If the - I(virtualenv) does not exist, it is created automatically + - An optional O(virtualenv) directory path to install into. If the + O(virtualenv) does not exist, it is created automatically. virtualenv_site_packages: description: - Whether the virtual environment will inherit packages from the @@ -46,21 +46,21 @@ options: type: str description: - The command to create the virtual environment with. For example - C(pyvenv), C(virtualenv), C(virtualenv2). + V(pyvenv), V(virtualenv), V(virtualenv2). default: virtualenv executable: type: str description: - The explicit executable or a pathname to the executable to be used to run easy_install for a specific version of Python installed in the - system. For example C(easy_install-3.3), if there are both Python 2.7 + system. For example V(easy_install-3.3), if there are both Python 2.7 and 3.3 installations in the system and you want to run easy_install for the Python 3.3 installation. default: easy_install state: type: str description: - - The desired state of the library. C(latest) ensures that the latest version is installed. + - The desired state of the library. V(latest) ensures that the latest version is installed. choices: [present, latest] default: present notes: @@ -68,8 +68,8 @@ notes: libraries. Thus this module is not able to remove libraries. It is generally recommended to use the M(ansible.builtin.pip) module which you can first install using M(community.general.easy_install). - - Also note that I(virtualenv) must be installed on the remote host if the - C(virtualenv) parameter is specified. + - Also note that C(virtualenv) must be installed on the remote host if the + O(virtualenv) parameter is specified. requirements: [ "virtualenv" ] author: "Matt Wright (@mattupstate)" ''' diff --git a/ansible_collections/community/general/plugins/modules/ejabberd_user.py b/ansible_collections/community/general/plugins/modules/ejabberd_user.py index 397207ae6..d0b575e1c 100644 --- a/ansible_collections/community/general/plugins/modules/ejabberd_user.py +++ b/ansible_collections/community/general/plugins/modules/ejabberd_user.py @@ -78,6 +78,7 @@ EXAMPLES = ''' import syslog from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt class EjabberdUser(object): @@ -85,7 +86,7 @@ class EjabberdUser(object): object manages user creation and deletion using ejabberdctl. The following commands are currently supported: * ejabberdctl register - * ejabberdctl deregister + * ejabberdctl unregister """ def __init__(self, module): @@ -95,6 +96,17 @@ class EjabberdUser(object): self.host = module.params.get('host') self.user = module.params.get('username') self.pwd = module.params.get('password') + self.runner = CmdRunner( + module, + command="ejabberdctl", + arg_formats=dict( + cmd=cmd_runner_fmt.as_list(), + host=cmd_runner_fmt.as_list(), + user=cmd_runner_fmt.as_list(), + pwd=cmd_runner_fmt.as_list(), + ), + check_rc=False, + ) @property def changed(self): @@ -102,7 +114,7 @@ class EjabberdUser(object): changed. It will return True if the user does not match the supplied credentials and False if it does not """ - return self.run_command('check_password', [self.user, self.host, self.pwd]) + return self.run_command('check_password', 'user host pwd', (lambda rc, out, err: bool(rc))) @property def exists(self): @@ -110,7 +122,7 @@ class EjabberdUser(object): host specified. If the user exists True is returned, otherwise False is returned """ - return self.run_command('check_account', [self.user, self.host]) + return self.run_command('check_account', 'user host', (lambda rc, out, err: not bool(rc))) def log(self, entry): """ This method will log information to the local syslog facility """ @@ -118,29 +130,36 @@ class EjabberdUser(object): syslog.openlog('ansible-%s' % self.module._name) syslog.syslog(syslog.LOG_NOTICE, entry) - def run_command(self, cmd, options): + def run_command(self, cmd, options, process=None): """ This method will run the any command specified and return the returns using the Ansible common module """ - cmd = [self.module.get_bin_path('ejabberdctl'), cmd] + options - self.log('command: %s' % " ".join(cmd)) - return self.module.run_command(cmd) + def _proc(*a): + return a + + if process is None: + process = _proc + + with self.runner("cmd " + options, output_process=process) as ctx: + res = ctx.run(cmd=cmd, host=self.host, user=self.user, pwd=self.pwd) + self.log('command: %s' % " ".join(ctx.run_info['cmd'])) + return res def update(self): """ The update method will update the credentials for the user provided """ - return self.run_command('change_password', [self.user, self.host, self.pwd]) + return self.run_command('change_password', 'user host pwd') def create(self): """ The create method will create a new user on the host with the password provided """ - return self.run_command('register', [self.user, self.host, self.pwd]) + return self.run_command('register', 'user host pwd') def delete(self): """ The delete method will delete the user from the host """ - return self.run_command('unregister', [self.user, self.host]) + return self.run_command('unregister', 'user host') def main(): @@ -150,7 +169,7 @@ def main(): username=dict(required=True, type='str'), password=dict(type='str', no_log=True), state=dict(default='present', choices=['present', 'absent']), - logging=dict(default=False, type='bool') # deprecate in favour of c.g.syslogger? + logging=dict(default=False, type='bool', removed_in_version='10.0.0', removed_from_collection='community.general'), ), required_if=[ ('state', 'present', ['password']), diff --git a/ansible_collections/community/general/plugins/modules/elasticsearch_plugin.py b/ansible_collections/community/general/plugins/modules/elasticsearch_plugin.py index cd4bb45de..92b628a74 100644 --- a/ansible_collections/community/general/plugins/modules/elasticsearch_plugin.py +++ b/ansible_collections/community/general/plugins/modules/elasticsearch_plugin.py @@ -69,7 +69,6 @@ options: plugin_bin: description: - Location of the plugin binary. If this file is not found, the default plugin binaries will be used. - - The default changed in Ansible 2.4 to None. type: path plugin_dir: description: diff --git a/ansible_collections/community/general/plugins/modules/emc_vnx_sg_member.py b/ansible_collections/community/general/plugins/modules/emc_vnx_sg_member.py index 487b6feef..b06cd01de 100644 --- a/ansible_collections/community/general/plugins/modules/emc_vnx_sg_member.py +++ b/ansible_collections/community/general/plugins/modules/emc_vnx_sg_member.py @@ -46,8 +46,8 @@ options: state: description: - Indicates the desired lunid state. - - C(present) ensures specified lunid is present in the Storage Group. - - C(absent) ensures specified lunid is absent from Storage Group. + - V(present) ensures specified lunid is present in the Storage Group. + - V(absent) ensures specified lunid is absent from Storage Group. default: present choices: [ "present", "absent"] type: str diff --git a/ansible_collections/community/general/plugins/modules/etcd3.py b/ansible_collections/community/general/plugins/modules/etcd3.py index 9cd027406..2fdc3f2f8 100644 --- a/ansible_collections/community/general/plugins/modules/etcd3.py +++ b/ansible_collections/community/general/plugins/modules/etcd3.py @@ -61,22 +61,22 @@ options: type: str description: - The password to use for authentication. - - Required if I(user) is defined. + - Required if O(user) is defined. ca_cert: type: path description: - The Certificate Authority to use to verify the etcd host. - - Required if I(client_cert) and I(client_key) are defined. + - Required if O(client_cert) and O(client_key) are defined. client_cert: type: path description: - PEM formatted certificate chain file to be used for SSL client authentication. - - Required if I(client_key) is defined. + - Required if O(client_key) is defined. client_key: type: path description: - PEM formatted file that contains your private key to be used for SSL client authentication. - - Required if I(client_cert) is defined. + - Required if O(client_cert) is defined. timeout: type: int description: diff --git a/ansible_collections/community/general/plugins/modules/facter.py b/ansible_collections/community/general/plugins/modules/facter.py index e7cf52e20..87017246a 100644 --- a/ansible_collections/community/general/plugins/modules/facter.py +++ b/ansible_collections/community/general/plugins/modules/facter.py @@ -11,7 +11,7 @@ __metaclass__ = type DOCUMENTATION = ''' --- module: facter -short_description: Runs the discovery program I(facter) on the remote system +short_description: Runs the discovery program C(facter) on the remote system description: - Runs the C(facter) discovery program (U(https://github.com/puppetlabs/facter)) on the remote system, returning diff --git a/ansible_collections/community/general/plugins/modules/facter_facts.py b/ansible_collections/community/general/plugins/modules/facter_facts.py new file mode 100644 index 000000000..abc3f87eb --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/facter_facts.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Alexei Znamensky +# Copyright (c) 2012, Michael DeHaan +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: facter_facts +short_description: Runs the discovery program C(facter) on the remote system and return Ansible facts +version_added: 8.0.0 +description: + - Runs the C(facter) discovery program + (U(https://github.com/puppetlabs/facter)) on the remote system, returning Ansible facts from the + JSON data that can be useful for inventory purposes. +extends_documentation_fragment: + - community.general.attributes + - community.general.attributes.facts + - community.general.attributes.facts_module +options: + arguments: + description: + - Specifies arguments for facter. + type: list + elements: str +requirements: + - facter + - ruby-json +author: + - Ansible Core Team + - Michael DeHaan +''' + +EXAMPLES = ''' +- name: Execute facter no arguments + community.general.facter_facts: + +- name: Execute facter with arguments + community.general.facter_facts: + arguments: + - -p + - system_uptime + - timezone + - is_virtual +''' + +RETURN = r''' +ansible_facts: + description: Dictionary with one key C(facter). + returned: always + type: dict + contains: + facter: + description: Dictionary containing facts discovered in the remote system. + returned: always + type: dict +''' + +import json + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec=dict( + arguments=dict(type='list', elements='str'), + ), + supports_check_mode=True, + ) + + facter_path = module.get_bin_path( + 'facter', + opt_dirs=['/opt/puppetlabs/bin']) + + cmd = [facter_path, "--json"] + if module.params['arguments']: + cmd += module.params['arguments'] + + rc, out, err = module.run_command(cmd, check_rc=True) + module.exit_json(ansible_facts=dict(facter=json.loads(out))) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/filesize.py b/ansible_collections/community/general/plugins/modules/filesize.py index b3eb90d61..83de68288 100644 --- a/ansible_collections/community/general/plugins/modules/filesize.py +++ b/ansible_collections/community/general/plugins/modules/filesize.py @@ -41,20 +41,20 @@ options: description: - Requested size of the file. - The value is a number (either C(int) or C(float)) optionally followed - by a multiplicative suffix, that can be one of C(B) (bytes), C(KB) or - C(kB) (= 1000B), C(MB) or C(mB) (= 1000kB), C(GB) or C(gB) (= 1000MB), - and so on for C(T), C(P), C(E), C(Z) and C(Y); or alternatively one of - C(K), C(k) or C(KiB) (= 1024B); C(M), C(m) or C(MiB) (= 1024KiB); - C(G), C(g) or C(GiB) (= 1024MiB); and so on. + by a multiplicative suffix, that can be one of V(B) (bytes), V(KB) or + V(kB) (= 1000B), V(MB) or V(mB) (= 1000kB), V(GB) or V(gB) (= 1000MB), + and so on for V(T), V(P), V(E), V(Z) and V(Y); or alternatively one of + V(K), V(k) or V(KiB) (= 1024B); V(M), V(m) or V(MiB) (= 1024KiB); + V(G), V(g) or V(GiB) (= 1024MiB); and so on. - If the multiplicative suffix is not provided, the value is treated as - an integer number of blocks of I(blocksize) bytes each (float values + an integer number of blocks of O(blocksize) bytes each (float values are rounded to the closest integer). - - When the I(size) value is equal to the current file size, does nothing. - - When the I(size) value is bigger than the current file size, bytes from - I(source) (if I(sparse) is not C(false)) are appended to the file + - When the O(size) value is equal to the current file size, does nothing. + - When the O(size) value is bigger than the current file size, bytes from + O(source) (if O(sparse) is not V(false)) are appended to the file without truncating it, in other words, without modifying the existing bytes of the file. - - When the I(size) value is smaller than the current file size, it is + - When the O(size) value is smaller than the current file size, it is truncated to the requested value without modifying bytes before this value. - That means that a file of any arbitrary size can be grown to any other @@ -65,24 +65,24 @@ options: blocksize: description: - Size of blocks, in bytes if not followed by a multiplicative suffix. - - The numeric value (before the unit) C(MUST) be an integer (or a C(float) + - The numeric value (before the unit) B(MUST) be an integer (or a C(float) if it equals an integer). - If not set, the size of blocks is guessed from the OS and commonly - results in C(512) or C(4096) bytes, that is used internally by the - module or when I(size) has no unit. + results in V(512) or V(4096) bytes, that is used internally by the + module or when O(size) has no unit. type: raw source: description: - Device or file that provides input data to provision the file. - - This parameter is ignored when I(sparse=true). + - This parameter is ignored when O(sparse=true). type: path default: /dev/zero force: description: - Whether or not to overwrite the file if it exists, in other words, to - truncate it from 0. When C(true), the module is not idempotent, that - means it always reports I(changed=true). - - I(force=true) and I(sparse=true) are mutually exclusive. + truncate it from 0. When V(true), the module is not idempotent, that + means it always reports C(changed=true). + - O(force=true) and O(sparse=true) are mutually exclusive. type: bool default: false sparse: @@ -91,7 +91,7 @@ options: - This option is effective only on newly created files, or when growing a file, only for the bytes to append. - This option is not supported on OSes or filesystems not supporting sparse files. - - I(force=true) and I(sparse=true) are mutually exclusive. + - O(force=true) and O(sparse=true) are mutually exclusive. type: bool default: false unsafe_writes: @@ -206,7 +206,7 @@ filesize: type: int sample: 1024 bytes: - description: Size of the file, in bytes, as the product of C(blocks) and C(blocksize). + description: Size of the file, in bytes, as the product of RV(filesize.blocks) and RV(filesize.blocksize). type: int sample: 512000 iec: diff --git a/ansible_collections/community/general/plugins/modules/filesystem.py b/ansible_collections/community/general/plugins/modules/filesystem.py index 0e6b815b4..ec361245b 100644 --- a/ansible_collections/community/general/plugins/modules/filesystem.py +++ b/ansible_collections/community/general/plugins/modules/filesystem.py @@ -29,12 +29,12 @@ attributes: options: state: description: - - If I(state=present), the filesystem is created if it doesn't already - exist, that is the default behaviour if I(state) is omitted. - - If I(state=absent), filesystem signatures on I(dev) are wiped if it + - If O(state=present), the filesystem is created if it doesn't already + exist, that is the default behaviour if O(state) is omitted. + - If O(state=absent), filesystem signatures on O(dev) are wiped if it contains a filesystem (as known by C(blkid)). - - When I(state=absent), all other options but I(dev) are ignored, and the - module doesn't fail if the device I(dev) doesn't actually exist. + - When O(state=absent), all other options but O(dev) are ignored, and the + module does not fail if the device O(dev) doesn't actually exist. type: str choices: [ present, absent ] default: present @@ -43,7 +43,7 @@ options: choices: [ btrfs, ext2, ext3, ext4, ext4dev, f2fs, lvm, ocfs2, reiserfs, xfs, vfat, swap, ufs ] description: - Filesystem type to be created. This option is required with - I(state=present) (or if I(state) is omitted). + O(state=present) (or if O(state) is omitted). - ufs support has been added in community.general 3.4.0. type: str aliases: [type] @@ -53,50 +53,68 @@ options: regular file (both). - When setting Linux-specific filesystem types on FreeBSD, this module only works when applying to regular files, aka disk images. - - Currently C(lvm) (Linux-only) and C(ufs) (FreeBSD-only) don't support - a regular file as their target I(dev). + - Currently V(lvm) (Linux-only) and V(ufs) (FreeBSD-only) do not support + a regular file as their target O(dev). - Support for character devices on FreeBSD has been added in community.general 3.4.0. type: path required: true aliases: [device] force: description: - - If C(true), allows to create new filesystem on devices that already has filesystem. + - If V(true), allows to create new filesystem on devices that already has filesystem. type: bool default: false resizefs: description: - - If C(true), if the block device and filesystem size differ, grow the filesystem into the space. + - If V(true), if the block device and filesystem size differ, grow the filesystem into the space. - Supported for C(btrfs), C(ext2), C(ext3), C(ext4), C(ext4dev), C(f2fs), C(lvm), C(xfs), C(ufs) and C(vfat) filesystems. Attempts to resize other filesystem types will fail. - XFS Will only grow if mounted. Currently, the module is based on commands from C(util-linux) package to perform operations, so resizing of XFS is not supported on FreeBSD systems. - vFAT will likely fail if C(fatresize < 1.04). + - Mutually exclusive with O(uuid). type: bool default: false opts: description: - List of options to be passed to C(mkfs) command. type: str + uuid: + description: + - Set filesystem's UUID to the given value. + - The UUID options specified in O(opts) take precedence over this value. + - See xfs_admin(8) (C(xfs)), tune2fs(8) (C(ext2), C(ext3), C(ext4), C(ext4dev)) for possible values. + - For O(fstype=lvm) the value is ignored, it resets the PV UUID if set. + - Supported for O(fstype) being one of C(ext2), C(ext3), C(ext4), C(ext4dev), C(lvm), or C(xfs). + - This is B(not idempotent). Specifying this option will always result in a change. + - Mutually exclusive with O(resizefs). + type: str + version_added: 7.1.0 requirements: - - Uses specific tools related to the I(fstype) for creating or resizing a + - Uses specific tools related to the O(fstype) for creating or resizing a filesystem (from packages e2fsprogs, xfsprogs, dosfstools, and so on). - Uses generic tools mostly related to the Operating System (Linux or FreeBSD) or available on both, as C(blkid). - On FreeBSD, either C(util-linux) or C(e2fsprogs) package is required. notes: - - Potential filesystems on I(dev) are checked using C(blkid). In case C(blkid) + - Potential filesystems on O(dev) are checked using C(blkid). In case C(blkid) is unable to detect a filesystem (and in case C(fstyp) on FreeBSD is also unable to detect a filesystem), this filesystem is overwritten even if - I(force) is C(false). + O(force) is V(false). - On FreeBSD systems, both C(e2fsprogs) and C(util-linux) packages provide a C(blkid) command that is compatible with this module. However, these packages conflict with each other, and only the C(util-linux) package - provides the command required to not fail when I(state=absent). + provides the command required to not fail when O(state=absent). seealso: - module: community.general.filesize - module: ansible.posix.mount + - name: xfs_admin(8) manpage for Linux + description: Manual page of the GNU/Linux's xfs_admin implementation + link: https://man7.org/linux/man-pages/man8/xfs_admin.8.html + - name: tune2fs(8) manpage for Linux + description: Manual page of the GNU/Linux's tune2fs implementation + link: https://man7.org/linux/man-pages/man8/tune2fs.8.html ''' EXAMPLES = ''' @@ -120,6 +138,24 @@ EXAMPLES = ''' community.general.filesystem: dev: /path/to/disk.img fstype: vfat + +- name: Reset an xfs filesystem UUID on /dev/sdb1 + community.general.filesystem: + fstype: xfs + dev: /dev/sdb1 + uuid: generate + +- name: Reset an ext4 filesystem UUID on /dev/sdb1 + community.general.filesystem: + fstype: ext4 + dev: /dev/sdb1 + uuid: random + +- name: Reset an LVM filesystem (PV) UUID on /dev/sdc + community.general.filesystem: + fstype: lvm + dev: /dev/sdc + uuid: random ''' import os @@ -178,10 +214,15 @@ class Filesystem(object): MKFS = None MKFS_FORCE_FLAGS = [] + MKFS_SET_UUID_OPTIONS = None + MKFS_SET_UUID_EXTRA_OPTIONS = [] INFO = None GROW = None GROW_MAX_SPACE_FLAGS = [] GROW_MOUNTPOINT_ONLY = False + CHANGE_UUID = None + CHANGE_UUID_OPTION = None + CHANGE_UUID_OPTION_HAS_ARG = True LANG_ENV = {'LANG': 'C', 'LC_ALL': 'C', 'LC_MESSAGES': 'C'} @@ -200,13 +241,19 @@ class Filesystem(object): """ raise NotImplementedError() - def create(self, opts, dev): + def create(self, opts, dev, uuid=None): if self.module.check_mode: return + if uuid and self.MKFS_SET_UUID_OPTIONS: + if not (set(self.MKFS_SET_UUID_OPTIONS) & set(opts)): + opts += [self.MKFS_SET_UUID_OPTIONS[0], uuid] + self.MKFS_SET_UUID_EXTRA_OPTIONS + mkfs = self.module.get_bin_path(self.MKFS, required=True) cmd = [mkfs] + self.MKFS_FORCE_FLAGS + opts + [str(dev)] self.module.run_command(cmd, check_rc=True) + if uuid and self.CHANGE_UUID and self.MKFS_SET_UUID_OPTIONS is None: + self.change_uuid(new_uuid=uuid, dev=dev) def wipefs(self, dev): if self.module.check_mode: @@ -255,11 +302,31 @@ class Filesystem(object): dummy, out, dummy = self.module.run_command(self.grow_cmd(grow_target), check_rc=True) return out + def change_uuid_cmd(self, new_uuid, target): + """Build and return the UUID change command line as list.""" + cmdline = [self.module.get_bin_path(self.CHANGE_UUID, required=True)] + if self.CHANGE_UUID_OPTION_HAS_ARG: + cmdline += [self.CHANGE_UUID_OPTION, new_uuid, target] + else: + cmdline += [self.CHANGE_UUID_OPTION, target] + return cmdline + + def change_uuid(self, new_uuid, dev): + """Change filesystem UUID. Returns stdout of used command""" + if self.module.check_mode: + self.module.exit_json(change=True, msg='Changing %s filesystem UUID on device %s' % (self.fstype, dev)) + + dummy, out, dummy = self.module.run_command(self.change_uuid_cmd(new_uuid=new_uuid, target=str(dev)), check_rc=True) + return out + class Ext(Filesystem): MKFS_FORCE_FLAGS = ['-F'] + MKFS_SET_UUID_OPTIONS = ['-U'] INFO = 'tune2fs' GROW = 'resize2fs' + CHANGE_UUID = 'tune2fs' + CHANGE_UUID_OPTION = "-U" def get_fs_size(self, dev): """Get Block count and Block size and return their product.""" @@ -298,6 +365,8 @@ class XFS(Filesystem): INFO = 'xfs_info' GROW = 'xfs_growfs' GROW_MOUNTPOINT_ONLY = True + CHANGE_UUID = "xfs_admin" + CHANGE_UUID_OPTION = "-U" def get_fs_size(self, dev): """Get bsize and blocks and return their product.""" @@ -451,8 +520,13 @@ class VFAT(Filesystem): class LVM(Filesystem): MKFS = 'pvcreate' MKFS_FORCE_FLAGS = ['-f'] + MKFS_SET_UUID_OPTIONS = ['-u', '--uuid'] + MKFS_SET_UUID_EXTRA_OPTIONS = ['--norestorefile'] INFO = 'pvs' GROW = 'pvresize' + CHANGE_UUID = 'pvchange' + CHANGE_UUID_OPTION = '-u' + CHANGE_UUID_OPTION_HAS_ARG = False def get_fs_size(self, dev): """Get and return PV size, in bytes.""" @@ -525,10 +599,14 @@ def main(): opts=dict(type='str'), force=dict(type='bool', default=False), resizefs=dict(type='bool', default=False), + uuid=dict(type='str', required=False), ), required_if=[ ('state', 'present', ['fstype']) ], + mutually_exclusive=[ + ('resizefs', 'uuid'), + ], supports_check_mode=True, ) @@ -538,6 +616,7 @@ def main(): opts = module.params['opts'] force = module.params['force'] resizefs = module.params['resizefs'] + uuid = module.params['uuid'] mkfs_opts = [] if opts is not None: @@ -576,21 +655,30 @@ def main(): filesystem = klass(module) + if uuid and not (filesystem.CHANGE_UUID or filesystem.MKFS_SET_UUID_OPTIONS): + module.fail_json(changed=False, msg="module does not support UUID option for this filesystem (%s) yet." % fstype) + same_fs = fs and FILESYSTEMS.get(fs) == FILESYSTEMS[fstype] - if same_fs and not resizefs and not force: + if same_fs and not resizefs and not uuid and not force: module.exit_json(changed=False) - elif same_fs and resizefs: - if not filesystem.GROW: - module.fail_json(changed=False, msg="module does not support resizing %s filesystem yet." % fstype) + elif same_fs: + if resizefs: + if not filesystem.GROW: + module.fail_json(changed=False, msg="module does not support resizing %s filesystem yet." % fstype) + + out = filesystem.grow(dev) + + module.exit_json(changed=True, msg=out) + elif uuid: - out = filesystem.grow(dev) + out = filesystem.change_uuid(new_uuid=uuid, dev=dev) - module.exit_json(changed=True, msg=out) + module.exit_json(changed=True, msg=out) elif fs and not force: module.fail_json(msg="'%s' is already used as %s, use force=true to overwrite" % (dev, fs), rc=rc, err=err) # create fs - filesystem.create(mkfs_opts, dev) + filesystem.create(opts=mkfs_opts, dev=dev, uuid=uuid) changed = True elif fs: diff --git a/ansible_collections/community/general/plugins/modules/flatpak.py b/ansible_collections/community/general/plugins/modules/flatpak.py index 40a13736f..80dbabdfa 100644 --- a/ansible_collections/community/general/plugins/modules/flatpak.py +++ b/ansible_collections/community/general/plugins/modules/flatpak.py @@ -39,8 +39,8 @@ options: method: description: - The installation method to use. - - Defines if the I(flatpak) is supposed to be installed globally for the whole C(system) - or only for the current C(user). + - Defines if the C(flatpak) is supposed to be installed globally for the whole V(system) + or only for the current V(user). type: str choices: [ system, user ] default: system @@ -48,14 +48,14 @@ options: description: - The name of the flatpak to manage. To operate on several packages this can accept a list of packages. - - When used with I(state=present), I(name) can be specified as a URL to a + - When used with O(state=present), O(name) can be specified as a URL to a C(flatpakref) file or the unique reverse DNS name that identifies a flatpak. - Both C(https://) and C(http://) URLs are supported. - - When supplying a reverse DNS name, you can use the I(remote) option to specify on what remote + - When supplying a reverse DNS name, you can use the O(remote) option to specify on what remote to look for the flatpak. An example for a reverse DNS name is C(org.gnome.gedit). - - When used with I(state=absent), it is recommended to specify the name in the reverse DNS + - When used with O(state=absent), it is recommended to specify the name in the reverse DNS format. - - When supplying a URL with I(state=absent), the module will try to match the + - When supplying a URL with O(state=absent), the module will try to match the installed flatpak based on the name of the flatpakref to remove it. However, there is no guarantee that the names of the flatpakref file and the reverse DNS name of the installed flatpak do match. @@ -74,7 +74,7 @@ options: remote: description: - The flatpak remote (repository) to install the flatpak from. - - By default, C(flathub) is assumed, but you do need to add the flathub flatpak_remote before + - By default, V(flathub) is assumed, but you do need to add the flathub flatpak_remote before you can use this. - See the M(community.general.flatpak_remote) module for managing flatpak remotes. type: str diff --git a/ansible_collections/community/general/plugins/modules/flatpak_remote.py b/ansible_collections/community/general/plugins/modules/flatpak_remote.py index 9c097c411..a4eb3ea27 100644 --- a/ansible_collections/community/general/plugins/modules/flatpak_remote.py +++ b/ansible_collections/community/general/plugins/modules/flatpak_remote.py @@ -18,7 +18,7 @@ description: - Allows users to add or remove flatpak remotes. - The flatpak remotes concept is comparable to what is called repositories in other packaging formats. - - Currently, remote addition is only supported via I(flatpakrepo) file URLs. + - Currently, remote addition is only supported via C(flatpakrepo) file URLs. - Existing remotes will not be updated. - See the M(community.general.flatpak) module for managing flatpaks. author: @@ -42,26 +42,26 @@ options: default: flatpak flatpakrepo_url: description: - - The URL to the I(flatpakrepo) file representing the repository remote to add. - - When used with I(state=present), the flatpak remote specified under the I(flatpakrepo_url) - is added using the specified installation C(method). - - When used with I(state=absent), this is not required. - - Required when I(state=present). + - The URL to the C(flatpakrepo) file representing the repository remote to add. + - When used with O(state=present), the flatpak remote specified under the O(flatpakrepo_url) + is added using the specified installation O(method). + - When used with O(state=absent), this is not required. + - Required when O(state=present). type: str method: description: - The installation method to use. - - Defines if the I(flatpak) is supposed to be installed globally for the whole C(system) - or only for the current C(user). + - Defines if the C(flatpak) is supposed to be installed globally for the whole V(system) + or only for the current V(user). type: str choices: [ system, user ] default: system name: description: - The desired name for the flatpak remote to be registered under on the managed host. - - When used with I(state=present), the remote will be added to the managed host under - the specified I(name). - - When used with I(state=absent) the remote with that name will be removed. + - When used with O(state=present), the remote will be added to the managed host under + the specified O(name). + - When used with O(state=absent) the remote with that name will be removed. type: str required: true state: diff --git a/ansible_collections/community/general/plugins/modules/flowdock.py b/ansible_collections/community/general/plugins/modules/flowdock.py index c78716ba4..0e8a7461d 100644 --- a/ansible_collections/community/general/plugins/modules/flowdock.py +++ b/ansible_collections/community/general/plugins/modules/flowdock.py @@ -11,6 +11,12 @@ __metaclass__ = type DOCUMENTATION = ''' --- + +deprecated: + removed_in: 9.0.0 + why: the endpoints this module relies on do not exist any more and do not resolve to IPs in DNS. + alternative: no known alternative at this point + module: flowdock author: "Matt Coddington (@mcodd)" short_description: Send a message to a flowdock @@ -87,7 +93,7 @@ options: required: false validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be used + - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. required: false default: true diff --git a/ansible_collections/community/general/plugins/modules/gandi_livedns.py b/ansible_collections/community/general/plugins/modules/gandi_livedns.py index cc9dd630b..fdb7993a5 100644 --- a/ansible_collections/community/general/plugins/modules/gandi_livedns.py +++ b/ansible_collections/community/general/plugins/modules/gandi_livedns.py @@ -44,7 +44,7 @@ options: ttl: description: - The TTL to give the new record. - - Required when I(state=present). + - Required when O(state=present). type: int type: description: @@ -54,7 +54,7 @@ options: values: description: - The record values. - - Required when I(state=present). + - Required when O(state=present). type: list elements: str domain: diff --git a/ansible_collections/community/general/plugins/modules/gconftool2.py b/ansible_collections/community/general/plugins/modules/gconftool2.py index 949e92b30..a40304a16 100644 --- a/ansible_collections/community/general/plugins/modules/gconftool2.py +++ b/ansible_collections/community/general/plugins/modules/gconftool2.py @@ -35,20 +35,20 @@ options: type: str description: - Preference keys typically have simple values such as strings, - integers, or lists of strings and integers. This is ignored if the state - is "get". See man gconftool-2(1). + integers, or lists of strings and integers. + This is ignored unless O(state=present). See man gconftool-2(1). value_type: type: str description: - - The type of value being set. This is ignored if the state is "get". + - The type of value being set. + This is ignored unless O(state=present). See man gconftool-2(1). choices: [ bool, float, int, string ] state: type: str description: - The action to take upon the key/value. - - State C(get) is deprecated and will be removed in community.general 8.0.0. Please use the module M(community.general.gconftool2_info) instead. required: true - choices: [ absent, get, present ] + choices: [ absent, present ] config_source: type: str description: @@ -56,8 +56,8 @@ options: See man gconftool-2(1). direct: description: - - Access the config database directly, bypassing server. If direct is - specified then the config_source must be specified as well. + - Access the config database directly, bypassing server. If O(direct) is + specified then the O(config_source) must be specified as well. See man gconftool-2(1). type: bool default: false @@ -73,17 +73,26 @@ EXAMPLES = """ RETURN = ''' key: - description: The key specified in the module parameters + description: The key specified in the module parameters. returned: success type: str sample: /desktop/gnome/interface/font_name value_type: - description: The type of the value that was changed + description: The type of the value that was changed. returned: success type: str sample: string value: - description: The value of the preference key after executing the module + description: + - The value of the preference key after executing the module or V(null) if key is removed. + - From community.general 7.0.0 onwards it returns V(null) for a non-existent O(key), and returned V("") before that. + returned: success + type: str + sample: "Serif 12" + previous_value: + description: + - The value of the preference key before executing the module. + - From community.general 7.0.0 onwards it returns V(null) for a non-existent O(key), and returned V("") before that. returned: success type: str sample: "Serif 12" @@ -95,7 +104,6 @@ from ansible_collections.community.general.plugins.module_utils.gconftool2 impor class GConftool(StateModuleHelper): - change_params = ('value', ) diff_params = ('value', ) output_params = ('key', 'value_type') facts_params = ('key', 'value_type') @@ -105,13 +113,12 @@ class GConftool(StateModuleHelper): key=dict(type='str', required=True, no_log=False), value_type=dict(type='str', choices=['bool', 'float', 'int', 'string']), value=dict(type='str'), - state=dict(type='str', required=True, choices=['absent', 'get', 'present']), + state=dict(type='str', required=True, choices=['absent', 'present']), direct=dict(type='bool', default=False), config_source=dict(type='str'), ), required_if=[ ('state', 'present', ['value', 'value_type']), - ('state', 'absent', ['value']), ('direct', True, ['config_source']), ], supports_check_mode=True, @@ -125,6 +132,7 @@ class GConftool(StateModuleHelper): self.vars.set('previous_value', self._get(), fact=True) self.vars.set('value_type', self.vars.value_type) + self.vars.set('_value', self.vars.previous_value, output=False, change=True) self.vars.set_meta('value', initial_value=self.vars.previous_value) self.vars.set('playbook_value', self.vars.value, fact=True) @@ -132,27 +140,29 @@ class GConftool(StateModuleHelper): def process(rc, out, err): if err and fail_on_err: self.ansible.fail_json(msg='gconftool-2 failed with error: %s' % (str(err))) - self.vars.value = out.rstrip() + out = out.rstrip() + self.vars.value = None if out == "" else out return self.vars.value return process def _get(self): return self.runner("state key", output_process=self._make_process(False)).run(state="get") - def state_get(self): - self.deprecate( - msg="State 'get' is deprecated. Please use the module community.general.gconftool2_info instead", - version="8.0.0", collection_name="community.general" - ) - def state_absent(self): with self.runner("state key", output_process=self._make_process(False)) as ctx: ctx.run() + if self.verbosity >= 4: + self.vars.run_info = ctx.run_info self.vars.set('new_value', None, fact=True) + self.vars._value = None def state_present(self): with self.runner("direct config_source value_type state key value", output_process=self._make_process(True)) as ctx: - self.vars.set('new_value', ctx.run(), fact=True) + ctx.run() + if self.verbosity >= 4: + self.vars.run_info = ctx.run_info + self.vars.set('new_value', self._get(), fact=True) + self.vars._value = self.vars.new_value def main(): diff --git a/ansible_collections/community/general/plugins/modules/gem.py b/ansible_collections/community/general/plugins/modules/gem.py index 4bc99d39e..f51e3350d 100644 --- a/ansible_collections/community/general/plugins/modules/gem.py +++ b/ansible_collections/community/general/plugins/modules/gem.py @@ -31,7 +31,7 @@ options: state: type: str description: - - The desired state of the gem. C(latest) ensures that the latest version is installed. + - The desired state of the gem. V(latest) ensures that the latest version is installed. required: false choices: [present, absent, latest] default: present @@ -80,7 +80,7 @@ options: default: true description: - Avoid loading any C(.gemrc) file. Ignored for RubyGems prior to 2.5.2. - - The default changed from C(false) to C(true) in community.general 6.0.0. + - The default changed from V(false) to V(true) in community.general 6.0.0. version_added: 3.3.0 env_shebang: description: diff --git a/ansible_collections/community/general/plugins/modules/gio_mime.py b/ansible_collections/community/general/plugins/modules/gio_mime.py new file mode 100644 index 000000000..27f90581e --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/gio_mime.py @@ -0,0 +1,108 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2022, Alexei Znamensky +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +module: gio_mime +author: + - "Alexei Znamensky (@russoz)" +short_description: Set default handler for MIME type, for applications using Gnome GIO +version_added: 7.5.0 +description: + - This module allows configuring the default handler for a specific MIME type, to be used by applications built with th Gnome GIO API. +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: full +options: + mime_type: + description: + - MIME type for which a default handler will be set. + type: str + required: true + handler: + description: + - Default handler will be set for the MIME type. + type: str + required: true +notes: + - This module is a thin wrapper around the C(gio mime) command (and subcommand). + - See man gio(1) for more details. +seealso: + - name: GIO Documentation + description: Reference documentation for the GIO API.. + link: https://docs.gtk.org/gio/ +''' + +EXAMPLES = """ +- name: Set chrome as the default handler for https + community.general.gio_mime: + mime_type: x-scheme-handler/https + handler: google-chrome.desktop + register: result +""" + +RETURN = ''' + handler: + description: + - The handler set as default. + returned: success + type: str + sample: google-chrome.desktop + stdout: + description: + - The output of the C(gio) command. + returned: success + type: str + sample: Set google-chrome.desktop as the default for x-scheme-handler/https + stderr: + description: + - The error output of the C(gio) command. + returned: failure + type: str + sample: 'gio: Failed to load info for handler "never-existed.desktop"' +''' + +from ansible_collections.community.general.plugins.module_utils.module_helper import ModuleHelper +from ansible_collections.community.general.plugins.module_utils.gio_mime import gio_mime_runner, gio_mime_get + + +class GioMime(ModuleHelper): + output_params = ['handler'] + module = dict( + argument_spec=dict( + mime_type=dict(type='str', required=True), + handler=dict(type='str', required=True), + ), + supports_check_mode=True, + ) + + def __init_module__(self): + self.runner = gio_mime_runner(self.module, check_rc=True) + self.vars.set_meta("handler", initial_value=gio_mime_get(self.runner, self.vars.mime_type), diff=True, change=True) + + def __run__(self): + check_mode_return = (0, 'Module executed in check mode', '') + if self.vars.has_changed("handler"): + with self.runner.context(args_order=["mime_type", "handler"], check_mode_skip=True, check_mode_return=check_mode_return) as ctx: + rc, out, err = ctx.run() + self.vars.stdout = out + self.vars.stderr = err + if self.verbosity >= 4: + self.vars.run_info = ctx.run_info + + +def main(): + GioMime.execute() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/git_config.py b/ansible_collections/community/general/plugins/modules/git_config.py index d67312174..a8d2ebe97 100644 --- a/ansible_collections/community/general/plugins/modules/git_config.py +++ b/ansible_collections/community/general/plugins/modules/git_config.py @@ -20,7 +20,7 @@ author: requirements: ['git'] short_description: Read and write git configuration description: - - The C(git_config) module changes git configuration by invoking 'git config'. + - The M(community.general.git_config) module changes git configuration by invoking C(git config). This is needed if you do not want to use M(ansible.builtin.template) for the entire git config file (for example because you need to change just C(user.email) in /etc/.git/config). Solutions involving M(ansible.builtin.command) are cumbersome or @@ -35,7 +35,7 @@ attributes: options: list_all: description: - - List all settings (optionally limited to a given I(scope)). + - List all settings (optionally limited to a given O(scope)). type: bool default: false name: @@ -50,23 +50,23 @@ options: type: path file: description: - - Path to an adhoc git configuration file to be managed using the C(file) scope. + - Path to an adhoc git configuration file to be managed using the V(file) scope. type: path version_added: 2.0.0 scope: description: - Specify which scope to read/set values from. - This is required when setting config values. - - If this is set to C(local), you must also specify the C(repo) parameter. - - If this is set to C(file), you must also specify the C(file) parameter. - - It defaults to system only when not using I(list_all)=C(true). + - If this is set to V(local), you must also specify the O(repo) parameter. + - If this is set to V(file), you must also specify the O(file) parameter. + - It defaults to system only when not using O(list_all=true). choices: [ "file", "local", "global", "system" ] type: str state: description: - "Indicates the setting should be set/unset. - This parameter has higher precedence than I(value) parameter: - when I(state)=absent and I(value) is defined, I(value) is discarded." + This parameter has higher precedence than O(value) parameter: + when O(state=absent) and O(value) is defined, O(value) is discarded." choices: [ 'present', 'absent' ] default: 'present' type: str @@ -75,6 +75,16 @@ options: - When specifying the name of a single setting, supply a value to set that setting to the given value. type: str + add_mode: + description: + - Specify if a value should replace the existing value(s) or if the new + value should be added alongside other values with the same name. + - This option is only relevant when adding/replacing values. If O(state=absent) or + values are just read out, this option is not considered. + choices: [ "add", "replace-all" ] + type: str + default: "replace-all" + version_added: 8.1.0 ''' EXAMPLES = ''' @@ -118,6 +128,15 @@ EXAMPLES = ''' name: color.ui value: auto +- name: Add several options for the same name + community.general.git_config: + name: push.pushoption + value: "{{ item }}" + add_mode: add + loop: + - merge_request.create + - merge_request.draft + - name: Make etckeeper not complaining when it is invoked by cron community.general.git_config: name: user.email @@ -152,13 +171,13 @@ EXAMPLES = ''' RETURN = ''' --- config_value: - description: When I(list_all=false) and value is not set, a string containing the value of the setting in name + description: When O(list_all=false) and value is not set, a string containing the value of the setting in name returned: success type: str sample: "vim" config_values: - description: When I(list_all=true), a dict containing key/value pairs of multiple configuration settings + description: When O(list_all=true), a dict containing key/value pairs of multiple configuration settings returned: success type: dict sample: @@ -178,6 +197,7 @@ def main(): name=dict(type='str'), repo=dict(type='path'), file=dict(type='path'), + add_mode=dict(required=False, type='str', default='replace-all', choices=['add', 'replace-all']), scope=dict(required=False, type='str', choices=['file', 'local', 'global', 'system']), state=dict(required=False, type='str', default='present', choices=['present', 'absent']), value=dict(required=False), @@ -197,94 +217,118 @@ def main(): # Set the locale to C to ensure consistent messages. module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') - if params['name']: - name = params['name'] - else: - name = None + name = params['name'] or '' + unset = params['state'] == 'absent' + new_value = params['value'] or '' + add_mode = params['add_mode'] - if params['scope']: - scope = params['scope'] - elif params['list_all']: - scope = None - else: - scope = 'system' + scope = determine_scope(params) + cwd = determine_cwd(scope, params) - if params['state'] == 'absent': - unset = 'unset' - params['value'] = None - else: - unset = None - - if params['value']: - new_value = params['value'] - else: - new_value = None + base_args = [git_path, "config", "--includes"] - args = [git_path, "config", "--includes"] - if params['list_all']: - args.append('-l') if scope == 'file': - args.append('-f') - args.append(params['file']) + base_args.append('-f') + base_args.append(params['file']) elif scope: - args.append("--" + scope) + base_args.append("--" + scope) + + list_args = list(base_args) + + if params['list_all']: + list_args.append('-l') + if name: - args.append(name) + list_args.append("--get-all") + list_args.append(name) - if scope == 'local': - dir = params['repo'] - elif params['list_all'] and params['repo']: - # Include local settings from a specific repo when listing all available settings - dir = params['repo'] - else: - # Run from root directory to avoid accidentally picking up any local config settings - dir = "/" + (rc, out, err) = module.run_command(list_args, cwd=cwd, expand_user_and_vars=False) - (rc, out, err) = module.run_command(args, cwd=dir, expand_user_and_vars=False) if params['list_all'] and scope and rc == 128 and 'unable to read config file' in err: # This just means nothing has been set at the given scope module.exit_json(changed=False, msg='', config_values={}) elif rc >= 2: # If the return code is 1, it just means the option hasn't been set yet, which is fine. - module.fail_json(rc=rc, msg=err, cmd=' '.join(args)) + module.fail_json(rc=rc, msg=err, cmd=' '.join(list_args)) + + old_values = out.rstrip().splitlines() if params['list_all']: - values = out.rstrip().splitlines() config_values = {} - for value in values: + for value in old_values: k, v = value.split('=', 1) config_values[k] = v module.exit_json(changed=False, msg='', config_values=config_values) elif not new_value and not unset: - module.exit_json(changed=False, msg='', config_value=out.rstrip()) + module.exit_json(changed=False, msg='', config_value=old_values[0] if old_values else '') elif unset and not out: module.exit_json(changed=False, msg='no setting to unset') + elif new_value in old_values and (len(old_values) == 1 or add_mode == "add"): + module.exit_json(changed=False, msg="") + + # Until this point, the git config was just read and in case no change is needed, the module has already exited. + + set_args = list(base_args) + if unset: + set_args.append("--unset-all") + set_args.append(name) else: - old_value = out.rstrip() - if old_value == new_value: - module.exit_json(changed=False, msg="") + set_args.append("--" + add_mode) + set_args.append(name) + set_args.append(new_value) if not module.check_mode: - if unset: - args.insert(len(args) - 1, "--" + unset) - cmd = args - else: - cmd = args + [new_value] - (rc, out, err) = module.run_command(cmd, cwd=dir, ignore_invalid_cwd=False, expand_user_and_vars=False) + (rc, out, err) = module.run_command(set_args, cwd=cwd, ignore_invalid_cwd=False, expand_user_and_vars=False) if err: - module.fail_json(rc=rc, msg=err, cmd=cmd) + module.fail_json(rc=rc, msg=err, cmd=set_args) + + if unset: + after_values = [] + elif add_mode == "add": + after_values = old_values + [new_value] + else: + after_values = [new_value] module.exit_json( msg='setting changed', diff=dict( - before_header=' '.join(args), - before=old_value + "\n", - after_header=' '.join(args), - after=(new_value or '') + "\n" + before_header=' '.join(set_args), + before=build_diff_value(old_values), + after_header=' '.join(set_args), + after=build_diff_value(after_values), ), changed=True ) +def determine_scope(params): + if params['scope']: + return params['scope'] + elif params['list_all']: + return "" + else: + return 'system' + + +def build_diff_value(value): + if not value: + return "\n" + elif len(value) == 1: + return value[0] + "\n" + else: + return value + + +def determine_cwd(scope, params): + if scope == 'local': + return params['repo'] + elif params['list_all'] and params['repo']: + # Include local settings from a specific repo when listing all available settings + return params['repo'] + else: + # Run from root directory to avoid accidentally picking up any local config settings + return "/" + + if __name__ == '__main__': main() diff --git a/ansible_collections/community/general/plugins/modules/git_config_info.py b/ansible_collections/community/general/plugins/modules/git_config_info.py new file mode 100644 index 000000000..147201fff --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/git_config_info.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Guenther Grill +# +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: git_config_info +author: + - Guenther Grill (@guenhter) +version_added: 8.1.0 +requirements: ['git'] +short_description: Read git configuration +description: + - The M(community.general.git_config_info) module reads the git configuration + by invoking C(git config). +extends_documentation_fragment: + - community.general.attributes + - community.general.attributes.info_module +options: + name: + description: + - The name of the setting to read. + - If not provided, all settings will be returned as RV(config_values). + type: str + path: + description: + - Path to a git repository or file for reading values from a specific repo. + - If O(scope) is V(local), this must point to a repository to read from. + - If O(scope) is V(file), this must point to specific git config file to read from. + - Otherwise O(path) is ignored if set. + type: path + scope: + description: + - Specify which scope to read values from. + - If set to V(global), the global git config is used. O(path) is ignored. + - If set to V(system), the system git config is used. O(path) is ignored. + - If set to V(local), O(path) must be set to the repo to read from. + - If set to V(file), O(path) must be set to the config file to read from. + choices: [ "global", "system", "local", "file" ] + default: "system" + type: str +''' + +EXAMPLES = ''' +- name: Read a system wide config + community.general.git_config_info: + name: core.editor + register: result + +- name: Show value of core.editor + ansible.builtin.debug: + msg: "{{ result.config_value | default('(not set)', true) }}" + +- name: Read a global config from ~/.gitconfig + community.general.git_config_info: + name: alias.remotev + scope: global + +- name: Read a project specific config + community.general.git_config_info: + name: color.ui + scope: local + path: /etc + +- name: Read all global values + community.general.git_config_info: + scope: global + +- name: Read all system wide values + community.general.git_config_info: + +- name: Read all values of a specific file + community.general.git_config_info: + scope: file + path: /etc/gitconfig +''' + +RETURN = ''' +--- +config_value: + description: > + When O(name) is set, a string containing the value of the setting in name. If O(name) is not set, empty. + If a config key such as V(push.pushoption) has more then one entry, just the first one is returned here. + returned: success if O(name) is set + type: str + sample: "vim" + +config_values: + description: + - This is a dictionary mapping a git configuration setting to a list of its values. + - When O(name) is not set, all configuration settings are returned here. + - When O(name) is set, only the setting specified in O(name) is returned here. + If that setting is not set, the key will still be present, and its value will be an empty list. + returned: success + type: dict + sample: + core.editor: ["vim"] + color.ui: ["auto"] + push.pushoption: ["merge_request.create", "merge_request.draft"] + alias.remotev: ["remote -v"] +''' + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type="str"), + path=dict(type="path"), + scope=dict(required=False, type="str", default="system", choices=["global", "system", "local", "file"]), + ), + required_if=[ + ("scope", "local", ["path"]), + ("scope", "file", ["path"]), + ], + required_one_of=[], + supports_check_mode=True, + ) + + # We check error message for a pattern, so we need to make sure the messages appear in the form we're expecting. + # Set the locale to C to ensure consistent messages. + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + + name = module.params["name"] + path = module.params["path"] + scope = module.params["scope"] + + run_cwd = path if scope == "local" else "/" + args = build_args(module, name, path, scope) + + (rc, out, err) = module.run_command(args, cwd=run_cwd, expand_user_and_vars=False) + + if rc == 128 and "unable to read config file" in err: + # This just means nothing has been set at the given scope + pass + elif rc >= 2: + # If the return code is 1, it just means the option hasn't been set yet, which is fine. + module.fail_json(rc=rc, msg=err, cmd=" ".join(args)) + + output_lines = out.strip("\0").split("\0") if out else [] + + if name: + first_value = output_lines[0] if output_lines else "" + config_values = {name: output_lines} + module.exit_json(changed=False, msg="", config_value=first_value, config_values=config_values) + else: + config_values = text_to_dict(output_lines) + module.exit_json(changed=False, msg="", config_value="", config_values=config_values) + + +def build_args(module, name, path, scope): + git_path = module.get_bin_path("git", True) + args = [git_path, "config", "--includes", "--null", "--" + scope] + + if scope == "file": + args.append(path) + + if name: + args.extend(["--get-all", name]) + else: + args.append("--list") + + return args + + +def text_to_dict(text_lines): + config_values = {} + for value in text_lines: + k, v = value.split("\n", 1) + if k in config_values: + config_values[k].append(v) + else: + config_values[k] = [v] + return config_values + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/github_deploy_key.py b/ansible_collections/community/general/plugins/modules/github_deploy_key.py index 322650bf7..ae90e04c9 100644 --- a/ansible_collections/community/general/plugins/modules/github_deploy_key.py +++ b/ansible_collections/community/general/plugins/modules/github_deploy_key.py @@ -58,7 +58,7 @@ options: type: str read_only: description: - - If C(true), the deploy key will only be able to read repository contents. Otherwise, the deploy key will be able to read and write. + - If V(true), the deploy key will only be able to read repository contents. Otherwise, the deploy key will be able to read and write. type: bool default: true state: @@ -69,7 +69,7 @@ options: type: str force: description: - - If C(true), forcefully adds the deploy key by deleting any existing deploy key with the same public key or title. + - If V(true), forcefully adds the deploy key by deleting any existing deploy key with the same public key or title. type: bool default: false username: @@ -78,15 +78,15 @@ options: type: str password: description: - - The password to authenticate with. Alternatively, a personal access token can be used instead of I(username) and I(password) combination. + - The password to authenticate with. Alternatively, a personal access token can be used instead of O(username) and O(password) combination. type: str token: description: - - The OAuth2 token or personal access token to authenticate with. Mutually exclusive with I(password). + - The OAuth2 token or personal access token to authenticate with. Mutually exclusive with O(password). type: str otp: description: - - The 6 digit One Time Password for 2-Factor Authentication. Required together with I(username) and I(password). + - The 6 digit One Time Password for 2-Factor Authentication. Required together with O(username) and O(password). type: int notes: - "Refer to GitHub's API documentation here: https://developer.github.com/v3/repos/keys/." @@ -227,7 +227,7 @@ class GithubDeployKey(object): yield self.module.from_json(resp.read()) links = {} - for x, y in findall(r'<([^>]+)>;\s*rel="(\w+)"', info["link"]): + for x, y in findall(r'<([^>]+)>;\s*rel="(\w+)"', info.get("link", '')): links[y] = x url = links.get('next') diff --git a/ansible_collections/community/general/plugins/modules/github_key.py b/ansible_collections/community/general/plugins/modules/github_key.py index 683a963a7..fa3a0a01f 100644 --- a/ansible_collections/community/general/plugins/modules/github_key.py +++ b/ansible_collections/community/general/plugins/modules/github_key.py @@ -34,7 +34,7 @@ options: type: str pubkey: description: - - SSH public key value. Required when I(state=present). + - SSH public key value. Required when O(state=present). type: str state: description: @@ -44,9 +44,9 @@ options: type: str force: description: - - The default is C(true), which will replace the existing remote key - if it's different than C(pubkey). If C(false), the key will only be - set if no key with the given I(name) exists. + - The default is V(true), which will replace the existing remote key + if it is different than O(pubkey). If V(false), the key will only be + set if no key with the given O(name) exists. type: bool default: true @@ -82,8 +82,14 @@ EXAMPLES = ''' name: Access Key for Some Machine token: '{{ github_access_token }}' pubkey: '{{ ssh_pub_key.stdout }}' -''' +# Alternatively, a single task can be used reading a key from a file on the controller +- name: Authorize key with GitHub + community.general.github_key: + name: Access Key for Some Machine + token: '{{ github_access_token }}' + pubkey: "{{ lookup('ansible.builtin.file', '/home/foo/.ssh/id_rsa.pub') }}" +''' import json import re diff --git a/ansible_collections/community/general/plugins/modules/github_release.py b/ansible_collections/community/general/plugins/modules/github_release.py index 3ddd6c882..d8ee155b8 100644 --- a/ansible_collections/community/general/plugins/modules/github_release.py +++ b/ansible_collections/community/general/plugins/modules/github_release.py @@ -25,7 +25,7 @@ attributes: options: token: description: - - GitHub Personal Access Token for authenticating. Mutually exclusive with C(password). + - GitHub Personal Access Token for authenticating. Mutually exclusive with O(password). type: str user: description: @@ -34,7 +34,7 @@ options: required: true password: description: - - The GitHub account password for the user. Mutually exclusive with C(token). + - The GitHub account password for the user. Mutually exclusive with O(token). type: str repo: description: @@ -49,7 +49,7 @@ options: choices: [ 'latest_release', 'create_release' ] tag: description: - - Tag name when creating a release. Required when using action is set to C(create_release). + - Tag name when creating a release. Required when using O(action=create_release). type: str target: description: @@ -94,7 +94,7 @@ EXAMPLES = ''' repo: testrepo action: latest_release -- name: Get latest release of test repo using username and password. Ansible 2.4. +- name: Get latest release of test repo using username and password community.general.github_release: user: testuser password: secret123 diff --git a/ansible_collections/community/general/plugins/modules/github_repo.py b/ansible_collections/community/general/plugins/modules/github_repo.py index 97076c58a..f02ad30ac 100644 --- a/ansible_collections/community/general/plugins/modules/github_repo.py +++ b/ansible_collections/community/general/plugins/modules/github_repo.py @@ -15,7 +15,7 @@ short_description: Manage your repositories on Github version_added: 2.2.0 description: - Manages Github repositories using PyGithub library. - - Authentication can be done with I(access_token) or with I(username) and I(password). + - Authentication can be done with O(access_token) or with O(username) and O(password). extends_documentation_fragment: - community.general.attributes attributes: @@ -27,19 +27,19 @@ options: username: description: - Username used for authentication. - - This is only needed when not using I(access_token). + - This is only needed when not using O(access_token). type: str required: false password: description: - Password used for authentication. - - This is only needed when not using I(access_token). + - This is only needed when not using O(access_token). type: str required: false access_token: description: - Token parameter for authentication. - - This is only needed when not using I(username) and I(password). + - This is only needed when not using O(username) and O(password). type: str required: false name: @@ -50,17 +50,17 @@ options: description: description: - Description for the repository. - - Defaults to empty if I(force_defaults=true), which is the default in this module. - - Defaults to empty if I(force_defaults=false) when creating a new repository. - - This is only used when I(state) is C(present). + - Defaults to empty if O(force_defaults=true), which is the default in this module. + - Defaults to empty if O(force_defaults=false) when creating a new repository. + - This is only used when O(state) is V(present). type: str required: false private: description: - Whether the repository should be private or not. - - Defaults to C(false) if I(force_defaults=true), which is the default in this module. - - Defaults to C(false) if I(force_defaults=false) when creating a new repository. - - This is only used when I(state) is C(present). + - Defaults to V(false) if O(force_defaults=true), which is the default in this module. + - Defaults to V(false) if O(force_defaults=false) when creating a new repository. + - This is only used when O(state=present). type: bool required: false state: @@ -73,7 +73,7 @@ options: organization: description: - Organization for the repository. - - When I(state) is C(present), the repository will be created in the current user profile. + - When O(state=present), the repository will be created in the current user profile. type: str required: false api_url: @@ -84,8 +84,8 @@ options: version_added: "3.5.0" force_defaults: description: - - Overwrite current I(description) and I(private) attributes with defaults if set to C(true), which currently is the default. - - The default for this option will be deprecated in a future version of this collection, and eventually change to C(false). + - Overwrite current O(description) and O(private) attributes with defaults if set to V(true), which currently is the default. + - The default for this option will be deprecated in a future version of this collection, and eventually change to V(false). type: bool default: true required: false @@ -125,7 +125,7 @@ EXAMPLES = ''' RETURN = ''' repo: description: Repository information as JSON. See U(https://docs.github.com/en/rest/reference/repos#get-a-repository). - returned: success and I(state) is C(present) + returned: success and O(state=present) type: dict ''' diff --git a/ansible_collections/community/general/plugins/modules/github_webhook.py b/ansible_collections/community/general/plugins/modules/github_webhook.py index d47b7a82f..11b115750 100644 --- a/ansible_collections/community/general/plugins/modules/github_webhook.py +++ b/ansible_collections/community/general/plugins/modules/github_webhook.py @@ -61,7 +61,7 @@ options: - > A list of GitHub events the hook is triggered for. Events are listed at U(https://developer.github.com/v3/activity/events/types/). Required - unless C(state) is C(absent) + unless O(state=absent) required: false type: list elements: str diff --git a/ansible_collections/community/general/plugins/modules/github_webhook_info.py b/ansible_collections/community/general/plugins/modules/github_webhook_info.py index a6f7c3e52..dcad02a36 100644 --- a/ansible_collections/community/general/plugins/modules/github_webhook_info.py +++ b/ansible_collections/community/general/plugins/modules/github_webhook_info.py @@ -14,7 +14,6 @@ module: github_webhook_info short_description: Query information about GitHub webhooks description: - "Query information about GitHub webhooks" - - This module was called C(github_webhook_facts) before Ansible 2.9. The usage did not change. requirements: - "PyGithub >= 1.3.5" extends_documentation_fragment: diff --git a/ansible_collections/community/general/plugins/modules/gitlab_branch.py b/ansible_collections/community/general/plugins/modules/gitlab_branch.py index d7eecb33f..623c25644 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_branch.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_branch.py @@ -16,7 +16,6 @@ description: author: - paytroff (@paytroff) requirements: - - python >= 2.7 - python-gitlab >= 2.3.0 extends_documentation_fragment: - community.general.auth_basic @@ -49,7 +48,7 @@ options: ref_branch: description: - Reference branch to create from. - - This must be specified if I(state=present). + - This must be specified if O(state=present). type: str ''' @@ -84,7 +83,7 @@ from ansible.module_utils.api import basic_auth_argument_spec from ansible_collections.community.general.plugins.module_utils.version import LooseVersion from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, gitlab_authentication, gitlab, ensure_gitlab_package + auth_argument_spec, gitlab_authentication, gitlab ) @@ -144,7 +143,9 @@ def main(): ], supports_check_mode=False ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) project = module.params['project'] branch = module.params['branch'] @@ -156,7 +157,6 @@ def main(): module.fail_json(msg="community.general.gitlab_proteched_branch requires python-gitlab Python module >= 2.3.0 (installed version: [%s])." " Please upgrade python-gitlab to version 2.3.0 or above." % gitlab_version) - gitlab_instance = gitlab_authentication(module) this_gitlab = GitlabBranch(module=module, project=project, gitlab_instance=gitlab_instance) this_branch = this_gitlab.get_branch(branch) diff --git a/ansible_collections/community/general/plugins/modules/gitlab_deploy_key.py b/ansible_collections/community/general/plugins/modules/gitlab_deploy_key.py index 27cb01f87..7c0ff06b7 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_deploy_key.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_deploy_key.py @@ -20,7 +20,6 @@ author: - Marcus Watkins (@marwatk) - Guillaume Martinez (@Lunik) requirements: - - python >= 2.7 - python-gitlab python module extends_documentation_fragment: - community.general.auth_basic @@ -56,8 +55,8 @@ options: default: false state: description: - - When C(present) the deploy key added to the project if it doesn't exist. - - When C(absent) it will be removed from the project if it exists. + - When V(present) the deploy key added to the project if it doesn't exist. + - When V(absent) it will be removed from the project if it exists. default: present type: str choices: [ "present", "absent" ] @@ -121,7 +120,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, find_project, gitlab_authentication, gitlab, ensure_gitlab_package + auth_argument_spec, find_project, gitlab_authentication, gitlab, list_all_kwargs ) @@ -209,8 +208,7 @@ class GitLabDeployKey(object): @param key_title Title of the key ''' def find_deploy_key(self, project, key_title): - deploy_keys = project.keys.list(all=True) - for deploy_key in deploy_keys: + for deploy_key in project.keys.list(**list_all_kwargs): if (deploy_key.title == key_title): return deploy_key @@ -261,7 +259,9 @@ def main(): ], supports_check_mode=True, ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) state = module.params['state'] project_identifier = module.params['project'] @@ -269,8 +269,6 @@ def main(): key_keyfile = module.params['key'] key_can_push = module.params['can_push'] - gitlab_instance = gitlab_authentication(module) - gitlab_deploy_key = GitLabDeployKey(module, gitlab_instance) project = find_project(gitlab_instance, project_identifier) diff --git a/ansible_collections/community/general/plugins/modules/gitlab_group.py b/ansible_collections/community/general/plugins/modules/gitlab_group.py index 4de1ffc5f..3d57b1852 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_group.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_group.py @@ -20,7 +20,6 @@ author: - Werner Dijkerman (@dj-wasabi) - Guillaume Martinez (@Lunik) requirements: - - python >= 2.7 - python-gitlab python module extends_documentation_fragment: - community.general.auth_basic @@ -94,6 +93,13 @@ options: - This option is only used on creation, not for updates. type: path version_added: 4.2.0 + force_delete: + description: + - Force delete group even if projects in it. + - Used only when O(state=absent). + type: bool + default: false + version_added: 7.5.0 ''' EXAMPLES = ''' @@ -101,7 +107,6 @@ EXAMPLES = ''' community.general.gitlab_group: api_url: https://gitlab.example.com/ api_token: "{{ access_token }}" - validate_certs: false name: my_first_group state: absent @@ -171,7 +176,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, find_group, gitlab_authentication, gitlab, ensure_gitlab_package + auth_argument_spec, find_group, gitlab_authentication, gitlab ) @@ -279,12 +284,18 @@ class GitLabGroup(object): return (changed, group) - def delete_group(self): + ''' + @param force To delete even if projects inside + ''' + def delete_group(self, force=False): group = self.group_object - if len(group.projects.list(all=False)) >= 1: + if not force and len(group.projects.list(all=False)) >= 1: self._module.fail_json( - msg="There are still projects in this group. These needs to be moved or deleted before this group can be removed.") + msg=("There are still projects in this group. " + "These needs to be moved or deleted before this group can be removed. " + "Use 'force_delete' to 'true' to force deletion of existing projects.") + ) else: if self._module.check_mode: return True @@ -295,7 +306,7 @@ class GitLabGroup(object): self._module.fail_json(msg="Failed to delete group: %s " % to_native(e)) ''' - @param name Name of the groupe + @param name Name of the group @param full_path Complete path of the Group including parent group path. / ''' def exists_group(self, project_identifier): @@ -322,6 +333,7 @@ def main(): subgroup_creation_level=dict(type='str', choices=['maintainer', 'owner']), require_two_factor_authentication=dict(type='bool'), avatar_path=dict(type='path'), + force_delete=dict(type='bool', default=False), )) module = AnsibleModule( @@ -341,7 +353,9 @@ def main(): ], supports_check_mode=True, ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) group_name = module.params['name'] group_path = module.params['path'] @@ -354,8 +368,7 @@ def main(): subgroup_creation_level = module.params['subgroup_creation_level'] require_two_factor_authentication = module.params['require_two_factor_authentication'] avatar_path = module.params['avatar_path'] - - gitlab_instance = gitlab_authentication(module) + force_delete = module.params['force_delete'] # Define default group_path based on group_name if group_path is None: @@ -375,7 +388,7 @@ def main(): if state == 'absent': if group_exists: - gitlab_group.delete_group() + gitlab_group.delete_group(force=force_delete) module.exit_json(changed=True, msg="Successfully deleted group %s" % group_name) else: module.exit_json(changed=False, msg="Group deleted or does not exists") diff --git a/ansible_collections/community/general/plugins/modules/gitlab_group_access_token.py b/ansible_collections/community/general/plugins/modules/gitlab_group_access_token.py new file mode 100644 index 000000000..85bba205d --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/gitlab_group_access_token.py @@ -0,0 +1,320 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Zoran Krleza (zoran.krleza@true-north.hr) +# Based on code: +# Copyright (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) +# Copyright (c) 2018, Marcus Watkins +# Copyright (c) 2013, Phillip Gentry +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +module: gitlab_group_access_token +short_description: Manages GitLab group access tokens +version_added: 8.4.0 +description: + - Creates and revokes group access tokens. +author: + - Zoran Krleza (@pixslx) +requirements: + - python-gitlab >= 3.1.0 +extends_documentation_fragment: + - community.general.auth_basic + - community.general.gitlab + - community.general.attributes +notes: + - Access tokens can not be changed. If a parameter needs to be changed, an acceess token has to be recreated. + Whether tokens will be recreated is controlled by the O(recreate) option, which defaults to V(never). + - Token string is contained in the result only when access token is created or recreated. It can not be fetched afterwards. + - Token matching is done by comparing O(name) option. + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + group: + description: + - ID or full path of group in the form of group/subgroup. + required: true + type: str + name: + description: + - Access token's name. + required: true + type: str + scopes: + description: + - Scope of the access token. + required: true + type: list + elements: str + aliases: ["scope"] + choices: ["api", "read_api", "read_registry", "write_registry", "read_repository", "write_repository", "create_runner", "ai_features", "k8s_proxy"] + access_level: + description: + - Access level of the access token. + type: str + default: maintainer + choices: ["guest", "reporter", "developer", "maintainer", "owner"] + expires_at: + description: + - Expiration date of the access token in C(YYYY-MM-DD) format. + - Make sure to quote this value in YAML to ensure it is kept as a string and not interpreted as a YAML date. + type: str + required: true + recreate: + description: + - Whether the access token will be recreated if it already exists. + - When V(never) the token will never be recreated. + - When V(always) the token will always be recreated. + - When V(state_change) the token will be recreated if there is a difference between desired state and actual state. + type: str + choices: ["never", "always", "state_change"] + default: never + state: + description: + - When V(present) the access token will be added to the group if it does not exist. + - When V(absent) it will be removed from the group if it exists. + default: present + type: str + choices: [ "present", "absent" ] +''' + +EXAMPLES = r''' +- name: "Creating a group access token" + community.general.gitlab_group_access_token: + api_url: https://gitlab.example.com/ + api_token: "somegitlabapitoken" + group: "my_group/my_subgroup" + name: "group_token" + expires_at: "2024-12-31" + access_level: developer + scopes: + - api + - read_api + - read_repository + - write_repository + state: present + +- name: "Revoking a group access token" + community.general.gitlab_group_access_token: + api_url: https://gitlab.example.com/ + api_token: "somegitlabapitoken" + group: "my_group/my_group" + name: "group_token" + expires_at: "2024-12-31" + scopes: + - api + - read_api + - read_repository + - write_repository + state: absent + +- name: "Change (recreate) existing token if its actual state is different than desired state" + community.general.gitlab_group_access_token: + api_url: https://gitlab.example.com/ + api_token: "somegitlabapitoken" + group: "my_group/my_group" + name: "group_token" + expires_at: "2024-12-31" + scopes: + - api + - read_api + - read_repository + - write_repository + recreate: state_change + state: present +''' + +RETURN = r''' +access_token: + description: + - API object. + - Only contains the value of the token if the token was created or recreated. + returned: success and O(state=present) + type: dict +''' + +from datetime import datetime + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.general.plugins.module_utils.gitlab import ( + auth_argument_spec, find_group, gitlab_authentication, gitlab +) + +ACCESS_LEVELS = dict(guest=10, reporter=20, developer=30, maintainer=40, owner=50) + + +class GitLabGroupAccessToken(object): + def __init__(self, module, gitlab_instance): + self._module = module + self._gitlab = gitlab_instance + self.access_token_object = None + + ''' + @param project Project Object + @param group Group Object + @param arguments Attributes of the access_token + ''' + def create_access_token(self, group, arguments): + changed = False + if self._module.check_mode: + return True + + try: + self.access_token_object = group.access_tokens.create(arguments) + changed = True + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json(msg="Failed to create access token: %s " % to_native(e)) + + return changed + + ''' + @param project Project object + @param group Group Object + @param name of the access token + ''' + def find_access_token(self, group, name): + access_tokens = group.access_tokens.list(all=True) + for access_token in access_tokens: + if (access_token.name == name): + self.access_token_object = access_token + return False + return False + + def revoke_access_token(self): + if self._module.check_mode: + return True + + changed = False + try: + self.access_token_object.delete() + changed = True + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json(msg="Failed to revoke access token: %s " % to_native(e)) + + return changed + + def access_tokens_equal(self): + if self.access_token_object.name != self._module.params['name']: + return False + if self.access_token_object.scopes != self._module.params['scopes']: + return False + if self.access_token_object.access_level != ACCESS_LEVELS[self._module.params['access_level']]: + return False + if self.access_token_object.expires_at != self._module.params['expires_at']: + return False + return True + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(auth_argument_spec()) + argument_spec.update(dict( + state=dict(type='str', default="present", choices=["absent", "present"]), + group=dict(type='str', required=True), + name=dict(type='str', required=True), + scopes=dict(type='list', + required=True, + aliases=['scope'], + elements='str', + choices=['api', + 'read_api', + 'read_registry', + 'write_registry', + 'read_repository', + 'write_repository', + 'create_runner', + 'ai_features', + 'k8s_proxy']), + access_level=dict(type='str', required=False, default='maintainer', choices=['guest', 'reporter', 'developer', 'maintainer', 'owner']), + expires_at=dict(type='str', required=True), + recreate=dict(type='str', default='never', choices=['never', 'always', 'state_change']) + )) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_username', 'api_token'], + ['api_username', 'api_oauth_token'], + ['api_username', 'api_job_token'], + ['api_token', 'api_oauth_token'], + ['api_token', 'api_job_token'] + ], + required_together=[ + ['api_username', 'api_password'] + ], + required_one_of=[ + ['api_username', 'api_token', 'api_oauth_token', 'api_job_token'] + ], + supports_check_mode=True + ) + + state = module.params['state'] + group_identifier = module.params['group'] + name = module.params['name'] + scopes = module.params['scopes'] + access_level_str = module.params['access_level'] + expires_at = module.params['expires_at'] + recreate = module.params['recreate'] + + access_level = ACCESS_LEVELS[access_level_str] + + try: + datetime.strptime(expires_at, '%Y-%m-%d') + except ValueError: + module.fail_json(msg="Argument expires_at is not in required format YYYY-MM-DD") + + gitlab_instance = gitlab_authentication(module) + + gitlab_access_token = GitLabGroupAccessToken(module, gitlab_instance) + + group = find_group(gitlab_instance, group_identifier) + if group is None: + module.fail_json(msg="Failed to create access token: group %s does not exists" % group_identifier) + + gitlab_access_token_exists = False + gitlab_access_token.find_access_token(group, name) + if gitlab_access_token.access_token_object is not None: + gitlab_access_token_exists = True + + if state == 'absent': + if gitlab_access_token_exists: + gitlab_access_token.revoke_access_token() + module.exit_json(changed=True, msg="Successfully deleted access token %s" % name) + else: + module.exit_json(changed=False, msg="Access token does not exists") + + if state == 'present': + if gitlab_access_token_exists: + if gitlab_access_token.access_tokens_equal(): + if recreate == 'always': + gitlab_access_token.revoke_access_token() + gitlab_access_token.create_access_token(group, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at}) + module.exit_json(changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs) + else: + module.exit_json(changed=False, msg="Access token already exists", access_token=gitlab_access_token.access_token_object._attrs) + else: + if recreate == 'never': + module.fail_json(msg="Access token already exists and its state is different. It can not be updated without recreating.") + else: + gitlab_access_token.revoke_access_token() + gitlab_access_token.create_access_token(group, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at}) + module.exit_json(changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs) + else: + gitlab_access_token.create_access_token(group, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at}) + module.exit_json(changed=True, msg="Successfully created access token", access_token=gitlab_access_token.access_token_object._attrs) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/gitlab_group_members.py b/ansible_collections/community/general/plugins/modules/gitlab_group_members.py index 66298e882..ca82891e3 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_group_members.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_group_members.py @@ -40,22 +40,22 @@ options: gitlab_user: description: - A username or a list of usernames to add to/remove from the GitLab group. - - Mutually exclusive with I(gitlab_users_access). + - Mutually exclusive with O(gitlab_users_access). type: list elements: str access_level: description: - The access level for the user. - - Required if I(state=present), user state is set to present. - - Mutually exclusive with I(gitlab_users_access). + - Required if O(state=present), user state is set to present. + - Mutually exclusive with O(gitlab_users_access). type: str choices: ['guest', 'reporter', 'developer', 'maintainer', 'owner'] gitlab_users_access: description: - Provide a list of user to access level mappings. - Every dictionary in this list specifies a user (by username) and the access level the user should have. - - Mutually exclusive with I(gitlab_user) and I(access_level). - - Use together with I(purge_users) to remove all users not specified here from the group. + - Mutually exclusive with O(gitlab_user) and O(access_level). + - Use together with O(purge_users) to remove all users not specified here from the group. type: list elements: dict suboptions: @@ -66,7 +66,7 @@ options: access_level: description: - The access level for the user. - - Required if I(state=present), user state is set to present. + - Required if O(state=present), user state is set to present. type: str choices: ['guest', 'reporter', 'developer', 'maintainer', 'owner'] required: true @@ -74,16 +74,16 @@ options: state: description: - State of the member in the group. - - On C(present), it adds a user to a GitLab group. - - On C(absent), it removes a user from a GitLab group. + - On V(present), it adds a user to a GitLab group. + - On V(absent), it removes a user from a GitLab group. choices: ['present', 'absent'] default: 'present' type: str purge_users: description: - - Adds/remove users of the given access_level to match the given I(gitlab_user)/I(gitlab_users_access) list. + - Adds/remove users of the given access_level to match the given O(gitlab_user)/O(gitlab_users_access) list. If omitted do not purge orphaned members. - - Is only used when I(state=present). + - Is only used when O(state=present). type: list elements: str choices: ['guest', 'reporter', 'developer', 'maintainer', 'owner'] @@ -160,7 +160,7 @@ from ansible.module_utils.api import basic_auth_argument_spec from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, gitlab_authentication, gitlab, ensure_gitlab_package + auth_argument_spec, gitlab_authentication, gitlab, list_all_kwargs ) @@ -171,16 +171,20 @@ class GitLabGroup(object): # get user id if the user exists def get_user_id(self, gitlab_user): - user_exists = self._gitlab.users.list(username=gitlab_user, all=True) - if user_exists: - return user_exists[0].id + return next( + (u.id for u in self._gitlab.users.list(username=gitlab_user, **list_all_kwargs)), + None + ) # get group id if group exists def get_group_id(self, gitlab_group): - groups = self._gitlab.groups.list(search=gitlab_group, all=True) - for group in groups: - if group.full_path == gitlab_group: - return group.id + return next( + ( + g.id for g in self._gitlab.groups.list(search=gitlab_group, **list_all_kwargs) + if g.full_path == gitlab_group + ), + None + ) # get all members in a group def get_members_in_a_group(self, gitlab_group_id): @@ -273,14 +277,16 @@ def main(): ], supports_check_mode=True, ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gl = gitlab_authentication(module) access_level_int = { - 'guest': gitlab.GUEST_ACCESS, - 'reporter': gitlab.REPORTER_ACCESS, - 'developer': gitlab.DEVELOPER_ACCESS, - 'maintainer': gitlab.MAINTAINER_ACCESS, - 'owner': gitlab.OWNER_ACCESS, + 'guest': gitlab.const.GUEST_ACCESS, + 'reporter': gitlab.const.REPORTER_ACCESS, + 'developer': gitlab.const.DEVELOPER_ACCESS, + 'maintainer': gitlab.const.MAINTAINER_ACCESS, + 'owner': gitlab.const.OWNER_ACCESS, } gitlab_group = module.params['gitlab_group'] @@ -291,9 +297,6 @@ def main(): if purge_users: purge_users = [access_level_int[level] for level in purge_users] - # connect to gitlab server - gl = gitlab_authentication(module) - group = GitLabGroup(module, gl) gitlab_group_id = group.get_group_id(gitlab_group) diff --git a/ansible_collections/community/general/plugins/modules/gitlab_group_variable.py b/ansible_collections/community/general/plugins/modules/gitlab_group_variable.py index c7befe123..32e5aaa90 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_group_variable.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_group_variable.py @@ -17,11 +17,10 @@ description: - Creates a group variable if it does not exist. - When a group variable does exist, its value will be updated when the values are different. - Variables which are untouched in the playbook, but are not untouched in the GitLab group, - they stay untouched (I(purge) is C(false)) or will be deleted (I(purge) is C(true)). + they stay untouched (O(purge=false)) or will be deleted (O(purge=true)). author: - Florent Madiot (@scodeman) requirements: - - python >= 2.7 - python-gitlab python module extends_documentation_fragment: - community.general.auth_basic @@ -48,20 +47,21 @@ options: type: str purge: description: - - When set to C(true), delete all variables which are not untouched in the task. + - When set to V(true), delete all variables which are not untouched in the task. default: false type: bool vars: description: - - When the list element is a simple key-value pair, set masked and protected to false. - - When the list element is a dict with the keys I(value), I(masked) and I(protected), the user can - have full control about whether a value should be masked, protected or both. + - When the list element is a simple key-value pair, masked, raw and protected will be set to false. + - When the list element is a dict with the keys C(value), C(masked), C(raw) and C(protected), the user can + have full control about whether a value should be masked, raw, protected or both. - Support for group variables requires GitLab >= 9.5. - Support for environment_scope requires GitLab Premium >= 13.11. - Support for protected values requires GitLab >= 9.3. - Support for masked values requires GitLab >= 11.10. - - A I(value) must be a string or a number. - - Field I(variable_type) must be a string with either C(env_var), which is the default, or C(file). + - Support for raw values requires GitLab >= 15.7. + - A C(value) must be a string or a number. + - Field C(variable_type) must be a string with either V(env_var), which is the default, or V(file). - When a value is masked, it must be in Base64 and have a length of at least 8 characters. See GitLab documentation on acceptable values for a masked variable (U(https://docs.gitlab.com/ce/ci/variables/#masked-variables)). default: {} @@ -70,7 +70,7 @@ options: version_added: 4.5.0 description: - A list of dictionaries that represents CI/CD variables. - - This modules works internal with this sructure, even if the older I(vars) parameter is used. + - This modules works internal with this structure, even if the older O(vars) parameter is used. default: [] type: list elements: dict @@ -83,21 +83,28 @@ options: value: description: - The variable value. - - Required when I(state=present). + - Required when O(state=present). type: str masked: description: - - Wether variable value is masked or not. + - Whether variable value is masked or not. type: bool default: false protected: description: - - Wether variable value is protected or not. + - Whether variable value is protected or not. type: bool default: false + raw: + description: + - Whether variable value is raw or not. + - Support for raw values requires GitLab >= 15.7. + type: bool + default: false + version_added: '7.4.0' variable_type: description: - - Wether a variable is an environment variable (C(env_var)) or a file (C(file)). + - Whether a variable is an environment variable (V(env_var)) or a file (V(file)). type: str choices: [ "env_var", "file" ] default: env_var @@ -126,6 +133,38 @@ EXAMPLES = r''' variable_type: env_var environment_scope: production +- name: Set or update some CI/CD variables with raw value + community.general.gitlab_group_variable: + api_url: https://gitlab.com + api_token: secret_access_token + group: scodeman/testgroup/ + purge: false + vars: + ACCESS_KEY_ID: abc123 + SECRET_ACCESS_KEY: + value: 3214cbad + masked: true + protected: true + raw: true + variable_type: env_var + environment_scope: '*' + +- name: Set or update some CI/CD variables with expandable value + community.general.gitlab_group_variable: + api_url: https://gitlab.com + api_token: secret_access_token + group: scodeman/testgroup/ + purge: false + vars: + ACCESS_KEY_ID: abc123 + SECRET_ACCESS_KEY: + value: '$MY_OTHER_VARIABLE' + masked: true + protected: true + raw: false + variable_type: env_var + environment_scope: '*' + - name: Delete one variable community.general.gitlab_group_variable: api_url: https://gitlab.com @@ -166,52 +205,12 @@ group_variable: from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.api import basic_auth_argument_spec -from ansible.module_utils.six import string_types -from ansible.module_utils.six import integer_types - from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, gitlab_authentication, ensure_gitlab_package, filter_returned_variables + auth_argument_spec, gitlab_authentication, filter_returned_variables, vars_to_variables, + list_all_kwargs ) -def vars_to_variables(vars, module): - # transform old vars to new variables structure - variables = list() - for item, value in vars.items(): - if (isinstance(value, string_types) or - isinstance(value, (integer_types, float))): - variables.append( - { - "name": item, - "value": str(value), - "masked": False, - "protected": False, - "variable_type": "env_var", - } - ) - - elif isinstance(value, dict): - new_item = {"name": item, "value": value.get('value')} - - new_item = { - "name": item, - "value": value.get('value'), - "masked": value.get('masked'), - "protected": value.get('protected'), - "variable_type": value.get('variable_type'), - } - - if value.get('environment_scope'): - new_item['environment_scope'] = value.get('environment_scope') - - variables.append(new_item) - - else: - module.fail_json(msg="value must be of type string, integer, float or dict") - - return variables - - class GitlabGroupVariables(object): def __init__(self, module, gitlab_instance): @@ -223,14 +222,7 @@ class GitlabGroupVariables(object): return self.repo.groups.get(group_name) def list_all_group_variables(self): - page_nb = 1 - variables = [] - vars_page = self.group.variables.list(page=page_nb) - while len(vars_page) > 0: - variables += vars_page - page_nb += 1 - vars_page = self.group.variables.list(page=page_nb) - return variables + return list(self.group.variables.list(**list_all_kwargs)) def create_variable(self, var_obj): if self._module.check_mode: @@ -240,6 +232,7 @@ class GitlabGroupVariables(object): "value": var_obj.get('value'), "masked": var_obj.get('masked'), "protected": var_obj.get('protected'), + "raw": var_obj.get('raw'), "variable_type": var_obj.get('variable_type'), } if var_obj.get('environment_scope') is not None: @@ -308,6 +301,8 @@ def native_python_main(this_gitlab, purge, requested_variables, state, module): item['value'] = str(item.get('value')) if item.get('protected') is None: item['protected'] = False + if item.get('raw') is None: + item['raw'] = False if item.get('masked') is None: item['masked'] = False if item.get('environment_scope') is None: @@ -379,11 +374,14 @@ def main(): group=dict(type='str', required=True), purge=dict(type='bool', required=False, default=False), vars=dict(type='dict', required=False, default=dict(), no_log=True), + # please mind whenever changing the variables dict to also change module_utils/gitlab.py's + # KNOWN dict in filter_returned_variables or bad evil will happen variables=dict(type='list', elements='dict', required=False, default=list(), options=dict( name=dict(type='str', required=True), value=dict(type='str', no_log=True), masked=dict(type='bool', default=False), protected=dict(type='bool', default=False), + raw=dict(type='bool', default=False), environment_scope=dict(type='str', default='*'), variable_type=dict(type='str', default='env_var', choices=["env_var", "file"]) )), @@ -408,7 +406,9 @@ def main(): ], supports_check_mode=True ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) purge = module.params['purge'] var_list = module.params['vars'] @@ -423,8 +423,6 @@ def main(): if any(x['value'] is None for x in variables): module.fail_json(msg='value parameter is required in state present') - gitlab_instance = gitlab_authentication(module) - this_gitlab = GitlabGroupVariables(module=module, gitlab_instance=gitlab_instance) changed, raw_return_value, before, after = native_python_main(this_gitlab, purge, variables, state, module) diff --git a/ansible_collections/community/general/plugins/modules/gitlab_hook.py b/ansible_collections/community/general/plugins/modules/gitlab_hook.py index adf90eb7b..58781d182 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_hook.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_hook.py @@ -21,7 +21,6 @@ author: - Marcus Watkins (@marwatk) - Guillaume Martinez (@Lunik) requirements: - - python >= 2.7 - python-gitlab python module extends_documentation_fragment: - community.general.auth_basic @@ -47,8 +46,8 @@ options: type: str state: description: - - When C(present) the hook will be updated to match the input or created if it doesn't exist. - - When C(absent) hook will be deleted if it exists. + - When V(present) the hook will be updated to match the input or created if it doesn't exist. + - When V(absent) hook will be deleted if it exists. default: present type: str choices: [ "present", "absent" ] @@ -98,6 +97,11 @@ options: - Trigger hook on wiki events. type: bool default: false + releases_events: + description: + - Trigger hook on release events. + type: bool + version_added: '8.4.0' hook_validate_certs: description: - Whether GitLab will do SSL verification when triggering the hook. @@ -123,7 +127,6 @@ EXAMPLES = ''' state: present push_events: true tag_push_events: true - hook_validate_certs: false token: "my-super-secret-token-that-my-ci-server-will-check" - name: "Delete the previous hook" @@ -171,7 +174,7 @@ from ansible.module_utils.api import basic_auth_argument_spec from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, find_project, gitlab_authentication, ensure_gitlab_package + auth_argument_spec, find_project, gitlab_authentication, list_all_kwargs ) @@ -203,6 +206,7 @@ class GitLabHook(object): 'job_events': options['job_events'], 'pipeline_events': options['pipeline_events'], 'wiki_page_events': options['wiki_page_events'], + 'releases_events': options['releases_events'], 'enable_ssl_verification': options['enable_ssl_verification'], 'token': options['token'], }) @@ -218,6 +222,7 @@ class GitLabHook(object): 'job_events': options['job_events'], 'pipeline_events': options['pipeline_events'], 'wiki_page_events': options['wiki_page_events'], + 'releases_events': options['releases_events'], 'enable_ssl_verification': options['enable_ssl_verification'], 'token': options['token'], }) @@ -266,8 +271,7 @@ class GitLabHook(object): @param hook_url Url to call on event ''' def find_hook(self, project, hook_url): - hooks = project.hooks.list(all=True) - for hook in hooks: + for hook in project.hooks.list(**list_all_kwargs): if (hook.url == hook_url): return hook @@ -304,6 +308,7 @@ def main(): job_events=dict(type='bool', default=False), pipeline_events=dict(type='bool', default=False), wiki_page_events=dict(type='bool', default=False), + releases_events=dict(type='bool', default=None), hook_validate_certs=dict(type='bool', default=False, aliases=['enable_ssl_verification']), token=dict(type='str', no_log=True), )) @@ -325,7 +330,9 @@ def main(): ], supports_check_mode=True, ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) state = module.params['state'] project_identifier = module.params['project'] @@ -339,11 +346,10 @@ def main(): job_events = module.params['job_events'] pipeline_events = module.params['pipeline_events'] wiki_page_events = module.params['wiki_page_events'] + releases_events = module.params['releases_events'] enable_ssl_verification = module.params['hook_validate_certs'] hook_token = module.params['token'] - gitlab_instance = gitlab_authentication(module) - gitlab_hook = GitLabHook(module, gitlab_instance) project = find_project(gitlab_instance, project_identifier) @@ -371,6 +377,7 @@ def main(): "job_events": job_events, "pipeline_events": pipeline_events, "wiki_page_events": wiki_page_events, + "releases_events": releases_events, "enable_ssl_verification": enable_ssl_verification, "token": hook_token, }): diff --git a/ansible_collections/community/general/plugins/modules/gitlab_instance_variable.py b/ansible_collections/community/general/plugins/modules/gitlab_instance_variable.py new file mode 100644 index 000000000..cc2d812ca --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/gitlab_instance_variable.py @@ -0,0 +1,360 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Benedikt Braunger (bebr@adm.ku.dk) +# Based on code: +# Copyright (c) 2020, Florent Madiot (scodeman@scode.io) +# Copyright (c) 2019, Markus Bergholz (markuman@gmail.com) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +module: gitlab_instance_variable +short_description: Creates, updates, or deletes GitLab instance variables +version_added: 7.1.0 +description: + - Creates a instance variable if it does not exist. + - When a instance variable does exist, its value will be updated if the values are different. + - Support for instance variables requires GitLab >= 13.0. + - Variables which are not mentioned in the modules options, but are present on the GitLab instance, + will either stay (O(purge=false)) or will be deleted (O(purge=true)). +author: + - Benedikt Braunger (@benibr) +requirements: + - python-gitlab python module +extends_documentation_fragment: + - community.general.auth_basic + - community.general.gitlab + - community.general.attributes + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + state: + description: + - Create or delete instance variable. + default: present + type: str + choices: ["present", "absent"] + purge: + description: + - When set to V(true), delete all variables which are not mentioned in the task. + default: false + type: bool + variables: + description: + - A list of dictionaries that represents CI/CD variables. + default: [] + type: list + elements: dict + suboptions: + name: + description: + - The name of the variable. + type: str + required: true + value: + description: + - The variable value. + - Required when O(state=present). + type: str + masked: + description: + - Whether variable value is masked or not. + type: bool + default: false + protected: + description: + - Whether variable value is protected or not. + type: bool + default: false + variable_type: + description: + - Whether a variable is an environment variable (V(env_var)) or a file (V(file)). + type: str + choices: [ "env_var", "file" ] + default: env_var +''' + + +EXAMPLES = r''' +- name: Set or update some CI/CD variables + community.general.gitlab_instance_variable: + api_url: https://gitlab.com + api_token: secret_access_token + purge: false + variables: + - name: ACCESS_KEY_ID + value: abc1312cba + - name: SECRET_ACCESS_KEY + value: 1337 + masked: true + protected: true + variable_type: env_var + +- name: Delete one variable + community.general.gitlab_instance_variable: + api_url: https://gitlab.com + api_token: secret_access_token + state: absent + variables: + - name: ACCESS_KEY_ID +''' + +RETURN = r''' +instance_variable: + description: Four lists of the variablenames which were added, updated, removed or exist. + returned: always + type: dict + contains: + added: + description: A list of variables which were created. + returned: always + type: list + sample: ['ACCESS_KEY_ID', 'SECRET_ACCESS_KEY'] + untouched: + description: A list of variables which exist. + returned: always + type: list + sample: ['ACCESS_KEY_ID', 'SECRET_ACCESS_KEY'] + removed: + description: A list of variables which were deleted. + returned: always + type: list + sample: ['ACCESS_KEY_ID', 'SECRET_ACCESS_KEY'] + updated: + description: A list pre-existing variables whose values have been set. + returned: always + type: list + sample: ['ACCESS_KEY_ID', 'SECRET_ACCESS_KEY'] +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.api import basic_auth_argument_spec +from ansible_collections.community.general.plugins.module_utils.gitlab import ( + auth_argument_spec, gitlab_authentication, filter_returned_variables, + list_all_kwargs +) + + +class GitlabInstanceVariables(object): + + def __init__(self, module, gitlab_instance): + self.instance = gitlab_instance + self._module = module + + def list_all_instance_variables(self): + return list(self.instance.variables.list(**list_all_kwargs)) + + def create_variable(self, var_obj): + if self._module.check_mode: + return True + var = { + "key": var_obj.get('key'), + "value": var_obj.get('value'), + "masked": var_obj.get('masked'), + "protected": var_obj.get('protected'), + "variable_type": var_obj.get('variable_type'), + } + + self.instance.variables.create(var) + return True + + def update_variable(self, var_obj): + if self._module.check_mode: + return True + self.delete_variable(var_obj) + self.create_variable(var_obj) + return True + + def delete_variable(self, var_obj): + if self._module.check_mode: + return True + self.instance.variables.delete(var_obj.get('key')) + return True + + +def compare(requested_variables, existing_variables, state): + # we need to do this, because it was determined in a previous version - more or less buggy + # basically it is not necessary and might results in more/other bugs! + # but it is required and only relevant for check mode!! + # logic represents state 'present' when not purge. all other can be derived from that + # untouched => equal in both + # updated => name and scope are equal + # added => name and scope does not exist + untouched = list() + updated = list() + added = list() + + if state == 'present': + existing_key_scope_vars = list() + for item in existing_variables: + existing_key_scope_vars.append({'key': item.get('key')}) + + for var in requested_variables: + if var in existing_variables: + untouched.append(var) + else: + compare_item = {'key': var.get('name')} + if compare_item in existing_key_scope_vars: + updated.append(var) + else: + added.append(var) + + return untouched, updated, added + + +def native_python_main(this_gitlab, purge, requested_variables, state, module): + + change = False + return_value = dict(added=list(), updated=list(), removed=list(), untouched=list()) + + gitlab_keys = this_gitlab.list_all_instance_variables() + before = [x.attributes for x in gitlab_keys] + + existing_variables = filter_returned_variables(gitlab_keys) + + for item in requested_variables: + item['key'] = item.pop('name') + item['value'] = str(item.get('value')) + if item.get('protected') is None: + item['protected'] = False + if item.get('masked') is None: + item['masked'] = False + if item.get('variable_type') is None: + item['variable_type'] = 'env_var' + + if module.check_mode: + untouched, updated, added = compare(requested_variables, existing_variables, state) + + if state == 'present': + add_or_update = [x for x in requested_variables if x not in existing_variables] + for item in add_or_update: + try: + if this_gitlab.create_variable(item): + return_value['added'].append(item) + + except Exception: + if this_gitlab.update_variable(item): + return_value['updated'].append(item) + + if purge: + # refetch and filter + gitlab_keys = this_gitlab.list_all_instance_variables() + existing_variables = filter_returned_variables(gitlab_keys) + + remove = [x for x in existing_variables if x not in requested_variables] + for item in remove: + if this_gitlab.delete_variable(item): + return_value['removed'].append(item) + + elif state == 'absent': + # value does not matter on removing variables. + # key and environment scope are sufficient + for item in existing_variables: + item.pop('value') + item.pop('variable_type') + for item in requested_variables: + item.pop('value') + item.pop('variable_type') + + if not purge: + remove_requested = [x for x in requested_variables if x in existing_variables] + for item in remove_requested: + if this_gitlab.delete_variable(item): + return_value['removed'].append(item) + + else: + for item in existing_variables: + if this_gitlab.delete_variable(item): + return_value['removed'].append(item) + + if module.check_mode: + return_value = dict(added=added, updated=updated, removed=return_value['removed'], untouched=untouched) + + if len(return_value['added'] + return_value['removed'] + return_value['updated']) > 0: + change = True + + gitlab_keys = this_gitlab.list_all_instance_variables() + after = [x.attributes for x in gitlab_keys] + + return change, return_value, before, after + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(auth_argument_spec()) + argument_spec.update( + purge=dict(type='bool', required=False, default=False), + variables=dict(type='list', elements='dict', required=False, default=list(), options=dict( + name=dict(type='str', required=True), + value=dict(type='str', no_log=True), + masked=dict(type='bool', default=False), + protected=dict(type='bool', default=False), + variable_type=dict(type='str', default='env_var', choices=["env_var", "file"]) + )), + state=dict(type='str', default="present", choices=["absent", "present"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_username', 'api_token'], + ['api_username', 'api_oauth_token'], + ['api_username', 'api_job_token'], + ['api_token', 'api_oauth_token'], + ['api_token', 'api_job_token'], + ], + required_together=[ + ['api_username', 'api_password'], + ], + required_one_of=[ + ['api_username', 'api_token', 'api_oauth_token', 'api_job_token'] + ], + supports_check_mode=True + ) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) + + purge = module.params['purge'] + state = module.params['state'] + + variables = module.params['variables'] + + if state == 'present': + if any(x['value'] is None for x in variables): + module.fail_json(msg='value parameter is required in state present') + + this_gitlab = GitlabInstanceVariables(module=module, gitlab_instance=gitlab_instance) + + changed, raw_return_value, before, after = native_python_main(this_gitlab, purge, variables, state, module) + + # postprocessing + for item in after: + item['name'] = item.pop('key') + for item in before: + item['name'] = item.pop('key') + + untouched_key_name = 'key' + if not module.check_mode: + untouched_key_name = 'name' + raw_return_value['untouched'] = [x for x in before if x in after] + + added = [x.get('key') for x in raw_return_value['added']] + updated = [x.get('key') for x in raw_return_value['updated']] + removed = [x.get('key') for x in raw_return_value['removed']] + untouched = [x.get(untouched_key_name) for x in raw_return_value['untouched']] + return_value = dict(added=added, updated=updated, removed=removed, untouched=untouched) + + module.exit_json(changed=changed, instance_variable=return_value) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/gitlab_issue.py b/ansible_collections/community/general/plugins/modules/gitlab_issue.py new file mode 100644 index 000000000..6d95bf6cf --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/gitlab_issue.py @@ -0,0 +1,408 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Ondrej Zvara (ozvara1@gmail.com) +# Based on code: +# Copyright (c) 2021, Lennert Mertens (lennert@nubera.be) +# Copyright (c) 2021, Werner Dijkerman (ikben@werner-dijkerman.nl) +# Copyright (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# Copyright (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +module: gitlab_issue +short_description: Create, update, or delete GitLab issues +version_added: '8.1.0' +description: + - Creates an issue if it does not exist. + - When an issue does exist, it will be updated if the provided parameters are different. + - When an issue does exist and O(state=absent), the issue will be deleted. + - When multiple issues are detected, the task fails. + - Existing issues are matched based on O(title) and O(state_filter) filters. +author: + - zvaraondrej (@zvaraondrej) +requirements: + - python-gitlab >= 2.3.0 +extends_documentation_fragment: + - community.general.auth_basic + - community.general.gitlab + - community.general.attributes + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + assignee_ids: + description: + - A list of assignee usernames omitting V(@) character. + - Set to an empty array to unassign all assignees. + type: list + elements: str + description: + description: + - A description of the issue. + - Gets overridden by a content of file specified at O(description_path), if found. + type: str + description_path: + description: + - A path of file containing issue's description. + - Accepts MarkDown formatted files. + type: path + issue_type: + description: + - Type of the issue. + default: issue + type: str + choices: ["issue", "incident", "test_case"] + labels: + description: + - A list of label names. + - Set to an empty array to remove all labels. + type: list + elements: str + milestone_search: + description: + - The name of the milestone. + - Set to empty string to unassign milestone. + type: str + milestone_group_id: + description: + - The path or numeric ID of the group hosting desired milestone. + type: str + project: + description: + - The path or name of the project. + required: true + type: str + state: + description: + - Create or delete issue. + default: present + type: str + choices: ["present", "absent"] + state_filter: + description: + - Filter specifying state of issues while searching. + type: str + choices: ["opened", "closed"] + default: opened + title: + description: + - A title for the issue. The title is used as a unique identifier to ensure idempotency. + type: str + required: true +''' + + +EXAMPLES = ''' +- name: Create Issue + community.general.gitlab_issue: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + title: "Ansible demo Issue" + description: "Demo Issue description" + labels: + - Ansible + - Demo + assignee_ids: + - testassignee + state_filter: "opened" + state: present + +- name: Delete Issue + community.general.gitlab_issue: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + title: "Ansible demo Issue" + state_filter: "opened" + state: absent +''' + +RETURN = r''' +msg: + description: Success or failure message. + returned: always + type: str + sample: "Success" + +issue: + description: API object. + returned: success + type: dict +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.common.text.converters import to_native, to_text + +from ansible_collections.community.general.plugins.module_utils.version import LooseVersion +from ansible_collections.community.general.plugins.module_utils.gitlab import ( + auth_argument_spec, gitlab_authentication, gitlab, find_project, find_group +) + + +class GitlabIssue(object): + + def __init__(self, module, project, gitlab_instance): + self._gitlab = gitlab_instance + self._module = module + self.project = project + + ''' + @param milestone_id Title of the milestone + ''' + def get_milestone(self, milestone_id, group): + milestones = [] + try: + milestones = group.milestones.list(search=milestone_id) + except gitlab.exceptions.GitlabGetError as e: + self._module.fail_json(msg="Failed to list the Milestones: %s" % to_native(e)) + + if len(milestones) > 1: + self._module.fail_json(msg="Multiple Milestones matched search criteria.") + if len(milestones) < 1: + self._module.fail_json(msg="No Milestones matched search criteria.") + if len(milestones) == 1: + try: + return group.milestones.get(id=milestones[0].id) + except gitlab.exceptions.GitlabGetError as e: + self._module.fail_json(msg="Failed to get the Milestones: %s" % to_native(e)) + + ''' + @param title Title of the Issue + @param state_filter Issue's state to filter on + ''' + def get_issue(self, title, state_filter): + issues = [] + try: + issues = self.project.issues.list(query_parameters={"search": title, "in": "title", "state": state_filter}) + except gitlab.exceptions.GitlabGetError as e: + self._module.fail_json(msg="Failed to list the Issues: %s" % to_native(e)) + + if len(issues) > 1: + self._module.fail_json(msg="Multiple Issues matched search criteria.") + if len(issues) == 1: + try: + return self.project.issues.get(id=issues[0].iid) + except gitlab.exceptions.GitlabGetError as e: + self._module.fail_json(msg="Failed to get the Issue: %s" % to_native(e)) + + ''' + @param username Name of the user + ''' + def get_user(self, username): + users = [] + try: + users = [user for user in self.project.users.list(username=username, all=True) if user.username == username] + except gitlab.exceptions.GitlabGetError as e: + self._module.fail_json(msg="Failed to list the users: %s" % to_native(e)) + + if len(users) > 1: + self._module.fail_json(msg="Multiple Users matched search criteria.") + elif len(users) < 1: + self._module.fail_json(msg="No User matched search criteria.") + else: + return users[0] + + ''' + @param users List of usernames + ''' + def get_user_ids(self, users): + return [self.get_user(user).id for user in users] + + ''' + @param options Options of the Issue + ''' + def create_issue(self, options): + if self._module.check_mode: + self._module.exit_json(changed=True, msg="Successfully created Issue '%s'." % options["title"]) + + try: + return self.project.issues.create(options) + except gitlab.exceptions.GitlabCreateError as e: + self._module.fail_json(msg="Failed to create Issue: %s " % to_native(e)) + + ''' + @param issue Issue object to delete + ''' + def delete_issue(self, issue): + if self._module.check_mode: + self._module.exit_json(changed=True, msg="Successfully deleted Issue '%s'." % issue["title"]) + + try: + return issue.delete() + except gitlab.exceptions.GitlabDeleteError as e: + self._module.fail_json(msg="Failed to delete Issue: '%s'." % to_native(e)) + + ''' + @param issue Issue object to update + @param options Options of the Issue + ''' + def update_issue(self, issue, options): + if self._module.check_mode: + self._module.exit_json(changed=True, msg="Successfully updated Issue '%s'." % issue["title"]) + + try: + return self.project.issues.update(issue.iid, options) + except gitlab.exceptions.GitlabUpdateError as e: + self._module.fail_json(msg="Failed to update Issue %s." % to_native(e)) + + ''' + @param issue Issue object to evaluate + @param options New options to update Issue with + ''' + def issue_has_changed(self, issue, options): + for key, value in options.items(): + if value is not None: + + if key == 'milestone_id': + old_milestone = getattr(issue, 'milestone')['id'] if getattr(issue, 'milestone') else "" + if options[key] != old_milestone: + return True + elif key == 'assignee_ids': + if options[key] != sorted([user["id"] for user in getattr(issue, 'assignees')]): + return True + + elif key == 'labels': + if options[key] != sorted(getattr(issue, key)): + return True + + elif getattr(issue, key) != value: + return True + + return False + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(auth_argument_spec()) + argument_spec.update( + assignee_ids=dict(type='list', elements='str', required=False), + description=dict(type='str', required=False), + description_path=dict(type='path', required=False), + issue_type=dict(type='str', default='issue', choices=["issue", "incident", "test_case"], required=False), + labels=dict(type='list', elements='str', required=False), + milestone_search=dict(type='str', required=False), + milestone_group_id=dict(type='str', required=False), + project=dict(type='str', required=True), + state=dict(type='str', default="present", choices=["absent", "present"]), + state_filter=dict(type='str', default="opened", choices=["opened", "closed"]), + title=dict(type='str', required=True), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_username', 'api_token'], + ['api_username', 'api_oauth_token'], + ['api_username', 'api_job_token'], + ['api_token', 'api_oauth_token'], + ['api_token', 'api_job_token'], + ['description', 'description_path'], + ], + required_together=[ + ['api_username', 'api_password'], + ['milestone_search', 'milestone_group_id'], + ], + required_one_of=[ + ['api_username', 'api_token', 'api_oauth_token', 'api_job_token'] + ], + supports_check_mode=True + ) + + assignee_ids = module.params['assignee_ids'] + description = module.params['description'] + description_path = module.params['description_path'] + issue_type = module.params['issue_type'] + labels = module.params['labels'] + milestone_id = module.params['milestone_search'] + milestone_group_id = module.params['milestone_group_id'] + project = module.params['project'] + state = module.params['state'] + state_filter = module.params['state_filter'] + title = module.params['title'] + + gitlab_version = gitlab.__version__ + if LooseVersion(gitlab_version) < LooseVersion('2.3.0'): + module.fail_json(msg="community.general.gitlab_issue requires python-gitlab Python module >= 2.3.0 (installed version: [%s])." + " Please upgrade python-gitlab to version 2.3.0 or above." % gitlab_version) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) + + this_project = find_project(gitlab_instance, project) + if this_project is None: + module.fail_json(msg="Failed to get the project: %s" % project) + + this_gitlab = GitlabIssue(module=module, project=this_project, gitlab_instance=gitlab_instance) + + if milestone_id and milestone_group_id: + this_group = find_group(gitlab_instance, milestone_group_id) + if this_group is None: + module.fail_json(msg="Failed to get the group: %s" % milestone_group_id) + + milestone_id = this_gitlab.get_milestone(milestone_id, this_group).id + + this_issue = this_gitlab.get_issue(title, state_filter) + + if state == "present": + if description_path: + try: + with open(description_path, 'rb') as f: + description = to_text(f.read(), errors='surrogate_or_strict') + except IOError as e: + module.fail_json(msg='Cannot open {0}: {1}'.format(description_path, e)) + + # sorting necessary in order to properly detect changes, as we don't want to get false positive + # results due to differences in ids ordering; + assignee_ids = sorted(this_gitlab.get_user_ids(assignee_ids)) if assignee_ids else assignee_ids + labels = sorted(labels) if labels else labels + + options = { + "title": title, + "description": description, + "labels": labels, + "issue_type": issue_type, + "milestone_id": milestone_id, + "assignee_ids": assignee_ids, + } + + if not this_issue: + issue = this_gitlab.create_issue(options) + module.exit_json( + changed=True, msg="Created Issue '{t}'.".format(t=title), + issue=issue.asdict() + ) + else: + if this_gitlab.issue_has_changed(this_issue, options): + issue = this_gitlab.update_issue(this_issue, options) + module.exit_json( + changed=True, msg="Updated Issue '{t}'.".format(t=title), + issue=issue + ) + else: + module.exit_json( + changed=False, msg="Issue '{t}' already exists".format(t=title), + issue=this_issue.asdict() + ) + elif state == "absent": + if not this_issue: + module.exit_json(changed=False, msg="Issue '{t}' does not exist or has already been deleted.".format(t=title)) + else: + issue = this_gitlab.delete_issue(this_issue) + module.exit_json( + changed=True, msg="Issue '{t}' deleted.".format(t=title), + issue=issue + ) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/gitlab_label.py b/ansible_collections/community/general/plugins/modules/gitlab_label.py new file mode 100644 index 000000000..f2c8393f2 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/gitlab_label.py @@ -0,0 +1,500 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Gabriele Pongelli (gabriele.pongelli@gmail.com) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +module: gitlab_label +short_description: Creates/updates/deletes GitLab Labels belonging to project or group. +version_added: 8.3.0 +description: + - When a label does not exist, it will be created. + - When a label does exist, its value will be updated when the values are different. + - Labels can be purged. +author: + - "Gabriele Pongelli (@gpongelli)" +requirements: + - python-gitlab python module +extends_documentation_fragment: + - community.general.auth_basic + - community.general.gitlab + - community.general.attributes + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + state: + description: + - Create or delete project or group label. + default: present + type: str + choices: ["present", "absent"] + purge: + description: + - When set to V(true), delete all labels which are not mentioned in the task. + default: false + type: bool + required: false + project: + description: + - The path and name of the project. Either this or O(group) is required. + required: false + type: str + group: + description: + - The path of the group. Either this or O(project) is required. + required: false + type: str + labels: + description: + - A list of dictionaries that represents gitlab project's or group's labels. + type: list + elements: dict + required: false + default: [] + suboptions: + name: + description: + - The name of the label. + type: str + required: true + color: + description: + - The color of the label. + - Required when O(state=present). + type: str + priority: + description: + - Integer value to give priority to the label. + type: int + required: false + default: null + description: + description: + - Label's description. + type: str + default: null + new_name: + description: + - Optional field to change label's name. + type: str + default: null +''' + + +EXAMPLES = ''' +# same project's task can be executed for group +- name: Create one Label + community.general.gitlab_label: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + labels: + - name: label_one + color: "#123456" + state: present + +- name: Create many group labels + community.general.gitlab_label: + api_url: https://gitlab.com + api_token: secret_access_token + group: "group1" + labels: + - name: label_one + color: "#123456" + description: this is a label + priority: 20 + - name: label_two + color: "#554422" + state: present + +- name: Create many project labels + community.general.gitlab_label: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + labels: + - name: label_one + color: "#123456" + description: this is a label + priority: 20 + - name: label_two + color: "#554422" + state: present + +- name: Set or update some labels + community.general.gitlab_label: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + labels: + - name: label_one + color: "#224488" + state: present + +- name: Add label in check mode + community.general.gitlab_label: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + labels: + - name: label_one + color: "#224488" + check_mode: true + +- name: Delete Label + community.general.gitlab_label: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + labels: + - name: label_one + state: absent + +- name: Change Label name + community.general.gitlab_label: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + labels: + - name: label_one + new_name: label_two + state: absent + +- name: Purge all labels + community.general.gitlab_label: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + purge: true + +- name: Delete many labels + community.general.gitlab_label: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + state: absent + labels: + - name: label-abc123 + - name: label-two +''' + +RETURN = ''' +labels: + description: Four lists of the labels which were added, updated, removed or exist. + returned: success + type: dict + contains: + added: + description: A list of labels which were created. + returned: always + type: list + sample: ['abcd', 'label-one'] + untouched: + description: A list of labels which exist. + returned: always + type: list + sample: ['defg', 'new-label'] + removed: + description: A list of labels which were deleted. + returned: always + type: list + sample: ['defg', 'new-label'] + updated: + description: A list pre-existing labels whose values have been set. + returned: always + type: list + sample: ['defg', 'new-label'] +labels_obj: + description: API object. + returned: success + type: dict +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.api import basic_auth_argument_spec + +from ansible_collections.community.general.plugins.module_utils.version import LooseVersion +from ansible_collections.community.general.plugins.module_utils.gitlab import ( + auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project, gitlab +) + + +class GitlabLabels(object): + + def __init__(self, module, gitlab_instance, group_id, project_id): + self._gitlab = gitlab_instance + self.gitlab_object = group_id if group_id else project_id + self.is_group_label = True if group_id else False + self._module = module + + def list_all_labels(self): + page_nb = 1 + labels = [] + vars_page = self.gitlab_object.labels.list(page=page_nb) + while len(vars_page) > 0: + labels += vars_page + page_nb += 1 + vars_page = self.gitlab_object.labels.list(page=page_nb) + return labels + + def create_label(self, var_obj): + if self._module.check_mode: + return True, True + + var = { + "name": var_obj.get('name'), + "color": var_obj.get('color'), + } + + if var_obj.get('description') is not None: + var["description"] = var_obj.get('description') + + if var_obj.get('priority') is not None: + var["priority"] = var_obj.get('priority') + + _obj = self.gitlab_object.labels.create(var) + return True, _obj.asdict() + + def update_label(self, var_obj): + if self._module.check_mode: + return True, True + _label = self.gitlab_object.labels.get(var_obj.get('name')) + + if var_obj.get('new_name') is not None: + _label.new_name = var_obj.get('new_name') + + if var_obj.get('description') is not None: + _label.description = var_obj.get('description') + if var_obj.get('priority') is not None: + _label.priority = var_obj.get('priority') + + # save returns None + _label.save() + return True, _label.asdict() + + def delete_label(self, var_obj): + if self._module.check_mode: + return True, True + _label = self.gitlab_object.labels.get(var_obj.get('name')) + # delete returns None + _label.delete() + return True, _label.asdict() + + +def compare(requested_labels, existing_labels, state): + # we need to do this, because it was determined in a previous version - more or less buggy + # basically it is not necessary and might result in more/other bugs! + # but it is required and only relevant for check mode!! + # logic represents state 'present' when not purge. all other can be derived from that + # untouched => equal in both + # updated => name and scope are equal + # added => name and scope does not exist + untouched = list() + updated = list() + added = list() + + if state == 'present': + _existing_labels = list() + for item in existing_labels: + _existing_labels.append({'name': item.get('name')}) + + for var in requested_labels: + if var in existing_labels: + untouched.append(var) + else: + compare_item = {'name': var.get('name')} + if compare_item in _existing_labels: + updated.append(var) + else: + added.append(var) + + return untouched, updated, added + + +def native_python_main(this_gitlab, purge, requested_labels, state, module): + change = False + return_value = dict(added=[], updated=[], removed=[], untouched=[]) + return_obj = dict(added=[], updated=[], removed=[]) + + labels_before = [x.asdict() for x in this_gitlab.list_all_labels()] + + # filter out and enrich before compare + for item in requested_labels: + # add defaults when not present + if item.get('description') is None: + item['description'] = "" + if item.get('new_name') is None: + item['new_name'] = None + if item.get('priority') is None: + item['priority'] = None + + # group label does not have priority, removing for comparison + if this_gitlab.is_group_label: + item.pop('priority') + + for item in labels_before: + # remove field only from server + item.pop('id') + item.pop('description_html') + item.pop('text_color') + item.pop('subscribed') + # field present only when it's a project's label + if 'is_project_label' in item: + item.pop('is_project_label') + item['new_name'] = None + + if state == 'present': + add_or_update = [x for x in requested_labels if x not in labels_before] + for item in add_or_update: + try: + _rv, _obj = this_gitlab.create_label(item) + if _rv: + return_value['added'].append(item) + return_obj['added'].append(_obj) + except Exception: + # create raises exception with following error message when label already exists + _rv, _obj = this_gitlab.update_label(item) + if _rv: + return_value['updated'].append(item) + return_obj['updated'].append(_obj) + + if purge: + # re-fetch + _labels = this_gitlab.list_all_labels() + + for item in labels_before: + _rv, _obj = this_gitlab.delete_label(item) + if _rv: + return_value['removed'].append(item) + return_obj['removed'].append(_obj) + + elif state == 'absent': + if not purge: + _label_names_requested = [x['name'] for x in requested_labels] + remove_requested = [x for x in labels_before if x['name'] in _label_names_requested] + for item in remove_requested: + _rv, _obj = this_gitlab.delete_label(item) + if _rv: + return_value['removed'].append(item) + return_obj['removed'].append(_obj) + else: + for item in labels_before: + _rv, _obj = this_gitlab.delete_label(item) + if _rv: + return_value['removed'].append(item) + return_obj['removed'].append(_obj) + + if module.check_mode: + _untouched, _updated, _added = compare(requested_labels, labels_before, state) + return_value = dict(added=_added, updated=_updated, removed=return_value['removed'], untouched=_untouched) + + if any(return_value[x] for x in ['added', 'removed', 'updated']): + change = True + + labels_after = [x.asdict() for x in this_gitlab.list_all_labels()] + + return change, return_value, labels_before, labels_after, return_obj + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(auth_argument_spec()) + argument_spec.update( + project=dict(type='str', required=False, default=None), + group=dict(type='str', required=False, default=None), + purge=dict(type='bool', required=False, default=False), + labels=dict(type='list', elements='dict', required=False, default=list(), + options=dict( + name=dict(type='str', required=True), + color=dict(type='str', required=False), + description=dict(type='str', required=False), + priority=dict(type='int', required=False), + new_name=dict(type='str', required=False),) + ), + state=dict(type='str', default="present", choices=["absent", "present"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_username', 'api_token'], + ['api_username', 'api_oauth_token'], + ['api_username', 'api_job_token'], + ['api_token', 'api_oauth_token'], + ['api_token', 'api_job_token'], + ['project', 'group'], + ], + required_together=[ + ['api_username', 'api_password'], + ], + required_one_of=[ + ['api_username', 'api_token', 'api_oauth_token', 'api_job_token'], + ['project', 'group'] + ], + supports_check_mode=True + ) + ensure_gitlab_package(module) + + gitlab_project = module.params['project'] + gitlab_group = module.params['group'] + purge = module.params['purge'] + label_list = module.params['labels'] + state = module.params['state'] + + gitlab_version = gitlab.__version__ + _min_gitlab = '3.2.0' + if LooseVersion(gitlab_version) < LooseVersion(_min_gitlab): + module.fail_json(msg="community.general.gitlab_label requires python-gitlab Python module >= %s " + "(installed version: [%s]). Please upgrade " + "python-gitlab to version %s or above." % (_min_gitlab, gitlab_version, _min_gitlab)) + + gitlab_instance = gitlab_authentication(module) + + # find_project can return None, but the other must exist + gitlab_project_id = find_project(gitlab_instance, gitlab_project) + + # find_group can return None, but the other must exist + gitlab_group_id = find_group(gitlab_instance, gitlab_group) + + # if both not found, module must exist + if not gitlab_project_id and not gitlab_group_id: + if gitlab_project and not gitlab_project_id: + module.fail_json(msg="project '%s' not found." % gitlab_project) + if gitlab_group and not gitlab_group_id: + module.fail_json(msg="group '%s' not found." % gitlab_group) + + this_gitlab = GitlabLabels(module=module, gitlab_instance=gitlab_instance, group_id=gitlab_group_id, + project_id=gitlab_project_id) + + if state == 'present': + _existing_labels = [x.asdict()['name'] for x in this_gitlab.list_all_labels()] + + # color is mandatory when creating label, but it's optional when changing name or updating other fields + if any(x['color'] is None and x['new_name'] is None and x['name'] not in _existing_labels for x in label_list): + module.fail_json(msg='color parameter is required for new labels') + + change, raw_return_value, before, after, _obj = native_python_main(this_gitlab, purge, label_list, state, module) + + if not module.check_mode: + raw_return_value['untouched'] = [x for x in before if x in after] + + added = [x.get('name') for x in raw_return_value['added']] + updated = [x.get('name') for x in raw_return_value['updated']] + removed = [x.get('name') for x in raw_return_value['removed']] + untouched = [x.get('name') for x in raw_return_value['untouched']] + return_value = dict(added=added, updated=updated, removed=removed, untouched=untouched) + + module.exit_json(changed=change, labels=return_value, labels_obj=_obj) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/gitlab_merge_request.py b/ansible_collections/community/general/plugins/modules/gitlab_merge_request.py new file mode 100644 index 000000000..5bb9cb9c7 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/gitlab_merge_request.py @@ -0,0 +1,416 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Ondrej Zvara (ozvara1@gmail.com) +# Based on code: +# Copyright (c) 2021, Lennert Mertens (lennert@nubera.be) +# Copyright (c) 2021, Werner Dijkerman (ikben@werner-dijkerman.nl) +# Copyright (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# Copyright (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +module: gitlab_merge_request +short_description: Create, update, or delete GitLab merge requests +version_added: 7.1.0 +description: + - Creates a merge request if it does not exist. + - When a single merge request does exist, it will be updated if the provided parameters are different. + - When a single merge request does exist and O(state=absent), the merge request will be deleted. + - When multiple merge requests are detected, the task fails. + - Existing merge requests are matched based on O(title), O(source_branch), O(target_branch), + and O(state_filter) filters. +author: + - zvaraondrej (@zvaraondrej) +requirements: + - python-gitlab >= 2.3.0 +extends_documentation_fragment: + - community.general.auth_basic + - community.general.gitlab + - community.general.attributes + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + state: + description: + - Create or delete merge request. + default: present + type: str + choices: ["present", "absent"] + project: + description: + - The path or name of the project. + required: true + type: str + source_branch: + description: + - Merge request's source branch. + - Ignored while updating existing merge request. + required: true + type: str + target_branch: + description: + - Merge request's target branch. + required: true + type: str + title: + description: + - A title for the merge request. + type: str + required: true + description: + description: + - A description for the merge request. + - Gets overridden by a content of file specified at O(description_path), if found. + type: str + description_path: + description: + - A path of file containing merge request's description. + - Accepts MarkDown formatted files. + type: path + labels: + description: + - Comma separated list of label names. + type: str + default: "" + remove_source_branch: + description: + - Flag indicating if a merge request should remove the source branch when merging. + type: bool + default: false + state_filter: + description: + - Filter specifying state of merge requests while searching. + type: str + choices: ["opened", "closed", "locked", "merged"] + default: opened + assignee_ids: + description: + - Comma separated list of assignees usernames omitting V(@) character. + - Set to empty string to unassign all assignees. + type: str + reviewer_ids: + description: + - Comma separated list of reviewers usernames omitting V(@) character. + - Set to empty string to unassign all reviewers. + type: str +''' + + +EXAMPLES = ''' +- name: Create Merge Request from branch1 to branch2 + community.general.gitlab_merge_request: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + source_branch: branch1 + target_branch: branch2 + title: "Ansible demo MR" + description: "Demo MR description" + labels: "Ansible,Demo" + state_filter: "opened" + remove_source_branch: True + state: present + +- name: Delete Merge Request from branch1 to branch2 + community.general.gitlab_merge_request: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + source_branch: branch1 + target_branch: branch2 + title: "Ansible demo MR" + state_filter: "opened" + state: absent +''' + +RETURN = r''' +msg: + description: Success or failure message. + returned: always + type: str + sample: "Success" + +mr: + description: API object. + returned: success + type: dict +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.common.text.converters import to_native, to_text + +from ansible_collections.community.general.plugins.module_utils.version import LooseVersion +from ansible_collections.community.general.plugins.module_utils.gitlab import ( + auth_argument_spec, gitlab_authentication, gitlab, find_project +) + + +class GitlabMergeRequest(object): + + def __init__(self, module, project, gitlab_instance): + self._gitlab = gitlab_instance + self._module = module + self.project = project + + ''' + @param branch Name of the branch + ''' + def get_branch(self, branch): + try: + return self.project.branches.get(branch) + except gitlab.exceptions.GitlabGetError as e: + self._module.fail_json(msg="Failed to get the branch: %s" % to_native(e)) + + ''' + @param title Title of the Merge Request + @param source_branch Merge Request's source branch + @param target_branch Merge Request's target branch + @param state_filter Merge Request's state to filter on + ''' + def get_mr(self, title, source_branch, target_branch, state_filter): + mrs = [] + try: + mrs = self.project.mergerequests.list(search=title, source_branch=source_branch, target_branch=target_branch, state=state_filter) + except gitlab.exceptions.GitlabGetError as e: + self._module.fail_json(msg="Failed to list the Merge Request: %s" % to_native(e)) + + if len(mrs) > 1: + self._module.fail_json(msg="Multiple Merge Requests matched search criteria.") + if len(mrs) == 1: + try: + return self.project.mergerequests.get(id=mrs[0].iid) + except gitlab.exceptions.GitlabGetError as e: + self._module.fail_json(msg="Failed to get the Merge Request: %s" % to_native(e)) + + ''' + @param username Name of the user + ''' + def get_user(self, username): + users = [] + try: + users = [user for user in self.project.users.list(username=username, all=True) if user.username == username] + except gitlab.exceptions.GitlabGetError as e: + self._module.fail_json(msg="Failed to list the users: %s" % to_native(e)) + + if len(users) > 1: + self._module.fail_json(msg="Multiple Users matched search criteria.") + elif len(users) < 1: + self._module.fail_json(msg="No User matched search criteria.") + else: + return users[0] + + ''' + @param users List of usernames + ''' + def get_user_ids(self, users): + return [self.get_user(user).id for user in users] + + ''' + @param options Options of the Merge Request + ''' + def create_mr(self, options): + if self._module.check_mode: + self._module.exit_json(changed=True, msg="Successfully created the Merge Request %s" % options["title"]) + + try: + return self.project.mergerequests.create(options) + except gitlab.exceptions.GitlabCreateError as e: + self._module.fail_json(msg="Failed to create Merge Request: %s " % to_native(e)) + + ''' + @param mr Merge Request object to delete + ''' + def delete_mr(self, mr): + if self._module.check_mode: + self._module.exit_json(changed=True, msg="Successfully deleted the Merge Request %s" % mr["title"]) + + try: + return mr.delete() + except gitlab.exceptions.GitlabDeleteError as e: + self._module.fail_json(msg="Failed to delete Merge Request: %s " % to_native(e)) + + ''' + @param mr Merge Request object to update + ''' + def update_mr(self, mr, options): + if self._module.check_mode: + self._module.exit_json(changed=True, msg="Successfully updated the Merge Request %s" % mr["title"]) + + try: + return self.project.mergerequests.update(mr.iid, options) + except gitlab.exceptions.GitlabUpdateError as e: + self._module.fail_json(msg="Failed to update Merge Request: %s " % to_native(e)) + + ''' + @param mr Merge Request object to evaluate + @param options New options to update MR with + ''' + def mr_has_changed(self, mr, options): + for key, value in options.items(): + if value is not None: + # see https://gitlab.com/gitlab-org/gitlab-foss/-/issues/27355 + if key == 'remove_source_branch': + key = 'force_remove_source_branch' + + if key == 'assignee_ids': + if options[key] != sorted([user["id"] for user in getattr(mr, 'assignees')]): + return True + + elif key == 'reviewer_ids': + if options[key] != sorted([user["id"] for user in getattr(mr, 'reviewers')]): + return True + + elif key == 'labels': + if options[key] != sorted(getattr(mr, key)): + return True + + elif getattr(mr, key) != value: + return True + + return False + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(auth_argument_spec()) + argument_spec.update( + project=dict(type='str', required=True), + source_branch=dict(type='str', required=True), + target_branch=dict(type='str', required=True), + title=dict(type='str', required=True), + description=dict(type='str', required=False), + labels=dict(type='str', default="", required=False), + description_path=dict(type='path', required=False), + remove_source_branch=dict(type='bool', default=False, required=False), + state_filter=dict(type='str', default="opened", choices=["opened", "closed", "locked", "merged"]), + assignee_ids=dict(type='str', required=False), + reviewer_ids=dict(type='str', required=False), + state=dict(type='str', default="present", choices=["absent", "present"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_username', 'api_token'], + ['api_username', 'api_oauth_token'], + ['api_username', 'api_job_token'], + ['api_token', 'api_oauth_token'], + ['api_token', 'api_job_token'], + ['description', 'description_path'], + ], + required_together=[ + ['api_username', 'api_password'], + ], + required_one_of=[ + ['api_username', 'api_token', 'api_oauth_token', 'api_job_token'] + ], + required_if=[ + ['state', 'present', ['source_branch', 'target_branch', 'title'], True], + ['state', 'absent', ['source_branch', 'target_branch', 'title'], True], + ], + supports_check_mode=True + ) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) + + project = module.params['project'] + source_branch = module.params['source_branch'] + target_branch = module.params['target_branch'] + title = module.params['title'] + description = module.params['description'] + labels = module.params['labels'] + description_path = module.params['description_path'] + remove_source_branch = module.params['remove_source_branch'] + state_filter = module.params['state_filter'] + assignee_ids = module.params['assignee_ids'] + reviewer_ids = module.params['reviewer_ids'] + state = module.params['state'] + + gitlab_version = gitlab.__version__ + if LooseVersion(gitlab_version) < LooseVersion('2.3.0'): + module.fail_json(msg="community.general.gitlab_merge_request requires python-gitlab Python module >= 2.3.0 (installed version: [%s])." + " Please upgrade python-gitlab to version 2.3.0 or above." % gitlab_version) + + this_project = find_project(gitlab_instance, project) + if this_project is None: + module.fail_json(msg="Failed to get the project: %s" % project) + + this_gitlab = GitlabMergeRequest(module=module, project=this_project, gitlab_instance=gitlab_instance) + + r_source_branch = this_gitlab.get_branch(source_branch) + if not r_source_branch: + module.fail_json(msg="Source branch {b} not exist.".format(b=r_source_branch)) + + r_target_branch = this_gitlab.get_branch(target_branch) + if not r_target_branch: + module.fail_json(msg="Destination branch {b} not exist.".format(b=r_target_branch)) + + this_mr = this_gitlab.get_mr(title, source_branch, target_branch, state_filter) + + if state == "present": + if description_path: + try: + with open(description_path, 'rb') as f: + description = to_text(f.read(), errors='surrogate_or_strict') + except IOError as e: + module.fail_json(msg='Cannot open {0}: {1}'.format(description_path, e)) + + # sorting necessary in order to properly detect changes, as we don't want to get false positive + # results due to differences in ids ordering; see `mr_has_changed()` + assignee_ids = sorted(this_gitlab.get_user_ids(assignee_ids.split(","))) if assignee_ids else [] + reviewer_ids = sorted(this_gitlab.get_user_ids(reviewer_ids.split(","))) if reviewer_ids else [] + labels = sorted(labels.split(",")) if labels else [] + + options = { + "target_branch": target_branch, + "title": title, + "description": description, + "labels": labels, + "remove_source_branch": remove_source_branch, + "reviewer_ids": reviewer_ids, + "assignee_ids": assignee_ids, + } + + if not this_mr: + options["source_branch"] = source_branch + + mr = this_gitlab.create_mr(options) + module.exit_json( + changed=True, msg="Created the Merge Request {t} from branch {s} to branch {d}.".format(t=title, d=target_branch, s=source_branch), + mr=mr.asdict() + ) + else: + if this_gitlab.mr_has_changed(this_mr, options): + mr = this_gitlab.update_mr(this_mr, options) + module.exit_json( + changed=True, msg="Merge Request {t} from branch {s} to branch {d} updated.".format(t=title, d=target_branch, s=source_branch), + mr=mr + ) + else: + module.exit_json( + changed=False, msg="Merge Request {t} from branch {s} to branch {d} already exist".format(t=title, d=target_branch, s=source_branch), + mr=this_mr.asdict() + ) + elif this_mr and state == "absent": + mr = this_gitlab.delete_mr(this_mr) + module.exit_json( + changed=True, msg="Merge Request {t} from branch {s} to branch {d} deleted.".format(t=title, d=target_branch, s=source_branch), + mr=mr + ) + else: + module.exit_json(changed=False, msg="No changes are needed.", mr=this_mr.asdict()) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/gitlab_milestone.py b/ansible_collections/community/general/plugins/modules/gitlab_milestone.py new file mode 100644 index 000000000..0a616ea47 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/gitlab_milestone.py @@ -0,0 +1,496 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Gabriele Pongelli (gabriele.pongelli@gmail.com) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +module: gitlab_milestone +short_description: Creates/updates/deletes GitLab Milestones belonging to project or group +version_added: 8.3.0 +description: + - When a milestone does not exist, it will be created. + - When a milestone does exist, its value will be updated when the values are different. + - Milestones can be purged. +author: + - "Gabriele Pongelli (@gpongelli)" +requirements: + - python-gitlab python module +extends_documentation_fragment: + - community.general.auth_basic + - community.general.gitlab + - community.general.attributes + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + state: + description: + - Create or delete milestone. + default: present + type: str + choices: ["present", "absent"] + purge: + description: + - When set to V(true), delete all milestone which are not mentioned in the task. + default: false + type: bool + required: false + project: + description: + - The path and name of the project. Either this or O(group) is required. + required: false + type: str + group: + description: + - The path of the group. Either this or O(project) is required. + required: false + type: str + milestones: + description: + - A list of dictionaries that represents gitlab project's or group's milestones. + type: list + elements: dict + required: false + default: [] + suboptions: + title: + description: + - The name of the milestone. + type: str + required: true + due_date: + description: + - Milestone due date in YYYY-MM-DD format. + type: str + required: false + default: null + start_date: + description: + - Milestone start date in YYYY-MM-DD format. + type: str + required: false + default: null + description: + description: + - Milestone's description. + type: str + default: null +''' + + +EXAMPLES = ''' +# same project's task can be executed for group +- name: Create one milestone + community.general.gitlab_milestone: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + milestones: + - title: milestone_one + start_date: "2024-01-04" + state: present + +- name: Create many group milestones + community.general.gitlab_milestone: + api_url: https://gitlab.com + api_token: secret_access_token + group: "group1" + milestones: + - title: milestone_one + start_date: "2024-01-04" + description: this is a milestone + due_date: "2024-02-04" + - title: milestone_two + state: present + +- name: Create many project milestones + community.general.gitlab_milestone: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + milestones: + - title: milestone_one + start_date: "2024-01-04" + description: this is a milestone + due_date: "2024-02-04" + - title: milestone_two + state: present + +- name: Set or update some milestones + community.general.gitlab_milestone: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + milestones: + - title: milestone_one + start_date: "2024-05-04" + state: present + +- name: Add milestone in check mode + community.general.gitlab_milestone: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + milestones: + - title: milestone_one + start_date: "2024-05-04" + check_mode: true + +- name: Delete milestone + community.general.gitlab_milestone: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + milestones: + - title: milestone_one + state: absent + +- name: Purge all milestones + community.general.gitlab_milestone: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + purge: true + +- name: Delete many milestones + community.general.gitlab_milestone: + api_url: https://gitlab.com + api_token: secret_access_token + project: "group1/project1" + state: absent + milestones: + - title: milestone-abc123 + - title: milestone-two +''' + +RETURN = ''' +milestones: + description: Four lists of the milestones which were added, updated, removed or exist. + returned: success + type: dict + contains: + added: + description: A list of milestones which were created. + returned: always + type: list + sample: ['abcd', 'milestone-one'] + untouched: + description: A list of milestones which exist. + returned: always + type: list + sample: ['defg', 'new-milestone'] + removed: + description: A list of milestones which were deleted. + returned: always + type: list + sample: ['defg', 'new-milestone'] + updated: + description: A list pre-existing milestones whose values have been set. + returned: always + type: list + sample: ['defg', 'new-milestone'] +milestones_obj: + description: API object. + returned: success + type: dict +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.api import basic_auth_argument_spec + +from ansible_collections.community.general.plugins.module_utils.version import LooseVersion +from ansible_collections.community.general.plugins.module_utils.gitlab import ( + auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project, gitlab +) +from datetime import datetime + + +class GitlabMilestones(object): + + def __init__(self, module, gitlab_instance, group_id, project_id): + self._gitlab = gitlab_instance + self.gitlab_object = group_id if group_id else project_id + self.is_group_milestone = True if group_id else False + self._module = module + + def list_all_milestones(self): + page_nb = 1 + milestones = [] + vars_page = self.gitlab_object.milestones.list(page=page_nb) + while len(vars_page) > 0: + milestones += vars_page + page_nb += 1 + vars_page = self.gitlab_object.milestones.list(page=page_nb) + return milestones + + def create_milestone(self, var_obj): + if self._module.check_mode: + return True, True + + var = { + "title": var_obj.get('title'), + } + + if var_obj.get('description') is not None: + var["description"] = var_obj.get('description') + + if var_obj.get('start_date') is not None: + var["start_date"] = self.check_date(var_obj.get('start_date')) + + if var_obj.get('due_date') is not None: + var["due_date"] = self.check_date(var_obj.get('due_date')) + + _obj = self.gitlab_object.milestones.create(var) + return True, _obj.asdict() + + def update_milestone(self, var_obj): + if self._module.check_mode: + return True, True + _milestone = self.gitlab_object.milestones.get(self.get_milestone_id(var_obj.get('title'))) + + if var_obj.get('description') is not None: + _milestone.description = var_obj.get('description') + + if var_obj.get('start_date') is not None: + _milestone.start_date = var_obj.get('start_date') + + if var_obj.get('due_date') is not None: + _milestone.due_date = var_obj.get('due_date') + + # save returns None + _milestone.save() + return True, _milestone.asdict() + + def get_milestone_id(self, _title): + _milestone_list = self.gitlab_object.milestones.list() + _found = list(filter(lambda x: x.title == _title, _milestone_list)) + if _found: + return _found[0].id + else: + self._module.fail_json(msg="milestone '%s' not found." % _title) + + def check_date(self, _date): + try: + datetime.strptime(_date, '%Y-%m-%d') + except ValueError: + self._module.fail_json(msg="milestone's date '%s' not in correct format." % _date) + return _date + + def delete_milestone(self, var_obj): + if self._module.check_mode: + return True, True + _milestone = self.gitlab_object.milestones.get(self.get_milestone_id(var_obj.get('title'))) + # delete returns None + _milestone.delete() + return True, _milestone.asdict() + + +def compare(requested_milestones, existing_milestones, state): + # we need to do this, because it was determined in a previous version - more or less buggy + # basically it is not necessary and might result in more/other bugs! + # but it is required and only relevant for check mode!! + # logic represents state 'present' when not purge. all other can be derived from that + # untouched => equal in both + # updated => title are equal + # added => title does not exist + untouched = list() + updated = list() + added = list() + + if state == 'present': + _existing_milestones = list() + for item in existing_milestones: + _existing_milestones.append({'title': item.get('title')}) + + for var in requested_milestones: + if var in existing_milestones: + untouched.append(var) + else: + compare_item = {'title': var.get('title')} + if compare_item in _existing_milestones: + updated.append(var) + else: + added.append(var) + + return untouched, updated, added + + +def native_python_main(this_gitlab, purge, requested_milestones, state, module): + change = False + return_value = dict(added=[], updated=[], removed=[], untouched=[]) + return_obj = dict(added=[], updated=[], removed=[]) + + milestones_before = [x.asdict() for x in this_gitlab.list_all_milestones()] + + # filter out and enrich before compare + for item in requested_milestones: + # add defaults when not present + if item.get('description') is None: + item['description'] = "" + if item.get('due_date') is None: + item['due_date'] = None + if item.get('start_date') is None: + item['start_date'] = None + + for item in milestones_before: + # remove field only from server + item.pop('id') + item.pop('iid') + item.pop('created_at') + item.pop('expired') + item.pop('state') + item.pop('updated_at') + item.pop('web_url') + # group milestone has group_id, while project has project_id + if 'group_id' in item: + item.pop('group_id') + if 'project_id' in item: + item.pop('project_id') + + if state == 'present': + add_or_update = [x for x in requested_milestones if x not in milestones_before] + for item in add_or_update: + try: + _rv, _obj = this_gitlab.create_milestone(item) + if _rv: + return_value['added'].append(item) + return_obj['added'].append(_obj) + except Exception: + # create raises exception with following error message when milestone already exists + _rv, _obj = this_gitlab.update_milestone(item) + if _rv: + return_value['updated'].append(item) + return_obj['updated'].append(_obj) + + if purge: + # re-fetch + _milestones = this_gitlab.list_all_milestones() + + for item in milestones_before: + _rv, _obj = this_gitlab.delete_milestone(item) + if _rv: + return_value['removed'].append(item) + return_obj['removed'].append(_obj) + + elif state == 'absent': + if not purge: + _milestone_titles_requested = [x['title'] for x in requested_milestones] + remove_requested = [x for x in milestones_before if x['title'] in _milestone_titles_requested] + for item in remove_requested: + _rv, _obj = this_gitlab.delete_milestone(item) + if _rv: + return_value['removed'].append(item) + return_obj['removed'].append(_obj) + else: + for item in milestones_before: + _rv, _obj = this_gitlab.delete_milestone(item) + if _rv: + return_value['removed'].append(item) + return_obj['removed'].append(_obj) + + if module.check_mode: + _untouched, _updated, _added = compare(requested_milestones, milestones_before, state) + return_value = dict(added=_added, updated=_updated, removed=return_value['removed'], untouched=_untouched) + + if any(return_value[x] for x in ['added', 'removed', 'updated']): + change = True + + milestones_after = [x.asdict() for x in this_gitlab.list_all_milestones()] + + return change, return_value, milestones_before, milestones_after, return_obj + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(auth_argument_spec()) + argument_spec.update( + project=dict(type='str', required=False, default=None), + group=dict(type='str', required=False, default=None), + purge=dict(type='bool', required=False, default=False), + milestones=dict(type='list', elements='dict', required=False, default=list(), + options=dict( + title=dict(type='str', required=True), + description=dict(type='str', required=False), + due_date=dict(type='str', required=False), + start_date=dict(type='str', required=False),) + ), + state=dict(type='str', default="present", choices=["absent", "present"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_username', 'api_token'], + ['api_username', 'api_oauth_token'], + ['api_username', 'api_job_token'], + ['api_token', 'api_oauth_token'], + ['api_token', 'api_job_token'], + ['project', 'group'], + ], + required_together=[ + ['api_username', 'api_password'], + ], + required_one_of=[ + ['api_username', 'api_token', 'api_oauth_token', 'api_job_token'], + ['project', 'group'] + ], + supports_check_mode=True + ) + ensure_gitlab_package(module) + + gitlab_project = module.params['project'] + gitlab_group = module.params['group'] + purge = module.params['purge'] + milestone_list = module.params['milestones'] + state = module.params['state'] + + gitlab_version = gitlab.__version__ + _min_gitlab = '3.2.0' + if LooseVersion(gitlab_version) < LooseVersion(_min_gitlab): + module.fail_json(msg="community.general.gitlab_milestone requires python-gitlab Python module >= %s " + "(installed version: [%s]). Please upgrade " + "python-gitlab to version %s or above." % (_min_gitlab, gitlab_version, _min_gitlab)) + + gitlab_instance = gitlab_authentication(module) + + # find_project can return None, but the other must exist + gitlab_project_id = find_project(gitlab_instance, gitlab_project) + + # find_group can return None, but the other must exist + gitlab_group_id = find_group(gitlab_instance, gitlab_group) + + # if both not found, module must exist + if not gitlab_project_id and not gitlab_group_id: + if gitlab_project and not gitlab_project_id: + module.fail_json(msg="project '%s' not found." % gitlab_project) + if gitlab_group and not gitlab_group_id: + module.fail_json(msg="group '%s' not found." % gitlab_group) + + this_gitlab = GitlabMilestones(module=module, gitlab_instance=gitlab_instance, group_id=gitlab_group_id, + project_id=gitlab_project_id) + + change, raw_return_value, before, after, _obj = native_python_main(this_gitlab, purge, milestone_list, state, + module) + + if not module.check_mode: + raw_return_value['untouched'] = [x for x in before if x in after] + + added = [x.get('title') for x in raw_return_value['added']] + updated = [x.get('title') for x in raw_return_value['updated']] + removed = [x.get('title') for x in raw_return_value['removed']] + untouched = [x.get('title') for x in raw_return_value['untouched']] + return_value = dict(added=added, updated=updated, removed=removed, untouched=untouched) + + module.exit_json(changed=change, milestones=return_value, milestones_obj=_obj) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/gitlab_project.py b/ansible_collections/community/general/plugins/modules/gitlab_project.py index db360d578..f1b96bfac 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_project.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_project.py @@ -15,13 +15,12 @@ module: gitlab_project short_description: Creates/updates/deletes GitLab Projects description: - When the project does not exist in GitLab, it will be created. - - When the project does exists and I(state=absent), the project will be deleted. + - When the project does exists and O(state=absent), the project will be deleted. - When changes are made to the project, the project will be updated. author: - Werner Dijkerman (@dj-wasabi) - Guillaume Martinez (@Lunik) requirements: - - python >= 2.7 - python-gitlab python module extends_documentation_fragment: - community.general.auth_basic @@ -84,9 +83,9 @@ options: default: true visibility: description: - - C(private) Project access must be granted explicitly for each user. - - C(internal) The project can be cloned by any logged in user. - - C(public) The project can be cloned without any authentication. + - V(private) Project access must be granted explicitly for each user. + - V(internal) The project can be cloned by any logged in user. + - V(public) The project can be cloned without any authentication. default: private type: str choices: ["private", "internal", "public"] @@ -108,7 +107,7 @@ options: merge_method: description: - What requirements are placed upon merges. - - Possible values are C(merge), C(rebase_merge) merge commit with semi-linear history, C(ff) fast-forward merges only. + - Possible values are V(merge), V(rebase_merge) merge commit with semi-linear history, V(ff) fast-forward merges only. type: str choices: ["ff", "merge", "rebase_merge"] default: merge @@ -175,79 +174,81 @@ options: version_added: "4.2.0" default_branch: description: - - Default branch name for a new project. - - This option is only used on creation, not for updates. This is also only used if I(initialize_with_readme=true). + - The default branch name for this project. + - For project creation, this option requires O(initialize_with_readme=true). + - For project update, the branch must exist. + - Supports project's default branch update since community.general 8.0.0. type: str version_added: "4.2.0" builds_access_level: description: - - C(private) means that repository CI/CD is allowed only to project members. - - C(disabled) means that repository CI/CD is disabled. - - C(enabled) means that repository CI/CD is enabled. + - V(private) means that repository CI/CD is allowed only to project members. + - V(disabled) means that repository CI/CD is disabled. + - V(enabled) means that repository CI/CD is enabled. type: str choices: ["private", "disabled", "enabled"] version_added: "6.2.0" forking_access_level: description: - - C(private) means that repository forks is allowed only to project members. - - C(disabled) means that repository forks are disabled. - - C(enabled) means that repository forks are enabled. + - V(private) means that repository forks is allowed only to project members. + - V(disabled) means that repository forks are disabled. + - V(enabled) means that repository forks are enabled. type: str choices: ["private", "disabled", "enabled"] version_added: "6.2.0" container_registry_access_level: description: - - C(private) means that container registry is allowed only to project members. - - C(disabled) means that container registry is disabled. - - C(enabled) means that container registry is enabled. + - V(private) means that container registry is allowed only to project members. + - V(disabled) means that container registry is disabled. + - V(enabled) means that container registry is enabled. type: str choices: ["private", "disabled", "enabled"] version_added: "6.2.0" releases_access_level: description: - - C(private) means that accessing release is allowed only to project members. - - C(disabled) means that accessing release is disabled. - - C(enabled) means that accessing release is enabled. + - V(private) means that accessing release is allowed only to project members. + - V(disabled) means that accessing release is disabled. + - V(enabled) means that accessing release is enabled. type: str choices: ["private", "disabled", "enabled"] version_added: "6.4.0" environments_access_level: description: - - C(private) means that deployment to environment is allowed only to project members. - - C(disabled) means that deployment to environment is disabled. - - C(enabled) means that deployment to environment is enabled. + - V(private) means that deployment to environment is allowed only to project members. + - V(disabled) means that deployment to environment is disabled. + - V(enabled) means that deployment to environment is enabled. type: str choices: ["private", "disabled", "enabled"] version_added: "6.4.0" feature_flags_access_level: description: - - C(private) means that feature rollout is allowed only to project members. - - C(disabled) means that feature rollout is disabled. - - C(enabled) means that feature rollout is enabled. + - V(private) means that feature rollout is allowed only to project members. + - V(disabled) means that feature rollout is disabled. + - V(enabled) means that feature rollout is enabled. type: str choices: ["private", "disabled", "enabled"] version_added: "6.4.0" infrastructure_access_level: description: - - C(private) means that configuring infrastructure is allowed only to project members. - - C(disabled) means that configuring infrastructure is disabled. - - C(enabled) means that configuring infrastructure is enabled. + - V(private) means that configuring infrastructure is allowed only to project members. + - V(disabled) means that configuring infrastructure is disabled. + - V(enabled) means that configuring infrastructure is enabled. type: str choices: ["private", "disabled", "enabled"] version_added: "6.4.0" monitor_access_level: description: - - C(private) means that monitoring health is allowed only to project members. - - C(disabled) means that monitoring health is disabled. - - C(enabled) means that monitoring health is enabled. + - V(private) means that monitoring health is allowed only to project members. + - V(disabled) means that monitoring health is disabled. + - V(enabled) means that monitoring health is enabled. type: str choices: ["private", "disabled", "enabled"] version_added: "6.4.0" security_and_compliance_access_level: description: - - C(private) means that accessing security and complicance tab is allowed only to project members. - - C(disabled) means that accessing security and complicance tab is disabled. - - C(enabled) means that accessing security and complicance tab is enabled. + - V(private) means that accessing security and complicance tab is allowed only to project members. + - V(disabled) means that accessing security and complicance tab is disabled. + - V(enabled) means that accessing security and complicance tab is enabled. type: str choices: ["private", "disabled", "enabled"] version_added: "6.4.0" @@ -272,7 +273,6 @@ EXAMPLES = r''' community.general.gitlab_project: api_url: https://gitlab.example.com/ api_token: "{{ access_token }}" - validate_certs: false name: my_first_project state: absent delegate_to: localhost @@ -338,7 +338,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, find_group, find_project, gitlab_authentication, gitlab, ensure_gitlab_package + auth_argument_spec, find_group, find_project, gitlab_authentication, gitlab ) from ansible_collections.community.general.plugins.module_utils.version import LooseVersion @@ -355,7 +355,7 @@ class GitLabProject(object): @param namespace Namespace Object (User or Group) @param options Options of the project ''' - def create_or_update_project(self, project_name, namespace, options): + def create_or_update_project(self, module, project_name, namespace, options): changed = False project_options = { 'name': project_name, @@ -395,6 +395,8 @@ class GitLabProject(object): # Because we have already call userExists in main() if self.project_object is None: + if options['default_branch'] and not options['initialize_with_readme']: + module.fail_json(msg="Param default_branch need param initialize_with_readme set to true") project_options.update({ 'path': options['path'], 'import_url': options['import_url'], @@ -416,6 +418,8 @@ class GitLabProject(object): changed = True else: + if options['default_branch']: + project_options['default_branch'] = options['default_branch'] changed, project = self.update_project(self.project_object, project_options) self.project_object = project @@ -552,7 +556,9 @@ def main(): ], supports_check_mode=True, ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) group_identifier = module.params['group'] project_name = module.params['name'] @@ -590,11 +596,6 @@ def main(): security_and_compliance_access_level = module.params['security_and_compliance_access_level'] topics = module.params['topics'] - if default_branch and not initialize_with_readme: - module.fail_json(msg="Param default_branch need param initialize_with_readme set to true") - - gitlab_instance = gitlab_authentication(module) - # Set project_path to project_name if it is empty. if project_path is None: project_path = project_name.replace(" ", "_") @@ -636,7 +637,7 @@ def main(): if state == 'present': - if gitlab_project.create_or_update_project(project_name, namespace, { + if gitlab_project.create_or_update_project(module, project_name, namespace, { "path": project_path, "description": project_description, "initialize_with_readme": initialize_with_readme, diff --git a/ansible_collections/community/general/plugins/modules/gitlab_project_access_token.py b/ansible_collections/community/general/plugins/modules/gitlab_project_access_token.py new file mode 100644 index 000000000..e692a3057 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/gitlab_project_access_token.py @@ -0,0 +1,318 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Zoran Krleza (zoran.krleza@true-north.hr) +# Based on code: +# Copyright (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) +# Copyright (c) 2018, Marcus Watkins +# Copyright (c) 2013, Phillip Gentry +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +module: gitlab_project_access_token +short_description: Manages GitLab project access tokens +version_added: 8.4.0 +description: + - Creates and revokes project access tokens. +author: + - Zoran Krleza (@pixslx) +requirements: + - python-gitlab >= 3.1.0 +extends_documentation_fragment: + - community.general.auth_basic + - community.general.gitlab + - community.general.attributes +notes: + - Access tokens can not be changed. If a parameter needs to be changed, an acceess token has to be recreated. + Whether tokens will be recreated is controlled by the O(recreate) option, which defaults to V(never). + - Token string is contained in the result only when access token is created or recreated. It can not be fetched afterwards. + - Token matching is done by comparing O(name) option. + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + project: + description: + - ID or full path of project in the form of group/name. + required: true + type: str + name: + description: + - Access token's name. + required: true + type: str + scopes: + description: + - Scope of the access token. + required: true + type: list + elements: str + aliases: ["scope"] + choices: ["api", "read_api", "read_registry", "write_registry", "read_repository", "write_repository", "create_runner", "ai_features", "k8s_proxy"] + access_level: + description: + - Access level of the access token. + type: str + default: maintainer + choices: ["guest", "reporter", "developer", "maintainer", "owner"] + expires_at: + description: + - Expiration date of the access token in C(YYYY-MM-DD) format. + - Make sure to quote this value in YAML to ensure it is kept as a string and not interpreted as a YAML date. + type: str + required: true + recreate: + description: + - Whether the access token will be recreated if it already exists. + - When V(never) the token will never be recreated. + - When V(always) the token will always be recreated. + - When V(state_change) the token will be recreated if there is a difference between desired state and actual state. + type: str + choices: ["never", "always", "state_change"] + default: never + state: + description: + - When V(present) the access token will be added to the project if it does not exist. + - When V(absent) it will be removed from the project if it exists. + default: present + type: str + choices: [ "present", "absent" ] +''' + +EXAMPLES = r''' +- name: "Creating a project access token" + community.general.gitlab_project_access_token: + api_url: https://gitlab.example.com/ + api_token: "somegitlabapitoken" + project: "my_group/my_project" + name: "project_token" + expires_at: "2024-12-31" + access_level: developer + scopes: + - api + - read_api + - read_repository + - write_repository + state: present + +- name: "Revoking a project access token" + community.general.gitlab_project_access_token: + api_url: https://gitlab.example.com/ + api_token: "somegitlabapitoken" + project: "my_group/my_project" + name: "project_token" + expires_at: "2024-12-31" + scopes: + - api + - read_api + - read_repository + - write_repository + state: absent + +- name: "Change (recreate) existing token if its actual state is different than desired state" + community.general.gitlab_project_access_token: + api_url: https://gitlab.example.com/ + api_token: "somegitlabapitoken" + project: "my_group/my_project" + name: "project_token" + expires_at: "2024-12-31" + scopes: + - api + - read_api + - read_repository + - write_repository + recreate: state_change + state: present +''' + +RETURN = r''' +access_token: + description: + - API object. + - Only contains the value of the token if the token was created or recreated. + returned: success and O(state=present) + type: dict +''' + +from datetime import datetime + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.general.plugins.module_utils.gitlab import ( + auth_argument_spec, find_project, gitlab_authentication, gitlab +) + +ACCESS_LEVELS = dict(guest=10, reporter=20, developer=30, maintainer=40, owner=50) + + +class GitLabProjectAccessToken(object): + def __init__(self, module, gitlab_instance): + self._module = module + self._gitlab = gitlab_instance + self.access_token_object = None + + ''' + @param project Project Object + @param arguments Attributes of the access_token + ''' + def create_access_token(self, project, arguments): + changed = False + if self._module.check_mode: + return True + + try: + self.access_token_object = project.access_tokens.create(arguments) + changed = True + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json(msg="Failed to create access token: %s " % to_native(e)) + + return changed + + ''' + @param project Project object + @param name of the access token + ''' + def find_access_token(self, project, name): + access_tokens = project.access_tokens.list(all=True) + for access_token in access_tokens: + if (access_token.name == name): + self.access_token_object = access_token + return False + return False + + def revoke_access_token(self): + if self._module.check_mode: + return True + + changed = False + try: + self.access_token_object.delete() + changed = True + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json(msg="Failed to revoke access token: %s " % to_native(e)) + + return changed + + def access_tokens_equal(self): + if self.access_token_object.name != self._module.params['name']: + return False + if self.access_token_object.scopes != self._module.params['scopes']: + return False + if self.access_token_object.access_level != ACCESS_LEVELS[self._module.params['access_level']]: + return False + if self.access_token_object.expires_at != self._module.params['expires_at']: + return False + return True + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(auth_argument_spec()) + argument_spec.update(dict( + state=dict(type='str', default="present", choices=["absent", "present"]), + project=dict(type='str', required=True), + name=dict(type='str', required=True), + scopes=dict(type='list', + required=True, + aliases=['scope'], + elements='str', + choices=['api', + 'read_api', + 'read_registry', + 'write_registry', + 'read_repository', + 'write_repository', + 'create_runner', + 'ai_features', + 'k8s_proxy']), + access_level=dict(type='str', required=False, default='maintainer', choices=['guest', 'reporter', 'developer', 'maintainer', 'owner']), + expires_at=dict(type='str', required=True), + recreate=dict(type='str', default='never', choices=['never', 'always', 'state_change']) + )) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_username', 'api_token'], + ['api_username', 'api_oauth_token'], + ['api_username', 'api_job_token'], + ['api_token', 'api_oauth_token'], + ['api_token', 'api_job_token'] + ], + required_together=[ + ['api_username', 'api_password'] + ], + required_one_of=[ + ['api_username', 'api_token', 'api_oauth_token', 'api_job_token'] + ], + supports_check_mode=True + ) + + state = module.params['state'] + project_identifier = module.params['project'] + name = module.params['name'] + scopes = module.params['scopes'] + access_level_str = module.params['access_level'] + expires_at = module.params['expires_at'] + recreate = module.params['recreate'] + + access_level = ACCESS_LEVELS[access_level_str] + + try: + datetime.strptime(expires_at, '%Y-%m-%d') + except ValueError: + module.fail_json(msg="Argument expires_at is not in required format YYYY-MM-DD") + + gitlab_instance = gitlab_authentication(module) + + gitlab_access_token = GitLabProjectAccessToken(module, gitlab_instance) + + project = find_project(gitlab_instance, project_identifier) + if project is None: + module.fail_json(msg="Failed to create access token: project %s does not exists" % project_identifier) + + gitlab_access_token_exists = False + gitlab_access_token.find_access_token(project, name) + if gitlab_access_token.access_token_object is not None: + gitlab_access_token_exists = True + + if state == 'absent': + if gitlab_access_token_exists: + gitlab_access_token.revoke_access_token() + module.exit_json(changed=True, msg="Successfully deleted access token %s" % name) + else: + module.exit_json(changed=False, msg="Access token does not exists") + + if state == 'present': + if gitlab_access_token_exists: + if gitlab_access_token.access_tokens_equal(): + if recreate == 'always': + gitlab_access_token.revoke_access_token() + gitlab_access_token.create_access_token(project, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at}) + module.exit_json(changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs) + else: + module.exit_json(changed=False, msg="Access token already exists", access_token=gitlab_access_token.access_token_object._attrs) + else: + if recreate == 'never': + module.fail_json(msg="Access token already exists and its state is different. It can not be updated without recreating.") + else: + gitlab_access_token.revoke_access_token() + gitlab_access_token.create_access_token(project, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at}) + module.exit_json(changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs) + else: + gitlab_access_token.create_access_token(project, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at}) + module.exit_json(changed=True, msg="Successfully created access token", access_token=gitlab_access_token.access_token_object._attrs) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/gitlab_project_badge.py b/ansible_collections/community/general/plugins/modules/gitlab_project_badge.py index 5b1a8d3f1..fee938949 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_project_badge.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_project_badge.py @@ -39,8 +39,8 @@ options: state: description: - State of the badge in the project. - - On C(present), it adds a badge to a GitLab project. - - On C(absent), it removes a badge from a GitLab project. + - On V(present), it adds a badge to a GitLab project. + - On V(absent), it removes a badge from a GitLab project. choices: ['present', 'absent'] default: 'present' type: str @@ -82,7 +82,7 @@ EXAMPLES = r''' RETURN = ''' badge: description: The badge information. - returned: when I(state=present) + returned: when O(state=present) type: dict sample: id: 1 @@ -97,7 +97,7 @@ from ansible.module_utils.api import basic_auth_argument_spec from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, gitlab_authentication, find_project, ensure_gitlab_package + auth_argument_spec, gitlab_authentication, find_project, list_all_kwargs ) @@ -105,7 +105,7 @@ def present_strategy(module, gl, project, wished_badge): changed = False existing_badge = None - for badge in project.badges.list(iterator=True): + for badge in project.badges.list(**list_all_kwargs): if badge.image_url == wished_badge["image_url"]: existing_badge = badge break @@ -135,7 +135,7 @@ def absent_strategy(module, gl, project, wished_badge): changed = False existing_badge = None - for badge in project.badges.list(iterator=True): + for badge in project.badges.list(**list_all_kwargs): if badge.image_url == wished_badge["image_url"]: existing_badge = badge break @@ -159,13 +159,12 @@ state_strategy = { def core(module): - ensure_gitlab_package(module) + # check prerequisites and connect to gitlab server + gl = gitlab_authentication(module) gitlab_project = module.params['project'] state = module.params['state'] - gl = gitlab_authentication(module) - project = find_project(gl, gitlab_project) # project doesn't exist if not project: diff --git a/ansible_collections/community/general/plugins/modules/gitlab_project_members.py b/ansible_collections/community/general/plugins/modules/gitlab_project_members.py index 905358443..2ce277f68 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_project_members.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_project_members.py @@ -42,21 +42,21 @@ options: gitlab_user: description: - A username or a list of usernames to add to/remove from the GitLab project. - - Mutually exclusive with I(gitlab_users_access). + - Mutually exclusive with O(gitlab_users_access). type: list elements: str access_level: description: - The access level for the user. - - Required if I(state=present), user state is set to present. + - Required if O(state=present), user state is set to present. type: str choices: ['guest', 'reporter', 'developer', 'maintainer'] gitlab_users_access: description: - Provide a list of user to access level mappings. - Every dictionary in this list specifies a user (by username) and the access level the user should have. - - Mutually exclusive with I(gitlab_user) and I(access_level). - - Use together with I(purge_users) to remove all users not specified here from the project. + - Mutually exclusive with O(gitlab_user) and O(access_level). + - Use together with O(purge_users) to remove all users not specified here from the project. type: list elements: dict suboptions: @@ -67,7 +67,7 @@ options: access_level: description: - The access level for the user. - - Required if I(state=present), user state is set to present. + - Required if O(state=present), user state is set to present. type: str choices: ['guest', 'reporter', 'developer', 'maintainer'] required: true @@ -75,16 +75,16 @@ options: state: description: - State of the member in the project. - - On C(present), it adds a user to a GitLab project. - - On C(absent), it removes a user from a GitLab project. + - On V(present), it adds a user to a GitLab project. + - On V(absent), it removes a user from a GitLab project. choices: ['present', 'absent'] default: 'present' type: str purge_users: description: - - Adds/remove users of the given access_level to match the given I(gitlab_user)/I(gitlab_users_access) list. + - Adds/remove users of the given access_level to match the given O(gitlab_user)/O(gitlab_users_access) list. If omitted do not purge orphaned members. - - Is only used when I(state=present). + - Is only used when O(state=present). type: list elements: str choices: ['guest', 'reporter', 'developer', 'maintainer'] @@ -106,7 +106,6 @@ EXAMPLES = r''' community.general.gitlab_project_members: api_url: 'https://gitlab.example.com' api_token: 'Your-Private-Token' - validate_certs: false project: projectname gitlab_user: username state: absent @@ -163,7 +162,7 @@ from ansible.module_utils.api import basic_auth_argument_spec from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, gitlab_authentication, gitlab, ensure_gitlab_package + auth_argument_spec, gitlab_authentication, gitlab ) @@ -279,13 +278,15 @@ def main(): ], supports_check_mode=True, ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gl = gitlab_authentication(module) access_level_int = { - 'guest': gitlab.GUEST_ACCESS, - 'reporter': gitlab.REPORTER_ACCESS, - 'developer': gitlab.DEVELOPER_ACCESS, - 'maintainer': gitlab.MAINTAINER_ACCESS, + 'guest': gitlab.const.GUEST_ACCESS, + 'reporter': gitlab.const.REPORTER_ACCESS, + 'developer': gitlab.const.DEVELOPER_ACCESS, + 'maintainer': gitlab.const.MAINTAINER_ACCESS, } gitlab_project = module.params['project'] @@ -296,9 +297,6 @@ def main(): if purge_users: purge_users = [access_level_int[level] for level in purge_users] - # connect to gitlab server - gl = gitlab_authentication(module) - project = GitLabProjectMembers(module, gl) gitlab_project_id = project.get_project(gitlab_project) diff --git a/ansible_collections/community/general/plugins/modules/gitlab_project_variable.py b/ansible_collections/community/general/plugins/modules/gitlab_project_variable.py index 63569dd78..329e7a414 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_project_variable.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_project_variable.py @@ -14,11 +14,10 @@ description: - When a project variable does not exist, it will be created. - When a project variable does exist, its value will be updated when the values are different. - Variables which are untouched in the playbook, but are not untouched in the GitLab project, - they stay untouched (I(purge) is C(false)) or will be deleted (I(purge) is C(true)). + they stay untouched (O(purge=false)) or will be deleted (O(purge=true)). author: - "Markus Bergholz (@markuman)" requirements: - - python >= 2.7 - python-gitlab python module extends_documentation_fragment: - community.general.auth_basic @@ -51,16 +50,17 @@ options: type: bool vars: description: - - When the list element is a simple key-value pair, masked and protected will be set to false. - - When the list element is a dict with the keys I(value), I(masked) and I(protected), the user can - have full control about whether a value should be masked, protected or both. + - When the list element is a simple key-value pair, masked, raw and protected will be set to false. + - When the list element is a dict with the keys C(value), C(masked), C(raw) and C(protected), the user can + have full control about whether a value should be masked, raw, protected or both. - Support for protected values requires GitLab >= 9.3. - Support for masked values requires GitLab >= 11.10. + - Support for raw values requires GitLab >= 15.7. - Support for environment_scope requires GitLab Premium >= 13.11. - Support for variable_type requires GitLab >= 11.11. - - A I(value) must be a string or a number. - - Field I(variable_type) must be a string with either C(env_var), which is the default, or C(file). - - Field I(environment_scope) must be a string defined by scope environment. + - A C(value) must be a string or a number. + - Field C(variable_type) must be a string with either V(env_var), which is the default, or V(file). + - Field C(environment_scope) must be a string defined by scope environment. - When a value is masked, it must be in Base64 and have a length of at least 8 characters. See GitLab documentation on acceptable values for a masked variable (https://docs.gitlab.com/ce/ci/variables/#masked-variables). default: {} @@ -69,7 +69,7 @@ options: version_added: 4.4.0 description: - A list of dictionaries that represents CI/CD variables. - - This module works internal with this structure, even if the older I(vars) parameter is used. + - This module works internal with this structure, even if the older O(vars) parameter is used. default: [] type: list elements: dict @@ -82,31 +82,38 @@ options: value: description: - The variable value. - - Required when I(state=present). + - Required when O(state=present). type: str masked: description: - - Wether variable value is masked or not. + - Whether variable value is masked or not. - Support for masked values requires GitLab >= 11.10. type: bool default: false protected: description: - - Wether variable value is protected or not. + - Whether variable value is protected or not. - Support for protected values requires GitLab >= 9.3. type: bool default: false + raw: + description: + - Whether variable value is raw or not. + - Support for raw values requires GitLab >= 15.7. + type: bool + default: false + version_added: '7.4.0' variable_type: description: - - Wether a variable is an environment variable (C(env_var)) or a file (C(file)). - - Support for I(variable_type) requires GitLab >= 11.11. + - Whether a variable is an environment variable (V(env_var)) or a file (V(file)). + - Support for O(variables[].variable_type) requires GitLab >= 11.11. type: str choices: ["env_var", "file"] default: env_var environment_scope: description: - The scope for the variable. - - Support for I(environment_scope) requires GitLab Premium >= 13.11. + - Support for O(variables[].environment_scope) requires GitLab Premium >= 13.11. type: str default: '*' ''' @@ -143,6 +150,38 @@ EXAMPLES = ''' variable_type: env_var environment_scope: '*' +- name: Set or update some CI/CD variables with raw value + community.general.gitlab_project_variable: + api_url: https://gitlab.com + api_token: secret_access_token + project: markuman/dotfiles + purge: false + vars: + ACCESS_KEY_ID: abc123 + SECRET_ACCESS_KEY: + value: 3214cbad + masked: true + protected: true + raw: true + variable_type: env_var + environment_scope: '*' + +- name: Set or update some CI/CD variables with expandable value + community.general.gitlab_project_variable: + api_url: https://gitlab.com + api_token: secret_access_token + project: markuman/dotfiles + purge: false + vars: + ACCESS_KEY_ID: abc123 + SECRET_ACCESS_KEY: + value: '$MY_OTHER_VARIABLE' + masked: true + protected: true + raw: false + variable_type: env_var + environment_scope: '*' + - name: Delete one variable community.general.gitlab_project_variable: api_url: https://gitlab.com @@ -181,62 +220,16 @@ project_variable: sample: ['ACCESS_KEY_ID', 'SECRET_ACCESS_KEY'] ''' -import traceback -from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.api import basic_auth_argument_spec -from ansible.module_utils.six import string_types -from ansible.module_utils.six import integer_types -GITLAB_IMP_ERR = None -try: - import gitlab # noqa: F401, pylint: disable=unused-import - HAS_GITLAB_PACKAGE = True -except Exception: - GITLAB_IMP_ERR = traceback.format_exc() - HAS_GITLAB_PACKAGE = False from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, gitlab_authentication, ensure_gitlab_package, filter_returned_variables + auth_argument_spec, gitlab_authentication, filter_returned_variables, vars_to_variables, + list_all_kwargs ) -def vars_to_variables(vars, module): - # transform old vars to new variables structure - variables = list() - for item, value in vars.items(): - if (isinstance(value, string_types) or - isinstance(value, (integer_types, float))): - variables.append( - { - "name": item, - "value": str(value), - "masked": False, - "protected": False, - "variable_type": "env_var", - } - ) - - elif isinstance(value, dict): - - new_item = { - "name": item, - "value": value.get('value'), - "masked": value.get('masked'), - "protected": value.get('protected'), - "variable_type": value.get('variable_type'), - } - - if value.get('environment_scope'): - new_item['environment_scope'] = value.get('environment_scope') - - variables.append(new_item) - - else: - module.fail_json(msg="value must be of type string, integer, float or dict") - - return variables - - class GitlabProjectVariables(object): def __init__(self, module, gitlab_instance): @@ -248,14 +241,7 @@ class GitlabProjectVariables(object): return self.repo.projects.get(project_name) def list_all_project_variables(self): - page_nb = 1 - variables = [] - vars_page = self.project.variables.list(page=page_nb) - while len(vars_page) > 0: - variables += vars_page - page_nb += 1 - vars_page = self.project.variables.list(page=page_nb) - return variables + return list(self.project.variables.list(**list_all_kwargs)) def create_variable(self, var_obj): if self._module.check_mode: @@ -266,6 +252,7 @@ class GitlabProjectVariables(object): "value": var_obj.get('value'), "masked": var_obj.get('masked'), "protected": var_obj.get('protected'), + "raw": var_obj.get('raw'), "variable_type": var_obj.get('variable_type'), } @@ -322,7 +309,7 @@ def compare(requested_variables, existing_variables, state): def native_python_main(this_gitlab, purge, requested_variables, state, module): change = False - return_value = dict(added=list(), updated=list(), removed=list(), untouched=list()) + return_value = dict(added=[], updated=[], removed=[], untouched=[]) gitlab_keys = this_gitlab.list_all_project_variables() before = [x.attributes for x in gitlab_keys] @@ -336,6 +323,8 @@ def native_python_main(this_gitlab, purge, requested_variables, state, module): item['value'] = str(item.get('value')) if item.get('protected') is None: item['protected'] = False + if item.get('raw') is None: + item['raw'] = False if item.get('masked') is None: item['masked'] = False if item.get('environment_scope') is None: @@ -391,7 +380,7 @@ def native_python_main(this_gitlab, purge, requested_variables, state, module): if module.check_mode: return_value = dict(added=added, updated=updated, removed=return_value['removed'], untouched=untouched) - if return_value['added'] or return_value['removed'] or return_value['updated']: + if any(return_value[x] for x in ['added', 'removed', 'updated']): change = True gitlab_keys = this_gitlab.list_all_project_variables() @@ -407,11 +396,14 @@ def main(): project=dict(type='str', required=True), purge=dict(type='bool', required=False, default=False), vars=dict(type='dict', required=False, default=dict(), no_log=True), + # please mind whenever changing the variables dict to also change module_utils/gitlab.py's + # KNOWN dict in filter_returned_variables or bad evil will happen variables=dict(type='list', elements='dict', required=False, default=list(), options=dict( name=dict(type='str', required=True), value=dict(type='str', no_log=True), masked=dict(type='bool', default=False), protected=dict(type='bool', default=False), + raw=dict(type='bool', default=False), environment_scope=dict(type='str', default='*'), variable_type=dict(type='str', default='env_var', choices=["env_var", "file"]), )), @@ -436,10 +428,9 @@ def main(): ], supports_check_mode=True ) - ensure_gitlab_package(module) - if not HAS_GITLAB_PACKAGE: - module.fail_json(msg=missing_required_lib("python-gitlab"), exception=GITLAB_IMP_ERR) + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) purge = module.params['purge'] var_list = module.params['vars'] @@ -452,9 +443,7 @@ def main(): if state == 'present': if any(x['value'] is None for x in variables): - module.fail_json(msg='value parameter is required in state present') - - gitlab_instance = gitlab_authentication(module) + module.fail_json(msg='value parameter is required for all variables in state present') this_gitlab = GitlabProjectVariables(module=module, gitlab_instance=gitlab_instance) diff --git a/ansible_collections/community/general/plugins/modules/gitlab_protected_branch.py b/ansible_collections/community/general/plugins/modules/gitlab_protected_branch.py index fea374cbf..8d2d75736 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_protected_branch.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_protected_branch.py @@ -16,7 +16,6 @@ description: author: - "Werner Dijkerman (@dj-wasabi)" requirements: - - python >= 2.7 - python-gitlab >= 2.3.0 extends_documentation_fragment: - community.general.auth_basic @@ -44,7 +43,7 @@ options: name: description: - The name of the branch that needs to be protected. - - Can make use a wildcard character for like C(production/*) or just have C(main) or C(develop) as value. + - Can make use a wildcard character for like V(production/*) or just have V(main) or V(develop) as value. required: true type: str merge_access_levels: @@ -83,7 +82,7 @@ from ansible.module_utils.api import basic_auth_argument_spec from ansible_collections.community.general.plugins.module_utils.version import LooseVersion from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, gitlab_authentication, gitlab, ensure_gitlab_package + auth_argument_spec, gitlab_authentication, gitlab ) @@ -94,9 +93,9 @@ class GitlabProtectedBranch(object): self._module = module self.project = self.get_project(project) self.ACCESS_LEVEL = { - 'nobody': gitlab.NO_ACCESS, - 'developer': gitlab.DEVELOPER_ACCESS, - 'maintainer': gitlab.MAINTAINER_ACCESS + 'nobody': gitlab.const.NO_ACCESS, + 'developer': gitlab.const.DEVELOPER_ACCESS, + 'maintainer': gitlab.const.MAINTAINER_ACCESS } def get_project(self, project_name): @@ -164,7 +163,9 @@ def main(): ], supports_check_mode=True ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) project = module.params['project'] name = module.params['name'] @@ -177,7 +178,6 @@ def main(): module.fail_json(msg="community.general.gitlab_proteched_branch requires python-gitlab Python module >= 2.3.0 (installed version: [%s])." " Please upgrade python-gitlab to version 2.3.0 or above." % gitlab_version) - gitlab_instance = gitlab_authentication(module) this_gitlab = GitlabProtectedBranch(module=module, project=project, gitlab_instance=gitlab_instance) p_branch = this_gitlab.protected_branch_exist(name=name) diff --git a/ansible_collections/community/general/plugins/modules/gitlab_runner.py b/ansible_collections/community/general/plugins/modules/gitlab_runner.py index a41b135fc..e6163a6b6 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_runner.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_runner.py @@ -24,13 +24,12 @@ description: To create shared runners, you need to ask your administrator to give you this token. It can be found at U(https://$GITLAB_URL/admin/runners/). notes: - - To create a new runner at least the C(api_token), C(description) and C(api_url) options are required. + - To create a new runner at least the O(api_token), O(description) and O(api_url) options are required. - Runners need to have unique descriptions. author: - Samy Coenen (@SamyCoenen) - Guillaume Martinez (@Lunik) requirements: - - python >= 2.7 - python-gitlab >= 1.5.0 extends_documentation_fragment: - community.general.auth_basic @@ -47,14 +46,16 @@ options: group: description: - ID or full path of the group in the form group/subgroup. - - Mutually exclusive with I(owned) and I(project). + - Mutually exclusive with O(owned) and O(project). + - Must be group's numeric ID if O(registration_token) is not set and O(state=present). type: str version_added: '6.5.0' project: description: - ID or full path of the project in the form of group/name. - - Mutually exclusive with I(owned) since community.general 4.5.0. - - Mutually exclusive with I(group). + - Mutually exclusive with O(owned) since community.general 4.5.0. + - Mutually exclusive with O(group). + - Must be project's numeric ID if O(registration_token) is not set and O(state=present). type: str version_added: '3.7.0' description: @@ -73,23 +74,35 @@ options: type: str registration_token: description: - - The registration token is used to register new runners. - - Required if I(state) is C(present). + - The registration token is used to register new runners before GitLab 16.0. + - Required if O(state=present) for GitLab < 16.0. + - If set, the runner will be created using the old runner creation workflow. + - If not set, the runner will be created using the new runner creation workflow, introduced in GitLab 16.0. + - If not set, requires python-gitlab >= 4.0.0. type: str owned: description: - Searches only runners available to the user when searching for existing, when false admin token required. - - Mutually exclusive with I(project) since community.general 4.5.0. - - Mutually exclusive with I(group). + - Mutually exclusive with O(project) since community.general 4.5.0. + - Mutually exclusive with O(group). default: false type: bool version_added: 2.0.0 active: description: - Define if the runners is immediately active after creation. + - Mutually exclusive with O(paused). required: false default: true type: bool + paused: + description: + - Define if the runners is active or paused after creation. + - Mutually exclusive with O(active). + required: false + default: false + type: bool + version_added: 8.1.0 locked: description: - Determines if the runner is locked or not. @@ -99,23 +112,24 @@ options: access_level: description: - Determines if a runner can pick up jobs only from protected branches. - - If I(access_level_on_creation) is not explicitly set to C(true), this option is ignored on registration and + - If O(access_level_on_creation) is not explicitly set to V(true), this option is ignored on registration and is only applied on updates. - - If set to C(not_protected), runner can pick up jobs from both protected and unprotected branches. - - If set to C(ref_protected), runner can pick up jobs only from protected branches. - - The current default is C(ref_protected). This will change to no default in community.general 8.0.0. - From that version on, if this option is not specified explicitly, GitLab will use C(not_protected) - on creation, and the value set will not be changed on any updates. + - If set to V(not_protected), runner can pick up jobs from both protected and unprotected branches. + - If set to V(ref_protected), runner can pick up jobs only from protected branches. + - Before community.general 8.0.0 the default was V(ref_protected). This was changed to no default in community.general 8.0.0. + If this option is not specified explicitly, GitLab will use V(not_protected) on creation, and the value set + will not be changed on any updates. required: false choices: ["not_protected", "ref_protected"] type: str access_level_on_creation: description: - Whether the runner should be registered with an access level or not. - - If set to C(true), the value of I(access_level) is used for runner registration. - - If set to C(false), GitLab registers the runner with the default access level. - - The current default of this option is C(false). This default is deprecated and will change to C(true) in commuinty.general 7.0.0. + - If set to V(true), the value of O(access_level) is used for runner registration. + - If set to V(false), GitLab registers the runner with the default access level. + - The default of this option changed to V(true) in community.general 7.0.0. Before, it was V(false). required: false + default: true type: bool version_added: 6.3.0 maximum_timeout: @@ -205,15 +219,11 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, gitlab_authentication, gitlab, ensure_gitlab_package + auth_argument_spec, gitlab_authentication, gitlab, list_all_kwargs ) -try: - cmp # pylint: disable=used-before-assignment -except NameError: - def cmp(a, b): - return (a > b) - (a < b) +from ansible_collections.community.general.plugins.module_utils.version import LooseVersion class GitLabRunner(object): @@ -238,27 +248,34 @@ class GitLabRunner(object): changed = False arguments = { - 'active': options['active'], 'locked': options['locked'], 'run_untagged': options['run_untagged'], 'maximum_timeout': options['maximum_timeout'], 'tag_list': options['tag_list'], } + + if options.get('paused') is not None: + arguments['paused'] = options['paused'] + else: + arguments['active'] = options['active'] + if options.get('access_level') is not None: arguments['access_level'] = options['access_level'] # Because we have already call userExists in main() if self.runner_object is None: arguments['description'] = description - arguments['token'] = options['registration_token'] + if options.get('registration_token') is not None: + arguments['token'] = options['registration_token'] + elif options.get('group') is not None: + arguments['runner_type'] = 'group_type' + arguments['group_id'] = options['group'] + elif options.get('project') is not None: + arguments['runner_type'] = 'project_type' + arguments['project_id'] = options['project'] + else: + arguments['runner_type'] = 'instance_type' access_level_on_creation = self._module.params['access_level_on_creation'] - if access_level_on_creation is None: - message = "The option 'access_level_on_creation' is unspecified, so 'false' is assumed. "\ - "That means any value of 'access_level' is ignored and GitLab registers the runner with its default value. "\ - "The option 'access_level_on_creation' will switch to 'true' in community.general 7.0.0" - self._module.deprecate(message, version='7.0.0', collection_name='community.general') - access_level_on_creation = False - if not access_level_on_creation: arguments.pop('access_level', None) @@ -266,19 +283,17 @@ class GitLabRunner(object): changed = True else: changed, runner = self.update_runner(self.runner_object, arguments) + if changed: + if self._module.check_mode: + self._module.exit_json(changed=True, msg="Successfully updated the runner %s" % description) + + try: + runner.save() + except Exception as e: + self._module.fail_json(msg="Failed to update runner: %s " % to_native(e)) self.runner_object = runner - if changed: - if self._module.check_mode: - self._module.exit_json(changed=True, msg="Successfully created or updated the runner %s" % description) - - try: - runner.save() - except Exception as e: - self._module.fail_json(msg="Failed to update runner: %s " % to_native(e)) - return True - else: - return False + return changed ''' @param arguments Attributes of the runner @@ -288,7 +303,12 @@ class GitLabRunner(object): return True try: - runner = self._gitlab.runners.create(arguments) + if arguments.get('token') is not None: + runner = self._gitlab.runners.create(arguments) + elif LooseVersion(gitlab.__version__) < LooseVersion('4.0.0'): + self._module.fail_json(msg="New runner creation workflow requires python-gitlab 4.0.0 or higher") + else: + runner = self._gitlab.user.runners.create(arguments) except (gitlab.exceptions.GitlabCreateError) as e: self._module.fail_json(msg="Failed to create runner: %s " % to_native(e)) @@ -308,7 +328,7 @@ class GitLabRunner(object): list1.sort() list2 = arguments[arg_key] list2.sort() - if cmp(list1, list2): + if list1 != list2: setattr(runner, arg_key, arguments[arg_key]) changed = True else: @@ -322,7 +342,7 @@ class GitLabRunner(object): @param description Description of the runner ''' def find_runner(self, description): - runners = self._runners_endpoint(as_list=False) + runners = self._runners_endpoint(**list_all_kwargs) for runner in runners: # python-gitlab 2.2 through at least 2.5 returns a list of dicts for list() instead of a Runner @@ -361,12 +381,13 @@ def main(): argument_spec.update(dict( description=dict(type='str', required=True, aliases=["name"]), active=dict(type='bool', default=True), + paused=dict(type='bool', default=False), owned=dict(type='bool', default=False), tag_list=dict(type='list', elements='str', default=[]), run_untagged=dict(type='bool', default=True), locked=dict(type='bool', default=False), access_level=dict(type='str', choices=["not_protected", "ref_protected"]), - access_level_on_creation=dict(type='bool'), + access_level_on_creation=dict(type='bool', default=True), maximum_timeout=dict(type='int', default=3600), registration_token=dict(type='str', no_log=True), project=dict(type='str'), @@ -385,6 +406,7 @@ def main(): ['project', 'owned'], ['group', 'owned'], ['project', 'group'], + ['active', 'paused'], ], required_together=[ ['api_username', 'api_password'], @@ -392,12 +414,11 @@ def main(): required_one_of=[ ['api_username', 'api_token', 'api_oauth_token', 'api_job_token'], ], - required_if=[ - ('state', 'present', ['registration_token']), - ], supports_check_mode=True, ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) state = module.params['state'] runner_description = module.params['description'] @@ -411,16 +432,6 @@ def main(): project = module.params['project'] group = module.params['group'] - if access_level is None: - message = "The option 'access_level' is unspecified, so 'ref_protected' is assumed. "\ - "In order to align the module with GitLab's runner API, this option will lose "\ - "its default value in community.general 8.0.0. From that version on, you must set "\ - "this option to 'ref_protected' explicitly, if you want to have a protected runner, "\ - "otherwise GitLab's default access level gets applied, which is 'not_protected'" - module.deprecate(message, version='8.0.0', collection_name='community.general') - access_level = 'ref_protected' - - gitlab_instance = gitlab_authentication(module) gitlab_project = None gitlab_group = None @@ -454,6 +465,8 @@ def main(): "access_level": access_level, "maximum_timeout": maximum_timeout, "registration_token": registration_token, + "group": group, + "project": project, }): module.exit_json(changed=True, runner=gitlab_runner.runner_object._attrs, msg="Successfully created or updated the runner %s" % runner_description) diff --git a/ansible_collections/community/general/plugins/modules/gitlab_user.py b/ansible_collections/community/general/plugins/modules/gitlab_user.py index 94f371316..6e5ab4ece 100644 --- a/ansible_collections/community/general/plugins/modules/gitlab_user.py +++ b/ansible_collections/community/general/plugins/modules/gitlab_user.py @@ -27,7 +27,6 @@ author: - Lennert Mertens (@LennertMertens) - Stef Graces (@stgrace) requirements: - - python >= 2.7 - python-gitlab python module - administrator rights on the GitLab server extends_documentation_fragment: @@ -45,7 +44,7 @@ options: name: description: - Name of the user you want to create. - - Required only if C(state) is set to C(present). + - Required only if O(state=present). type: str username: description: @@ -66,7 +65,7 @@ options: email: description: - The email that belongs to the user. - - Required only if C(state) is set to C(present). + - Required only if O(state=present). type: str sshkey_name: description: @@ -123,7 +122,7 @@ options: identities: description: - List of identities to be added/updated for this user. - - To remove all other identities from this user, set I(overwrite_identities=true). + - To remove all other identities from this user, set O(overwrite_identities=true). type: list elements: dict suboptions: @@ -139,8 +138,8 @@ options: overwrite_identities: description: - Overwrite identities with identities added in this module. - - This means that all identities that the user has and that are not listed in I(identities) are removed from the user. - - This is only done if a list is provided for I(identities). To remove all identities, provide an empty list. + - This means that all identities that the user has and that are not listed in O(identities) are removed from the user. + - This is only done if a list is provided for O(identities). To remove all identities, provide an empty list. type: bool default: false version_added: 3.3.0 @@ -151,7 +150,6 @@ EXAMPLES = ''' community.general.gitlab_user: api_url: https://gitlab.example.com/ api_token: "{{ access_token }}" - validate_certs: false username: myusername state: absent @@ -191,7 +189,6 @@ EXAMPLES = ''' community.general.gitlab_user: api_url: https://gitlab.example.com/ api_token: "{{ access_token }}" - validate_certs: false username: myusername state: blocked @@ -199,7 +196,6 @@ EXAMPLES = ''' community.general.gitlab_user: api_url: https://gitlab.example.com/ api_token: "{{ access_token }}" - validate_certs: false username: myusername state: unblocked ''' @@ -234,7 +230,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.general.plugins.module_utils.gitlab import ( - auth_argument_spec, find_group, gitlab_authentication, gitlab, ensure_gitlab_package + auth_argument_spec, find_group, gitlab_authentication, gitlab, list_all_kwargs ) @@ -244,12 +240,12 @@ class GitLabUser(object): self._gitlab = gitlab_instance self.user_object = None self.ACCESS_LEVEL = { - 'guest': gitlab.GUEST_ACCESS, - 'reporter': gitlab.REPORTER_ACCESS, - 'developer': gitlab.DEVELOPER_ACCESS, - 'master': gitlab.MAINTAINER_ACCESS, - 'maintainer': gitlab.MAINTAINER_ACCESS, - 'owner': gitlab.OWNER_ACCESS, + 'guest': gitlab.const.GUEST_ACCESS, + 'reporter': gitlab.const.REPORTER_ACCESS, + 'developer': gitlab.const.DEVELOPER_ACCESS, + 'master': gitlab.const.MAINTAINER_ACCESS, + 'maintainer': gitlab.const.MAINTAINER_ACCESS, + 'owner': gitlab.const.OWNER_ACCESS, } ''' @@ -349,9 +345,10 @@ class GitLabUser(object): @param sshkey_name Name of the ssh key ''' def ssh_key_exists(self, user, sshkey_name): - keyList = map(lambda k: k.title, user.keys.list(all=True)) - - return sshkey_name in keyList + return any( + k.title == sshkey_name + for k in user.keys.list(**list_all_kwargs) + ) ''' @param user User object @@ -485,7 +482,7 @@ class GitLabUser(object): ''' @param user User object - @param identites List of identities to be added/updated + @param identities List of identities to be added/updated @param overwrite_identities Overwrite user identities with identities passed to this module ''' def add_identities(self, user, identities, overwrite_identities=False): @@ -504,7 +501,7 @@ class GitLabUser(object): ''' @param user User object - @param identites List of identities to be added/updated + @param identities List of identities to be added/updated ''' def delete_identities(self, user, identities): changed = False @@ -519,10 +516,13 @@ class GitLabUser(object): @param username Username of the user ''' def find_user(self, username): - users = self._gitlab.users.list(search=username, all=True) - for user in users: - if (user.username == username): - return user + return next( + ( + user for user in self._gitlab.users.list(search=username, **list_all_kwargs) + if user.username == username + ), + None + ) ''' @param username Username of the user @@ -616,7 +616,9 @@ def main(): ('state', 'present', ['name', 'email']), ) ) - ensure_gitlab_package(module) + + # check prerequisites and connect to gitlab server + gitlab_instance = gitlab_authentication(module) user_name = module.params['name'] state = module.params['state'] @@ -635,8 +637,6 @@ def main(): user_identities = module.params['identities'] overwrite_identities = module.params['overwrite_identities'] - gitlab_instance = gitlab_authentication(module) - gitlab_user = GitLabUser(module, gitlab_instance) user_exists = gitlab_user.exists_user(user_username) if user_exists: diff --git a/ansible_collections/community/general/plugins/modules/grove.py b/ansible_collections/community/general/plugins/modules/grove.py index b3e0508ff..b50546b4d 100644 --- a/ansible_collections/community/general/plugins/modules/grove.py +++ b/ansible_collections/community/general/plugins/modules/grove.py @@ -39,7 +39,7 @@ options: type: str description: - Message content. - - The alias I(message) is deprecated and will be removed in community.general 4.0.0. + - The alias O(ignore:message) has been removed in community.general 4.0.0. required: true url: type: str @@ -53,7 +53,7 @@ options: required: false validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be used + - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. default: true type: bool diff --git a/ansible_collections/community/general/plugins/modules/hana_query.py b/ansible_collections/community/general/plugins/modules/hana_query.py deleted file mode 100644 index 0b12e9935..000000000 --- a/ansible_collections/community/general/plugins/modules/hana_query.py +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright (c) 2021, Rainer Leber -# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) -# SPDX-License-Identifier: GPL-3.0-or-later -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -DOCUMENTATION = r''' ---- -module: hana_query -short_description: Execute SQL on HANA -version_added: 3.2.0 -description: This module executes SQL statements on HANA with hdbsql. -extends_documentation_fragment: - - community.general.attributes -attributes: - check_mode: - support: none - diff_mode: - support: none -options: - sid: - description: The system ID. - type: str - required: true - instance: - description: The instance number. - type: str - required: true - user: - description: A dedicated username. The user could be also in hdbuserstore. Defaults to C(SYSTEM). - type: str - default: SYSTEM - userstore: - description: If C(true) the user must be in hdbuserstore. - type: bool - default: false - version_added: 3.5.0 - password: - description: - - The password to connect to the database. - - "B(Note:) Since the passwords have to be passed as command line arguments, I(userstore=true) should - be used whenever possible, as command line arguments can be seen by other users - on the same machine." - type: str - autocommit: - description: Autocommit the statement. - type: bool - default: true - host: - description: The Host IP address. The port can be defined as well. - type: str - database: - description: Define the database on which to connect. - type: str - encrypted: - description: Use encrypted connection. Defaults to C(false). - type: bool - default: false - filepath: - description: - - One or more files each containing one SQL query to run. - - Must be a string or list containing strings. - type: list - elements: path - query: - description: - - SQL query to run. - - Must be a string or list containing strings. Please note that if you supply a string, it will be split by commas (C(,)) to a list. - It is better to supply a one-element list instead to avoid mangled input. - type: list - elements: str -author: - - Rainer Leber (@rainerleber) -''' - -EXAMPLES = r''' -- name: Simple select query - community.general.hana_query: - sid: "hdb" - instance: "01" - password: "Test123" - query: "select user_name from users" - -- name: Run several queries - community.general.hana_query: - sid: "hdb" - instance: "01" - password: "Test123" - query: - - "select user_name from users;" - - select * from SYSTEM; - host: "localhost" - autocommit: false - -- name: Run several queries from file - community.general.hana_query: - sid: "hdb" - instance: "01" - password: "Test123" - filepath: - - /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt - - /tmp/HANA.txt - host: "localhost" - -- name: Run several queries from user store - community.general.hana_query: - sid: "hdb" - instance: "01" - user: hdbstoreuser - userstore: true - query: - - "select user_name from users;" - - select * from users; - autocommit: false -''' - -RETURN = r''' -query_result: - description: List containing results of all queries executed (one sublist for every query). - returned: on success - type: list - elements: list - sample: [[{"Column": "Value1"}, {"Column": "Value2"}], [{"Column": "Value1"}, {"Column": "Value2"}]] -''' - -import csv -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.six import StringIO -from ansible.module_utils.common.text.converters import to_native - - -def csv_to_list(rawcsv): - reader_raw = csv.DictReader(StringIO(rawcsv)) - reader = [dict((k, v.strip()) for k, v in row.items()) for row in reader_raw] - return list(reader) - - -def main(): - module = AnsibleModule( - argument_spec=dict( - sid=dict(type='str', required=True), - instance=dict(type='str', required=True), - encrypted=dict(type='bool', default=False), - host=dict(type='str', required=False), - user=dict(type='str', default="SYSTEM"), - userstore=dict(type='bool', default=False), - password=dict(type='str', no_log=True), - database=dict(type='str', required=False), - query=dict(type='list', elements='str', required=False), - filepath=dict(type='list', elements='path', required=False), - autocommit=dict(type='bool', default=True), - ), - required_one_of=[('query', 'filepath')], - required_if=[('userstore', False, ['password'])], - supports_check_mode=False, - ) - rc, out, err, out_raw = [0, [], "", ""] - - params = module.params - - sid = (params['sid']).upper() - instance = params['instance'] - user = params['user'] - userstore = params['userstore'] - password = params['password'] - autocommit = params['autocommit'] - host = params['host'] - database = params['database'] - encrypted = params['encrypted'] - - filepath = params['filepath'] - query = params['query'] - - bin_path = "/usr/sap/{sid}/HDB{instance}/exe/hdbsql".format(sid=sid, instance=instance) - - try: - command = [module.get_bin_path(bin_path, required=True)] - except Exception as e: - module.fail_json(msg='Failed to find hdbsql at the expected path "{0}". Please check SID and instance number: "{1}"'.format(bin_path, to_native(e))) - - if encrypted is True: - command.extend(['-attemptencrypt']) - if autocommit is False: - command.extend(['-z']) - if host is not None: - command.extend(['-n', host]) - if database is not None: - command.extend(['-d', database]) - # -x Suppresses additional output, such as the number of selected rows in a result set. - if userstore: - command.extend(['-x', '-U', user]) - else: - command.extend(['-x', '-i', instance, '-u', user, '-p', password]) - - if filepath is not None: - command.extend(['-I']) - for p in filepath: - # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# -I /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt, - # iterates through files and append the output to var out. - query_command = command + [p] - (rc, out_raw, err) = module.run_command(query_command) - out.append(csv_to_list(out_raw)) - if query is not None: - for q in query: - # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# "select user_name from users", - # iterates through multiple commands and append the output to var out. - query_command = command + [q] - (rc, out_raw, err) = module.run_command(query_command) - out.append(csv_to_list(out_raw)) - changed = True - - module.exit_json(changed=changed, rc=rc, query_result=out, stderr=err) - - -if __name__ == '__main__': - main() diff --git a/ansible_collections/community/general/plugins/modules/haproxy.py b/ansible_collections/community/general/plugins/modules/haproxy.py index 56f987d80..05f52d55c 100644 --- a/ansible_collections/community/general/plugins/modules/haproxy.py +++ b/ansible_collections/community/general/plugins/modules/haproxy.py @@ -65,7 +65,7 @@ options: state: description: - Desired state of the provided backend host. - - Note that C(drain) state was added in version 2.4. + - Note that V(drain) state was added in version 2.4. - It is supported only by HAProxy version 1.5 or later, - When used on versions < 1.5, it will be ignored. type: str @@ -73,13 +73,13 @@ options: choices: [ disabled, drain, enabled ] agent: description: - - Disable/enable agent checks (depending on I(state) value). + - Disable/enable agent checks (depending on O(state) value). type: bool default: false version_added: 1.0.0 health: description: - - Disable/enable health checks (depending on I(state) value). + - Disable/enable health checks (depending on O(state) value). type: bool default: false version_added: "1.0.0" @@ -90,8 +90,8 @@ options: default: false wait: description: - - Wait until the server reports a status of C(UP) when I(state=enabled), - status of C(MAINT) when I(state=disabled) or status of C(DRAIN) when I(state=drain). + - Wait until the server reports a status of C(UP) when O(state=enabled), + status of C(MAINT) when O(state=disabled) or status of C(DRAIN) when O(state=drain). type: bool default: false wait_interval: @@ -107,7 +107,7 @@ options: weight: description: - The value passed in argument. - - If the value ends with the C(%) sign, then the new weight will be + - If the value ends with the V(%) sign, then the new weight will be relative to the initially configured weight. - Relative weights are only permitted between 0 and 100% and absolute weights are permitted between 0 and 256. diff --git a/ansible_collections/community/general/plugins/modules/heroku_collaborator.py b/ansible_collections/community/general/plugins/modules/heroku_collaborator.py index e7b0de3f9..e07ae333d 100644 --- a/ansible_collections/community/general/plugins/modules/heroku_collaborator.py +++ b/ansible_collections/community/general/plugins/modules/heroku_collaborator.py @@ -14,9 +14,9 @@ module: heroku_collaborator short_description: Add or delete app collaborators on Heroku description: - Manages collaborators for Heroku apps. - - If set to C(present) and heroku user is already collaborator, then do nothing. - - If set to C(present) and heroku user is not collaborator, then add user to app. - - If set to C(absent) and heroku user is collaborator, then delete user from app. + - If set to V(present) and heroku user is already collaborator, then do nothing. + - If set to V(present) and heroku user is not collaborator, then add user to app. + - If set to V(absent) and heroku user is collaborator, then delete user from app. author: - Marcel Arns (@marns93) requirements: @@ -56,8 +56,8 @@ options: choices: ["present", "absent"] default: "present" notes: - - C(HEROKU_API_KEY) and C(TF_VAR_HEROKU_API_KEY) env variable can be used instead setting C(api_key). - - If you use I(--check), you can also pass the I(-v) flag to see affected apps in C(msg), e.g. ["heroku-example-app"]. + - E(HEROKU_API_KEY) and E(TF_VAR_HEROKU_API_KEY) environment variables can be used instead setting O(api_key). + - If you use C(check_mode), you can also pass the C(-v) flag to see affected apps in C(msg), e.g. ["heroku-example-app"]. ''' EXAMPLES = ''' diff --git a/ansible_collections/community/general/plugins/modules/hg.py b/ansible_collections/community/general/plugins/modules/hg.py index dbbd504b4..4b6b7c433 100644 --- a/ansible_collections/community/general/plugins/modules/hg.py +++ b/ansible_collections/community/general/plugins/modules/hg.py @@ -43,8 +43,7 @@ options: type: str force: description: - - Discards uncommitted changes. Runs C(hg update -C). Prior to - 1.9, the default was C(true). + - Discards uncommitted changes. Runs C(hg update -C). type: bool default: false purge: @@ -54,12 +53,12 @@ options: default: false update: description: - - If C(false), do not retrieve new revisions from the origin repository + - If V(false), do not retrieve new revisions from the origin repository type: bool default: true clone: description: - - If C(false), do not clone the repository if it does not exist locally. + - If V(false), do not clone the repository if it does not exist locally. type: bool default: true executable: diff --git a/ansible_collections/community/general/plugins/modules/hipchat.py b/ansible_collections/community/general/plugins/modules/hipchat.py index 11b5fb735..83e253679 100644 --- a/ansible_collections/community/general/plugins/modules/hipchat.py +++ b/ansible_collections/community/general/plugins/modules/hipchat.py @@ -64,7 +64,7 @@ options: default: true validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be used + - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. type: bool default: true diff --git a/ansible_collections/community/general/plugins/modules/homebrew.py b/ansible_collections/community/general/plugins/modules/homebrew.py index 7592f95a4..5d471797a 100644 --- a/ansible_collections/community/general/plugins/modules/homebrew.py +++ b/ansible_collections/community/general/plugins/modules/homebrew.py @@ -42,9 +42,9 @@ options: elements: str path: description: - - "A C(:) separated list of paths to search for C(brew) executable. - Since a package (I(formula) in homebrew parlance) location is prefixed relative to the actual path of I(brew) command, - providing an alternative I(brew) path enables managing different set of packages in an alternative location in the system." + - "A V(:) separated list of paths to search for C(brew) executable. + Since a package (I(formula) in homebrew parlance) location is prefixed relative to the actual path of C(brew) command, + providing an alternative C(brew) path enables managing different set of packages in an alternative location in the system." default: '/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin' type: path state: @@ -78,7 +78,7 @@ options: version_added: '0.2.0' notes: - When used with a C(loop:) each package will be processed individually, - it is much more efficient to pass the list directly to the I(name) option. + it is much more efficient to pass the list directly to the O(name) option. ''' EXAMPLES = ''' @@ -87,7 +87,7 @@ EXAMPLES = ''' name: foo state: present -# Install formula foo with 'brew' in alternate path C(/my/other/location/bin) +# Install formula foo with 'brew' in alternate path (/my/other/location/bin) - community.general.homebrew: name: foo path: /my/other/location/bin @@ -165,6 +165,7 @@ changed_pkgs: version_added: '0.2.0' ''' +import json import os.path import re @@ -184,6 +185,10 @@ def _create_regex_group_complement(s): chars = filter(None, (line.split('#')[0].strip() for line in lines)) group = r'[^' + r''.join(chars) + r']' return re.compile(group) + + +def _check_package_in_json(json_output, package_type): + return bool(json_output.get(package_type, []) and json_output[package_type][0].get("installed")) # /utils ------------------------------------------------------------------ }}} @@ -479,17 +484,17 @@ class Homebrew(object): cmd = [ "{brew_path}".format(brew_path=self.brew_path), "info", + "--json=v2", self.current_package, ] rc, out, err = self.module.run_command(cmd) - for line in out.split('\n'): - if ( - re.search(r'Built from source', line) - or re.search(r'Poured from bottle', line) - ): - return True - - return False + if err: + self.failed = True + self.message = err.strip() + raise HomebrewException(self.message) + data = json.loads(out) + + return _check_package_in_json(data, "formulae") or _check_package_in_json(data, "casks") def _current_package_is_outdated(self): if not self.valid_package(self.current_package): diff --git a/ansible_collections/community/general/plugins/modules/homebrew_tap.py b/ansible_collections/community/general/plugins/modules/homebrew_tap.py index b230dbb34..151d09d32 100644 --- a/ansible_collections/community/general/plugins/modules/homebrew_tap.py +++ b/ansible_collections/community/general/plugins/modules/homebrew_tap.py @@ -42,7 +42,7 @@ options: - The optional git URL of the repository to tap. The URL is not assumed to be on GitHub, and the protocol doesn't have to be HTTP. Any location and protocol that git can handle is fine. - - I(name) option may not be a list of multiple taps (but a single + - O(name) option may not be a list of multiple taps (but a single tap instead) when this option is provided. required: false type: str @@ -55,7 +55,7 @@ options: type: str path: description: - - "A C(:) separated list of paths to search for C(brew) executable." + - "A V(:) separated list of paths to search for C(brew) executable." default: '/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin' type: path version_added: '2.1.0' diff --git a/ansible_collections/community/general/plugins/modules/homectl.py b/ansible_collections/community/general/plugins/modules/homectl.py index 301e388d3..ca4c19a87 100644 --- a/ansible_collections/community/general/plugins/modules/homectl.py +++ b/ansible_collections/community/general/plugins/modules/homectl.py @@ -37,7 +37,7 @@ options: - Homed requires this value to be in cleartext on user creation and updating a user. - The module takes the password and generates a password hash in SHA-512 with 10000 rounds of salt generation using crypt. - See U(https://systemd.io/USER_RECORD/). - - This is required for I(state=present). When an existing user is updated this is checked against the stored hash in homed. + - This is required for O(state=present). When an existing user is updated this is checked against the stored hash in homed. type: str state: description: @@ -55,11 +55,11 @@ options: disksize: description: - The intended home directory disk space. - - Human readable value such as C(10G), C(10M), or C(10B). + - Human readable value such as V(10G), V(10M), or V(10B). type: str resize: description: - - When used with I(disksize) this will attempt to resize the home directory immediately. + - When used with O(disksize) this will attempt to resize the home directory immediately. default: false type: bool realname: @@ -90,7 +90,7 @@ options: description: - Path to use as home directory for the user. - This is the directory the user's home directory is mounted to while the user is logged in. - - This is not where the user's data is actually stored, see I(imagepath) for that. + - This is not where the user's data is actually stored, see O(imagepath) for that. - Only used when a user is first created. type: path imagepath: @@ -102,25 +102,25 @@ options: uid: description: - Sets the UID of the user. - - If using I(gid) homed requires the value to be the same. + - If using O(gid) homed requires the value to be the same. - Only used when a user is first created. type: int gid: description: - Sets the gid of the user. - - If using I(uid) homed requires the value to be the same. + - If using O(uid) homed requires the value to be the same. - Only used when a user is first created. type: int mountopts: description: - String separated by comma each indicating mount options for a users home directory. - - Valid options are C(nosuid), C(nodev) or C(noexec). - - Homed by default uses C(nodev) and C(nosuid) while C(noexec) is off. + - Valid options are V(nosuid), V(nodev) or V(noexec). + - Homed by default uses V(nodev) and V(nosuid) while V(noexec) is off. type: str umask: description: - Sets the umask for the user's login sessions - - Value from C(0000) to C(0777). + - Value from V(0000) to V(0777). type: int memberof: description: @@ -132,13 +132,13 @@ options: description: - The absolute path to the skeleton directory to populate a new home directory from. - This is only used when a home directory is first created. - - If not specified homed by default uses C(/etc/skel). + - If not specified homed by default uses V(/etc/skel). aliases: [ 'skel' ] type: path shell: description: - Shell binary to use for terminal logins of given user. - - If not specified homed by default uses C(/bin/bash). + - If not specified homed by default uses V(/bin/bash). type: str environment: description: @@ -151,7 +151,7 @@ options: timezone: description: - Preferred timezone to use for the user. - - Should be a tzdata compatible location string such as C(America/New_York). + - Should be a tzdata compatible location string such as V(America/New_York). type: str locked: description: @@ -160,7 +160,7 @@ options: language: description: - The preferred language/locale for the user. - - This should be in a format compatible with the C($LANG) environment variable. + - This should be in a format compatible with the E(LANG) environment variable. type: str passwordhint: description: @@ -393,7 +393,7 @@ class Homectl(object): user_metadata.pop('status', None) # Let last change Usec be updated by homed when command runs. user_metadata.pop('lastChangeUSec', None) - # Now only change fields that are called on leaving whats currently in the record intact. + # Now only change fields that are called on leaving what's currently in the record intact. record = user_metadata record['userName'] = self.name @@ -439,7 +439,7 @@ class Homectl(object): self.result['changed'] = True if self.disksize: - # convert humand readble to bytes + # convert human readable to bytes if self.disksize != record.get('diskSize'): record['diskSize'] = human_to_bytes(self.disksize) self.result['changed'] = True diff --git a/ansible_collections/community/general/plugins/modules/honeybadger_deployment.py b/ansible_collections/community/general/plugins/modules/honeybadger_deployment.py index 820e4538e..cf52745ac 100644 --- a/ansible_collections/community/general/plugins/modules/honeybadger_deployment.py +++ b/ansible_collections/community/general/plugins/modules/honeybadger_deployment.py @@ -52,7 +52,7 @@ options: default: "https://api.honeybadger.io/v1/deploys" validate_certs: description: - - If C(false), SSL certificates for the target url will not be validated. This should only be used + - If V(false), SSL certificates for the target url will not be validated. This should only be used on personally controlled sites using self-signed certificates. type: bool default: true diff --git a/ansible_collections/community/general/plugins/modules/hpilo_info.py b/ansible_collections/community/general/plugins/modules/hpilo_info.py index cef6597e4..d329764b4 100644 --- a/ansible_collections/community/general/plugins/modules/hpilo_info.py +++ b/ansible_collections/community/general/plugins/modules/hpilo_info.py @@ -19,8 +19,6 @@ description: These information includes hardware and network related information useful for provisioning (e.g. macaddress, uuid). - This module requires the C(hpilo) python module. -- This module was called C(hpilo_facts) before Ansible 2.9, returning C(ansible_facts). - Note that the M(community.general.hpilo_info) module no longer returns C(ansible_facts)! extends_documentation_fragment: - community.general.attributes - community.general.attributes.info_module @@ -125,7 +123,7 @@ hw_uuid: host_power_status: description: - Power status of host. - - Will be one of C(ON), C(OFF) and C(UNKNOWN). + - Will be one of V(ON), V(OFF) and V(UNKNOWN). returned: always type: str sample: "ON" diff --git a/ansible_collections/community/general/plugins/modules/htpasswd.py b/ansible_collections/community/general/plugins/modules/htpasswd.py index 180b02073..9633ce2fb 100644 --- a/ansible_collections/community/general/plugins/modules/htpasswd.py +++ b/ansible_collections/community/general/plugins/modules/htpasswd.py @@ -26,51 +26,53 @@ options: required: true aliases: [ dest, destfile ] description: - - Path to the file that contains the usernames and passwords + - Path to the file that contains the usernames and passwords. name: type: str required: true aliases: [ username ] description: - - User name to add or remove + - User name to add or remove. password: type: str required: false description: - Password associated with user. - Must be specified if user does not exist yet. - crypt_scheme: + hash_scheme: type: str required: false default: "apr_md5_crypt" description: - - Encryption scheme to be used. As well as the four choices listed + - Hashing scheme to be used. As well as the four choices listed here, you can also use any other hash supported by passlib, such as - C(portable_apache22) and C(host_apache24); or C(md5_crypt) and C(sha256_crypt), - which are Linux passwd hashes. Only some schemes in addition to + V(portable_apache22) and V(host_apache24); or V(md5_crypt) and V(sha256_crypt), + which are Linux passwd hashes. Only some schemes in addition to the four choices below will be compatible with Apache or Nginx, and supported schemes depend on passlib version and its dependencies. - See U(https://passlib.readthedocs.io/en/stable/lib/passlib.apache.html#passlib.apache.HtpasswdFile) parameter C(default_scheme). - - 'Some of the available choices might be: C(apr_md5_crypt), C(des_crypt), C(ldap_sha1), C(plaintext).' + - 'Some of the available choices might be: V(apr_md5_crypt), V(des_crypt), V(ldap_sha1), V(plaintext).' + aliases: [crypt_scheme] state: type: str required: false choices: [ present, absent ] default: "present" description: - - Whether the user entry should be present or not + - Whether the user entry should be present or not. create: required: false type: bool default: true description: - - Used with I(state=present). If specified, the file will be created - if it does not already exist. If set to C(false), will fail if the - file does not exist + - Used with O(state=present). If V(true), the file will be created + if it does not exist. Conversely, if set to V(false) and the file + does not exist it will fail. notes: - - "This module depends on the I(passlib) Python library, which needs to be installed on all target systems." - - "On Debian, Ubuntu, or Fedora: install I(python-passlib)." - - "On RHEL or CentOS: Enable EPEL, then install I(python-passlib)." + - "This module depends on the C(passlib) Python library, which needs to be installed on all target systems." + - "On Debian < 11, Ubuntu <= 20.04, or Fedora: install C(python-passlib)." + - "On Debian, Ubuntu: install C(python3-passlib)." + - "On RHEL or CentOS: Enable EPEL, then install C(python-passlib)." requirements: [ passlib>=1.6 ] author: "Ansible Core Team" extends_documentation_fragment: @@ -99,28 +101,22 @@ EXAMPLES = """ path: /etc/mail/passwords name: alex password: oedu2eGh - crypt_scheme: md5_crypt + hash_scheme: md5_crypt """ import os import tempfile -import traceback -from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils import deps from ansible.module_utils.common.text.converters import to_native -from ansible_collections.community.general.plugins.module_utils.version import LooseVersion -PASSLIB_IMP_ERR = None -try: +with deps.declare("passlib"): from passlib.apache import HtpasswdFile, htpasswd_context from passlib.context import CryptContext - import passlib -except ImportError: - PASSLIB_IMP_ERR = traceback.format_exc() - passlib_installed = False -else: - passlib_installed = True + apache_hashes = ["apr_md5_crypt", "des_crypt", "ldap_sha1", "plaintext"] @@ -131,50 +127,34 @@ def create_missing_directories(dest): os.makedirs(destpath) -def present(dest, username, password, crypt_scheme, create, check_mode): +def present(dest, username, password, hash_scheme, create, check_mode): """ Ensures user is present Returns (msg, changed) """ - if crypt_scheme in apache_hashes: + if hash_scheme in apache_hashes: context = htpasswd_context else: - context = CryptContext(schemes=[crypt_scheme] + apache_hashes) + context = CryptContext(schemes=[hash_scheme] + apache_hashes) if not os.path.exists(dest): if not create: raise ValueError('Destination %s does not exist' % dest) if check_mode: return ("Create %s" % dest, True) create_missing_directories(dest) - if LooseVersion(passlib.__version__) >= LooseVersion('1.6'): - ht = HtpasswdFile(dest, new=True, default_scheme=crypt_scheme, context=context) - else: - ht = HtpasswdFile(dest, autoload=False, default=crypt_scheme, context=context) - if getattr(ht, 'set_password', None): - ht.set_password(username, password) - else: - ht.update(username, password) + ht = HtpasswdFile(dest, new=True, default_scheme=hash_scheme, context=context) + ht.set_password(username, password) ht.save() return ("Created %s and added %s" % (dest, username), True) else: - if LooseVersion(passlib.__version__) >= LooseVersion('1.6'): - ht = HtpasswdFile(dest, new=False, default_scheme=crypt_scheme, context=context) - else: - ht = HtpasswdFile(dest, default=crypt_scheme, context=context) + ht = HtpasswdFile(dest, new=False, default_scheme=hash_scheme, context=context) - found = None - if getattr(ht, 'check_password', None): - found = ht.check_password(username, password) - else: - found = ht.verify(username, password) + found = ht.check_password(username, password) if found: return ("%s already present" % username, False) else: if not check_mode: - if getattr(ht, 'set_password', None): - ht.set_password(username, password) - else: - ht.update(username, password) + ht.set_password(username, password) ht.save() return ("Add/update %s" % username, True) @@ -183,10 +163,7 @@ def absent(dest, username, check_mode): """ Ensures user is absent Returns (msg, changed) """ - if LooseVersion(passlib.__version__) >= LooseVersion('1.6'): - ht = HtpasswdFile(dest, new=False) - else: - ht = HtpasswdFile(dest) + ht = HtpasswdFile(dest, new=False) if username not in ht.users(): return ("%s not present" % username, False) @@ -215,7 +192,7 @@ def main(): path=dict(type='path', required=True, aliases=["dest", "destfile"]), name=dict(type='str', required=True, aliases=["username"]), password=dict(type='str', required=False, default=None, no_log=True), - crypt_scheme=dict(type='str', required=False, default="apr_md5_crypt"), + hash_scheme=dict(type='str', required=False, default="apr_md5_crypt", aliases=["crypt_scheme"]), state=dict(type='str', required=False, default="present", choices=["present", "absent"]), create=dict(type='bool', default=True), @@ -227,25 +204,18 @@ def main(): path = module.params['path'] username = module.params['name'] password = module.params['password'] - crypt_scheme = module.params['crypt_scheme'] + hash_scheme = module.params['hash_scheme'] state = module.params['state'] create = module.params['create'] check_mode = module.check_mode - if not passlib_installed: - module.fail_json(msg=missing_required_lib("passlib"), exception=PASSLIB_IMP_ERR) + deps.validate(module) + # TODO double check if this hack below is still needed. # Check file for blank lines in effort to avoid "need more than 1 value to unpack" error. try: - f = open(path, "r") - except IOError: - # No preexisting file to remove blank lines from - f = None - else: - try: + with open(path, "r") as f: lines = f.readlines() - finally: - f.close() # If the file gets edited, it returns true, so only edit the file if it has blank lines strip = False @@ -259,15 +229,16 @@ def main(): if check_mode: temp = tempfile.NamedTemporaryFile() path = temp.name - f = open(path, "w") - try: - [f.write(line) for line in lines if line.strip()] - finally: - f.close() + with open(path, "w") as f: + f.writelines(line for line in lines if line.strip()) + + except IOError: + # No preexisting file to remove blank lines from + pass try: if state == 'present': - (msg, changed) = present(path, username, password, crypt_scheme, create, check_mode) + (msg, changed) = present(path, username, password, hash_scheme, create, check_mode) elif state == 'absent': if not os.path.exists(path): module.exit_json(msg="%s not present" % username, diff --git a/ansible_collections/community/general/plugins/modules/hwc_ecs_instance.py b/ansible_collections/community/general/plugins/modules/hwc_ecs_instance.py index 434db242f..9ba95dc96 100644 --- a/ansible_collections/community/general/plugins/modules/hwc_ecs_instance.py +++ b/ansible_collections/community/general/plugins/modules/hwc_ecs_instance.py @@ -73,8 +73,8 @@ options: name: description: - Specifies the ECS name. Value requirements consists of 1 to 64 - characters, including letters, digits, underscores C(_), hyphens - (-), periods (.). + characters, including letters, digits, underscores (V(_)), hyphens + (V(-)), periods (V(.)). type: str required: true nics: @@ -306,8 +306,8 @@ RETURN = ''' name: description: - Specifies the ECS name. Value requirements "Consists of 1 to 64 - characters, including letters, digits, underscores C(_), hyphens - (-), periods (.)". + characters, including letters, digits, underscores (V(_)), hyphens + (V(-)), periods (V(.)).". type: str returned: success nics: diff --git a/ansible_collections/community/general/plugins/modules/hwc_smn_topic.py b/ansible_collections/community/general/plugins/modules/hwc_smn_topic.py index 88207d3f9..bb983fba7 100644 --- a/ansible_collections/community/general/plugins/modules/hwc_smn_topic.py +++ b/ansible_collections/community/general/plugins/modules/hwc_smn_topic.py @@ -45,7 +45,7 @@ options: description: - Name of the topic to be created. The topic name is a string of 1 to 256 characters. It must contain upper- or lower-case letters, - digits, hyphens (-), and underscores C(_), and must start with a + digits, hyphens (V(-)), and underscores (V(_)), and must start with a letter or digit. type: str required: true @@ -85,7 +85,7 @@ name: description: - Name of the topic to be created. The topic name is a string of 1 to 256 characters. It must contain upper- or lower-case letters, - digits, hyphens (-), and underscores C(_), and must start with a + digits, hyphens (V(-)), and underscores (V(_)), and must start with a letter or digit. returned: success type: str diff --git a/ansible_collections/community/general/plugins/modules/hwc_vpc_eip.py b/ansible_collections/community/general/plugins/modules/hwc_vpc_eip.py index 9fc0361b3..5c4431940 100644 --- a/ansible_collections/community/general/plugins/modules/hwc_vpc_eip.py +++ b/ansible_collections/community/general/plugins/modules/hwc_vpc_eip.py @@ -75,7 +75,7 @@ options: description: - Specifies the bandwidth name. The value is a string of 1 to 64 characters that can contain letters, digits, - underscores C(_), hyphens (-), and periods (.). + underscores (V(_)), hyphens (V(-)), and periods (V(.)). type: str required: true size: @@ -187,7 +187,7 @@ RETURN = ''' description: - Specifies the bandwidth name. The value is a string of 1 to 64 characters that can contain letters, digits, - underscores C(_), hyphens (-), and periods (.). + underscores (V(_)), hyphens (V(-)), and periods (V(.)). type: str returned: success size: diff --git a/ansible_collections/community/general/plugins/modules/hwc_vpc_private_ip.py b/ansible_collections/community/general/plugins/modules/hwc_vpc_private_ip.py index c57ddc670..95e759f6f 100644 --- a/ansible_collections/community/general/plugins/modules/hwc_vpc_private_ip.py +++ b/ansible_collections/community/general/plugins/modules/hwc_vpc_private_ip.py @@ -19,8 +19,8 @@ description: - vpc private ip management. short_description: Creates a resource of Vpc/PrivateIP in Huawei Cloud notes: - - If I(id) option is provided, it takes precedence over I(subnet_id), I(ip_address) for private ip selection. - - I(subnet_id), I(ip_address) are used for private ip selection. If more than one private ip with this options exists, execution is aborted. + - If O(id) option is provided, it takes precedence over O(subnet_id), O(ip_address) for private ip selection. + - O(subnet_id), O(ip_address) are used for private ip selection. If more than one private ip with this options exists, execution is aborted. - No parameter support updating. If one of option is changed, the module will create a new resource. version_added: '0.2.0' author: Huawei Inc. (@huaweicloud) diff --git a/ansible_collections/community/general/plugins/modules/hwc_vpc_route.py b/ansible_collections/community/general/plugins/modules/hwc_vpc_route.py index 1612cac50..091b49b0c 100644 --- a/ansible_collections/community/general/plugins/modules/hwc_vpc_route.py +++ b/ansible_collections/community/general/plugins/modules/hwc_vpc_route.py @@ -19,8 +19,8 @@ description: - vpc route management. short_description: Creates a resource of Vpc/Route in Huawei Cloud notes: - - If I(id) option is provided, it takes precedence over I(destination), I(vpc_id), I(type) and I(next_hop) for route selection. - - I(destination), I(vpc_id), I(type) and I(next_hop) are used for route selection. If more than one route with this options exists, execution is aborted. + - If O(id) option is provided, it takes precedence over O(destination), O(vpc_id), O(type), and O(next_hop) for route selection. + - O(destination), O(vpc_id), O(type) and O(next_hop) are used for route selection. If more than one route with this options exists, execution is aborted. - No parameter support updating. If one of option is changed, the module will create a new resource. version_added: '0.2.0' author: Huawei Inc. (@huaweicloud) diff --git a/ansible_collections/community/general/plugins/modules/hwc_vpc_security_group.py b/ansible_collections/community/general/plugins/modules/hwc_vpc_security_group.py index c210b912d..aa65e801c 100644 --- a/ansible_collections/community/general/plugins/modules/hwc_vpc_security_group.py +++ b/ansible_collections/community/general/plugins/modules/hwc_vpc_security_group.py @@ -19,9 +19,9 @@ description: - vpc security group management. short_description: Creates a resource of Vpc/SecurityGroup in Huawei Cloud notes: - - If I(id) option is provided, it takes precedence over I(name), - I(enterprise_project_id) and I(vpc_id) for security group selection. - - I(name), I(enterprise_project_id) and I(vpc_id) are used for security + - If O(id) option is provided, it takes precedence over O(name), + O(enterprise_project_id), and O(vpc_id) for security group selection. + - O(name), O(enterprise_project_id) and O(vpc_id) are used for security group selection. If more than one security group with this options exists, execution is aborted. - No parameter support updating. If one of option is changed, the module @@ -45,8 +45,8 @@ options: name: description: - Specifies the security group name. The value is a string of 1 to - 64 characters that can contain letters, digits, underscores C(_), - hyphens (-), and periods (.). + 64 characters that can contain letters, digits, underscores (V(_)), + hyphens (V(-)), and periods (V(.)). type: str required: true enterprise_project_id: @@ -79,8 +79,8 @@ RETURN = ''' name: description: - Specifies the security group name. The value is a string of 1 to - 64 characters that can contain letters, digits, underscores C(_), - hyphens (-), and periods (.). + 64 characters that can contain letters, digits, underscores (V(_)), + hyphens (V(-)), and periods (V(.)). type: str returned: success enterprise_project_id: diff --git a/ansible_collections/community/general/plugins/modules/hwc_vpc_security_group_rule.py b/ansible_collections/community/general/plugins/modules/hwc_vpc_security_group_rule.py index bfb5d6a61..899647e8c 100644 --- a/ansible_collections/community/general/plugins/modules/hwc_vpc_security_group_rule.py +++ b/ansible_collections/community/general/plugins/modules/hwc_vpc_security_group_rule.py @@ -19,9 +19,9 @@ description: - vpc security group management. short_description: Creates a resource of Vpc/SecurityGroupRule in Huawei Cloud notes: - - If I(id) option is provided, it takes precedence over - I(enterprise_project_id) for security group rule selection. - - I(security_group_id) is used for security group rule selection. If more + - If O(id) option is provided, it takes precedence over + O(security_group_id) for security group rule selection. + - O(security_group_id) is used for security group rule selection. If more than one security group rule with this options exists, execution is aborted. - No parameter support updating. If one of option is changed, the module diff --git a/ansible_collections/community/general/plugins/modules/hwc_vpc_subnet.py b/ansible_collections/community/general/plugins/modules/hwc_vpc_subnet.py index 7fb107f53..7ba747330 100644 --- a/ansible_collections/community/general/plugins/modules/hwc_vpc_subnet.py +++ b/ansible_collections/community/general/plugins/modules/hwc_vpc_subnet.py @@ -66,8 +66,8 @@ options: name: description: - Specifies the subnet name. The value is a string of 1 to 64 - characters that can contain letters, digits, underscores C(_), - hyphens (-), and periods (.). + characters that can contain letters, digits, underscores (V(_)), + hyphens (V(-)), and periods (V(.)). type: str required: true vpc_id: @@ -137,8 +137,8 @@ RETURN = ''' name: description: - Specifies the subnet name. The value is a string of 1 to 64 - characters that can contain letters, digits, underscores C(_), - hyphens (-), and periods (.). + characters that can contain letters, digits, underscores (V(_)), + hyphens (V(-)), and periods (V(.)). type: str returned: success vpc_id: diff --git a/ansible_collections/community/general/plugins/modules/icinga2_feature.py b/ansible_collections/community/general/plugins/modules/icinga2_feature.py index 6e6bc5416..0c79f6cba 100644 --- a/ansible_collections/community/general/plugins/modules/icinga2_feature.py +++ b/ansible_collections/community/general/plugins/modules/icinga2_feature.py @@ -37,10 +37,10 @@ options: state: type: str description: - - If set to C(present) and feature is disabled, then feature is enabled. - - If set to C(present) and feature is already enabled, then nothing is changed. - - If set to C(absent) and feature is enabled, then feature is disabled. - - If set to C(absent) and feature is already disabled, then nothing is changed. + - If set to V(present) and feature is disabled, then feature is enabled. + - If set to V(present) and feature is already enabled, then nothing is changed. + - If set to V(absent) and feature is enabled, then feature is disabled. + - If set to V(absent) and feature is already disabled, then nothing is changed. choices: [ "present", "absent" ] default: present ''' diff --git a/ansible_collections/community/general/plugins/modules/icinga2_host.py b/ansible_collections/community/general/plugins/modules/icinga2_host.py index 7f25c55d9..ec04d8df7 100644 --- a/ansible_collections/community/general/plugins/modules/icinga2_host.py +++ b/ansible_collections/community/general/plugins/modules/icinga2_host.py @@ -31,13 +31,13 @@ options: - HTTP, HTTPS, or FTP URL in the form (http|https|ftp)://[user[:pass]]@host.domain[:port]/path use_proxy: description: - - If C(false), it will not use a proxy, even if one is defined in + - If V(false), it will not use a proxy, even if one is defined in an environment variable on the target hosts. type: bool default: true validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be used + - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. type: bool default: true @@ -45,12 +45,12 @@ options: type: str description: - The username for use in HTTP basic authentication. - - This parameter can be used without C(url_password) for sites that allow empty passwords. + - This parameter can be used without O(url_password) for sites that allow empty passwords. url_password: type: str description: - The password for use in HTTP basic authentication. - - If the C(url_username) parameter is not specified, the C(url_password) parameter will not be used. + - If the O(url_username) parameter is not specified, the O(url_password) parameter will not be used. force_basic_auth: description: - httplib2, the library used by the uri module only sends authentication information when a webservice @@ -64,12 +64,12 @@ options: description: - PEM formatted certificate chain file to be used for SSL client authentication. This file can also include the key as well, and if - the key is included, C(client_key) is not required. + the key is included, O(client_key) is not required. client_key: type: path description: - PEM formatted file that contains your private key to be used for SSL - client authentication. If C(client_cert) contains both the certificate + client authentication. If O(client_cert) contains both the certificate and key, this option is not required. state: type: str @@ -101,12 +101,12 @@ options: type: str description: - The name used to display the host. - - If not specified, it defaults to the value of the I(name) parameter. + - If not specified, it defaults to the value of the O(name) parameter. ip: type: str description: - The IP address of the host. - required: true + - This is no longer required since community.general 8.0.0. variables: type: dict description: @@ -243,7 +243,7 @@ def main(): template=dict(default=None), check_command=dict(default="hostalive"), display_name=dict(default=None), - ip=dict(required=True), + ip=dict(), variables=dict(type='dict', default=None), ) @@ -306,7 +306,7 @@ def main(): module.exit_json(changed=False, name=name, data=data) # Template attribute is not allowed in modification - del data['attrs']['templates'] + del data['templates'] ret = icinga.modify(name, data) diff --git a/ansible_collections/community/general/plugins/modules/idrac_redfish_config.py b/ansible_collections/community/general/plugins/modules/idrac_redfish_config.py index cc47e62d2..0388bf00f 100644 --- a/ansible_collections/community/general/plugins/modules/idrac_redfish_config.py +++ b/ansible_collections/community/general/plugins/modules/idrac_redfish_config.py @@ -33,9 +33,9 @@ options: required: true description: - List of commands to execute on iDRAC. - - I(SetManagerAttributes), I(SetLifecycleControllerAttributes) and - I(SetSystemAttributes) are mutually exclusive commands when C(category) - is I(Manager). + - V(SetManagerAttributes), V(SetLifecycleControllerAttributes) and + V(SetSystemAttributes) are mutually exclusive commands when O(category) + is V(Manager). type: list elements: str baseuri: diff --git a/ansible_collections/community/general/plugins/modules/idrac_redfish_info.py b/ansible_collections/community/general/plugins/modules/idrac_redfish_info.py index aece61664..90b355d13 100644 --- a/ansible_collections/community/general/plugins/modules/idrac_redfish_info.py +++ b/ansible_collections/community/general/plugins/modules/idrac_redfish_info.py @@ -16,8 +16,6 @@ description: - Builds Redfish URIs locally and sends them to remote iDRAC controllers to get information back. - For use with Dell EMC iDRAC operations that require Redfish OEM extensions. - - This module was called C(idrac_redfish_facts) before Ansible 2.9, returning C(ansible_facts). - Note that the M(community.general.idrac_redfish_info) module no longer returns C(ansible_facts)! extends_documentation_fragment: - community.general.attributes - community.general.attributes.info_module @@ -35,7 +33,7 @@ options: required: true description: - List of commands to execute on iDRAC. - - C(GetManagerAttributes) returns the list of dicts containing iDRAC, + - V(GetManagerAttributes) returns the list of dicts containing iDRAC, LifecycleController and System attributes. type: list elements: str diff --git a/ansible_collections/community/general/plugins/modules/ilo_redfish_command.py b/ansible_collections/community/general/plugins/modules/ilo_redfish_command.py index 0ec385e73..e0e28f855 100644 --- a/ansible_collections/community/general/plugins/modules/ilo_redfish_command.py +++ b/ansible_collections/community/general/plugins/modules/ilo_redfish_command.py @@ -84,7 +84,7 @@ ilo_redfish_command: type: dict contains: ret: - description: Return True/False based on whether the operation was performed succesfully. + description: Return True/False based on whether the operation was performed successfully. type: bool msg: description: Status of the operation performed on the iLO. diff --git a/ansible_collections/community/general/plugins/modules/imc_rest.py b/ansible_collections/community/general/plugins/modules/imc_rest.py index 4bbaad23a..113d341e8 100644 --- a/ansible_collections/community/general/plugins/modules/imc_rest.py +++ b/ansible_collections/community/general/plugins/modules/imc_rest.py @@ -51,16 +51,16 @@ options: description: - Name of the absolute path of the filename that includes the body of the http request being sent to the Cisco IMC REST API. - - Parameter C(path) is mutual exclusive with parameter C(content). + - Parameter O(path) is mutual exclusive with parameter O(content). aliases: [ 'src', 'config_file' ] type: path content: description: - - When used instead of C(path), sets the content of the API requests directly. + - When used instead of O(path), sets the content of the API requests directly. - This may be convenient to template simple requests, for anything complex use the M(ansible.builtin.template) module. - You can collate multiple IMC XML fragments and they will be processed sequentially in a single stream, the Cisco IMC output is subsequently merged. - - Parameter C(content) is mutual exclusive with parameter C(path). + - Parameter O(content) is mutual exclusive with parameter O(path). type: str protocol: description: @@ -72,14 +72,14 @@ options: description: - The socket level timeout in seconds. - This is the time that every single connection (every fragment) can spend. - If this C(timeout) is reached, the module will fail with a + If this O(timeout) is reached, the module will fail with a C(Connection failure) indicating that C(The read operation timed out). default: 60 type: int validate_certs: description: - - If C(false), SSL certificates will not be validated. - - This should only set to C(false) used on personally controlled sites using self-signed certificates. + - If V(false), SSL certificates will not be validated. + - This should only set to V(false) used on personally controlled sites using self-signed certificates. type: bool default: true notes: @@ -88,7 +88,7 @@ notes: - Any configConfMo change requested has a return status of 'modified', even if there was no actual change from the previous configuration. As a result, this module will always report a change on subsequent runs. In case this behaviour is fixed in a future update to Cisco IMC, this module will automatically adapt. -- If you get a C(Connection failure) related to C(The read operation timed out) increase the C(timeout) +- If you get a C(Connection failure) related to C(The read operation timed out) increase the O(timeout) parameter. Some XML fragments can take longer than the default timeout. - More information about the IMC REST API is available from U(http://www.cisco.com/c/en/us/td/docs/unified_computing/ucs/c/sw/api/3_0/b_Cisco_IMC_api_301.html) @@ -100,7 +100,7 @@ EXAMPLES = r''' hostname: '{{ imc_hostname }}' username: '{{ imc_username }}' password: '{{ imc_password }}' - validate_certs: false + validate_certs: false # only do this when you trust the network! content: | @@ -112,7 +112,7 @@ EXAMPLES = r''' hostname: '{{ imc_hostname }}' username: '{{ imc_username }}' password: '{{ imc_password }}' - validate_certs: false + validate_certs: false # only do this when you trust the network! timeout: 120 content: | @@ -137,7 +137,7 @@ EXAMPLES = r''' hostname: '{{ imc_hostname }}' username: '{{ imc_username }}' password: '{{ imc_password }}' - validate_certs: false + validate_certs: false # only do this when you trust the network! content: | @@ -155,7 +155,7 @@ EXAMPLES = r''' hostname: '{{ imc_host }}' username: '{{ imc_username }}' password: '{{ imc_password }}' - validate_certs: false + validate_certs: false # only do this when you trust the network! content: | @@ -167,7 +167,7 @@ EXAMPLES = r''' hostname: '{{ imc_host }}' username: '{{ imc_username }}' password: '{{ imc_password }}' - validate_certs: false + validate_certs: false # only do this when you trust the network! content: | @@ -179,7 +179,7 @@ EXAMPLES = r''' hostname: '{{ imc_host }}' username: '{{ imc_username }}' password: '{{ imc_password }}' - validate_certs: false + validate_certs: false # only do this when you trust the network! timeout: 120 content: | diff --git a/ansible_collections/community/general/plugins/modules/imgadm.py b/ansible_collections/community/general/plugins/modules/imgadm.py index 6e4b81098..a247547fc 100644 --- a/ansible_collections/community/general/plugins/modules/imgadm.py +++ b/ansible_collections/community/general/plugins/modules/imgadm.py @@ -44,9 +44,9 @@ options: required: true choices: [ present, absent, deleted, imported, updated, vacuumed ] description: - - State the object operated on should be in. C(imported) is an alias for - for C(present) and C(deleted) for C(absent). When set to C(vacuumed) - and C(uuid) to C(*), it will remove all unused images. + - State the object operated on should be in. V(imported) is an alias for + for V(present) and V(deleted) for V(absent). When set to V(vacuumed) + and O(uuid=*), it will remove all unused images. type: str type: @@ -60,11 +60,8 @@ options: uuid: required: false description: - - Image UUID. Can either be a full UUID or C(*) for all images. + - Image UUID. Can either be a full UUID or V(*) for all images. type: str - -requirements: - - python >= 2.6 ''' EXAMPLES = ''' @@ -142,7 +139,7 @@ class Imgadm(object): self.uuid = module.params['uuid'] # Since there are a number of (natural) aliases, prevent having to look - # them up everytime we operate on `state`. + # them up every time we operate on `state`. if self.params['state'] in ['present', 'imported', 'updated']: self.present = True else: @@ -174,7 +171,7 @@ class Imgadm(object): # There is no feedback from imgadm(1M) to determine if anything # was actually changed. So treat this as an 'always-changes' operation. - # Note that 'imgadm -v' produces unparseable JSON... + # Note that 'imgadm -v' produces unparsable JSON... self.changed = True def manage_sources(self): diff --git a/ansible_collections/community/general/plugins/modules/influxdb_database.py b/ansible_collections/community/general/plugins/modules/influxdb_database.py index 046b16e18..a12326da5 100644 --- a/ansible_collections/community/general/plugins/modules/influxdb_database.py +++ b/ansible_collections/community/general/plugins/modules/influxdb_database.py @@ -17,7 +17,6 @@ description: - Manage InfluxDB databases. author: "Kamil Szczygiel (@kamsz)" requirements: - - "python >= 2.6" - "influxdb >= 0.9" - requests attributes: diff --git a/ansible_collections/community/general/plugins/modules/influxdb_query.py b/ansible_collections/community/general/plugins/modules/influxdb_query.py index c2e3d8acc..fda98d184 100644 --- a/ansible_collections/community/general/plugins/modules/influxdb_query.py +++ b/ansible_collections/community/general/plugins/modules/influxdb_query.py @@ -16,7 +16,6 @@ description: - Query data points from InfluxDB. author: "René Moser (@resmo)" requirements: - - "python >= 2.6" - "influxdb >= 0.9" attributes: check_mode: diff --git a/ansible_collections/community/general/plugins/modules/influxdb_retention_policy.py b/ansible_collections/community/general/plugins/modules/influxdb_retention_policy.py index 28d5450ff..f1c13a811 100644 --- a/ansible_collections/community/general/plugins/modules/influxdb_retention_policy.py +++ b/ansible_collections/community/general/plugins/modules/influxdb_retention_policy.py @@ -17,7 +17,6 @@ description: - Manage InfluxDB retention policies. author: "Kamil Szczygiel (@kamsz)" requirements: - - "python >= 2.6" - "influxdb >= 0.9" - requests attributes: @@ -46,14 +45,14 @@ options: duration: description: - Determines how long InfluxDB should keep the data. If specified, it - should be C(INF) or at least one hour. If not specified, C(INF) is + should be V(INF) or at least one hour. If not specified, V(INF) is assumed. Supports complex duration expressions with multiple units. - - Required only if I(state) is set to C(present). + - Required only if O(state) is set to V(present). type: str replication: description: - Determines how many independent copies of each point are stored in the cluster. - - Required only if I(state) is set to C(present). + - Required only if O(state) is set to V(present). type: int default: description: @@ -115,7 +114,6 @@ EXAMPLES = r''' duration: INF replication: 1 ssl: false - validate_certs: false shard_group_duration: 1w state: present @@ -127,7 +125,6 @@ EXAMPLES = r''' duration: 5d1h30m replication: 1 ssl: false - validate_certs: false shard_group_duration: 1d10h30m state: present diff --git a/ansible_collections/community/general/plugins/modules/influxdb_user.py b/ansible_collections/community/general/plugins/modules/influxdb_user.py index bbd0f8f5a..ca4201db1 100644 --- a/ansible_collections/community/general/plugins/modules/influxdb_user.py +++ b/ansible_collections/community/general/plugins/modules/influxdb_user.py @@ -18,7 +18,6 @@ description: - Manage InfluxDB users. author: "Vitaliy Zhhuta (@zhhuta)" requirements: - - "python >= 2.6" - "influxdb >= 0.9" attributes: check_mode: diff --git a/ansible_collections/community/general/plugins/modules/influxdb_write.py b/ansible_collections/community/general/plugins/modules/influxdb_write.py index f95b6dae8..76e6449bb 100644 --- a/ansible_collections/community/general/plugins/modules/influxdb_write.py +++ b/ansible_collections/community/general/plugins/modules/influxdb_write.py @@ -16,7 +16,6 @@ description: - Write data points into InfluxDB. author: "René Moser (@resmo)" requirements: - - "python >= 2.6" - "influxdb >= 0.9" attributes: check_mode: diff --git a/ansible_collections/community/general/plugins/modules/ini_file.py b/ansible_collections/community/general/plugins/modules/ini_file.py index 874f10ae0..ec71a9473 100644 --- a/ansible_collections/community/general/plugins/modules/ini_file.py +++ b/ansible_collections/community/general/plugins/modules/ini_file.py @@ -4,6 +4,7 @@ # Copyright (c) 2012, Jan-Piet Mens # Copyright (c) 2015, Ales Nosek # Copyright (c) 2017, Ansible Project +# Copyright (c) 2023, Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later @@ -22,8 +23,7 @@ description: - Manage (add, remove, change) individual settings in an INI-style file without having to manage the file as a whole with, say, M(ansible.builtin.template) or M(ansible.builtin.assemble). - Adds missing sections if they don't exist. - - Before Ansible 2.0, comments are discarded when the source file is read, and therefore will not show up in the destination file. - - Since Ansible 2.3, this module adds missing ending newlines to files to keep in line with the POSIX standard, even when + - This module adds missing ending newlines to files to keep in line with the POSIX standard, even when no other modifications need to be applied. attributes: check_mode: @@ -34,35 +34,34 @@ options: path: description: - Path to the INI-style file; this file is created if required. - - Before Ansible 2.3 this option was only usable as I(dest). type: path required: true aliases: [ dest ] section: description: - - Section name in INI file. This is added if I(state=present) automatically when + - Section name in INI file. This is added if O(state=present) automatically when a single value is being set. - - If left empty, being omitted, or being set to C(null), the I(option) will be placed before the first I(section). - - Using C(null) is also required if the config format does not support sections. + - If being omitted, the O(option) will be placed before the first O(section). + - Omitting O(section) is also required if the config format does not support sections. type: str option: description: - - If set (required for changing a I(value)), this is the name of the option. - - May be omitted if adding/removing a whole I(section). + - If set (required for changing a O(value)), this is the name of the option. + - May be omitted if adding/removing a whole O(section). type: str value: description: - - The string value to be associated with an I(option). - - May be omitted when removing an I(option). - - Mutually exclusive with I(values). - - I(value=v) is equivalent to I(values=[v]). + - The string value to be associated with an O(option). + - May be omitted when removing an O(option). + - Mutually exclusive with O(values). + - O(value=v) is equivalent to O(values=[v]). type: str values: description: - - The string value to be associated with an I(option). - - May be omitted when removing an I(option). - - Mutually exclusive with I(value). - - I(value=v) is equivalent to I(values=[v]). + - The string value to be associated with an O(option). + - May be omitted when removing an O(option). + - Mutually exclusive with O(value). + - O(value=v) is equivalent to O(values=[v]). type: list elements: str version_added: 3.6.0 @@ -74,22 +73,22 @@ options: default: false state: description: - - If set to C(absent) and I(exclusive) set to C(true) all matching I(option) lines are removed. - - If set to C(absent) and I(exclusive) set to C(false) the specified I(option=value) lines are removed, - but the other I(option)s with the same name are not touched. - - If set to C(present) and I(exclusive) set to C(false) the specified I(option=values) lines are added, - but the other I(option)s with the same name are not touched. - - If set to C(present) and I(exclusive) set to C(true) all given I(option=values) lines will be - added and the other I(option)s with the same name are removed. + - If set to V(absent) and O(exclusive) set to V(true) all matching O(option) lines are removed. + - If set to V(absent) and O(exclusive) set to V(false) the specified O(option=value) lines are removed, + but the other O(option)s with the same name are not touched. + - If set to V(present) and O(exclusive) set to V(false) the specified O(option=values) lines are added, + but the other O(option)s with the same name are not touched. + - If set to V(present) and O(exclusive) set to V(true) all given O(option=values) lines will be + added and the other O(option)s with the same name are removed. type: str choices: [ absent, present ] default: present exclusive: description: - - If set to C(true) (default), all matching I(option) lines are removed when I(state=absent), - or replaced when I(state=present). - - If set to C(false), only the specified I(value(s)) are added when I(state=present), - or removed when I(state=absent), and existing ones are not modified. + - If set to V(true) (default), all matching O(option) lines are removed when O(state=absent), + or replaced when O(state=present). + - If set to V(false), only the specified O(value)/O(values) are added when O(state=present), + or removed when O(state=absent), and existing ones are not modified. type: bool default: true version_added: 3.6.0 @@ -98,9 +97,15 @@ options: - Do not insert spaces before and after '=' symbol. type: bool default: false + ignore_spaces: + description: + - Do not change a line if doing so would only add or remove spaces before or after the V(=) symbol. + type: bool + default: false + version_added: 7.5.0 create: description: - - If set to C(false), the module will fail if the file does not already exist. + - If set to V(false), the module will fail if the file does not already exist. - By default it will create the file if it is missing. type: bool default: true @@ -109,9 +114,23 @@ options: - Allow option without value and without '=' symbol. type: bool default: false + modify_inactive_option: + description: + - By default the module replaces a commented line that matches the given option. + - Set this option to V(false) to avoid this. This is useful when you want to keep commented example + C(key=value) pairs for documentation purposes. + type: bool + default: true + version_added: 8.0.0 + follow: + description: + - This flag indicates that filesystem links, if they exist, should be followed. + - O(follow=true) can modify O(path) when combined with parameters such as O(mode). + type: bool + default: false + version_added: 7.1.0 notes: - - While it is possible to add an I(option) without specifying a I(value), this makes no sense. - - As of Ansible 2.3, the I(dest) option has been changed to I(path) as default, but I(dest) still works as well. + - While it is possible to add an O(option) without specifying a O(value), this makes no sense. - As of community.general 3.2.0, UTF-8 BOM markers are discarded when reading files. author: - Jan-Piet Mens (@jpmens) @@ -119,7 +138,6 @@ author: ''' EXAMPLES = r''' -# Before Ansible 2.3, option 'dest' was used instead of 'path' - name: Ensure "fav=lemonade is in section "[drinks]" in specified file community.general.ini_file: path: /etc/conf @@ -157,6 +175,13 @@ EXAMPLES = r''' - pepsi mode: '0600' state: present + +- name: Add "beverage=lemon juice" outside a section in specified file + community.general.ini_file: + path: /etc/conf + option: beverage + value: lemon juice + state: present ''' import io @@ -171,27 +196,35 @@ from ansible.module_utils.common.text.converters import to_bytes, to_text def match_opt(option, line): option = re.escape(option) - return re.match('[#;]?( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line) + return re.match('([#;]?)( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line) def match_active_opt(option, line): option = re.escape(option) - return re.match('( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line) - - -def update_section_line(changed, section_lines, index, changed_lines, newline, msg): - option_changed = section_lines[index] != newline + return re.match('()( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line) + + +def update_section_line(option, changed, section_lines, index, changed_lines, ignore_spaces, newline, msg): + option_changed = None + if ignore_spaces: + old_match = match_opt(option, section_lines[index]) + if not old_match.group(1): + new_match = match_opt(option, newline) + option_changed = old_match.group(7) != new_match.group(7) + if option_changed is None: + option_changed = section_lines[index] != newline + if option_changed: + section_lines[index] = newline changed = changed or option_changed if option_changed: msg = 'option changed' - section_lines[index] = newline changed_lines[index] = 1 return (changed, msg) def do_ini(module, filename, section=None, option=None, values=None, state='present', exclusive=True, backup=False, no_extra_spaces=False, - create=True, allow_no_value=False): + ignore_spaces=False, create=True, allow_no_value=False, modify_inactive_option=True, follow=False): if section is not None: section = to_text(section) @@ -210,15 +243,20 @@ def do_ini(module, filename, section=None, option=None, values=None, after_header='%s (content)' % filename, ) - if not os.path.exists(filename): + if follow and os.path.islink(filename): + target_filename = os.path.realpath(filename) + else: + target_filename = filename + + if not os.path.exists(target_filename): if not create: - module.fail_json(rc=257, msg='Destination %s does not exist!' % filename) - destpath = os.path.dirname(filename) + module.fail_json(rc=257, msg='Destination %s does not exist!' % target_filename) + destpath = os.path.dirname(target_filename) if not os.path.exists(destpath) and not module.check_mode: os.makedirs(destpath) ini_lines = [] else: - with io.open(filename, 'r', encoding="utf-8-sig") as ini_file: + with io.open(target_filename, 'r', encoding="utf-8-sig") as ini_file: ini_lines = [to_text(line) for line in ini_file.readlines()] if module._diff: @@ -266,9 +304,11 @@ def do_ini(module, filename, section=None, option=None, values=None, before = after = [] section_lines = [] + section_pattern = re.compile(to_text(r'^\[\s*%s\s*]' % re.escape(section.strip()))) + for index, line in enumerate(ini_lines): # find start and end of section - if line.startswith(u'[%s]' % section): + if section_pattern.match(line): within_section = True section_start = index elif line.startswith(u'['): @@ -283,6 +323,12 @@ def do_ini(module, filename, section=None, option=None, values=None, # Keep track of changed section_lines changed_lines = [0] * len(section_lines) + # Determine whether to consider using commented out/inactive options or only active ones + if modify_inactive_option: + match_function = match_opt + else: + match_function = match_active_opt + # handling multiple instances of option=value when state is 'present' with/without exclusive is a bit complex # # 1. edit all lines where we have a option=value pair with a matching value in values[] @@ -292,10 +338,10 @@ def do_ini(module, filename, section=None, option=None, values=None, if state == 'present' and option: for index, line in enumerate(section_lines): - if match_opt(option, line): - match = match_opt(option, line) - if values and match.group(6) in values: - matched_value = match.group(6) + if match_function(option, line): + match = match_function(option, line) + if values and match.group(7) in values: + matched_value = match.group(7) if not matched_value and allow_no_value: # replace existing option with no value line(s) newline = u'%s\n' % option @@ -303,12 +349,12 @@ def do_ini(module, filename, section=None, option=None, values=None, else: # replace existing option=value line(s) newline = assignment_format % (option, matched_value) - (changed, msg) = update_section_line(changed, section_lines, index, changed_lines, newline, msg) + (changed, msg) = update_section_line(option, changed, section_lines, index, changed_lines, ignore_spaces, newline, msg) values.remove(matched_value) elif not values and allow_no_value: # replace existing option with no value line(s) newline = u'%s\n' % option - (changed, msg) = update_section_line(changed, section_lines, index, changed_lines, newline, msg) + (changed, msg) = update_section_line(option, changed, section_lines, index, changed_lines, ignore_spaces, newline, msg) option_no_value_present = True break @@ -316,14 +362,14 @@ def do_ini(module, filename, section=None, option=None, values=None, # override option with no value to option with value if not allow_no_value if len(values) > 0: for index, line in enumerate(section_lines): - if not changed_lines[index] and match_opt(option, line): + if not changed_lines[index] and match_function(option, line): newline = assignment_format % (option, values.pop(0)) - (changed, msg) = update_section_line(changed, section_lines, index, changed_lines, newline, msg) + (changed, msg) = update_section_line(option, changed, section_lines, index, changed_lines, ignore_spaces, newline, msg) if len(values) == 0: break # remove all remaining option occurrences from the rest of the section for index in range(len(section_lines) - 1, 0, -1): - if not changed_lines[index] and match_opt(option, section_lines[index]): + if not changed_lines[index] and match_function(option, section_lines[index]): del section_lines[index] del changed_lines[index] changed = True @@ -367,7 +413,7 @@ def do_ini(module, filename, section=None, option=None, values=None, section_lines = new_section_lines elif not exclusive and len(values) > 0: # delete specified option=value line(s) - new_section_lines = [i for i in section_lines if not (match_active_opt(option, i) and match_active_opt(option, i).group(6) in values)] + new_section_lines = [i for i in section_lines if not (match_active_opt(option, i) and match_active_opt(option, i).group(7) in values)] if section_lines != new_section_lines: changed = True msg = 'option changed' @@ -404,7 +450,7 @@ def do_ini(module, filename, section=None, option=None, values=None, backup_file = None if changed and not module.check_mode: if backup: - backup_file = module.backup_local(filename) + backup_file = module.backup_local(target_filename) encoded_ini_lines = [to_bytes(line) for line in ini_lines] try: @@ -416,10 +462,10 @@ def do_ini(module, filename, section=None, option=None, values=None, module.fail_json(msg="Unable to create temporary file %s", traceback=traceback.format_exc()) try: - module.atomic_move(tmpfile, filename) + module.atomic_move(tmpfile, target_filename) except IOError: module.ansible.fail_json(msg='Unable to move temporary \ - file %s to %s, IOError' % (tmpfile, filename), traceback=traceback.format_exc()) + file %s to %s, IOError' % (tmpfile, target_filename), traceback=traceback.format_exc()) return (changed, backup_file, diff, msg) @@ -437,8 +483,11 @@ def main(): state=dict(type='str', default='present', choices=['absent', 'present']), exclusive=dict(type='bool', default=True), no_extra_spaces=dict(type='bool', default=False), + ignore_spaces=dict(type='bool', default=False), allow_no_value=dict(type='bool', default=False), - create=dict(type='bool', default=True) + modify_inactive_option=dict(type='bool', default=True), + create=dict(type='bool', default=True), + follow=dict(type='bool', default=False) ), mutually_exclusive=[ ['value', 'values'] @@ -456,8 +505,11 @@ def main(): exclusive = module.params['exclusive'] backup = module.params['backup'] no_extra_spaces = module.params['no_extra_spaces'] + ignore_spaces = module.params['ignore_spaces'] allow_no_value = module.params['allow_no_value'] + modify_inactive_option = module.params['modify_inactive_option'] create = module.params['create'] + follow = module.params['follow'] if state == 'present' and not allow_no_value and value is None and not values: module.fail_json(msg="Parameter 'value(s)' must be defined if state=present and allow_no_value=False.") @@ -467,7 +519,9 @@ def main(): elif values is None: values = [] - (changed, backup_file, diff, msg) = do_ini(module, path, section, option, values, state, exclusive, backup, no_extra_spaces, create, allow_no_value) + (changed, backup_file, diff, msg) = do_ini( + module, path, section, option, values, state, exclusive, backup, + no_extra_spaces, ignore_spaces, create, allow_no_value, modify_inactive_option, follow) if not module.check_mode and os.path.exists(path): file_args = module.load_file_common_arguments(module.params) diff --git a/ansible_collections/community/general/plugins/modules/installp.py b/ansible_collections/community/general/plugins/modules/installp.py index 41064363d..4b5a6949c 100644 --- a/ansible_collections/community/general/plugins/modules/installp.py +++ b/ansible_collections/community/general/plugins/modules/installp.py @@ -32,7 +32,7 @@ options: name: description: - One or more packages to install or remove. - - Use C(all) to install all packages available on informed C(repository_path). + - Use V(all) to install all packages available on informed O(repository_path). type: list elements: str required: true @@ -133,7 +133,7 @@ def _check_new_pkg(module, package, repository_path): def _check_installed_pkg(module, package, repository_path): """ Check the package on AIX. - It verifies if the package is installed and informations + It verifies if the package is installed and information :param module: Ansible module parameters spec. :param package: Package/fileset name. diff --git a/ansible_collections/community/general/plugins/modules/interfaces_file.py b/ansible_collections/community/general/plugins/modules/interfaces_file.py index f19c019f4..98103082e 100644 --- a/ansible_collections/community/general/plugins/modules/interfaces_file.py +++ b/ansible_collections/community/general/plugins/modules/interfaces_file.py @@ -12,14 +12,14 @@ __metaclass__ = type DOCUMENTATION = ''' --- module: interfaces_file -short_description: Tweak settings in /etc/network/interfaces files +short_description: Tweak settings in C(/etc/network/interfaces) files extends_documentation_fragment: - ansible.builtin.files - community.general.attributes description: - Manage (add, remove, change) individual interface options in an interfaces-style file without having to manage the file as a whole with, say, M(ansible.builtin.template) or M(ansible.builtin.assemble). Interface has to be presented in a file. - - Read information about interfaces from interfaces-styled files + - Read information about interfaces from interfaces-styled files. attributes: check_mode: support: full @@ -29,27 +29,27 @@ options: dest: type: path description: - - Path to the interfaces file + - Path to the interfaces file. default: /etc/network/interfaces iface: type: str description: - - Name of the interface, required for value changes or option remove + - Name of the interface, required for value changes or option remove. address_family: type: str description: - - Address family of the interface, useful if same interface name is used for both inet and inet6 + - Address family of the interface, useful if same interface name is used for both V(inet) and V(inet6). option: type: str description: - - Name of the option, required for value changes or option remove + - Name of the option, required for value changes or option remove. value: type: str description: - - If I(option) is not presented for the I(interface) and I(state) is C(present) option will be added. - If I(option) already exists and is not C(pre-up), C(up), C(post-up) or C(down), it's value will be updated. - C(pre-up), C(up), C(post-up) and C(down) options can't be updated, only adding new options, removing existing - ones or cleaning the whole option set are supported + - If O(option) is not presented for the O(iface) and O(state) is V(present) option will be added. + If O(option) already exists and is not V(pre-up), V(up), V(post-up) or V(down), it's value will be updated. + V(pre-up), V(up), V(post-up) and V(down) options cannot be updated, only adding new options, removing existing + ones or cleaning the whole option set are supported. backup: description: - Create a backup file including the timestamp information so you can get @@ -59,77 +59,81 @@ options: state: type: str description: - - If set to C(absent) the option or section will be removed if present instead of created. + - If set to V(absent) the option or section will be removed if present instead of created. default: "present" choices: [ "present", "absent" ] notes: - - If option is defined multiple times last one will be updated but all will be deleted in case of an absent state + - If option is defined multiple times last one will be updated but all will be deleted in case of an absent state. requirements: [] author: "Roman Belyakovsky (@hryamzik)" ''' RETURN = ''' dest: - description: destination file/path + description: Destination file/path. returned: success type: str sample: "/etc/network/interfaces" ifaces: - description: interfaces dictionary + description: Interfaces dictionary. returned: success - type: complex + type: dict contains: ifaces: - description: interface dictionary + description: Interface dictionary. returned: success type: dict contains: eth0: - description: Name of the interface + description: Name of the interface. returned: success type: dict contains: address_family: - description: interface address family + description: Interface address family. returned: success type: str sample: "inet" method: - description: interface method + description: Interface method. returned: success type: str sample: "manual" mtu: - description: other options, all values returned as strings + description: Other options, all values returned as strings. returned: success type: str sample: "1500" pre-up: - description: list of C(pre-up) scripts + description: List of C(pre-up) scripts. returned: success type: list + elements: str sample: - "route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth1" - "route add -net 10.10.11.0/24 gw 10.10.11.1 dev eth2" up: - description: list of C(up) scripts + description: List of C(up) scripts. returned: success type: list + elements: str sample: - "route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth1" - "route add -net 10.10.11.0/24 gw 10.10.11.1 dev eth2" post-up: - description: list of C(post-up) scripts + description: List of C(post-up) scripts. returned: success type: list + elements: str sample: - "route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth1" - "route add -net 10.10.11.0/24 gw 10.10.11.1 dev eth2" down: - description: list of C(down) scripts + description: List of C(down) scripts. returned: success type: list + elements: str sample: - "route del -net 10.10.10.0/24 gw 10.10.10.1 dev eth1" - "route del -net 10.10.11.0/24 gw 10.10.11.1 dev eth2" @@ -336,6 +340,8 @@ def addOptionAfterLine(option, value, iface, lines, last_line_dict, iface_option changed = False for ln in lines: if ln.get('line_type', '') == 'iface' and ln.get('iface', '') == iface and value != ln.get('params', {}).get('method', ''): + if address_family is not None and ln.get('address_family') != address_family: + continue changed = True ln['line'] = re.sub(ln.get('params', {}).get('method', '') + '$', value, ln.get('line')) ln['params']['method'] = value diff --git a/ansible_collections/community/general/plugins/modules/ipa_config.py b/ansible_collections/community/general/plugins/modules/ipa_config.py index ec94b58d4..871643fd7 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_config.py +++ b/ansible_collections/community/general/plugins/modules/ipa_config.py @@ -40,6 +40,12 @@ options: aliases: ["primarygroup"] type: str version_added: '2.5.0' + ipagroupobjectclasses: + description: A list of group objectclasses. + aliases: ["groupobjectclasses"] + type: list + elements: str + version_added: '7.3.0' ipagroupsearchfields: description: A list of fields to search in when searching for groups. aliases: ["groupsearchfields"] @@ -85,12 +91,21 @@ options: elements: str version_added: '3.7.0' ipauserauthtype: - description: The authentication type to use by default. + description: + - The authentication type to use by default. + - The choice V(idp) has been added in community.general 7.3.0. + - The choice V(passkey) has been added in community.general 8.1.0. aliases: ["userauthtype"] - choices: ["password", "radius", "otp", "pkinit", "hardened", "disabled"] + choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", "passkey", "disabled"] type: list elements: str version_added: '2.5.0' + ipauserobjectclasses: + description: A list of user objectclasses. + aliases: ["userobjectclasses"] + type: list + elements: str + version_added: '7.3.0' ipausersearchfields: description: A list of fields to search in when searching for users. aliases: ["usersearchfields"] @@ -235,11 +250,12 @@ class ConfigIPAClient(IPAClient): def get_config_dict(ipaconfigstring=None, ipadefaultloginshell=None, ipadefaultemaildomain=None, ipadefaultprimarygroup=None, - ipagroupsearchfields=None, ipahomesrootdir=None, - ipakrbauthzdata=None, ipamaxusernamelength=None, - ipapwdexpadvnotify=None, ipasearchrecordslimit=None, - ipasearchtimelimit=None, ipaselinuxusermaporder=None, - ipauserauthtype=None, ipausersearchfields=None): + ipagroupsearchfields=None, ipagroupobjectclasses=None, + ipahomesrootdir=None, ipakrbauthzdata=None, + ipamaxusernamelength=None, ipapwdexpadvnotify=None, + ipasearchrecordslimit=None, ipasearchtimelimit=None, + ipaselinuxusermaporder=None, ipauserauthtype=None, + ipausersearchfields=None, ipauserobjectclasses=None): config = {} if ipaconfigstring is not None: config['ipaconfigstring'] = ipaconfigstring @@ -249,6 +265,8 @@ def get_config_dict(ipaconfigstring=None, ipadefaultloginshell=None, config['ipadefaultemaildomain'] = ipadefaultemaildomain if ipadefaultprimarygroup is not None: config['ipadefaultprimarygroup'] = ipadefaultprimarygroup + if ipagroupobjectclasses is not None: + config['ipagroupobjectclasses'] = ipagroupobjectclasses if ipagroupsearchfields is not None: config['ipagroupsearchfields'] = ','.join(ipagroupsearchfields) if ipahomesrootdir is not None: @@ -267,6 +285,8 @@ def get_config_dict(ipaconfigstring=None, ipadefaultloginshell=None, config['ipaselinuxusermaporder'] = '$'.join(ipaselinuxusermaporder) if ipauserauthtype is not None: config['ipauserauthtype'] = ipauserauthtype + if ipauserobjectclasses is not None: + config['ipauserobjectclasses'] = ipauserobjectclasses if ipausersearchfields is not None: config['ipausersearchfields'] = ','.join(ipausersearchfields) @@ -283,6 +303,7 @@ def ensure(module, client): ipadefaultloginshell=module.params.get('ipadefaultloginshell'), ipadefaultemaildomain=module.params.get('ipadefaultemaildomain'), ipadefaultprimarygroup=module.params.get('ipadefaultprimarygroup'), + ipagroupobjectclasses=module.params.get('ipagroupobjectclasses'), ipagroupsearchfields=module.params.get('ipagroupsearchfields'), ipahomesrootdir=module.params.get('ipahomesrootdir'), ipakrbauthzdata=module.params.get('ipakrbauthzdata'), @@ -293,6 +314,7 @@ def ensure(module, client): ipaselinuxusermaporder=module.params.get('ipaselinuxusermaporder'), ipauserauthtype=module.params.get('ipauserauthtype'), ipausersearchfields=module.params.get('ipausersearchfields'), + ipauserobjectclasses=module.params.get('ipauserobjectclasses'), ) ipa_config = client.config_show() diff = get_config_diff(client, ipa_config, module_config) @@ -322,6 +344,8 @@ def main(): ipadefaultloginshell=dict(type='str', aliases=['loginshell']), ipadefaultemaildomain=dict(type='str', aliases=['emaildomain']), ipadefaultprimarygroup=dict(type='str', aliases=['primarygroup']), + ipagroupobjectclasses=dict(type='list', elements='str', + aliases=['groupobjectclasses']), ipagroupsearchfields=dict(type='list', elements='str', aliases=['groupsearchfields']), ipahomesrootdir=dict(type='str', aliases=['homesrootdir']), @@ -337,9 +361,11 @@ def main(): ipauserauthtype=dict(type='list', elements='str', aliases=['userauthtype'], choices=["password", "radius", "otp", "pkinit", - "hardened", "disabled"]), + "hardened", "idp", "passkey", "disabled"]), ipausersearchfields=dict(type='list', elements='str', aliases=['usersearchfields']), + ipauserobjectclasses=dict(type='list', elements='str', + aliases=['userobjectclasses']), ) module = AnsibleModule( diff --git a/ansible_collections/community/general/plugins/modules/ipa_dnsrecord.py b/ansible_collections/community/general/plugins/modules/ipa_dnsrecord.py index b1a90141b..cb4ce03dd 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_dnsrecord.py +++ b/ansible_collections/community/general/plugins/modules/ipa_dnsrecord.py @@ -35,22 +35,24 @@ options: record_type: description: - The type of DNS record name. - - Currently, 'A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'PTR', 'TXT', 'SRV' and 'MX' are supported. + - Currently, 'A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'NS', 'PTR', 'TXT', 'SRV' and 'MX' are supported. - "'A6', 'CNAME', 'DNAME' and 'TXT' are added in version 2.5." - "'SRV' and 'MX' are added in version 2.8." + - "'NS' are added in comunity.general 8.2.0." required: false default: 'A' - choices: ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'MX', 'PTR', 'SRV', 'TXT'] + choices: ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT'] type: str record_value: description: - Manage DNS record name with this value. - - Mutually exclusive with I(record_values), and exactly one of I(record_value) and I(record_values) has to be specified. - - Use I(record_values) if you need to specify multiple values. + - Mutually exclusive with O(record_values), and exactly one of O(record_value) and O(record_values) has to be specified. + - Use O(record_values) if you need to specify multiple values. - In the case of 'A' or 'AAAA' record types, this will be the IP address. - In the case of 'A6' record type, this will be the A6 Record data. - In the case of 'CNAME' record type, this will be the hostname. - In the case of 'DNAME' record type, this will be the DNAME target. + - In the case of 'NS' record type, this will be the name server hostname. Hostname must already have a valid A or AAAA record. - In the case of 'PTR' record type, this will be the hostname. - In the case of 'TXT' record type, this will be a text. - In the case of 'SRV' record type, this will be a service record. @@ -59,11 +61,12 @@ options: record_values: description: - Manage DNS record name with this value. - - Mutually exclusive with I(record_value), and exactly one of I(record_value) and I(record_values) has to be specified. + - Mutually exclusive with O(record_value), and exactly one of O(record_value) and O(record_values) has to be specified. - In the case of 'A' or 'AAAA' record types, this will be the IP address. - In the case of 'A6' record type, this will be the A6 Record data. - In the case of 'CNAME' record type, this will be the hostname. - In the case of 'DNAME' record type, this will be the DNAME target. + - In the case of 'NS' record type, this will be the name server hostname. Hostname must already have a valid A or AAAA record. - In the case of 'PTR' record type, this will be the hostname. - In the case of 'TXT' record type, this will be a text. - In the case of 'SRV' record type, this will be a service record. @@ -73,7 +76,7 @@ options: record_ttl: description: - Set the TTL for the record. - - Applies only when adding a new or changing the value of I(record_value) or I(record_values). + - Applies only when adding a new or changing the value of O(record_value) or O(record_values). required: false type: int state: @@ -162,6 +165,16 @@ EXAMPLES = r''' ipa_user: admin ipa_pass: topsecret state: absent + +- name: Ensure an NS record for a subdomain is present + community,general.ipa_dnsrecord: + name: subdomain + zone_name: example.com + record_type: 'NS' + record_value: 'ns1.subdomain.exmaple.com' + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: ChangeMe! ''' RETURN = r''' @@ -205,6 +218,8 @@ class DNSRecordIPAClient(IPAClient): item.update(cname_part_hostname=value) elif details['record_type'] == 'DNAME': item.update(dname_part_target=value) + elif details['record_type'] == 'NS': + item.update(ns_part_hostname=value) elif details['record_type'] == 'PTR': item.update(ptr_part_hostname=value) elif details['record_type'] == 'TXT': @@ -241,6 +256,8 @@ def get_dnsrecord_dict(details=None): module_dnsrecord.update(cnamerecord=details['record_values']) elif details['record_type'] == 'DNAME' and details['record_values']: module_dnsrecord.update(dnamerecord=details['record_values']) + elif details['record_type'] == 'NS' and details['record_values']: + module_dnsrecord.update(nsrecord=details['record_values']) elif details['record_type'] == 'PTR' and details['record_values']: module_dnsrecord.update(ptrrecord=details['record_values']) elif details['record_type'] == 'TXT' and details['record_values']: @@ -311,7 +328,7 @@ def ensure(module, client): def main(): - record_types = ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'PTR', 'TXT', 'SRV', 'MX'] + record_types = ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'NS', 'PTR', 'TXT', 'SRV', 'MX'] argument_spec = ipa_argument_spec() argument_spec.update( zone_name=dict(type='str', required=True), diff --git a/ansible_collections/community/general/plugins/modules/ipa_dnszone.py b/ansible_collections/community/general/plugins/modules/ipa_dnszone.py index 06c93841e..6699b0525 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_dnszone.py +++ b/ansible_collections/community/general/plugins/modules/ipa_dnszone.py @@ -152,7 +152,8 @@ def ensure(module, client): changed = True if not module.check_mode: client.dnszone_add(zone_name=zone_name, details={'idnsallowdynupdate': dynamicupdate, 'idnsallowsyncptr': allowsyncptr}) - elif ipa_dnszone['idnsallowdynupdate'][0] != str(dynamicupdate).upper() or ipa_dnszone['idnsallowsyncptr'][0] != str(allowsyncptr).upper(): + elif ipa_dnszone['idnsallowdynupdate'][0] != str(dynamicupdate).upper() or \ + ipa_dnszone.get('idnsallowsyncptr') and ipa_dnszone['idnsallowsyncptr'][0] != str(allowsyncptr).upper(): changed = True if not module.check_mode: client.dnszone_mod(zone_name=zone_name, details={'idnsallowdynupdate': dynamicupdate, 'idnsallowsyncptr': allowsyncptr}) diff --git a/ansible_collections/community/general/plugins/modules/ipa_group.py b/ansible_collections/community/general/plugins/modules/ipa_group.py index 87e7f0e66..92470606f 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_group.py +++ b/ansible_collections/community/general/plugins/modules/ipa_group.py @@ -22,8 +22,8 @@ attributes: options: append: description: - - If C(true), add the listed I(user) and I(group) to the group members. - - If C(false), only the listed I(user) and I(group) will be group members, removing any other members. + - If V(true), add the listed O(user) and O(group) to the group members. + - If V(false), only the listed O(user) and O(group) will be group members, removing any other members. default: false type: bool version_added: 4.0.0 @@ -50,9 +50,9 @@ options: group: description: - List of group names assigned to this group. - - If I(append=false) and an empty list is passed all groups will be removed from this group. + - If O(append=false) and an empty list is passed all groups will be removed from this group. - Groups that are already assigned but not passed will be removed. - - If I(append=true) the listed groups will be assigned without removing other groups. + - If O(append=true) the listed groups will be assigned without removing other groups. - If option is omitted assigned groups will not be checked or changed. type: list elements: str @@ -63,20 +63,20 @@ options: user: description: - List of user names assigned to this group. - - If I(append=false) and an empty list is passed all users will be removed from this group. + - If O(append=false) and an empty list is passed all users will be removed from this group. - Users that are already assigned but not passed will be removed. - - If I(append=true) the listed users will be assigned without removing other users. + - If O(append=true) the listed users will be assigned without removing other users. - If option is omitted assigned users will not be checked or changed. type: list elements: str external_user: description: - List of external users assigned to this group. - - Behaves identically to I(user) with respect to I(append) attribute. - - List entries can be in C(DOMAIN\\username) or SID format. + - Behaves identically to O(user) with respect to O(append) attribute. + - List entries can be in V(DOMAIN\\\\username) or SID format. - Unless SIDs are provided, the module will always attempt to make changes even if the group already has all the users. This is because only SIDs are returned by IPA query. - - I(external=true) is needed for this option to work. + - O(external=true) is needed for this option to work. type: list elements: str version_added: 6.3.0 diff --git a/ansible_collections/community/general/plugins/modules/ipa_hbacrule.py b/ansible_collections/community/general/plugins/modules/ipa_hbacrule.py index b7633262b..77a4d0d48 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_hbacrule.py +++ b/ansible_collections/community/general/plugins/modules/ipa_hbacrule.py @@ -161,6 +161,7 @@ import traceback from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec from ansible.module_utils.common.text.converters import to_native +from ansible_collections.community.general.plugins.module_utils.version import LooseVersion class HBACRuleIPAClient(IPAClient): @@ -231,10 +232,17 @@ def ensure(module, client): name = module.params['cn'] state = module.params['state'] + ipa_version = client.get_ipa_version() if state in ['present', 'enabled']: - ipaenabledflag = 'TRUE' + if LooseVersion(ipa_version) < LooseVersion('4.9.10'): + ipaenabledflag = 'TRUE' + else: + ipaenabledflag = True else: - ipaenabledflag = 'FALSE' + if LooseVersion(ipa_version) < LooseVersion('4.9.10'): + ipaenabledflag = 'FALSE' + else: + ipaenabledflag = False host = module.params['host'] hostcategory = module.params['hostcategory'] diff --git a/ansible_collections/community/general/plugins/modules/ipa_host.py b/ansible_collections/community/general/plugins/modules/ipa_host.py index d561401d4..b37a606d7 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_host.py +++ b/ansible_collections/community/general/plugins/modules/ipa_host.py @@ -80,7 +80,7 @@ options: type: str update_dns: description: - - If set C("True") with state as C("absent"), then removes DNS records of the host managed by FreeIPA DNS. + - If set V(true) with O(state=absent), then removes DNS records of the host managed by FreeIPA DNS. - This option has no effect for states other than "absent". type: bool random_password: @@ -118,7 +118,6 @@ EXAMPLES = r''' ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret - validate_certs: false random_password: true - name: Ensure host is disabled diff --git a/ansible_collections/community/general/plugins/modules/ipa_hostgroup.py b/ansible_collections/community/general/plugins/modules/ipa_hostgroup.py index 12232de89..70749c35b 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_hostgroup.py +++ b/ansible_collections/community/general/plugins/modules/ipa_hostgroup.py @@ -22,8 +22,8 @@ attributes: options: append: description: - - If C(true), add the listed I(host) to the I(hostgroup). - - If C(false), only the listed I(host) will be in I(hostgroup), removing any other hosts. + - If V(true), add the listed O(host) to the O(hostgroup). + - If V(false), only the listed O(host) will be in O(hostgroup), removing any other hosts. default: false type: bool version_added: 6.6.0 diff --git a/ansible_collections/community/general/plugins/modules/ipa_otptoken.py b/ansible_collections/community/general/plugins/modules/ipa_otptoken.py index f25ab6023..567674f93 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_otptoken.py +++ b/ansible_collections/community/general/plugins/modules/ipa_otptoken.py @@ -48,7 +48,7 @@ options: description: Assigned user of the token. type: str enabled: - description: Mark the token as enabled (default C(true)). + description: Mark the token as enabled (default V(true)). default: true type: bool notbefore: @@ -237,7 +237,7 @@ def get_otptoken_dict(ansible_to_ipa, uniqueid=None, newuniqueid=None, otptype=N if owner is not None: otptoken[ansible_to_ipa['owner']] = owner if enabled is not None: - otptoken[ansible_to_ipa['enabled']] = 'FALSE' if enabled else 'TRUE' + otptoken[ansible_to_ipa['enabled']] = False if enabled else True if notbefore is not None: otptoken[ansible_to_ipa['notbefore']] = notbefore + 'Z' if notafter is not None: diff --git a/ansible_collections/community/general/plugins/modules/ipa_pwpolicy.py b/ansible_collections/community/general/plugins/modules/ipa_pwpolicy.py index 6a6c4318b..ba7d70291 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_pwpolicy.py +++ b/ansible_collections/community/general/plugins/modules/ipa_pwpolicy.py @@ -64,6 +64,26 @@ options: lockouttime: description: Period (in seconds) for which users are locked out. type: str + gracelimit: + description: Maximum number of LDAP logins after password expiration. + type: int + version_added: 8.2.0 + maxrepeat: + description: Maximum number of allowed same consecutive characters in the new password. + type: int + version_added: 8.2.0 + maxsequence: + description: Maximum length of monotonic character sequences in the new password. An example of a monotonic sequence of length 5 is V(12345). + type: int + version_added: 8.2.0 + dictcheck: + description: Check whether the password (with possible modifications) matches a word in a dictionary (using cracklib). + type: bool + version_added: 8.2.0 + usercheck: + description: Check whether the password (with possible modifications) contains the user name in some form (if the name has > 3 characters). + type: bool + version_added: 8.2.0 extends_documentation_fragment: - community.general.ipa.documentation - community.general.attributes @@ -93,9 +113,15 @@ EXAMPLES = r''' historylength: '16' minclasses: '4' priority: '10' + minlength: '6' maxfailcount: '4' failinterval: '600' lockouttime: '1200' + gracelimit: 3 + maxrepeat: 3 + maxsequence: 3 + dictcheck: true + usercheck: true ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret @@ -159,26 +185,35 @@ class PwPolicyIPAClient(IPAClient): def get_pwpolicy_dict(maxpwdlife=None, minpwdlife=None, historylength=None, minclasses=None, minlength=None, priority=None, maxfailcount=None, failinterval=None, - lockouttime=None): + lockouttime=None, gracelimit=None, maxrepeat=None, maxsequence=None, dictcheck=None, usercheck=None): pwpolicy = {} - if maxpwdlife is not None: - pwpolicy['krbmaxpwdlife'] = maxpwdlife - if minpwdlife is not None: - pwpolicy['krbminpwdlife'] = minpwdlife - if historylength is not None: - pwpolicy['krbpwdhistorylength'] = historylength - if minclasses is not None: - pwpolicy['krbpwdmindiffchars'] = minclasses - if minlength is not None: - pwpolicy['krbpwdminlength'] = minlength - if priority is not None: - pwpolicy['cospriority'] = priority - if maxfailcount is not None: - pwpolicy['krbpwdmaxfailure'] = maxfailcount - if failinterval is not None: - pwpolicy['krbpwdfailurecountinterval'] = failinterval - if lockouttime is not None: - pwpolicy['krbpwdlockoutduration'] = lockouttime + pwpolicy_options = { + 'krbmaxpwdlife': maxpwdlife, + 'krbminpwdlife': minpwdlife, + 'krbpwdhistorylength': historylength, + 'krbpwdmindiffchars': minclasses, + 'krbpwdminlength': minlength, + 'cospriority': priority, + 'krbpwdmaxfailure': maxfailcount, + 'krbpwdfailurecountinterval': failinterval, + 'krbpwdlockoutduration': lockouttime, + 'passwordgracelimit': gracelimit, + 'ipapwdmaxrepeat': maxrepeat, + 'ipapwdmaxsequence': maxsequence, + } + + pwpolicy_boolean_options = { + 'ipapwddictcheck': dictcheck, + 'ipapwdusercheck': usercheck, + } + + for option, value in pwpolicy_options.items(): + if value is not None: + pwpolicy[option] = to_native(value) + + for option, value in pwpolicy_boolean_options.items(): + if value is not None: + pwpolicy[option] = bool(value) return pwpolicy @@ -199,7 +234,13 @@ def ensure(module, client): priority=module.params.get('priority'), maxfailcount=module.params.get('maxfailcount'), failinterval=module.params.get('failinterval'), - lockouttime=module.params.get('lockouttime')) + lockouttime=module.params.get('lockouttime'), + gracelimit=module.params.get('gracelimit'), + maxrepeat=module.params.get('maxrepeat'), + maxsequence=module.params.get('maxsequence'), + dictcheck=module.params.get('dictcheck'), + usercheck=module.params.get('usercheck'), + ) ipa_pwpolicy = client.pwpolicy_find(name=name) @@ -236,7 +277,13 @@ def main(): priority=dict(type='str'), maxfailcount=dict(type='str'), failinterval=dict(type='str'), - lockouttime=dict(type='str')) + lockouttime=dict(type='str'), + gracelimit=dict(type='int'), + maxrepeat=dict(type='int'), + maxsequence=dict(type='int'), + dictcheck=dict(type='bool'), + usercheck=dict(type='bool'), + ) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) diff --git a/ansible_collections/community/general/plugins/modules/ipa_sudorule.py b/ansible_collections/community/general/plugins/modules/ipa_sudorule.py index 59b4eb19e..223f6b6de 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_sudorule.py +++ b/ansible_collections/community/general/plugins/modules/ipa_sudorule.py @@ -47,6 +47,22 @@ options: type: list elements: str version_added: 2.0.0 + deny_cmd: + description: + - List of denied commands assigned to the rule. + - If an empty list is passed all commands will be removed from the rule. + - If option is omitted commands will not be checked or changed. + type: list + elements: str + version_added: 8.1.0 + deny_cmdgroup: + description: + - List of denied command groups assigned to the rule. + - If an empty list is passed all command groups will be removed from the rule. + - If option is omitted command groups will not be checked or changed. + type: list + elements: str + version_added: 8.1.0 description: description: - Description of the sudo rule. @@ -56,14 +72,14 @@ options: - List of hosts assigned to the rule. - If an empty list is passed all hosts will be removed from the rule. - If option is omitted hosts will not be checked or changed. - - Option C(hostcategory) must be omitted to assign hosts. + - Option O(hostcategory) must be omitted to assign hosts. type: list elements: str hostcategory: description: - Host category the rule applies to. - - If 'all' is passed one must omit C(host) and C(hostgroup). - - Option C(host) and C(hostgroup) must be omitted to assign 'all'. + - If V(all) is passed one must omit O(host) and O(hostgroup). + - Option O(host) and O(hostgroup) must be omitted to assign V(all). choices: ['all'] type: str hostgroup: @@ -71,7 +87,7 @@ options: - List of host groups assigned to the rule. - If an empty list is passed all host groups will be removed from the rule. - If option is omitted host groups will not be checked or changed. - - Option C(hostcategory) must be omitted to assign host groups. + - Option O(hostcategory) must be omitted to assign host groups. type: list elements: str runasextusers: @@ -186,6 +202,7 @@ import traceback from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec from ansible.module_utils.common.text.converters import to_native +from ansible_collections.community.general.plugins.module_utils.version import LooseVersion class SudoRuleIPAClient(IPAClient): @@ -246,6 +263,12 @@ class SudoRuleIPAClient(IPAClient): def sudorule_add_allow_command_group(self, name, item): return self._post_json(method='sudorule_add_allow_command', name=name, item={'sudocmdgroup': item}) + def sudorule_add_deny_command(self, name, item): + return self._post_json(method='sudorule_add_deny_command', name=name, item={'sudocmd': item}) + + def sudorule_add_deny_command_group(self, name, item): + return self._post_json(method='sudorule_add_deny_command', name=name, item={'sudocmdgroup': item}) + def sudorule_remove_allow_command(self, name, item): return self._post_json(method='sudorule_remove_allow_command', name=name, item=item) @@ -303,6 +326,8 @@ def ensure(module, client): cmd = module.params['cmd'] cmdgroup = module.params['cmdgroup'] cmdcategory = module.params['cmdcategory'] + deny_cmd = module.params['deny_cmd'] + deny_cmdgroup = module.params['deny_cmdgroup'] host = module.params['host'] hostcategory = module.params['hostcategory'] hostgroup = module.params['hostgroup'] @@ -310,10 +335,17 @@ def ensure(module, client): runasgroupcategory = module.params['runasgroupcategory'] runasextusers = module.params['runasextusers'] + ipa_version = client.get_ipa_version() if state in ['present', 'enabled']: - ipaenabledflag = 'TRUE' + if LooseVersion(ipa_version) < LooseVersion('4.9.10'): + ipaenabledflag = 'TRUE' + else: + ipaenabledflag = True else: - ipaenabledflag = 'FALSE' + if LooseVersion(ipa_version) < LooseVersion('4.9.10'): + ipaenabledflag = 'FALSE' + else: + ipaenabledflag = False sudoopt = module.params['sudoopt'] user = module.params['user'] @@ -359,6 +391,16 @@ def ensure(module, client): if not module.check_mode: client.sudorule_add_allow_command_group(name=name, item=cmdgroup) + if deny_cmd is not None: + changed = category_changed(module, client, 'cmdcategory', ipa_sudorule) or changed + if not module.check_mode: + client.sudorule_add_deny_command(name=name, item=deny_cmd) + + if deny_cmdgroup is not None: + changed = category_changed(module, client, 'cmdcategory', ipa_sudorule) or changed + if not module.check_mode: + client.sudorule_add_deny_command_group(name=name, item=deny_cmdgroup) + if runasusercategory is not None: changed = category_changed(module, client, 'iparunasusercategory', ipa_sudorule) or changed @@ -433,6 +475,8 @@ def main(): cmdgroup=dict(type='list', elements='str'), cmdcategory=dict(type='str', choices=['all']), cn=dict(type='str', required=True, aliases=['name']), + deny_cmd=dict(type='list', elements='str'), + deny_cmdgroup=dict(type='list', elements='str'), description=dict(type='str'), host=dict(type='list', elements='str'), hostcategory=dict(type='str', choices=['all']), @@ -447,7 +491,9 @@ def main(): runasextusers=dict(type='list', elements='str')) module = AnsibleModule(argument_spec=argument_spec, mutually_exclusive=[['cmdcategory', 'cmd'], + ['cmdcategory', 'deny_cmd'], ['cmdcategory', 'cmdgroup'], + ['cmdcategory', 'deny_cmdgroup'], ['hostcategory', 'host'], ['hostcategory', 'hostgroup'], ['usercategory', 'user'], diff --git a/ansible_collections/community/general/plugins/modules/ipa_user.py b/ansible_collections/community/general/plugins/modules/ipa_user.py index 17b72176e..e8a1858d0 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_user.py +++ b/ansible_collections/community/general/plugins/modules/ipa_user.py @@ -30,7 +30,9 @@ options: default: 'always' choices: [ always, on_create ] givenname: - description: First name. + description: + - First name. + - If user does not exist and O(state=present), the usage of O(givenname) is required. type: str krbpasswordexpiration: description: @@ -51,10 +53,12 @@ options: password: description: - Password for a user. - - Will not be set for an existing user unless I(update_password=always), which is the default. + - Will not be set for an existing user unless O(update_password=always), which is the default. type: str sn: - description: Surname. + description: + - Surname. + - If user does not exist and O(state=present), the usage of O(sn) is required. type: str sshpubkey: description: @@ -99,7 +103,9 @@ options: userauthtype: description: - The authentication type to use for the user. - choices: ["password", "radius", "otp", "pkinit", "hardened"] + - To remove all authentication types from the user, use an empty list V([]). + - The choice V(idp) and V(passkey) has been added in community.general 8.1.0. + choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", "passkey"] type: list elements: str version_added: '1.2.0' @@ -374,7 +380,7 @@ def main(): title=dict(type='str'), homedirectory=dict(type='str'), userauthtype=dict(type='list', elements='str', - choices=['password', 'radius', 'otp', 'pkinit', 'hardened'])) + choices=['password', 'radius', 'otp', 'pkinit', 'hardened', 'idp', 'passkey'])) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) diff --git a/ansible_collections/community/general/plugins/modules/ipa_vault.py b/ansible_collections/community/general/plugins/modules/ipa_vault.py index 84b72c1ab..88947e470 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_vault.py +++ b/ansible_collections/community/general/plugins/modules/ipa_vault.py @@ -93,7 +93,6 @@ EXAMPLES = r''' ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret - validate_certs: false - name: Ensure vault is present for Admin user community.general.ipa_vault: diff --git a/ansible_collections/community/general/plugins/modules/ipbase_info.py b/ansible_collections/community/general/plugins/modules/ipbase_info.py new file mode 100644 index 000000000..c6a5511b7 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/ipbase_info.py @@ -0,0 +1,304 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023, Dominik Kukacka +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: "ipbase_info" +version_added: "7.0.0" +short_description: "Retrieve IP geolocation and other facts of a host's IP address using the ipbase.com API" +description: + - "Retrieve IP geolocation and other facts of a host's IP address using the ipbase.com API" +author: "Dominik Kukacka (@dominikkukacka)" +extends_documentation_fragment: + - "community.general.attributes" + - "community.general.attributes.info_module" +options: + ip: + description: + - "The IP you want to get the info for. If not specified the API will detect the IP automatically." + required: false + type: str + apikey: + description: + - "The API key for the request if you need more requests." + required: false + type: str + hostname: + description: + - "If the O(hostname) parameter is set to V(true), the API response will contain the hostname of the IP." + required: false + type: bool + default: false + language: + description: + - "An ISO Alpha 2 Language Code for localizing the IP data" + required: false + type: str + default: "en" +notes: + - "Check U(https://ipbase.com/) for more information." +''' + +EXAMPLES = ''' +- name: "Get IP geolocation information of the primary outgoing IP" + community.general.ipbase_info: + register: my_ip_info + +- name: "Get IP geolocation information of a specific IP" + community.general.ipbase_info: + ip: "8.8.8.8" + register: my_ip_info + + +- name: "Get IP geolocation information of a specific IP with all other possible parameters" + community.general.ipbase_info: + ip: "8.8.8.8" + apikey: "xxxxxxxxxxxxxxxxxxxxxx" + hostname: true + language: "de" + register: my_ip_info + +''' + +RETURN = ''' +data: + description: "JSON parsed response from ipbase.com. Please refer to U(https://ipbase.com/docs/info) for the detailed structure of the response." + returned: success + type: dict + sample: { + "ip": "1.1.1.1", + "hostname": "one.one.one.one", + "type": "v4", + "range_type": { + "type": "PUBLIC", + "description": "Public address" + }, + "connection": { + "asn": 13335, + "organization": "Cloudflare, Inc.", + "isp": "APNIC Research and Development", + "range": "1.1.1.1/32" + }, + "location": { + "geonames_id": 5332870, + "latitude": 34.053611755371094, + "longitude": -118.24549865722656, + "zip": "90012", + "continent": { + "code": "NA", + "name": "North America", + "name_translated": "North America" + }, + "country": { + "alpha2": "US", + "alpha3": "USA", + "calling_codes": [ + "+1" + ], + "currencies": [ + { + "symbol": "$", + "name": "US Dollar", + "symbol_native": "$", + "decimal_digits": 2, + "rounding": 0, + "code": "USD", + "name_plural": "US dollars" + } + ], + "emoji": "...", + "ioc": "USA", + "languages": [ + { + "name": "English", + "name_native": "English" + } + ], + "name": "United States", + "name_translated": "United States", + "timezones": [ + "America/New_York", + "America/Detroit", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Indiana/Indianapolis", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Vevay", + "America/Chicago", + "America/Indiana/Tell_City", + "America/Indiana/Knox", + "America/Menominee", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/North_Dakota/Beulah", + "America/Denver", + "America/Boise", + "America/Phoenix", + "America/Los_Angeles", + "America/Anchorage", + "America/Juneau", + "America/Sitka", + "America/Metlakatla", + "America/Yakutat", + "America/Nome", + "America/Adak", + "Pacific/Honolulu" + ], + "is_in_european_union": false, + "fips": "US", + "geonames_id": 6252001, + "hasc_id": "US", + "wikidata_id": "Q30" + }, + "city": { + "fips": "644000", + "alpha2": null, + "geonames_id": 5368753, + "hasc_id": null, + "wikidata_id": "Q65", + "name": "Los Angeles", + "name_translated": "Los Angeles" + }, + "region": { + "fips": "US06", + "alpha2": "US-CA", + "geonames_id": 5332921, + "hasc_id": "US.CA", + "wikidata_id": "Q99", + "name": "California", + "name_translated": "California" + } + }, + "tlds": [ + ".us" + ], + "timezone": { + "id": "America/Los_Angeles", + "current_time": "2023-05-04T04:30:28-07:00", + "code": "PDT", + "is_daylight_saving": true, + "gmt_offset": -25200 + }, + "security": { + "is_anonymous": false, + "is_datacenter": false, + "is_vpn": false, + "is_bot": false, + "is_abuser": true, + "is_known_attacker": true, + "is_proxy": false, + "is_spam": false, + "is_tor": false, + "is_icloud_relay": false, + "threat_score": 100 + }, + "domains": { + "count": 10943, + "domains": [ + "eliwise.academy", + "accountingprose.academy", + "pistola.academy", + "1and1-test-ntlds-fr.accountant", + "omnergy.africa" + ] + } + } +''' + +from ansible.module_utils.basic import AnsibleModule + +from ansible.module_utils.urls import fetch_url + +from ansible.module_utils.six.moves.urllib.parse import urlencode + + +USER_AGENT = 'ansible-community.general.ipbase_info/0.1.0' +BASE_URL = 'https://api.ipbase.com/v2/info' + + +class IpbaseInfo(object): + + def __init__(self, module): + self.module = module + + def _get_url_data(self, url): + response, info = fetch_url( + self.module, + url, + force=True, + timeout=10, + headers={ + 'Accept': 'application/json', + 'User-Agent': USER_AGENT, + }) + + if info['status'] != 200: + self.module.fail_json(msg='The API request to ipbase.com returned an error status code {0}'.format(info['status'])) + else: + try: + content = response.read() + result = self.module.from_json(content.decode('utf8')) + except ValueError: + self.module.fail_json( + msg='Failed to parse the ipbase.com response: ' + '{0} {1}'.format(url, content)) + else: + return result + + def info(self): + + ip = self.module.params['ip'] + apikey = self.module.params['apikey'] + hostname = self.module.params['hostname'] + language = self.module.params['language'] + + url = BASE_URL + + params = {} + if ip: + params['ip'] = ip + + if apikey: + params['apikey'] = apikey + + if hostname: + params['hostname'] = 1 + + if language: + params['language'] = language + + if params: + url += '?' + urlencode(params) + + return self._get_url_data(url) + + +def main(): + module_args = dict( + ip=dict(type='str', required=False, no_log=False), + apikey=dict(type='str', required=False, no_log=True), + hostname=dict(type='bool', required=False, no_log=False, default=False), + language=dict(type='str', required=False, no_log=False, default='en'), + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + + ipbase = IpbaseInfo(module) + module.exit_json(**ipbase.info()) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/ipify_facts.py b/ansible_collections/community/general/plugins/modules/ipify_facts.py index ab96d7e94..ff17d7e54 100644 --- a/ansible_collections/community/general/plugins/modules/ipify_facts.py +++ b/ansible_collections/community/general/plugins/modules/ipify_facts.py @@ -35,7 +35,7 @@ options: default: 10 validate_certs: description: - - When set to C(NO), SSL certificates will not be validated. + - When set to V(false), SSL certificates will not be validated. type: bool default: true notes: diff --git a/ansible_collections/community/general/plugins/modules/ipmi_boot.py b/ansible_collections/community/general/plugins/modules/ipmi_boot.py index 7a4d2b6ec..9f0016560 100644 --- a/ansible_collections/community/general/plugins/modules/ipmi_boot.py +++ b/ansible_collections/community/general/plugins/modules/ipmi_boot.py @@ -93,7 +93,6 @@ options: type: bool default: false requirements: - - "python >= 2.6" - pyghmi author: "Bulat Gaifullin (@bgaifullin) " ''' diff --git a/ansible_collections/community/general/plugins/modules/ipmi_power.py b/ansible_collections/community/general/plugins/modules/ipmi_power.py index e152f35eb..587cee06f 100644 --- a/ansible_collections/community/general/plugins/modules/ipmi_power.py +++ b/ansible_collections/community/general/plugins/modules/ipmi_power.py @@ -58,7 +58,7 @@ options: - shutdown -- Have system request OS proper shutdown - reset -- Request system reset without waiting for OS - boot -- If system is off, then 'on', else 'reset'" - - Either this option or I(machine) is required. + - Either this option or O(machine) is required. choices: ['on', 'off', shutdown, reset, boot] type: str timeout: @@ -70,7 +70,7 @@ options: description: - Provide a list of the remote target address for the bridge IPMI request, and the power status. - - Either this option or I(state) is required. + - Either this option or O(state) is required. required: false type: list elements: dict @@ -83,14 +83,13 @@ options: required: true state: description: - - Whether to ensure that the machine specified by I(targetAddress) in desired state. - - If this option is not set, the power state is set by I(state). - - If both this option and I(state) are set, this option takes precedence over I(state). + - Whether to ensure that the machine specified by O(machine[].targetAddress) in desired state. + - If this option is not set, the power state is set by O(state). + - If both this option and O(state) are set, this option takes precedence over O(state). choices: ['on', 'off', shutdown, reset, boot] type: str requirements: - - "python >= 2.6" - pyghmi author: "Bulat Gaifullin (@bgaifullin) " ''' @@ -98,18 +97,18 @@ author: "Bulat Gaifullin (@bgaifullin) " RETURN = ''' powerstate: description: The current power state of the machine. - returned: success and I(machine) is not provided + returned: success and O(machine) is not provided type: str sample: 'on' status: description: The current power state of the machine when the machine option is set. - returned: success and I(machine) is provided + returned: success and O(machine) is provided type: list elements: dict version_added: 4.3.0 contains: powerstate: - description: The current power state of the machine specified by I(targetAddress). + description: The current power state of the machine specified by RV(status[].targetAddress). type: str targetAddress: description: The remote target address. diff --git a/ansible_collections/community/general/plugins/modules/iptables_state.py b/ansible_collections/community/general/plugins/modules/iptables_state.py index d0ea7ad79..b0cc3bd3f 100644 --- a/ansible_collections/community/general/plugins/modules/iptables_state.py +++ b/ansible_collections/community/general/plugins/modules/iptables_state.py @@ -34,8 +34,8 @@ description: notes: - The rollback feature is not a module option and depends on task's attributes. To enable it, the module must be played asynchronously, i.e. - by setting task attributes I(poll) to C(0), and I(async) to a value less - or equal to C(ANSIBLE_TIMEOUT). If I(async) is greater, the rollback will + by setting task attributes C(poll) to V(0), and C(async) to a value less + or equal to C(ANSIBLE_TIMEOUT). If C(async) is greater, the rollback will still happen if it shall happen, but you will experience a connection timeout instead of more relevant info returned by the module after its failure. @@ -52,7 +52,7 @@ options: counters: description: - Save or restore the values of all packet and byte counters. - - When C(true), the module is not idempotent. + - When V(true), the module is not idempotent. type: bool default: false ip_version: @@ -65,14 +65,14 @@ options: description: - Specify the path to the C(modprobe) program internally used by iptables related commands to load kernel modules. - - By default, C(/proc/sys/kernel/modprobe) is inspected to determine the + - By default, V(/proc/sys/kernel/modprobe) is inspected to determine the executable's path. type: path noflush: description: - - For I(state=restored), ignored otherwise. - - If C(false), restoring iptables rules from a file flushes (deletes) - all previous contents of the respective table(s). If C(true), the + - For O(state=restored), ignored otherwise. + - If V(false), restoring iptables rules from a file flushes (deletes) + all previous contents of the respective table(s). If V(true), the previous rules are left untouched (but policies are updated anyway, for all built-in chains). type: bool @@ -92,10 +92,10 @@ options: required: true table: description: - - When I(state=restored), restore only the named table even if the input + - When O(state=restored), restore only the named table even if the input file contains other tables. Fail if the named table is not declared in the file. - - When I(state=saved), restrict output to the specified table. If not + - When O(state=saved), restrict output to the specified table. If not specified, output includes all active tables. type: str choices: [ filter, nat, mangle, raw, security ] @@ -207,7 +207,9 @@ saved: "# Completed" ] tables: - description: The iptables we have interest for when module starts. + description: + - The iptables on the system before the module has run, separated by table. + - If the option O(table) is used, only this table is included. type: dict contains: table: @@ -346,20 +348,27 @@ def filter_and_format_state(string): return lines -def per_table_state(command, state): +def parse_per_table_state(all_states_dump): ''' Convert raw iptables-save output into usable datastructure, for reliable comparisons between initial and final states. ''' + lines = filter_and_format_state(all_states_dump) tables = dict() - for t in TABLES: - COMMAND = list(command) - if '*%s' % t in state.splitlines(): - COMMAND.extend(['--table', t]) - dummy, out, dummy = module.run_command(COMMAND, check_rc=True) - out = re.sub(r'(^|\n)(# Generated|# Completed|[*]%s|COMMIT)[^\n]*' % t, r'', out) - out = re.sub(r' *\[[0-9]+:[0-9]+\] *', r'', out) - tables[t] = [tt for tt in out.splitlines() if tt != ''] + current_table = '' + current_list = list() + for line in lines: + if re.match(r'^[*](filter|mangle|nat|raw|security)$', line): + current_table = line[1:] + continue + if line == 'COMMIT': + tables[current_table] = current_list + current_table = '' + current_list = list() + continue + if line.startswith('# '): + continue + current_list.append(line) return tables @@ -458,7 +467,7 @@ def main(): # The issue comes when wanting to restore state from empty iptable-save's # output... what happens when, say: # - no table is specified, and iptables-save's output is only nat table; - # - we give filter's ruleset to iptables-restore, that locks ourselve out + # - we give filter's ruleset to iptables-restore, that locks ourselves out # of the host; # then trying to roll iptables state back to the previous (working) setup # doesn't override current filter table because no filter table is stored @@ -486,7 +495,7 @@ def main(): # Depending on the value of 'table', initref_state may differ from # initial_state. (rc, stdout, stderr) = module.run_command(SAVECOMMAND, check_rc=True) - tables_before = per_table_state(SAVECOMMAND, stdout) + tables_before = parse_per_table_state(stdout) initref_state = filter_and_format_state(stdout) if state == 'saved': @@ -583,14 +592,17 @@ def main(): (rc, stdout, stderr) = module.run_command(SAVECOMMAND, check_rc=True) restored_state = filter_and_format_state(stdout) - + tables_after = parse_per_table_state('\n'.join(restored_state)) if restored_state not in (initref_state, initial_state): - if module.check_mode: - changed = True - else: - tables_after = per_table_state(SAVECOMMAND, stdout) - if tables_after != tables_before: + for table_name, table_content in tables_after.items(): + if table_name not in tables_before: + # Would initialize a table, which doesn't exist yet + changed = True + break + if tables_before[table_name] != table_content: + # Content of some table changes changed = True + break if _back is None or module.check_mode: module.exit_json( @@ -633,7 +645,7 @@ def main(): os.remove(b_back) (rc, stdout, stderr) = module.run_command(SAVECOMMAND, check_rc=True) - tables_rollback = per_table_state(SAVECOMMAND, stdout) + tables_rollback = parse_per_table_state(stdout) msg = ( "Failed to confirm state restored from %s after %ss. " diff --git a/ansible_collections/community/general/plugins/modules/ipwcli_dns.py b/ansible_collections/community/general/plugins/modules/ipwcli_dns.py index 7b05aefb7..3ffad79fb 100644 --- a/ansible_collections/community/general/plugins/modules/ipwcli_dns.py +++ b/ansible_collections/community/general/plugins/modules/ipwcli_dns.py @@ -54,7 +54,7 @@ options: address: description: - The IP address for the A or AAAA record. - - Required for I(type=A) or I(type=AAAA). + - Required for O(type=A) or O(type=AAAA). type: str ttl: description: @@ -80,38 +80,38 @@ options: port: description: - Sets the port of the SRV record. - - Required for I(type=SRV). + - Required for O(type=SRV). type: int target: description: - Sets the target of the SRV record. - - Required for I(type=SRV). + - Required for O(type=SRV). type: str order: description: - Sets the order of the NAPTR record. - - Required for I(type=NAPTR). + - Required for O(type=NAPTR). type: int preference: description: - Sets the preference of the NAPTR record. - - Required for I(type=NAPTR). + - Required for O(type=NAPTR). type: int flags: description: - Sets one of the possible flags of NAPTR record. - - Required for I(type=NAPTR). + - Required for O(type=NAPTR). type: str choices: ['S', 'A', 'U', 'P'] service: description: - Sets the service of the NAPTR record. - - Required for I(type=NAPTR). + - Required for O(type=NAPTR). type: str replacement: description: - Sets the replacement of the NAPTR record. - - Required for I(type=NAPTR). + - Required for O(type=NAPTR). type: str username: description: diff --git a/ansible_collections/community/general/plugins/modules/irc.py b/ansible_collections/community/general/plugins/modules/irc.py index 6cd7bc120..00ff299ee 100644 --- a/ansible_collections/community/general/plugins/modules/irc.py +++ b/ansible_collections/community/general/plugins/modules/irc.py @@ -50,8 +50,7 @@ options: color: type: str description: - - Text color for the message. ("none" is a valid option in 1.6 or later, in 1.6 and prior, the default color is black, not "none"). - Added 11 more colors in version 2.0. + - Text color for the message. default: "none" choices: [ "none", "white", "black", "blue", "green", "red", "brown", "purple", "orange", "yellow", "light_green", "teal", "light_cyan", "light_blue", "pink", "gray", "light_gray"] @@ -79,11 +78,17 @@ options: - Timeout to use while waiting for successful registration and join messages, this is to prevent an endless loop default: 30 - use_ssl: + use_tls: description: - Designates whether TLS/SSL should be used when connecting to the IRC server + - O(use_tls) is available since community.general 8.1.0, before the option + was exlusively called O(use_ssl). The latter is now an alias of O(use_tls). + - B(Note:) for security reasons, you should always set O(use_tls=true) and + O(validate_certs=true) whenever possible. type: bool default: false + aliases: + - use_ssl part: description: - Designates whether user should part from channel after sending message or not. @@ -96,6 +101,16 @@ options: - Text style for the message. Note italic does not work on some clients choices: [ "bold", "underline", "reverse", "italic", "none" ] default: none + validate_certs: + description: + - If set to V(false), the SSL certificates will not be validated. + - This should always be set to V(true). Using V(false) is unsafe and should only be done + if the network between between Ansible and the IRC server is known to be safe. + - B(Note:) for security reasons, you should always set O(use_tls=true) and + O(validate_certs=true) whenever possible. + default: false + type: bool + version_added: 8.1.0 # informational: requirements for nodes requirements: [ socket ] @@ -108,6 +123,8 @@ EXAMPLES = ''' - name: Send a message to an IRC channel from nick ansible community.general.irc: server: irc.example.net + use_tls: true + validate_certs: true channel: #t1 msg: Hello world @@ -116,6 +133,8 @@ EXAMPLES = ''' module: irc port: 6669 server: irc.example.net + use_tls: true + validate_certs: true channel: #t1 msg: 'All finished at {{ ansible_date_time.iso8601 }}' color: red @@ -126,6 +145,8 @@ EXAMPLES = ''' module: irc port: 6669 server: irc.example.net + use_tls: true + validate_certs: true channel: #t1 nick_to: - nick1 @@ -150,7 +171,8 @@ from ansible.module_utils.basic import AnsibleModule def send_msg(msg, server='localhost', port='6667', channel=None, nick_to=None, key=None, topic=None, - nick="ansible", color='none', passwd=False, timeout=30, use_ssl=False, part=True, style=None): + nick="ansible", color='none', passwd=False, timeout=30, use_tls=False, validate_certs=True, + part=True, style=None): '''send message to IRC''' nick_to = [] if nick_to is None else nick_to @@ -194,8 +216,20 @@ def send_msg(msg, server='localhost', port='6667', channel=None, nick_to=None, k message = styletext + colortext + msg irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - if use_ssl: - irc = ssl.wrap_socket(irc) + if use_tls: + if validate_certs: + try: + context = ssl.create_default_context() + except AttributeError: + raise Exception('Need at least Python 2.7.9 for SSL certificate validation') + else: + if getattr(ssl, 'PROTOCOL_TLS', None) is not None: + # Supported since Python 2.7.13 + context = ssl.SSLContext(ssl.PROTOCOL_TLS) + else: + context = ssl.SSLContext() + context.verify_mode = ssl.CERT_NONE + irc = context.wrap_socket(irc) irc.connect((server, int(port))) if passwd: @@ -275,7 +309,8 @@ def main(): passwd=dict(no_log=True), timeout=dict(type='int', default=30), part=dict(type='bool', default=True), - use_ssl=dict(type='bool', default=False) + use_tls=dict(type='bool', default=False, aliases=['use_ssl']), + validate_certs=dict(type='bool', default=False), ), supports_check_mode=True, required_one_of=[['channel', 'nick_to']] @@ -294,12 +329,13 @@ def main(): key = module.params["key"] passwd = module.params["passwd"] timeout = module.params["timeout"] - use_ssl = module.params["use_ssl"] + use_tls = module.params["use_tls"] part = module.params["part"] style = module.params["style"] + validate_certs = module.params["validate_certs"] try: - send_msg(msg, server, port, channel, nick_to, key, topic, nick, color, passwd, timeout, use_ssl, part, style) + send_msg(msg, server, port, channel, nick_to, key, topic, nick, color, passwd, timeout, use_tls, validate_certs, part, style) except Exception as e: module.fail_json(msg="unable to send to IRC: %s" % to_native(e), exception=traceback.format_exc()) diff --git a/ansible_collections/community/general/plugins/modules/iso_create.py b/ansible_collections/community/general/plugins/modules/iso_create.py index 4b51be96d..c39c710d5 100644 --- a/ansible_collections/community/general/plugins/modules/iso_create.py +++ b/ansible_collections/community/general/plugins/modules/iso_create.py @@ -19,7 +19,6 @@ author: - Diane Wang (@Tomorrow9) requirements: - "pycdlib" - - "python >= 2.7" version_added: '0.2.0' extends_documentation_fragment: @@ -35,7 +34,7 @@ options: src_files: description: - This is a list of absolute paths of source files or folders which will be contained in the new generated ISO file. - - Will fail if specified file or folder in C(src_files) does not exist on local machine. + - Will fail if specified file or folder in O(src_files) does not exist on local machine. - 'Note: With all ISO9660 levels from 1 to 3, all file names are restricted to uppercase letters, numbers and underscores (_). File names are limited to 31 characters, directory nesting is limited to 8 levels, and path names are limited to 255 characters.' @@ -51,9 +50,9 @@ options: interchange_level: description: - The ISO9660 interchange level to use, it dictates the rules on the names of files. - - Levels and valid values C(1), C(2), C(3), C(4) are supported. - - The default value is level C(1), which is the most conservative, level C(3) is recommended. - - ISO9660 file names at interchange level C(1) cannot have more than 8 characters or 3 characters in the extension. + - Levels and valid values V(1), V(2), V(3), V(4) are supported. + - The default value is level V(1), which is the most conservative, level V(3) is recommended. + - ISO9660 file names at interchange level V(1) cannot have more than 8 characters or 3 characters in the extension. type: int default: 1 choices: [1, 2, 3, 4] @@ -64,23 +63,23 @@ options: rock_ridge: description: - Whether to make this ISO have the Rock Ridge extensions or not. - - Valid values are C(1.09), C(1.10) or C(1.12), means adding the specified Rock Ridge version to the ISO. - - If unsure, set C(1.09) to ensure maximum compatibility. + - Valid values are V(1.09), V(1.10) or V(1.12), means adding the specified Rock Ridge version to the ISO. + - If unsure, set V(1.09) to ensure maximum compatibility. - If not specified, then not add Rock Ridge extension to the ISO. type: str choices: ['1.09', '1.10', '1.12'] joliet: description: - - Support levels and valid values are C(1), C(2), or C(3). - - Level C(3) is by far the most common. + - Support levels and valid values are V(1), V(2), or V(3). + - Level V(3) is by far the most common. - If not specified, then no Joliet support is added. type: int choices: [1, 2, 3] udf: description: - Whether to add UDF support to this ISO. - - If set to C(True), then version 2.60 of the UDF spec is used. - - If not specified or set to C(False), then no UDF support is added. + - If set to V(true), then version 2.60 of the UDF spec is used. + - If not specified or set to V(false), then no UDF support is added. type: bool default: false ''' diff --git a/ansible_collections/community/general/plugins/modules/iso_customize.py b/ansible_collections/community/general/plugins/modules/iso_customize.py index 9add080b1..543faaa5e 100644 --- a/ansible_collections/community/general/plugins/modules/iso_customize.py +++ b/ansible_collections/community/general/plugins/modules/iso_customize.py @@ -15,12 +15,11 @@ module: iso_customize short_description: Add/remove/change files in ISO file description: - This module is used to add/remove/change files in ISO file. - - The file inside ISO will be overwritten if it exists by option I(add_files). + - The file inside ISO will be overwritten if it exists by option O(add_files). author: - Yuhua Zou (@ZouYuhua) requirements: - "pycdlib" - - "python >= 2.7" version_added: '5.8.0' extends_documentation_fragment: @@ -70,9 +69,9 @@ options: type: str required: true notes: -- The C(pycdlib) library states it supports Python 2.7 and 3.4 only. +- The C(pycdlib) library states it supports Python 2.7 and 3.4+. - > - The function I(add_file) in pycdlib will overwrite the existing file in ISO with type ISO9660 / Rock Ridge 1.12 / Joliet / UDF. + The function C(add_file) in pycdlib will overwrite the existing file in ISO with type ISO9660 / Rock Ridge 1.12 / Joliet / UDF. But it will not overwrite the existing file in ISO with Rock Ridge 1.09 / 1.10. So we take workaround "delete the existing file and then add file for ISO with Rock Ridge". ''' diff --git a/ansible_collections/community/general/plugins/modules/iso_extract.py b/ansible_collections/community/general/plugins/modules/iso_extract.py index 599cbe4de..087ef2843 100644 --- a/ansible_collections/community/general/plugins/modules/iso_extract.py +++ b/ansible_collections/community/general/plugins/modules/iso_extract.py @@ -58,21 +58,18 @@ options: required: true force: description: - - If C(true), which will replace the remote file when contents are different than the source. - - If C(false), the file will only be extracted and copied if the destination does not already exist. + - If V(true), which will replace the remote file when contents are different than the source. + - If V(false), the file will only be extracted and copied if the destination does not already exist. type: bool default: true executable: description: - The path to the C(7z) executable to use for extracting files from the ISO. - - If not provided, it will assume the value C(7z). + - If not provided, it will assume the value V(7z). type: path notes: - Only the file checksum (content) is taken into account when extracting files - from the ISO image. If I(force=false), only checks the presence of the file. -- In Ansible 2.3 this module was using C(mount) and C(umount) commands only, - requiring root access. This is no longer needed with the introduction of 7zip - for extraction. + from the ISO image. If O(force=false), only checks the presence of the file. ''' EXAMPLES = r''' diff --git a/ansible_collections/community/general/plugins/modules/java_cert.py b/ansible_collections/community/general/plugins/modules/java_cert.py index a188b16c3..72302b12c 100644 --- a/ansible_collections/community/general/plugins/modules/java_cert.py +++ b/ansible_collections/community/general/plugins/modules/java_cert.py @@ -18,6 +18,7 @@ description: and optionally private keys to a given java keystore, or remove them from it. extends_documentation_fragment: - community.general.attributes + - ansible.builtin.files attributes: check_mode: support: full @@ -27,7 +28,7 @@ options: cert_url: description: - Basic URL to fetch SSL certificate from. - - Exactly one of C(cert_url), C(cert_path) or C(pkcs12_path) is required to load certificate. + - Exactly one of O(cert_url), O(cert_path), or O(pkcs12_path) is required to load certificate. type: str cert_port: description: @@ -38,7 +39,7 @@ options: cert_path: description: - Local path to load certificate from. - - Exactly one of C(cert_url), C(cert_path) or C(pkcs12_path) is required to load certificate. + - Exactly one of O(cert_url), O(cert_path), or O(pkcs12_path) is required to load certificate. type: path cert_alias: description: @@ -54,10 +55,10 @@ options: pkcs12_path: description: - Local path to load PKCS12 keystore from. - - Unlike C(cert_url) and C(cert_path), the PKCS12 keystore embeds the private key matching + - Unlike O(cert_url) and O(cert_path), the PKCS12 keystore embeds the private key matching the certificate, and is used to import both the certificate and its private key into the java keystore. - - Exactly one of C(cert_url), C(cert_path) or C(pkcs12_path) is required to load certificate. + - Exactly one of O(cert_url), O(cert_path), or O(pkcs12_path) is required to load certificate. type: path pkcs12_password: description: @@ -98,6 +99,24 @@ options: type: str choices: [ absent, present ] default: present + mode: + version_added: 8.5.0 + owner: + version_added: 8.5.0 + group: + version_added: 8.5.0 + seuser: + version_added: 8.5.0 + serole: + version_added: 8.5.0 + setype: + version_added: 8.5.0 + selevel: + version_added: 8.5.0 + unsafe_writes: + version_added: 8.5.0 + attributes: + version_added: 8.5.0 requirements: [openssl, keytool] author: - Adam Hamsik (@haad) @@ -331,6 +350,12 @@ def build_proxy_options(): return proxy_opts +def _update_permissions(module, keystore_path): + """ Updates keystore file attributes as necessary """ + file_args = module.load_file_common_arguments(module.params, path=keystore_path) + return module.set_fs_attributes_if_different(file_args, False) + + def _download_cert_url(module, executable, url, port): """ Fetches the certificate from the remote URL using `keytool -printcert...` The PEM formatted string is returned """ @@ -375,15 +400,15 @@ def import_pkcs12_path(module, executable, pkcs12_path, pkcs12_pass, pkcs12_alia # Use local certificate from local path and import it to a java keystore (import_rc, import_out, import_err) = module.run_command(import_cmd, data=secret_data, check_rc=False) - diff = {'before': '\n', 'after': '%s\n' % keystore_alias} - if import_rc == 0 and os.path.exists(keystore_path): - module.exit_json(changed=True, msg=import_out, - rc=import_rc, cmd=import_cmd, stdout=import_out, - error=import_err, diff=diff) - else: + + if import_rc != 0 or not os.path.exists(keystore_path): module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd, error=import_err) + return dict(changed=True, msg=import_out, + rc=import_rc, cmd=import_cmd, stdout=import_out, + error=import_err, diff=diff) + def import_cert_path(module, executable, path, keystore_path, keystore_pass, alias, keystore_type, trust_cacert): ''' Import certificate from path into keystore located on @@ -408,17 +433,17 @@ def import_cert_path(module, executable, path, keystore_path, keystore_pass, ali (import_rc, import_out, import_err) = module.run_command(import_cmd, data="%s\n%s" % (keystore_pass, keystore_pass), check_rc=False) - diff = {'before': '\n', 'after': '%s\n' % alias} - if import_rc == 0: - module.exit_json(changed=True, msg=import_out, - rc=import_rc, cmd=import_cmd, stdout=import_out, - error=import_err, diff=diff) - else: - module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd) + if import_rc != 0: + module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd, error=import_err) + + return dict(changed=True, msg=import_out, + rc=import_rc, cmd=import_cmd, stdout=import_out, + error=import_err, diff=diff) -def delete_cert(module, executable, keystore_path, keystore_pass, alias, keystore_type, exit_after=True): + +def delete_cert(module, executable, keystore_path, keystore_pass, alias, keystore_type): ''' Delete certificate identified with alias from keystore on keystore_path ''' del_cmd = [ executable, @@ -434,13 +459,13 @@ def delete_cert(module, executable, keystore_path, keystore_pass, alias, keystor # Delete SSL certificate from keystore (del_rc, del_out, del_err) = module.run_command(del_cmd, data=keystore_pass, check_rc=True) + diff = {'before': '%s\n' % alias, 'after': None} - if exit_after: - diff = {'before': '%s\n' % alias, 'after': None} + if del_rc != 0: + module.fail_json(msg=del_out, rc=del_rc, cmd=del_cmd, error=del_err) - module.exit_json(changed=True, msg=del_out, - rc=del_rc, cmd=del_cmd, stdout=del_out, - error=del_err, diff=diff) + return dict(changed=True, msg=del_out, rc=del_rc, cmd=del_cmd, + stdout=del_out, error=del_err, diff=diff) def test_keytool(module, executable): @@ -485,6 +510,7 @@ def main(): ['cert_url', 'cert_path', 'pkcs12_path'] ], supports_check_mode=True, + add_file_common_args=True, ) url = module.params.get('cert_url') @@ -526,12 +552,14 @@ def main(): module.add_cleanup_file(new_certificate) module.add_cleanup_file(old_certificate) + result = dict() + if state == 'absent' and alias_exists: if module.check_mode: module.exit_json(changed=True) - # delete and exit - delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type) + # delete + result = delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type) # dump certificate to enroll in the keystore on disk and compute digest if state == 'present': @@ -569,16 +597,20 @@ def main(): if alias_exists: # The certificate in the keystore does not match with the one we want to be present # The existing certificate must first be deleted before we insert the correct one - delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type, exit_after=False) + delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type) if pkcs12_path: - import_pkcs12_path(module, executable, pkcs12_path, pkcs12_pass, pkcs12_alias, - keystore_path, keystore_pass, cert_alias, keystore_type) + result = import_pkcs12_path(module, executable, pkcs12_path, pkcs12_pass, pkcs12_alias, + keystore_path, keystore_pass, cert_alias, keystore_type) else: - import_cert_path(module, executable, new_certificate, keystore_path, - keystore_pass, cert_alias, keystore_type, trust_cacert) + result = import_cert_path(module, executable, new_certificate, keystore_path, + keystore_pass, cert_alias, keystore_type, trust_cacert) + + if os.path.exists(keystore_path): + changed_permissions = _update_permissions(module, keystore_path) + result['changed'] = result.get('changed', False) or changed_permissions - module.exit_json(changed=False) + module.exit_json(**result) if __name__ == "__main__": diff --git a/ansible_collections/community/general/plugins/modules/java_keystore.py b/ansible_collections/community/general/plugins/modules/java_keystore.py index 7c2c4884d..2aeab75c0 100644 --- a/ansible_collections/community/general/plugins/modules/java_keystore.py +++ b/ansible_collections/community/general/plugins/modules/java_keystore.py @@ -36,7 +36,7 @@ options: - If the fingerprint of the provided certificate does not match the fingerprint of the certificate bundled in the keystore, the keystore is regenerated with the provided certificate. - - Exactly one of I(certificate) or I(certificate_path) is required. + - Exactly one of O(certificate) or O(certificate_path) is required. type: str certificate_path: description: @@ -44,18 +44,18 @@ options: - If the fingerprint of the provided certificate does not match the fingerprint of the certificate bundled in the keystore, the keystore is regenerated with the provided certificate. - - Exactly one of I(certificate) or I(certificate_path) is required. + - Exactly one of O(certificate) or O(certificate_path) is required. type: path version_added: '3.0.0' private_key: description: - Content of the private key used to create the keystore. - - Exactly one of I(private_key) or I(private_key_path) is required. + - Exactly one of O(private_key) or O(private_key_path) is required. type: str private_key_path: description: - Location of the private key used to create the keystore. - - Exactly one of I(private_key) or I(private_key_path) is required. + - Exactly one of O(private_key) or O(private_key_path) is required. type: path version_added: '3.0.0' private_key_passphrase: @@ -108,13 +108,13 @@ options: - Type of the Java keystore. - When this option is omitted and the keystore doesn't already exist, the behavior follows C(keytool)'s default store type which depends on - Java version; C(pkcs12) since Java 9 and C(jks) prior (may also - be C(pkcs12) if new default has been backported to this version). + Java version; V(pkcs12) since Java 9 and V(jks) prior (may also + be V(pkcs12) if new default has been backported to this version). - When this option is omitted and the keystore already exists, the current type is left untouched, unless another option leads to overwrite the keystore (in that case, this option behaves like for keystore creation). - - When I(keystore_type) is set, the keystore is created with this type if - it doesn't already exist, or is overwritten to match the given type in + - When O(keystore_type) is set, the keystore is created with this type if + it does not already exist, or is overwritten to match the given type in case of mismatch. type: str choices: @@ -122,9 +122,9 @@ options: - pkcs12 version_added: 3.3.0 requirements: - - openssl in PATH (when I(ssl_backend=openssl)) + - openssl in PATH (when O(ssl_backend=openssl)) - keytool in PATH - - cryptography >= 3.0 (when I(ssl_backend=cryptography)) + - cryptography >= 3.0 (when O(ssl_backend=cryptography)) author: - Guillaume Grossetie (@Mogztter) - quidame (@quidame) @@ -135,13 +135,13 @@ seealso: - module: community.crypto.openssl_pkcs12 - module: community.general.java_cert notes: - - I(certificate) and I(private_key) require that their contents are available - on the controller (either inline in a playbook, or with the C(file) lookup), - while I(certificate_path) and I(private_key_path) require that the files are + - O(certificate) and O(private_key) require that their contents are available + on the controller (either inline in a playbook, or with the P(ansible.builtin.file#lookup) lookup), + while O(certificate_path) and O(private_key_path) require that the files are available on the target host. - - By design, any change of a value of options I(keystore_type), I(name) or - I(password), as well as changes of key or certificate materials will cause - the existing I(dest) to be overwritten. + - By design, any change of a value of options O(keystore_type), O(name) or + O(password), as well as changes of key or certificate materials will cause + the existing O(dest) to be overwritten. ''' EXAMPLES = ''' diff --git a/ansible_collections/community/general/plugins/modules/jboss.py b/ansible_collections/community/general/plugins/modules/jboss.py index b389e7e66..3d07a38d6 100644 --- a/ansible_collections/community/general/plugins/modules/jboss.py +++ b/ansible_collections/community/general/plugins/modules/jboss.py @@ -30,8 +30,8 @@ options: src: description: - The remote path of the application ear or war to deploy. - - Required when I(state=present). - - Ignored when I(state=absent). + - Required when O(state=present). + - Ignored when O(state=absent). type: path deploy_path: default: /var/lib/jbossas/standalone/deployments @@ -46,7 +46,7 @@ options: type: str notes: - The JBoss standalone deployment-scanner has to be enabled in standalone.xml - - The module can wait until I(deployment) file is deployed/undeployed by deployment-scanner. + - The module can wait until O(deployment) file is deployed/undeployed by deployment-scanner. Duration of waiting time depends on scan-interval parameter from standalone.xml. - Ensure no identically named application is deployed through the JBoss CLI seealso: diff --git a/ansible_collections/community/general/plugins/modules/jenkins_build.py b/ansible_collections/community/general/plugins/modules/jenkins_build.py index 4f9520224..6d830849e 100644 --- a/ansible_collections/community/general/plugins/modules/jenkins_build.py +++ b/ansible_collections/community/general/plugins/modules/jenkins_build.py @@ -20,6 +20,7 @@ requirements: author: - Brett Milford (@brettmilford) - Tong He (@unnecessary-username) + - Juan Casanova (@juanmcasanova) extends_documentation_fragment: - community.general.attributes attributes: @@ -48,7 +49,7 @@ options: state: description: - Attribute that specifies if the build is to be created, deleted or stopped. - - The C(stopped) state has been added in community.general 3.3.0. + - The V(stopped) state has been added in community.general 3.3.0. default: present choices: ['present', 'absent', 'stopped'] type: str @@ -65,6 +66,19 @@ options: description: - User to authenticate with the Jenkins server. type: str + detach: + description: + - Enable detached mode to not wait for the build end. + default: false + type: bool + version_added: 7.4.0 + time_between_checks: + description: + - Time in seconds to wait between requests to the Jenkins server. + - This times must be higher than the configured quiet time for the job. + default: 10 + type: int + version_added: 7.4.0 ''' EXAMPLES = ''' @@ -152,6 +166,8 @@ class JenkinsBuild: self.user = module.params.get('user') self.jenkins_url = module.params.get('url') self.build_number = module.params.get('build_number') + self.detach = module.params.get('detach') + self.time_between_checks = module.params.get('time_between_checks') self.server = self.get_jenkins_connection() self.result = { @@ -235,7 +251,14 @@ class JenkinsBuild: build_status = self.get_build_status() if build_status['result'] is None: - sleep(10) + # If detached mode is active mark as success, we wouldn't be able to get here if it didn't exist + if self.detach: + result['changed'] = True + result['build_info'] = build_status + + return result + + sleep(self.time_between_checks) self.get_result() else: if self.state == "stopped" and build_status['result'] == "ABORTED": @@ -273,6 +296,8 @@ def main(): token=dict(no_log=True), url=dict(default="http://localhost:8080"), user=dict(), + detach=dict(type='bool', default=False), + time_between_checks=dict(type='int', default=10), ), mutually_exclusive=[['password', 'token']], required_if=[['state', 'absent', ['build_number'], True], ['state', 'stopped', ['build_number'], True]], @@ -288,7 +313,7 @@ def main(): else: jenkins_build.absent_build() - sleep(10) + sleep(jenkins_build.time_between_checks) result = jenkins_build.get_result() module.exit_json(**result) diff --git a/ansible_collections/community/general/plugins/modules/jenkins_build_info.py b/ansible_collections/community/general/plugins/modules/jenkins_build_info.py new file mode 100644 index 000000000..eae6eb937 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/jenkins_build_info.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: jenkins_build_info +short_description: Get information about Jenkins builds +version_added: 7.4.0 +description: + - Get information about Jenkins builds with Jenkins REST API. +requirements: + - "python-jenkins >= 0.4.12" +author: + - Juan Casanova (@juanmcasanova) +extends_documentation_fragment: + - community.general.attributes + - community.general.attributes.info_module +options: + name: + description: + - Name of the Jenkins job to which the build belongs. + required: true + type: str + build_number: + description: + - An integer which specifies a build of a job. + - If not specified the last build information will be returned. + type: int + password: + description: + - Password to authenticate with the Jenkins server. + type: str + token: + description: + - API token used to authenticate with the Jenkins server. + type: str + url: + description: + - URL of the Jenkins server. + default: http://localhost:8080 + type: str + user: + description: + - User to authenticate with the Jenkins server. + type: str +''' + +EXAMPLES = ''' +- name: Get information about a jenkins build using basic authentication + community.general.jenkins_build_info: + name: "test-check" + build_number: 1 + user: admin + password: asdfg + url: http://localhost:8080 + +- name: Get information about a jenkins build anonymously + community.general.jenkins_build_info: + name: "stop-check" + build_number: 3 + url: http://localhost:8080 + +- name: Get information about a jenkins build using token authentication + community.general.jenkins_build_info: + name: "delete-experiment" + build_number: 30 + user: Jenkins + token: abcdefghijklmnopqrstuvwxyz123456 + url: http://localhost:8080 +''' + +RETURN = ''' +--- +name: + description: Name of the jenkins job. + returned: success + type: str + sample: "test-job" +state: + description: State of the jenkins job. + returned: success + type: str + sample: present +user: + description: User used for authentication. + returned: success + type: str + sample: admin +url: + description: URL to connect to the Jenkins server. + returned: success + type: str + sample: https://jenkins.mydomain.com +build_info: + description: Build info of the jenkins job. + returned: success + type: dict +''' + +import traceback + +JENKINS_IMP_ERR = None +try: + import jenkins + python_jenkins_installed = True +except ImportError: + JENKINS_IMP_ERR = traceback.format_exc() + python_jenkins_installed = False + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_native + + +class JenkinsBuildInfo: + + def __init__(self, module): + self.module = module + + self.name = module.params.get('name') + self.password = module.params.get('password') + self.token = module.params.get('token') + self.user = module.params.get('user') + self.jenkins_url = module.params.get('url') + self.build_number = module.params.get('build_number') + self.server = self.get_jenkins_connection() + + self.result = { + 'changed': False, + 'url': self.jenkins_url, + 'name': self.name, + 'user': self.user, + } + + def get_jenkins_connection(self): + try: + if (self.user and self.password): + return jenkins.Jenkins(self.jenkins_url, self.user, self.password) + elif (self.user and self.token): + return jenkins.Jenkins(self.jenkins_url, self.user, self.token) + elif (self.user and not (self.password or self.token)): + return jenkins.Jenkins(self.jenkins_url, self.user) + else: + return jenkins.Jenkins(self.jenkins_url) + except Exception as e: + self.module.fail_json(msg='Unable to connect to Jenkins server, %s' % to_native(e)) + + def get_build_status(self): + try: + if self.build_number is None: + job_info = self.server.get_job_info(self.name) + self.build_number = job_info['lastBuild']['number'] + + return self.server.get_build_info(self.name, self.build_number) + except jenkins.JenkinsException as e: + response = {} + response["result"] = "ABSENT" + return response + except Exception as e: + self.module.fail_json(msg='Unable to fetch build information, %s' % to_native(e), + exception=traceback.format_exc()) + + def get_result(self): + result = self.result + build_status = self.get_build_status() + + if build_status['result'] == "ABSENT": + result['failed'] = True + result['build_info'] = build_status + + return result + + +def test_dependencies(module): + if not python_jenkins_installed: + module.fail_json( + msg=missing_required_lib("python-jenkins", + url="https://python-jenkins.readthedocs.io/en/latest/install.html"), + exception=JENKINS_IMP_ERR) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + build_number=dict(type='int'), + name=dict(required=True), + password=dict(no_log=True), + token=dict(no_log=True), + url=dict(default="http://localhost:8080"), + user=dict(), + ), + mutually_exclusive=[['password', 'token']], + supports_check_mode=True, + ) + + test_dependencies(module) + jenkins_build_info = JenkinsBuildInfo(module) + + result = jenkins_build_info.get_result() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/jenkins_job.py b/ansible_collections/community/general/plugins/modules/jenkins_job.py index 09b006448..e8301041f 100644 --- a/ansible_collections/community/general/plugins/modules/jenkins_job.py +++ b/ansible_collections/community/general/plugins/modules/jenkins_job.py @@ -30,14 +30,14 @@ options: description: - config in XML format. - Required if job does not yet exist. - - Mutually exclusive with I(enabled). - - Considered if I(state=present). + - Mutually exclusive with O(enabled). + - Considered if O(state=present). required: false enabled: description: - Whether the job should be enabled or disabled. - - Mutually exclusive with I(config). - - Considered if I(state=present). + - Mutually exclusive with O(config). + - Considered if O(state=present). type: bool required: false name: @@ -77,10 +77,10 @@ options: type: bool default: true description: - - If set to C(false), the SSL certificates will not be validated. - This should only set to C(false) used on personally controlled sites + - If set to V(false), the SSL certificates will not be validated. + This should only set to V(false) used on personally controlled sites using self-signed certificates as it avoids verifying the source site. - - The C(python-jenkins) library only handles this by using the environment variable C(PYTHONHTTPSVERIFY). + - The C(python-jenkins) library only handles this by using the environment variable E(PYTHONHTTPSVERIFY). version_added: 2.3.0 ''' diff --git a/ansible_collections/community/general/plugins/modules/jenkins_job_info.py b/ansible_collections/community/general/plugins/modules/jenkins_job_info.py index ba6a53117..40e1d7aea 100644 --- a/ansible_collections/community/general/plugins/modules/jenkins_job_info.py +++ b/ansible_collections/community/general/plugins/modules/jenkins_job_info.py @@ -15,7 +15,6 @@ module: jenkins_job_info short_description: Get information about Jenkins jobs description: - This module can be used to query information about which Jenkins jobs which already exists. - - This module was called C(jenkins_job_info) before Ansible 2.9. The usage did not change. requirements: - "python-jenkins >= 0.4.12" extends_documentation_fragment: @@ -38,12 +37,12 @@ options: type: str description: - Password to authenticate with the Jenkins server. - - This is mutually exclusive with I(token). + - This is mutually exclusive with O(token). token: type: str description: - API token used to authenticate with the Jenkins server. - - This is mutually exclusive with I(password). + - This is mutually exclusive with O(password). url: type: str description: @@ -55,8 +54,8 @@ options: - User to authenticate with the Jenkins server. validate_certs: description: - - If set to C(False), the SSL certificates will not be validated. - - This should only set to C(False) used on personally controlled sites using self-signed certificates. + - If set to V(false), the SSL certificates will not be validated. + - This should only set to V(false) used on personally controlled sites using self-signed certificates. default: true type: bool author: @@ -122,7 +121,6 @@ EXAMPLES = ''' user: admin token: 126df5c60d66c66e3b75b11104a16a8a url: https://jenkins.example.com - validate_certs: false register: my_jenkins_job_info ''' diff --git a/ansible_collections/community/general/plugins/modules/jenkins_plugin.py b/ansible_collections/community/general/plugins/modules/jenkins_plugin.py index 2fbc83e03..13a804a50 100644 --- a/ansible_collections/community/general/plugins/modules/jenkins_plugin.py +++ b/ansible_collections/community/general/plugins/modules/jenkins_plugin.py @@ -27,7 +27,7 @@ options: group: type: str description: - - Name of the Jenkins group on the OS. + - GID or name of the Jenkins group on the OS. default: jenkins jenkins_home: type: path @@ -47,13 +47,13 @@ options: owner: type: str description: - - Name of the Jenkins user on the OS. + - UID or name of the Jenkins user on the OS. default: jenkins state: type: str description: - Desired plugin state. - - If the C(latest) is set, the check for new version will be performed + - If set to V(latest), the check for new version will be performed every time. This is suitable to keep the plugin up-to-date. choices: [absent, present, pinned, unpinned, enabled, disabled, latest] default: present @@ -65,18 +65,18 @@ options: updates_expiration: type: int description: - - Number of seconds after which a new copy of the I(update-center.json) + - Number of seconds after which a new copy of the C(update-center.json) file is downloaded. This is used to avoid the need to download the - plugin to calculate its checksum when C(latest) is specified. - - Set it to C(0) if no cache file should be used. In that case, the + plugin to calculate its checksum when O(state=latest) is specified. + - Set it to V(0) if no cache file should be used. In that case, the plugin file will always be downloaded to calculate its checksum when - C(latest) is specified. + O(state=latest) is specified. default: 86400 updates_url: type: list elements: str description: - - A list of base URL(s) to retrieve I(update-center.json), and direct plugin files from. + - A list of base URL(s) to retrieve C(update-center.json), and direct plugin files from. - This can be a list since community.general 3.3.0. default: ['https://updates.jenkins.io', 'http://mirrors.jenkins.io'] update_json_url_segment: @@ -90,14 +90,14 @@ options: type: list elements: str description: - - Path inside the I(updates_url) to get latest plugins from. + - Path inside the O(updates_url) to get latest plugins from. default: ['latest'] version_added: 3.3.0 versioned_plugins_url_segments: type: list elements: str description: - - Path inside the I(updates_url) to get specific version of plugins from. + - Path inside the O(updates_url) to get specific version of plugins from. default: ['download/plugins', 'plugins'] version_added: 3.3.0 url: @@ -114,11 +114,11 @@ options: - It might take longer to verify that the correct version is installed. This is especially true if a specific version number is specified. - Quote the version to prevent the value to be interpreted as float. For - example if C(1.20) would be unquoted, it would become C(1.2). + example if V(1.20) would be unquoted, it would become V(1.2). with_dependencies: description: - Defines whether to install plugin dependencies. - - This option takes effect only if the I(version) is not defined. + - This option takes effect only if the O(version) is not defined. type: bool default: true @@ -127,11 +127,11 @@ notes: the plugin files on the disk. Only if the plugin is not installed yet and no version is specified, the API installation is performed which requires only the Web UI credentials. - - It's necessary to notify the handler or call the I(service) module to + - It is necessary to notify the handler or call the M(ansible.builtin.service) module to restart the Jenkins service after a new plugin was installed. - Pinning works only if the plugin is installed and Jenkins service was successfully restarted after the plugin installation. - - It is not possible to run the module remotely by changing the I(url) + - It is not possible to run the module remotely by changing the O(url) parameter to point to the Jenkins server. The module must be used on the host where Jenkins runs as it needs direct access to the plugin files. extends_documentation_fragment: @@ -195,6 +195,29 @@ EXAMPLES = ''' url_password: p4ssw0rd url: http://localhost:8888 +# +# Example of how to authenticate with serverless deployment +# +- name: Update plugins on ECS Fargate Jenkins instance + community.general.jenkins_plugin: + # plugin name and version + name: ws-cleanup + version: '0.45' + # Jenkins home path mounted on ec2-helper VM (example) + jenkins_home: "/mnt/{{ jenkins_instance }}" + # matching the UID/GID to one in official Jenkins image + owner: 1000 + group: 1000 + # Jenkins instance URL and admin credentials + url: "https://{{ jenkins_instance }}.com/" + url_username: admin + url_password: p4ssw0rd + # make module work from EC2 which has local access + # to EFS mount as well as Jenkins URL + delegate_to: ec2-helper + vars: + jenkins_instance: foobar + # # Example of a Play which handles Jenkins restarts during the state changes # diff --git a/ansible_collections/community/general/plugins/modules/jenkins_script.py b/ansible_collections/community/general/plugins/modules/jenkins_script.py index 7f83ebcdb..030c8e6fa 100644 --- a/ansible_collections/community/general/plugins/modules/jenkins_script.py +++ b/ansible_collections/community/general/plugins/modules/jenkins_script.py @@ -42,8 +42,8 @@ options: default: http://localhost:8080 validate_certs: description: - - If set to C(false), the SSL certificates will not be validated. - This should only set to C(false) used on personally controlled sites + - If set to V(false), the SSL certificates will not be validated. + This should only set to V(false) used on personally controlled sites using self-signed certificates as it avoids verifying the source site. type: bool default: true @@ -99,7 +99,7 @@ EXAMPLES = ''' user: admin password: admin url: https://localhost - validate_certs: false + validate_certs: false # only do this when you trust the network! ''' RETURN = ''' diff --git a/ansible_collections/community/general/plugins/modules/jira.py b/ansible_collections/community/general/plugins/modules/jira.py index 85097c4b7..c36cf9937 100644 --- a/ansible_collections/community/general/plugins/modules/jira.py +++ b/ansible_collections/community/general/plugins/modules/jira.py @@ -44,25 +44,25 @@ options: choices: [ attach, comment, create, edit, fetch, link, search, transition, update, worklog ] description: - The operation to perform. - - C(worklog) was added in community.genereal 6.5.0. + - V(worklog) was added in community.general 6.5.0. username: type: str description: - The username to log-in with. - - Must be used with I(password). Mutually exclusive with I(token). + - Must be used with O(password). Mutually exclusive with O(token). password: type: str description: - The password to log-in with. - - Must be used with I(username). Mutually exclusive with I(token). + - Must be used with O(username). Mutually exclusive with O(token). token: type: str description: - The personal access token to log-in with. - - Mutually exclusive with I(username) and I(password). + - Mutually exclusive with O(username) and O(password). version_added: 4.2.0 project: @@ -128,20 +128,20 @@ options: type: str required: false description: - - Only used when I(operation) is C(transition), and a bit of a misnomer, it actually refers to the transition name. + - Only used when O(operation) is V(transition), and a bit of a misnomer, it actually refers to the transition name. assignee: type: str required: false description: - - Sets the the assignee when I(operation) is C(create), C(transition) or C(edit). - - Recent versions of JIRA no longer accept a user name as a user identifier. In that case, use I(account_id) instead. + - Sets the the assignee when O(operation) is V(create), V(transition), or V(edit). + - Recent versions of JIRA no longer accept a user name as a user identifier. In that case, use O(account_id) instead. - Note that JIRA may not allow changing field values on specific transitions or states. account_id: type: str description: - - Sets the account identifier for the assignee when I(operation) is C(create), C(transition) or C(edit). + - Sets the account identifier for the assignee when O(operation) is V(create), V(transition), or V(edit). - Note that JIRA may not allow changing field values on specific transitions or states. version_added: 2.5.0 @@ -183,8 +183,8 @@ options: maxresults: required: false description: - - Limit the result of I(operation=search). If no value is specified, the default jira limit will be used. - - Used when I(operation=search) only, ignored otherwise. + - Limit the result of O(operation=search). If no value is specified, the default jira limit will be used. + - Used when O(operation=search) only, ignored otherwise. type: int version_added: '0.2.0' @@ -198,7 +198,7 @@ options: validate_certs: required: false description: - - Require valid SSL certificates (set to C(false) if you'd like to use self-signed certificates) + - Require valid SSL certificates (set to V(false) if you would like to use self-signed certificates) default: true type: bool @@ -212,12 +212,12 @@ options: required: true type: path description: - - The path to the file to upload (from the remote node) or, if I(content) is specified, + - The path to the file to upload (from the remote node) or, if O(attachment.content) is specified, the filename to use for the attachment. content: type: str description: - - The Base64 encoded contents of the file to attach. If not specified, the contents of I(filename) will be + - The Base64 encoded contents of the file to attach. If not specified, the contents of O(attachment.filename) will be used instead. mimetype: type: str @@ -227,7 +227,7 @@ options: notes: - "Currently this only works with basic-auth, or tokens." - - "To use with JIRA Cloud, pass the login e-mail as the I(username) and the API token as I(password)." + - "To use with JIRA Cloud, pass the login e-mail as the O(username) and the API token as O(password)." author: - "Steve Smith (@tarka)" @@ -799,7 +799,7 @@ class JIRA(StateModuleHelper): if msg: self.module.fail_json(msg=', '.join(msg)) self.module.fail_json(msg=to_native(error)) - # Fallback print body, if it cant be decoded + # Fallback print body, if it can't be decoded self.module.fail_json(msg=to_native(info['body'])) body = response.read() diff --git a/ansible_collections/community/general/plugins/modules/kdeconfig.py b/ansible_collections/community/general/plugins/modules/kdeconfig.py index 42a08dd64..4e8d39521 100644 --- a/ansible_collections/community/general/plugins/modules/kdeconfig.py +++ b/ansible_collections/community/general/plugins/modules/kdeconfig.py @@ -35,11 +35,11 @@ options: suboptions: group: description: - - The option's group. One between this and I(groups) is required. + - The option's group. One between this and O(values[].groups) is required. type: str groups: description: - - List of the option's groups. One between this and I(group) is required. + - List of the option's groups. One between this and O(values[].group) is required. type: list elements: str key: @@ -49,12 +49,12 @@ options: required: true value: description: - - The option's value. One between this and I(bool_value) is required. + - The option's value. One between this and O(values[].bool_value) is required. type: str bool_value: description: - Boolean value. - - One between this and I(value) is required. + - One between this and O(values[].value) is required. type: bool required: true backup: diff --git a/ansible_collections/community/general/plugins/modules/kernel_blacklist.py b/ansible_collections/community/general/plugins/modules/kernel_blacklist.py index 1b40999ca..b5bd90403 100644 --- a/ansible_collections/community/general/plugins/modules/kernel_blacklist.py +++ b/ansible_collections/community/general/plugins/modules/kernel_blacklist.py @@ -53,7 +53,6 @@ EXAMPLES = ''' import os import re -import tempfile from ansible_collections.community.general.plugins.module_utils.module_helper import StateModuleHelper @@ -106,16 +105,10 @@ class Blacklist(StateModuleHelper): def __quit_module__(self): if self.has_changed() and not self.module.check_mode: - dummy, tmpfile = tempfile.mkstemp() - try: - os.remove(tmpfile) - self.module.preserved_copy(self.vars.filename, tmpfile) # ensure right perms/ownership - with open(tmpfile, 'w') as fd: - fd.writelines(["{0}\n".format(x) for x in self.vars.lines]) - self.module.atomic_move(tmpfile, self.vars.filename) - finally: - if os.path.exists(tmpfile): - os.remove(tmpfile) + bkp = self.module.backup_local(self.vars.filename) + with open(self.vars.filename, "w") as fd: + fd.writelines(["{0}\n".format(x) for x in self.vars.lines]) + self.module.add_cleanup_file(bkp) def main(): diff --git a/ansible_collections/community/general/plugins/modules/keycloak_authentication.py b/ansible_collections/community/general/plugins/modules/keycloak_authentication.py index 6143d9d5c..bc2898d9b 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_authentication.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_authentication.py @@ -43,6 +43,7 @@ options: providerId: description: - C(providerId) for the new flow when not copied from an existing flow. + choices: [ "basic-flow", "client-flow" ] type: str copyFrom: description: @@ -97,7 +98,7 @@ options: type: bool default: false description: - - If C(true), allows to remove the authentication flow and recreate it. + - If V(true), allows to remove the authentication flow and recreate it. extends_documentation_fragment: - community.general.keycloak @@ -109,77 +110,77 @@ author: ''' EXAMPLES = ''' - - name: Create an authentication flow from first broker login and add an execution to it. - community.general.keycloak_authentication: - auth_keycloak_url: http://localhost:8080/auth - auth_realm: master - auth_username: admin - auth_password: password - realm: master - alias: "Copy of first broker login" - copyFrom: "first broker login" - authenticationExecutions: - - providerId: "test-execution1" - requirement: "REQUIRED" - authenticationConfig: - alias: "test.execution1.property" - config: - test1.property: "value" - - providerId: "test-execution2" - requirement: "REQUIRED" - authenticationConfig: - alias: "test.execution2.property" - config: - test2.property: "value" - state: present - - - name: Re-create the authentication flow - community.general.keycloak_authentication: - auth_keycloak_url: http://localhost:8080/auth - auth_realm: master - auth_username: admin - auth_password: password - realm: master - alias: "Copy of first broker login" - copyFrom: "first broker login" - authenticationExecutions: - - providerId: "test-provisioning" - requirement: "REQUIRED" - authenticationConfig: - alias: "test.provisioning.property" - config: - test.provisioning.property: "value" - state: present - force: true - - - name: Create an authentication flow with subflow containing an execution. - community.general.keycloak_authentication: - auth_keycloak_url: http://localhost:8080/auth - auth_realm: master - auth_username: admin - auth_password: password - realm: master - alias: "Copy of first broker login" - copyFrom: "first broker login" - authenticationExecutions: - - providerId: "test-execution1" - requirement: "REQUIRED" - - displayName: "New Subflow" - requirement: "REQUIRED" - - providerId: "auth-cookie" - requirement: "REQUIRED" - flowAlias: "New Sublow" - state: present - - - name: Remove authentication. - community.general.keycloak_authentication: - auth_keycloak_url: http://localhost:8080/auth - auth_realm: master - auth_username: admin - auth_password: password - realm: master - alias: "Copy of first broker login" - state: absent +- name: Create an authentication flow from first broker login and add an execution to it. + community.general.keycloak_authentication: + auth_keycloak_url: http://localhost:8080/auth + auth_realm: master + auth_username: admin + auth_password: password + realm: master + alias: "Copy of first broker login" + copyFrom: "first broker login" + authenticationExecutions: + - providerId: "test-execution1" + requirement: "REQUIRED" + authenticationConfig: + alias: "test.execution1.property" + config: + test1.property: "value" + - providerId: "test-execution2" + requirement: "REQUIRED" + authenticationConfig: + alias: "test.execution2.property" + config: + test2.property: "value" + state: present + +- name: Re-create the authentication flow + community.general.keycloak_authentication: + auth_keycloak_url: http://localhost:8080/auth + auth_realm: master + auth_username: admin + auth_password: password + realm: master + alias: "Copy of first broker login" + copyFrom: "first broker login" + authenticationExecutions: + - providerId: "test-provisioning" + requirement: "REQUIRED" + authenticationConfig: + alias: "test.provisioning.property" + config: + test.provisioning.property: "value" + state: present + force: true + +- name: Create an authentication flow with subflow containing an execution. + community.general.keycloak_authentication: + auth_keycloak_url: http://localhost:8080/auth + auth_realm: master + auth_username: admin + auth_password: password + realm: master + alias: "Copy of first broker login" + copyFrom: "first broker login" + authenticationExecutions: + - providerId: "test-execution1" + requirement: "REQUIRED" + - displayName: "New Subflow" + requirement: "REQUIRED" + - providerId: "auth-cookie" + requirement: "REQUIRED" + flowAlias: "New Sublow" + state: present + +- name: Remove authentication. + community.general.keycloak_authentication: + auth_keycloak_url: http://localhost:8080/auth + auth_realm: master + auth_username: admin + auth_password: password + realm: master + alias: "Copy of first broker login" + state: absent ''' RETURN = ''' @@ -279,6 +280,8 @@ def create_or_update_executions(kc, config, realm='master'): # Compare the executions to see if it need changes if not is_struct_included(new_exec, existing_executions[exec_index], exclude_key) or exec_index != new_exec_index: exec_found = True + if new_exec['index'] is None: + new_exec_index = exec_index before += str(existing_executions[exec_index]) + '\n' id_to_update = existing_executions[exec_index]["id"] # Remove exec from list in case 2 exec with same name @@ -331,7 +334,7 @@ def main(): meta_args = dict( realm=dict(type='str', required=True), alias=dict(type='str', required=True), - providerId=dict(type='str'), + providerId=dict(type='str', choices=["basic-flow", "client-flow"]), description=dict(type='str'), copyFrom=dict(type='str'), authenticationExecutions=dict(type='list', elements='dict', diff --git a/ansible_collections/community/general/plugins/modules/keycloak_authentication_required_actions.py b/ansible_collections/community/general/plugins/modules/keycloak_authentication_required_actions.py new file mode 100644 index 000000000..5ffbd2033 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/keycloak_authentication_required_actions.py @@ -0,0 +1,457 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Eike Frost +# Copyright (c) 2021, Christophe Gilles +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_authentication_required_actions + +short_description: Allows administration of Keycloak authentication required actions + +description: + - This module can register, update and delete required actions. + - It also filters out any duplicate required actions by their alias. The first occurrence is preserved. + +version_added: 7.1.0 + +attributes: + check_mode: + support: full + diff_mode: + support: full + +options: + realm: + description: + - The name of the realm in which are the authentication required actions. + required: true + type: str + required_actions: + elements: dict + description: + - Authentication required action. + suboptions: + alias: + description: + - Unique name of the required action. + required: true + type: str + config: + description: + - Configuration for the required action. + type: dict + defaultAction: + description: + - Indicates, if any new user will have the required action assigned to it. + type: bool + enabled: + description: + - Indicates, if the required action is enabled or not. + type: bool + name: + description: + - Displayed name of the required action. Required for registration. + type: str + priority: + description: + - Priority of the required action. + type: int + providerId: + description: + - Provider ID of the required action. Required for registration. + type: str + type: list + state: + choices: [ "absent", "present" ] + description: + - Control if the realm authentication required actions are going to be registered/updated (V(present)) or deleted (V(absent)). + required: true + type: str + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + +author: + - Skrekulko (@Skrekulko) +''' + +EXAMPLES = ''' +- name: Register a new required action. + community.general.keycloak_authentication_required_actions: + auth_client_id: "admin-cli" + auth_keycloak_url: "http://localhost:8080" + auth_password: "password" + auth_realm: "master" + auth_username: "admin" + realm: "master" + required_action: + - alias: "TERMS_AND_CONDITIONS" + name: "Terms and conditions" + providerId: "TERMS_AND_CONDITIONS" + enabled: true + state: "present" + +- name: Update the newly registered required action. + community.general.keycloak_authentication_required_actions: + auth_client_id: "admin-cli" + auth_keycloak_url: "http://localhost:8080" + auth_password: "password" + auth_realm: "master" + auth_username: "admin" + realm: "master" + required_action: + - alias: "TERMS_AND_CONDITIONS" + enabled: false + state: "present" + +- name: Delete the updated registered required action. + community.general.keycloak_authentication_required_actions: + auth_client_id: "admin-cli" + auth_keycloak_url: "http://localhost:8080" + auth_password: "password" + auth_realm: "master" + auth_username: "admin" + realm: "master" + required_action: + - alias: "TERMS_AND_CONDITIONS" + state: "absent" +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + +end_state: + description: Representation of the authentication required actions after module execution. + returned: on success + type: complex + contains: + alias: + description: + - Unique name of the required action. + sample: test-provider-id + type: str + config: + description: + - Configuration for the required action. + sample: {} + type: dict + defaultAction: + description: + - Indicates, if any new user will have the required action assigned to it. + sample: false + type: bool + enabled: + description: + - Indicates, if the required action is enabled or not. + sample: false + type: bool + name: + description: + - Displayed name of the required action. Required for registration. + sample: Test provider ID + type: str + priority: + description: + - Priority of the required action. + sample: 90 + type: int + providerId: + description: + - Provider ID of the required action. Required for registration. + sample: test-provider-id + type: str + +''' + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \ + keycloak_argument_spec, get_token, KeycloakError +from ansible.module_utils.basic import AnsibleModule + + +def sanitize_required_actions(objects): + for obj in objects: + alias = obj['alias'] + name = obj['name'] + provider_id = obj['providerId'] + + if not name: + obj['name'] = alias + + if provider_id != alias: + obj['providerId'] = alias + + return objects + + +def filter_duplicates(objects): + filtered_objects = {} + + for obj in objects: + alias = obj["alias"] + + if alias not in filtered_objects: + filtered_objects[alias] = obj + + return list(filtered_objects.values()) + + +def main(): + """ + Module execution + + :return: + """ + argument_spec = keycloak_argument_spec() + + meta_args = dict( + realm=dict(type='str', required=True), + required_actions=dict( + type='list', + elements='dict', + options=dict( + alias=dict(type='str', required=True), + config=dict(type='dict'), + defaultAction=dict(type='bool'), + enabled=dict(type='bool'), + name=dict(type='str'), + priority=dict(type='int'), + providerId=dict(type='str') + ) + ), + state=dict(type='str', choices=['present', 'absent'], required=True) + ) + + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=([['token', 'auth_realm', 'auth_username', 'auth_password']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']]) + ) + + result = dict(changed=False, msg='', end_state={}, diff=dict(before={}, after={})) + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + # Convenience variables + realm = module.params.get('realm') + desired_required_actions = module.params.get('required_actions') + state = module.params.get('state') + + # Sanitize required actions + desired_required_actions = sanitize_required_actions(desired_required_actions) + + # Filter out duplicate required actions + desired_required_actions = filter_duplicates(desired_required_actions) + + # Get required actions + before_required_actions = kc.get_required_actions(realm=realm) + + if state == 'present': + # Initialize empty lists to hold the required actions that need to be + # registered, updated, and original ones of the updated one + register_required_actions = [] + before_updated_required_actions = [] + updated_required_actions = [] + + # Loop through the desired required actions and check if they exist in the before required actions + for desired_required_action in desired_required_actions: + found = False + + # Loop through the before required actions and check if the aliases match + for before_required_action in before_required_actions: + if desired_required_action['alias'] == before_required_action['alias']: + update_required = False + + # Fill in the parameters + for k, v in before_required_action.items(): + if k not in desired_required_action or desired_required_action[k] is None: + desired_required_action[k] = v + + # Loop through the keys of the desired and before required actions + # and check if there are any differences between them + for key in desired_required_action.keys(): + if key in before_required_action and desired_required_action[key] != before_required_action[key]: + update_required = True + break + + # If there are differences, add the before and desired required actions + # to their respective lists for updating + if update_required: + before_updated_required_actions.append(before_required_action) + updated_required_actions.append(desired_required_action) + found = True + break + # If the desired required action is not found in the before required actions, + # add it to the list of required actions to register + if not found: + # Check if name is provided + if 'name' not in desired_required_action or desired_required_action['name'] is None: + module.fail_json( + msg='Unable to register required action %s in realm %s: name not included' + % (desired_required_action['alias'], realm) + ) + + # Check if provider ID is provided + if 'providerId' not in desired_required_action or desired_required_action['providerId'] is None: + module.fail_json( + msg='Unable to register required action %s in realm %s: providerId not included' + % (desired_required_action['alias'], realm) + ) + + register_required_actions.append(desired_required_action) + + # Handle diff + if module._diff: + diff_required_actions = updated_required_actions.copy() + diff_required_actions.extend(register_required_actions) + + result['diff'] = dict( + before=before_updated_required_actions, + after=diff_required_actions + ) + + # Handle changed + if register_required_actions or updated_required_actions: + result['changed'] = True + + # Handle check mode + if module.check_mode: + if register_required_actions or updated_required_actions: + result['change'] = True + result['msg'] = 'Required actions would be registered/updated' + else: + result['change'] = False + result['msg'] = 'Required actions would not be registered/updated' + + module.exit_json(**result) + + # Register required actions + if register_required_actions: + for register_required_action in register_required_actions: + kc.register_required_action(realm=realm, rep=register_required_action) + kc.update_required_action(alias=register_required_action['alias'], realm=realm, rep=register_required_action) + + # Update required actions + if updated_required_actions: + for updated_required_action in updated_required_actions: + kc.update_required_action(alias=updated_required_action['alias'], realm=realm, rep=updated_required_action) + + # Initialize the final list of required actions + final_required_actions = [] + + # Iterate over the before_required_actions + for before_required_action in before_required_actions: + # Check if there is an updated_required_action with the same alias + updated_required_action_found = False + + for updated_required_action in updated_required_actions: + if updated_required_action['alias'] == before_required_action['alias']: + # Merge the two dictionaries, favoring the values from updated_required_action + merged_dict = {} + for key in before_required_action.keys(): + if key in updated_required_action: + merged_dict[key] = updated_required_action[key] + else: + merged_dict[key] = before_required_action[key] + + for key in updated_required_action.keys(): + if key not in before_required_action: + merged_dict[key] = updated_required_action[key] + + # Add the merged dictionary to the final list of required actions + final_required_actions.append(merged_dict) + + # Mark the updated_required_action as found + updated_required_action_found = True + + # Stop looking for updated_required_action + break + + # If no matching updated_required_action was found, add the before_required_action to the final list of required actions + if not updated_required_action_found: + final_required_actions.append(before_required_action) + + # Append any remaining updated_required_actions that were not merged + for updated_required_action in updated_required_actions: + if not any(updated_required_action['alias'] == action['alias'] for action in final_required_actions): + final_required_actions.append(updated_required_action) + + # Append newly registered required actions + final_required_actions.extend(register_required_actions) + + # Handle message and end state + result['msg'] = 'Required actions registered/updated' + result['end_state'] = final_required_actions + else: + # Filter out the deleted required actions + final_required_actions = [] + delete_required_actions = [] + + for before_required_action in before_required_actions: + delete_action = False + + for desired_required_action in desired_required_actions: + if before_required_action['alias'] == desired_required_action['alias']: + delete_action = True + break + + if not delete_action: + final_required_actions.append(before_required_action) + else: + delete_required_actions.append(before_required_action) + + # Handle diff + if module._diff: + result['diff'] = dict( + before=before_required_actions, + after=final_required_actions + ) + + # Handle changed + if delete_required_actions: + result['changed'] = True + + # Handle check mode + if module.check_mode: + if final_required_actions: + result['change'] = True + result['msg'] = 'Required actions would be deleted' + else: + result['change'] = False + result['msg'] = 'Required actions would not be deleted' + + module.exit_json(**result) + + # Delete required actions + if delete_required_actions: + for delete_required_action in delete_required_actions: + kc.delete_required_action(alias=delete_required_action['alias'], realm=realm) + + # Handle message and end state + result['msg'] = 'Required actions deleted' + result['end_state'] = final_required_actions + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/keycloak_authz_authorization_scope.py b/ansible_collections/community/general/plugins/modules/keycloak_authz_authorization_scope.py index c451d3751..5eef9ac76 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_authz_authorization_scope.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_authz_authorization_scope.py @@ -40,8 +40,8 @@ options: state: description: - State of the authorization scope. - - On C(present), the authorization scope will be created (or updated if it exists already). - - On C(absent), the authorization scope will be removed if it exists. + - On V(present), the authorization scope will be created (or updated if it exists already). + - On V(absent), the authorization scope will be removed if it exists. choices: ['present', 'absent'] default: 'present' type: str @@ -108,22 +108,22 @@ end_state: id: description: ID of the authorization scope. type: str - returned: when I(state=present) + returned: when O(state=present) sample: a6ab1cf2-1001-40ec-9f39-48f23b6a0a41 name: description: Name of the authorization scope. type: str - returned: when I(state=present) + returned: when O(state=present) sample: file:delete display_name: description: Display name of the authorization scope. type: str - returned: when I(state=present) + returned: when O(state=present) sample: File delete icon_uri: description: Icon URI for the authorization scope. type: str - returned: when I(state=present) + returned: when O(state=present) sample: http://localhost/icon.png ''' diff --git a/ansible_collections/community/general/plugins/modules/keycloak_authz_custom_policy.py b/ansible_collections/community/general/plugins/modules/keycloak_authz_custom_policy.py new file mode 100644 index 000000000..8363c252e --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/keycloak_authz_custom_policy.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Eike Frost +# Copyright (c) 2021, Christophe Gilles +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_authz_custom_policy + +short_description: Allows administration of Keycloak client custom Javascript policies via Keycloak API + +version_added: 7.5.0 + +description: + - This module allows the administration of Keycloak client custom Javascript via the Keycloak REST + API. Custom Javascript policies are only available if a client has Authorization enabled and if + they have been deployed to the Keycloak server as JAR files. + + - This module requires access to the REST API via OpenID Connect; the user connecting and the realm + being used must have the requisite access rights. In a default Keycloak installation, admin-cli + and an admin user would work, as would a separate realm definition with the scope tailored + to your needs and a user having the expected roles. + + - The names of module options are snake_cased versions of the camelCase options used by Keycloak. + The Authorization Services paths and payloads have not officially been documented by the Keycloak project. + U(https://www.puppeteers.net/blog/keycloak-authorization-services-rest-api-paths-and-payload/) + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + state: + description: + - State of the custom policy. + - On V(present), the custom policy will be created (or updated if it exists already). + - On V(absent), the custom policy will be removed if it exists. + choices: ['present', 'absent'] + default: 'present' + type: str + name: + description: + - Name of the custom policy to create. + type: str + required: true + policy_type: + description: + - The type of the policy. This must match the name of the custom policy deployed to the server. + - Multiple policies pointing to the same policy type can be created, but their names have to differ. + type: str + required: true + client_id: + description: + - The V(clientId) of the Keycloak client that should have the custom policy attached to it. + - This is usually a human-readable name of the Keycloak client. + type: str + required: true + realm: + description: + - The name of the Keycloak realm the Keycloak client is in. + type: str + required: true + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + +author: + - Samuli Seppänen (@mattock) +''' + +EXAMPLES = ''' +- name: Manage Keycloak custom authorization policy + community.general.keycloak_authz_custom_policy: + name: OnlyOwner + state: present + policy_type: script-policy.js + client_id: myclient + realm: myrealm + auth_keycloak_url: http://localhost:8080/auth + auth_username: keycloak + auth_password: keycloak + auth_realm: master +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + +end_state: + description: Representation of the custom policy after module execution. + returned: on success + type: dict + contains: + name: + description: Name of the custom policy. + type: str + returned: when I(state=present) + sample: file:delete + policy_type: + description: Type of custom policy. + type: str + returned: when I(state=present) + sample: File delete + +''' + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \ + keycloak_argument_spec, get_token, KeycloakError +from ansible.module_utils.basic import AnsibleModule + + +def main(): + """ + Module execution + + :return: + """ + argument_spec = keycloak_argument_spec() + + meta_args = dict( + state=dict(type='str', default='present', + choices=['present', 'absent']), + name=dict(type='str', required=True), + policy_type=dict(type='str', required=True), + client_id=dict(type='str', required=True), + realm=dict(type='str', required=True) + ) + + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=( + [['token', 'auth_realm', 'auth_username', 'auth_password']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']])) + + result = dict(changed=False, msg='', end_state={}) + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + # Convenience variables + state = module.params.get('state') + name = module.params.get('name') + policy_type = module.params.get('policy_type') + client_id = module.params.get('client_id') + realm = module.params.get('realm') + + cid = kc.get_client_id(client_id, realm=realm) + if not cid: + module.fail_json(msg='Invalid client %s for realm %s' % + (client_id, realm)) + + before_authz_custom_policy = kc.get_authz_policy_by_name( + name=name, client_id=cid, realm=realm) + + desired_authz_custom_policy = {} + desired_authz_custom_policy['name'] = name + desired_authz_custom_policy['type'] = policy_type + + # Modifying existing custom policies is not possible + if before_authz_custom_policy and state == 'present': + result['msg'] = "Custom policy %s already exists" % (name) + result['changed'] = False + result['end_state'] = desired_authz_custom_policy + elif not before_authz_custom_policy and state == 'present': + if module.check_mode: + result['msg'] = "Would create custom policy %s" % (name) + else: + kc.create_authz_custom_policy( + payload=desired_authz_custom_policy, policy_type=policy_type, client_id=cid, realm=realm) + result['msg'] = "Custom policy %s created" % (name) + + result['changed'] = True + result['end_state'] = desired_authz_custom_policy + elif before_authz_custom_policy and state == 'absent': + if module.check_mode: + result['msg'] = "Would remove custom policy %s" % (name) + else: + kc.remove_authz_custom_policy( + policy_id=before_authz_custom_policy['id'], client_id=cid, realm=realm) + result['msg'] = "Custom policy %s removed" % (name) + + result['changed'] = True + result['end_state'] = {} + elif not before_authz_custom_policy and state == 'absent': + result['msg'] = "Custom policy %s does not exist" % (name) + result['changed'] = False + result['end_state'] = {} + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/keycloak_authz_permission.py b/ansible_collections/community/general/plugins/modules/keycloak_authz_permission.py new file mode 100644 index 000000000..ef81fb8c3 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/keycloak_authz_permission.py @@ -0,0 +1,433 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Eike Frost +# Copyright (c) 2021, Christophe Gilles +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_authz_permission + +version_added: 7.2.0 + +short_description: Allows administration of Keycloak client authorization permissions via Keycloak API + +description: + - This module allows the administration of Keycloak client authorization permissions via the Keycloak REST + API. Authorization permissions are only available if a client has Authorization enabled. + + - There are some peculiarities in JSON paths and payloads for authorization permissions. In particular + POST and PUT operations are targeted at permission endpoints, whereas GET requests go to policies + endpoint. To make matters more interesting the JSON responses from GET requests return data in a + different format than what is expected for POST and PUT. The end result is that it is not possible to + detect changes to things like policies, scopes or resources - at least not without a large number of + additional API calls. Therefore this module always updates authorization permissions instead of + attempting to determine if changes are truly needed. + + - This module requires access to the REST API via OpenID Connect; the user connecting and the realm + being used must have the requisite access rights. In a default Keycloak installation, admin-cli + and an admin user would work, as would a separate realm definition with the scope tailored + to your needs and a user having the expected roles. + + - The names of module options are snake_cased versions of the camelCase options used by Keycloak. + The Authorization Services paths and payloads have not officially been documented by the Keycloak project. + U(https://www.puppeteers.net/blog/keycloak-authorization-services-rest-api-paths-and-payload/) + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + state: + description: + - State of the authorization permission. + - On V(present), the authorization permission will be created (or updated if it exists already). + - On V(absent), the authorization permission will be removed if it exists. + choices: ['present', 'absent'] + default: 'present' + type: str + name: + description: + - Name of the authorization permission to create. + type: str + required: true + description: + description: + - The description of the authorization permission. + type: str + required: false + permission_type: + description: + - The type of authorization permission. + - On V(scope) create a scope-based permission. + - On V(resource) create a resource-based permission. + type: str + required: true + choices: + - resource + - scope + decision_strategy: + description: + - The decision strategy to use with this permission. + type: str + default: UNANIMOUS + required: false + choices: + - UNANIMOUS + - AFFIRMATIVE + - CONSENSUS + resources: + description: + - Resource names to attach to this permission. + - Scope-based permissions can only include one resource. + - Resource-based permissions can include multiple resources. + type: list + elements: str + default: [] + required: false + scopes: + description: + - Scope names to attach to this permission. + - Resource-based permissions cannot have scopes attached to them. + type: list + elements: str + default: [] + required: false + policies: + description: + - Policy names to attach to this permission. + type: list + elements: str + default: [] + required: false + client_id: + description: + - The clientId of the keycloak client that should have the authorization scope. + - This is usually a human-readable name of the Keycloak client. + type: str + required: true + realm: + description: + - The name of the Keycloak realm the Keycloak client is in. + type: str + required: true + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + +author: + - Samuli Seppänen (@mattock) +''' + +EXAMPLES = ''' +- name: Manage scope-based Keycloak authorization permission + community.general.keycloak_authz_permission: + name: ScopePermission + state: present + description: Scope permission + permission_type: scope + scopes: + - file:delete + policies: + - Default Policy + client_id: myclient + realm: myrealm + auth_keycloak_url: http://localhost:8080/auth + auth_username: keycloak + auth_password: keycloak + auth_realm: master + +- name: Manage resource-based Keycloak authorization permission + community.general.keycloak_authz_permission: + name: ResourcePermission + state: present + description: Resource permission + permission_type: resource + resources: + - Default Resource + policies: + - Default Policy + client_id: myclient + realm: myrealm + auth_keycloak_url: http://localhost:8080/auth + auth_username: keycloak + auth_password: keycloak + auth_realm: master +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + +end_state: + description: Representation of the authorization permission after module execution. + returned: on success + type: complex + contains: + id: + description: ID of the authorization permission. + type: str + returned: when O(state=present) + sample: 9da05cd2-b273-4354-bbd8-0c133918a454 + name: + description: Name of the authorization permission. + type: str + returned: when O(state=present) + sample: ResourcePermission + description: + description: Description of the authorization permission. + type: str + returned: when O(state=present) + sample: Resource Permission + type: + description: Type of the authorization permission. + type: str + returned: when O(state=present) + sample: resource + decisionStrategy: + description: The decision strategy to use. + type: str + returned: when O(state=present) + sample: UNANIMOUS + logic: + description: The logic used for the permission (part of the payload, but has a fixed value). + type: str + returned: when O(state=present) + sample: POSITIVE + resources: + description: IDs of resources attached to this permission. + type: list + returned: when O(state=present) + sample: + - 49e052ff-100d-4b79-a9dd-52669ed3c11d + scopes: + description: IDs of scopes attached to this permission. + type: list + returned: when O(state=present) + sample: + - 9da05cd2-b273-4354-bbd8-0c133918a454 + policies: + description: IDs of policies attached to this permission. + type: list + returned: when O(state=present) + sample: + - 9da05cd2-b273-4354-bbd8-0c133918a454 +''' + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \ + keycloak_argument_spec, get_token, KeycloakError +from ansible.module_utils.basic import AnsibleModule + + +def main(): + """ + Module execution + + :return: + """ + argument_spec = keycloak_argument_spec() + + meta_args = dict( + state=dict(type='str', default='present', + choices=['present', 'absent']), + name=dict(type='str', required=True), + description=dict(type='str', required=False), + permission_type=dict(type='str', choices=['scope', 'resource'], required=True), + decision_strategy=dict(type='str', default='UNANIMOUS', + choices=['UNANIMOUS', 'AFFIRMATIVE', 'CONSENSUS']), + resources=dict(type='list', elements='str', default=[], required=False), + scopes=dict(type='list', elements='str', default=[], required=False), + policies=dict(type='list', elements='str', default=[], required=False), + client_id=dict(type='str', required=True), + realm=dict(type='str', required=True) + ) + + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=( + [['token', 'auth_realm', 'auth_username', 'auth_password']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']])) + + # Convenience variables + state = module.params.get('state') + name = module.params.get('name') + description = module.params.get('description') + permission_type = module.params.get('permission_type') + decision_strategy = module.params.get('decision_strategy') + realm = module.params.get('realm') + client_id = module.params.get('client_id') + realm = module.params.get('realm') + resources = module.params.get('resources') + scopes = module.params.get('scopes') + policies = module.params.get('policies') + + if permission_type == 'scope' and state == 'present': + if scopes == []: + module.fail_json(msg='Scopes need to defined when permission type is set to scope!') + if len(resources) > 1: + module.fail_json(msg='Only one resource can be defined for a scope permission!') + + if permission_type == 'resource' and state == 'present': + if resources == []: + module.fail_json(msg='A resource need to defined when permission type is set to resource!') + if scopes != []: + module.fail_json(msg='Scopes cannot be defined when permission type is set to resource!') + + result = dict(changed=False, msg='', end_state={}) + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + # Get id of the client based on client_id + cid = kc.get_client_id(client_id, realm=realm) + if not cid: + module.fail_json(msg='Invalid client %s for realm %s' % + (client_id, realm)) + + # Get current state of the permission using its name as the search + # filter. This returns False if it is not found. + permission = kc.get_authz_permission_by_name( + name=name, client_id=cid, realm=realm) + + # Generate a JSON payload for Keycloak Admin API. This is needed for + # "create" and "update" operations. + payload = {} + payload['name'] = name + payload['description'] = description + payload['type'] = permission_type + payload['decisionStrategy'] = decision_strategy + payload['logic'] = 'POSITIVE' + payload['scopes'] = [] + payload['resources'] = [] + payload['policies'] = [] + + if permission_type == 'scope': + # Add the resource id, if any, to the payload. While the data type is a + # list, it is only possible to have one entry in it based on what Keycloak + # Admin Console does. + r = False + resource_scopes = [] + + if resources: + r = kc.get_authz_resource_by_name(resources[0], cid, realm) + if not r: + module.fail_json(msg='Unable to find authorization resource with name %s for client %s in realm %s' % (resources[0], cid, realm)) + else: + payload['resources'].append(r['_id']) + + for rs in r['scopes']: + resource_scopes.append(rs['id']) + + # Generate a list of scope ids based on scope names. Fail if the + # defined resource does not include all those scopes. + for scope in scopes: + s = kc.get_authz_authorization_scope_by_name(scope, cid, realm) + if r and not s['id'] in resource_scopes: + module.fail_json(msg='Resource %s does not include scope %s for client %s in realm %s' % (resources[0], scope, client_id, realm)) + else: + payload['scopes'].append(s['id']) + + elif permission_type == 'resource': + if resources: + for resource in resources: + r = kc.get_authz_resource_by_name(resource, cid, realm) + if not r: + module.fail_json(msg='Unable to find authorization resource with name %s for client %s in realm %s' % (resource, cid, realm)) + else: + payload['resources'].append(r['_id']) + + # Add policy ids, if any, to the payload. + if policies: + for policy in policies: + p = kc.get_authz_policy_by_name(policy, cid, realm) + + if p: + payload['policies'].append(p['id']) + else: + module.fail_json(msg='Unable to find authorization policy with name %s for client %s in realm %s' % (policy, client_id, realm)) + + # Add "id" to payload for update operations + if permission: + payload['id'] = permission['id'] + + # Handle the special case where the user attempts to change an already + # existing permission's type - something that can't be done without a + # full delete -> (re)create cycle. + if permission['type'] != payload['type']: + module.fail_json(msg='Modifying the type of permission (scope/resource) is not supported: \ + permission %s of client %s in realm %s unchanged' % (permission['id'], cid, realm)) + + # Updating an authorization permission is tricky for several reasons. + # Firstly, the current permission is retrieved using a _policy_ endpoint, + # not from a permission endpoint. Also, the data that is returned is in a + # different format than what is expected by the payload. So, comparing the + # current state attribute by attribute to the payload is not possible. For + # example the data contains a JSON object "config" which may contain the + # authorization type, but which is no required in the payload. Moreover, + # information about resources, scopes and policies is _not_ present in the + # data. So, there is no way to determine if any of those fields have + # changed. Therefore the best options we have are + # + # a) Always apply the payload without checking the current state + # b) Refuse to make any changes to any settings (only support create and delete) + # + # The approach taken here is a). + # + if permission and state == 'present': + if module.check_mode: + result['msg'] = 'Notice: unable to check current resources, scopes and policies for permission. \ + Would apply desired state without checking the current state.' + else: + kc.update_authz_permission(payload=payload, permission_type=permission_type, id=permission['id'], client_id=cid, realm=realm) + result['msg'] = 'Notice: unable to check current resources, scopes and policies for permission. \ + Applying desired state without checking the current state.' + + # Assume that something changed, although we don't know if that is the case. + result['changed'] = True + result['end_state'] = payload + elif not permission and state == 'present': + if module.check_mode: + result['msg'] = 'Would create permission' + else: + kc.create_authz_permission(payload=payload, permission_type=permission_type, client_id=cid, realm=realm) + result['msg'] = 'Permission created' + + result['changed'] = True + result['end_state'] = payload + elif permission and state == 'absent': + if module.check_mode: + result['msg'] = 'Would remove permission' + else: + kc.remove_authz_permission(id=permission['id'], client_id=cid, realm=realm) + result['msg'] = 'Permission removed' + + result['changed'] = True + + elif not permission and state == 'absent': + result['changed'] = False + else: + module.fail_json(msg='Unable to determine what to do with permission %s of client %s in realm %s' % ( + name, client_id, realm)) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/keycloak_authz_permission_info.py b/ansible_collections/community/general/plugins/modules/keycloak_authz_permission_info.py new file mode 100644 index 000000000..8b4e96b41 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/keycloak_authz_permission_info.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Eike Frost +# Copyright (c) 2021, Christophe Gilles +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_authz_permission_info + +version_added: 7.2.0 + +short_description: Query Keycloak client authorization permissions information + +description: + - This module allows querying information about Keycloak client authorization permissions from the + resources endpoint via the Keycloak REST API. Authorization permissions are only available if a + client has Authorization enabled. + + - This module requires access to the REST API via OpenID Connect; the user connecting and the realm + being used must have the requisite access rights. In a default Keycloak installation, admin-cli + and an admin user would work, as would a separate realm definition with the scope tailored + to your needs and a user having the expected roles. + + - The names of module options are snake_cased versions of the camelCase options used by Keycloak. + The Authorization Services paths and payloads have not officially been documented by the Keycloak project. + U(https://www.puppeteers.net/blog/keycloak-authorization-services-rest-api-paths-and-payload/) + +options: + name: + description: + - Name of the authorization permission to create. + type: str + required: true + client_id: + description: + - The clientId of the keycloak client that should have the authorization scope. + - This is usually a human-readable name of the Keycloak client. + type: str + required: true + realm: + description: + - The name of the Keycloak realm the Keycloak client is in. + type: str + required: true + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + - community.general.attributes.info_module + +author: + - Samuli Seppänen (@mattock) +''' + +EXAMPLES = ''' +- name: Query Keycloak authorization permission + community.general.keycloak_authz_permission_info: + name: ScopePermission + client_id: myclient + realm: myrealm + auth_keycloak_url: http://localhost:8080/auth + auth_username: keycloak + auth_password: keycloak + auth_realm: master +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + +queried_state: + description: State of the resource (a policy) as seen by Keycloak. + returned: on success + type: complex + contains: + id: + description: ID of the authorization permission. + type: str + sample: 9da05cd2-b273-4354-bbd8-0c133918a454 + name: + description: Name of the authorization permission. + type: str + sample: ResourcePermission + description: + description: Description of the authorization permission. + type: str + sample: Resource Permission + type: + description: Type of the authorization permission. + type: str + sample: resource + decisionStrategy: + description: The decision strategy. + type: str + sample: UNANIMOUS + logic: + description: The logic used for the permission (part of the payload, but has a fixed value). + type: str + sample: POSITIVE + config: + description: Configuration of the permission (empty in all observed cases). + type: dict + sample: {} +''' + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \ + keycloak_argument_spec, get_token, KeycloakError +from ansible.module_utils.basic import AnsibleModule + + +def main(): + """ + Module execution + + :return: + """ + argument_spec = keycloak_argument_spec() + + meta_args = dict( + name=dict(type='str', required=True), + client_id=dict(type='str', required=True), + realm=dict(type='str', required=True) + ) + + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=( + [['token', 'auth_realm', 'auth_username', 'auth_password']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']])) + + # Convenience variables + name = module.params.get('name') + client_id = module.params.get('client_id') + realm = module.params.get('realm') + + result = dict(changed=False, msg='', queried_state={}) + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + # Get id of the client based on client_id + cid = kc.get_client_id(client_id, realm=realm) + if not cid: + module.fail_json(msg='Invalid client %s for realm %s' % + (client_id, realm)) + + # Get current state of the permission using its name as the search + # filter. This returns False if it is not found. + permission = kc.get_authz_permission_by_name( + name=name, client_id=cid, realm=realm) + + result['queried_state'] = permission + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/keycloak_client.py b/ansible_collections/community/general/plugins/modules/keycloak_client.py index ee687fcb4..b151e4541 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_client.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_client.py @@ -40,8 +40,8 @@ options: state: description: - State of the client - - On C(present), the client will be created (or updated if it exists already). - - On C(absent), the client will be removed if it exists + - On V(present), the client will be created (or updated if it exists already). + - On V(absent), the client will be removed if it exists choices: ['present', 'absent'] default: 'present' type: str @@ -55,7 +55,7 @@ options: client_id: description: - Client id of client to be worked on. This is usually an alphanumeric name chosen by - you. Either this or I(id) is required. If you specify both, I(id) takes precedence. + you. Either this or O(id) is required. If you specify both, O(id) takes precedence. This is 'clientId' in the Keycloak REST API. aliases: - clientId @@ -63,13 +63,13 @@ options: id: description: - - Id of client to be worked on. This is usually an UUID. Either this or I(client_id) + - Id of client to be worked on. This is usually an UUID. Either this or O(client_id) is required. If you specify both, this takes precedence. type: str name: description: - - Name of the client (this is not the same as I(client_id)). + - Name of the client (this is not the same as O(client_id)). type: str description: @@ -108,12 +108,12 @@ options: client_authenticator_type: description: - - How do clients authenticate with the auth server? Either C(client-secret) or - C(client-jwt) can be chosen. When using C(client-secret), the module parameter - I(secret) can set it, while for C(client-jwt), you can use the keys C(use.jwks.url), - C(jwks.url), and C(jwt.credential.certificate) in the I(attributes) module parameter + - How do clients authenticate with the auth server? Either V(client-secret) or + V(client-jwt) can be chosen. When using V(client-secret), the module parameter + O(secret) can set it, while for V(client-jwt), you can use the keys C(use.jwks.url), + C(jwks.url), and C(jwt.credential.certificate) in the O(attributes) module parameter to configure its behavior. - This is 'clientAuthenticatorType' in the Keycloak REST API. + - This is 'clientAuthenticatorType' in the Keycloak REST API. choices: ['client-secret', 'client-jwt'] aliases: - clientAuthenticatorType @@ -121,7 +121,7 @@ options: secret: description: - - When using I(client_authenticator_type) C(client-secret) (the default), you can + - When using O(client_authenticator_type=client-secret) (the default), you can specify a secret here (otherwise one will be generated if it does not exit). If changing this secret, the module will not register a change currently (but the changed secret will be saved). @@ -246,7 +246,8 @@ options: protocol: description: - - Type of client (either C(openid-connect) or C(saml). + - Type of client. + - At creation only, default value will be V(openid-connect) if O(protocol) is omitted. type: str choices: ['openid-connect', 'saml'] @@ -286,7 +287,7 @@ options: use_template_config: description: - - Whether or not to use configuration from the I(client_template). + - Whether or not to use configuration from the O(client_template). This is 'useTemplateConfig' in the Keycloak REST API. aliases: - useTemplateConfig @@ -294,7 +295,7 @@ options: use_template_scope: description: - - Whether or not to use scope configuration from the I(client_template). + - Whether or not to use scope configuration from the O(client_template). This is 'useTemplateScope' in the Keycloak REST API. aliases: - useTemplateScope @@ -302,7 +303,7 @@ options: use_template_mappers: description: - - Whether or not to use mapper configuration from the I(client_template). + - Whether or not to use mapper configuration from the O(client_template). This is 'useTemplateMappers' in the Keycloak REST API. aliases: - useTemplateMappers @@ -391,38 +392,37 @@ options: protocol: description: - - This is either C(openid-connect) or C(saml), this specifies for which protocol this protocol mapper. - is active. + - This specifies for which protocol this protocol mapper is active. choices: ['openid-connect', 'saml'] type: str protocolMapper: description: - - The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is + - "The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is impossible to provide since this may be extended through SPIs by the user of Keycloak, - by default Keycloak as of 3.4 ships with at least - - C(docker-v2-allow-all-mapper) - - C(oidc-address-mapper) - - C(oidc-full-name-mapper) - - C(oidc-group-membership-mapper) - - C(oidc-hardcoded-claim-mapper) - - C(oidc-hardcoded-role-mapper) - - C(oidc-role-name-mapper) - - C(oidc-script-based-protocol-mapper) - - C(oidc-sha256-pairwise-sub-mapper) - - C(oidc-usermodel-attribute-mapper) - - C(oidc-usermodel-client-role-mapper) - - C(oidc-usermodel-property-mapper) - - C(oidc-usermodel-realm-role-mapper) - - C(oidc-usersessionmodel-note-mapper) - - C(saml-group-membership-mapper) - - C(saml-hardcode-attribute-mapper) - - C(saml-hardcode-role-mapper) - - C(saml-role-list-mapper) - - C(saml-role-name-mapper) - - C(saml-user-attribute-mapper) - - C(saml-user-property-mapper) - - C(saml-user-session-note-mapper) + by default Keycloak as of 3.4 ships with at least:" + - V(docker-v2-allow-all-mapper) + - V(oidc-address-mapper) + - V(oidc-full-name-mapper) + - V(oidc-group-membership-mapper) + - V(oidc-hardcoded-claim-mapper) + - V(oidc-hardcoded-role-mapper) + - V(oidc-role-name-mapper) + - V(oidc-script-based-protocol-mapper) + - V(oidc-sha256-pairwise-sub-mapper) + - V(oidc-usermodel-attribute-mapper) + - V(oidc-usermodel-client-role-mapper) + - V(oidc-usermodel-property-mapper) + - V(oidc-usermodel-realm-role-mapper) + - V(oidc-usersessionmodel-note-mapper) + - V(saml-group-membership-mapper) + - V(saml-hardcode-attribute-mapper) + - V(saml-hardcode-role-mapper) + - V(saml-role-list-mapper) + - V(saml-role-name-mapper) + - V(saml-user-attribute-mapper) + - V(saml-user-property-mapper) + - V(saml-user-session-note-mapper) - An exhaustive list of available mappers on your installation can be obtained on the admin console by going to Server Info -> Providers and looking under 'protocol-mapper'. @@ -431,10 +431,10 @@ options: config: description: - Dict specifying the configuration options for the protocol mapper; the - contents differ depending on the value of I(protocolMapper) and are not documented + contents differ depending on the value of O(protocol_mappers[].protocolMapper) and are not documented other than by the source of the mappers and its parent class(es). An example is given below. It is easiest to obtain valid config values by dumping an already-existing - protocol mapper configuration through check-mode in the I(existing) field. + protocol mapper configuration through check-mode in the RV(existing) field. type: dict attributes: @@ -478,7 +478,7 @@ options: saml.signature.algorithm: description: - - Signature algorithm used to sign SAML documents. One of C(RSA_SHA256), C(RSA_SHA1), C(RSA_SHA512), or C(DSA_SHA1). + - Signature algorithm used to sign SAML documents. One of V(RSA_SHA256), V(RSA_SHA1), V(RSA_SHA512), or V(DSA_SHA1). saml.signing.certificate: description: @@ -503,15 +503,15 @@ options: saml_name_id_format: description: - - For SAML clients, the NameID format to use (one of C(username), C(email), C(transient), or C(persistent)) + - For SAML clients, the NameID format to use (one of V(username), V(email), V(transient), or V(persistent)) saml_signature_canonicalization_method: description: - SAML signature canonicalization method. This is one of four values, namely - C(http://www.w3.org/2001/10/xml-exc-c14n#) for EXCLUSIVE, - C(http://www.w3.org/2001/10/xml-exc-c14n#WithComments) for EXCLUSIVE_WITH_COMMENTS, - C(http://www.w3.org/TR/2001/REC-xml-c14n-20010315) for INCLUSIVE, and - C(http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments) for INCLUSIVE_WITH_COMMENTS. + V(http://www.w3.org/2001/10/xml-exc-c14n#) for EXCLUSIVE, + V(http://www.w3.org/2001/10/xml-exc-c14n#WithComments) for EXCLUSIVE_WITH_COMMENTS, + V(http://www.w3.org/TR/2001/REC-xml-c14n-20010315) for INCLUSIVE, and + V(http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments) for INCLUSIVE_WITH_COMMENTS. saml_single_logout_service_url_post: description: @@ -523,12 +523,12 @@ options: user.info.response.signature.alg: description: - - For OpenID-Connect clients, JWA algorithm for signed UserInfo-endpoint responses. One of C(RS256) or C(unsigned). + - For OpenID-Connect clients, JWA algorithm for signed UserInfo-endpoint responses. One of V(RS256) or V(unsigned). request.object.signature.alg: description: - For OpenID-Connect clients, JWA algorithm which the client needs to use when sending - OIDC request object. One of C(any), C(none), C(RS256). + OIDC request object. One of V(any), V(none), V(RS256). use.jwks.url: description: @@ -717,11 +717,16 @@ end_state: ''' from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ - keycloak_argument_spec, get_token, KeycloakError + keycloak_argument_spec, get_token, KeycloakError, is_struct_included from ansible.module_utils.basic import AnsibleModule import copy +PROTOCOL_OPENID_CONNECT = 'openid-connect' +PROTOCOL_SAML = 'saml' +CLIENT_META_DATA = ['authorizationServicesEnabled'] + + def normalise_cr(clientrep, remove_ids=False): """ Re-sorts any properties where the order so that diff's is minimised, and adds default values where appropriate so that the the change detection is more effective. @@ -780,7 +785,7 @@ def main(): consentText=dict(type='str'), id=dict(type='str'), name=dict(type='str'), - protocol=dict(type='str', choices=['openid-connect', 'saml']), + protocol=dict(type='str', choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML]), protocolMapper=dict(type='str'), config=dict(type='dict'), ) @@ -814,7 +819,7 @@ def main(): authorization_services_enabled=dict(type='bool', aliases=['authorizationServicesEnabled']), public_client=dict(type='bool', aliases=['publicClient']), frontchannel_logout=dict(type='bool', aliases=['frontchannelLogout']), - protocol=dict(type='str', choices=['openid-connect', 'saml']), + protocol=dict(type='str', choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML]), attributes=dict(type='dict'), full_scope_allowed=dict(type='bool', aliases=['fullScopeAllowed']), node_re_registration_timeout=dict(type='int', aliases=['nodeReRegistrationTimeout']), @@ -912,6 +917,8 @@ def main(): if 'clientId' not in desired_client: module.fail_json(msg='client_id needs to be specified when creating a new client') + if 'protocol' not in desired_client: + desired_client['protocol'] = PROTOCOL_OPENID_CONNECT if module._diff: result['diff'] = dict(before='', after=sanitize_cr(desired_client)) @@ -940,7 +947,7 @@ def main(): if module._diff: result['diff'] = dict(before=sanitize_cr(before_norm), after=sanitize_cr(desired_norm)) - result['changed'] = (before_norm != desired_norm) + result['changed'] = not is_struct_included(desired_norm, before_norm, CLIENT_META_DATA) module.exit_json(**result) diff --git a/ansible_collections/community/general/plugins/modules/keycloak_client_rolemapping.py b/ansible_collections/community/general/plugins/modules/keycloak_client_rolemapping.py index 57dcac48d..be419904a 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_client_rolemapping.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_client_rolemapping.py @@ -43,8 +43,8 @@ options: state: description: - State of the client_rolemapping. - - On C(present), the client_rolemapping will be created if it does not yet exist, or updated with the parameters you provide. - - On C(absent), the client_rolemapping will be removed if it exists. + - On V(present), the client_rolemapping will be created if it does not yet exist, or updated with the parameters you provide. + - On V(absent), the client_rolemapping will be removed if it exists. default: 'present' type: str choices: @@ -63,6 +63,33 @@ options: - Name of the group to be mapped. - This parameter is required (can be replaced by gid for less API call). + parents: + version_added: "7.1.0" + type: list + description: + - List of parent groups for the group to handle sorted top to bottom. + - >- + Set this if your group is a subgroup and you do not provide the GID in O(gid). + elements: dict + suboptions: + id: + type: str + description: + - Identify parent by ID. + - Needs less API calls than using O(parents[].name). + - A deep parent chain can be started at any point when first given parent is given as ID. + - Note that in principle both ID and name can be specified at the same time + but current implementation only always use just one of them, with ID + being preferred. + name: + type: str + description: + - Identify parent by name. + - Needs more internal API calls than using O(parents[].id) to map names to ID's under the hood. + - When giving a parent chain with only names it must be complete up to the top. + - Note that in principle both ID and name can be specified at the same time + but current implementation only always use just one of them, with ID + being preferred. gid: type: str description: @@ -73,7 +100,7 @@ options: client_id: type: str description: - - Name of the client to be mapped (different than I(cid)). + - Name of the client to be mapped (different than O(cid)). - This parameter is required (can be replaced by cid for less API call). cid: @@ -144,6 +171,24 @@ EXAMPLES = ''' id: role_id2 delegate_to: localhost +- name: Map a client role to a subgroup, authentication with token + community.general.keycloak_client_rolemapping: + realm: MyCustomRealm + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + token: TOKEN + state: present + client_id: client1 + group_name: subgroup1 + parents: + - name: parent-group + roles: + - name: role_name1 + id: role_id1 + - name: role_name2 + id: role_id2 + delegate_to: localhost + - name: Unmap client role from a group community.general.keycloak_client_rolemapping: realm: MyCustomRealm @@ -230,6 +275,13 @@ def main(): realm=dict(default='master'), gid=dict(type='str'), group_name=dict(type='str'), + parents=dict( + type='list', elements='dict', + options=dict( + id=dict(type='str'), + name=dict(type='str') + ), + ), cid=dict(type='str'), client_id=dict(type='str'), roles=dict(type='list', elements='dict', options=roles_spec), @@ -259,6 +311,7 @@ def main(): gid = module.params.get('gid') group_name = module.params.get('group_name') roles = module.params.get('roles') + parents = module.params.get('parents') # Check the parameters if cid is None and client_id is None: @@ -268,7 +321,7 @@ def main(): # Get the potential missing parameters if gid is None: - group_rep = kc.get_group_by_name(group_name, realm=realm) + group_rep = kc.get_group_by_name(group_name, realm=realm, parents=parents) if group_rep is not None: gid = group_rep['id'] else: diff --git a/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py b/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py index a23d92867..d37af5f0c 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py @@ -43,8 +43,8 @@ options: state: description: - State of the client_scope. - - On C(present), the client_scope will be created if it does not yet exist, or updated with the parameters you provide. - - On C(absent), the client_scope will be removed if it exists. + - On V(present), the client_scope will be created if it does not yet exist, or updated with the parameters you provide. + - On V(absent), the client_scope will be removed if it exists. default: 'present' type: str choices: @@ -103,28 +103,28 @@ options: - "The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is impossible to provide since this may be extended through SPIs by the user of Keycloak, by default Keycloak as of 3.4 ships with at least:" - - C(docker-v2-allow-all-mapper) - - C(oidc-address-mapper) - - C(oidc-full-name-mapper) - - C(oidc-group-membership-mapper) - - C(oidc-hardcoded-claim-mapper) - - C(oidc-hardcoded-role-mapper) - - C(oidc-role-name-mapper) - - C(oidc-script-based-protocol-mapper) - - C(oidc-sha256-pairwise-sub-mapper) - - C(oidc-usermodel-attribute-mapper) - - C(oidc-usermodel-client-role-mapper) - - C(oidc-usermodel-property-mapper) - - C(oidc-usermodel-realm-role-mapper) - - C(oidc-usersessionmodel-note-mapper) - - C(saml-group-membership-mapper) - - C(saml-hardcode-attribute-mapper) - - C(saml-hardcode-role-mapper) - - C(saml-role-list-mapper) - - C(saml-role-name-mapper) - - C(saml-user-attribute-mapper) - - C(saml-user-property-mapper) - - C(saml-user-session-note-mapper) + - V(docker-v2-allow-all-mapper) + - V(oidc-address-mapper) + - V(oidc-full-name-mapper) + - V(oidc-group-membership-mapper) + - V(oidc-hardcoded-claim-mapper) + - V(oidc-hardcoded-role-mapper) + - V(oidc-role-name-mapper) + - V(oidc-script-based-protocol-mapper) + - V(oidc-sha256-pairwise-sub-mapper) + - V(oidc-usermodel-attribute-mapper) + - V(oidc-usermodel-client-role-mapper) + - V(oidc-usermodel-property-mapper) + - V(oidc-usermodel-realm-role-mapper) + - V(oidc-usersessionmodel-note-mapper) + - V(saml-group-membership-mapper) + - V(saml-hardcode-attribute-mapper) + - V(saml-hardcode-role-mapper) + - V(saml-role-list-mapper) + - V(saml-role-name-mapper) + - V(saml-user-attribute-mapper) + - V(saml-user-property-mapper) + - V(saml-user-session-note-mapper) - An exhaustive list of available mappers on your installation can be obtained on the admin console by going to Server Info -> Providers and looking under 'protocol-mapper'. @@ -143,10 +143,10 @@ options: config: description: - Dict specifying the configuration options for the protocol mapper; the - contents differ depending on the value of I(protocolMapper) and are not documented + contents differ depending on the value of O(protocol_mappers[].protocolMapper) and are not documented other than by the source of the mappers and its parent class(es). An example is given below. It is easiest to obtain valid config values by dumping an already-existing - protocol mapper configuration through check-mode in the C(existing) return value. + protocol mapper configuration through check-mode in the RV(existing) return value. type: dict attributes: diff --git a/ansible_collections/community/general/plugins/modules/keycloak_clientscope_type.py b/ansible_collections/community/general/plugins/modules/keycloak_clientscope_type.py index facf02aa4..37a5d3be9 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_clientscope_type.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_clientscope_type.py @@ -40,7 +40,7 @@ options: client_id: description: - - The I(client_id) of the client. If not set the clientscop types are set as a default for the realm. + - The O(client_id) of the client. If not set the clientscop types are set as a default for the realm. aliases: - clientId type: str @@ -67,7 +67,7 @@ author: EXAMPLES = ''' - name: Set default client scopes on realm level - community.general.keycloak_clientsecret_info: + community.general.keycloak_clientscope_type: auth_client_id: admin-cli auth_keycloak_url: https://auth.example.com/auth auth_realm: master @@ -79,7 +79,7 @@ EXAMPLES = ''' - name: Set default and optional client scopes on client level with token auth - community.general.keycloak_clientsecret_info: + community.general.keycloak_clientscope_type: auth_client_id: admin-cli auth_keycloak_url: https://auth.example.com/auth token: TOKEN diff --git a/ansible_collections/community/general/plugins/modules/keycloak_clientsecret_info.py b/ansible_collections/community/general/plugins/modules/keycloak_clientsecret_info.py index 98a41ad20..c77262035 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_clientsecret_info.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_clientsecret_info.py @@ -26,8 +26,8 @@ description: and a user having the expected roles. - When retrieving a new client secret, where possible provide the client's - I(id) (not I(client_id)) to the module. This removes a lookup to the API to - translate the I(client_id) into the client ID. + O(id) (not O(client_id)) to the module. This removes a lookup to the API to + translate the O(client_id) into the client ID. - "Note that this module returns the client secret. To avoid this showing up in the logs, please add C(no_log: true) to the task." @@ -48,7 +48,7 @@ options: client_id: description: - - The I(client_id) of the client. Passing this instead of I(id) results in an + - The O(client_id) of the client. Passing this instead of O(id) results in an extra API call. aliases: - clientId diff --git a/ansible_collections/community/general/plugins/modules/keycloak_clienttemplate.py b/ansible_collections/community/general/plugins/modules/keycloak_clienttemplate.py index d2555afc5..cd7f6c09b 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_clienttemplate.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_clienttemplate.py @@ -38,8 +38,8 @@ options: state: description: - State of the client template. - - On C(present), the client template will be created (or updated if it exists already). - - On C(absent), the client template will be removed if it exists + - On V(present), the client template will be created (or updated if it exists already). + - On V(absent), the client template will be removed if it exists choices: ['present', 'absent'] default: 'present' type: str @@ -67,7 +67,7 @@ options: protocol: description: - - Type of client template (either C(openid-connect) or C(saml). + - Type of client template. choices: ['openid-connect', 'saml'] type: str @@ -106,38 +106,37 @@ options: protocol: description: - - This is either C(openid-connect) or C(saml), this specifies for which protocol this protocol mapper. - is active. + - This specifies for which protocol this protocol mapper is active. choices: ['openid-connect', 'saml'] type: str protocolMapper: description: - - The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is + - "The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is impossible to provide since this may be extended through SPIs by the user of Keycloak, - by default Keycloak as of 3.4 ships with at least - - C(docker-v2-allow-all-mapper) - - C(oidc-address-mapper) - - C(oidc-full-name-mapper) - - C(oidc-group-membership-mapper) - - C(oidc-hardcoded-claim-mapper) - - C(oidc-hardcoded-role-mapper) - - C(oidc-role-name-mapper) - - C(oidc-script-based-protocol-mapper) - - C(oidc-sha256-pairwise-sub-mapper) - - C(oidc-usermodel-attribute-mapper) - - C(oidc-usermodel-client-role-mapper) - - C(oidc-usermodel-property-mapper) - - C(oidc-usermodel-realm-role-mapper) - - C(oidc-usersessionmodel-note-mapper) - - C(saml-group-membership-mapper) - - C(saml-hardcode-attribute-mapper) - - C(saml-hardcode-role-mapper) - - C(saml-role-list-mapper) - - C(saml-role-name-mapper) - - C(saml-user-attribute-mapper) - - C(saml-user-property-mapper) - - C(saml-user-session-note-mapper) + by default Keycloak as of 3.4 ships with at least:" + - V(docker-v2-allow-all-mapper) + - V(oidc-address-mapper) + - V(oidc-full-name-mapper) + - V(oidc-group-membership-mapper) + - V(oidc-hardcoded-claim-mapper) + - V(oidc-hardcoded-role-mapper) + - V(oidc-role-name-mapper) + - V(oidc-script-based-protocol-mapper) + - V(oidc-sha256-pairwise-sub-mapper) + - V(oidc-usermodel-attribute-mapper) + - V(oidc-usermodel-client-role-mapper) + - V(oidc-usermodel-property-mapper) + - V(oidc-usermodel-realm-role-mapper) + - V(oidc-usersessionmodel-note-mapper) + - V(saml-group-membership-mapper) + - V(saml-hardcode-attribute-mapper) + - V(saml-hardcode-role-mapper) + - V(saml-role-list-mapper) + - V(saml-role-name-mapper) + - V(saml-user-attribute-mapper) + - V(saml-user-property-mapper) + - V(saml-user-session-note-mapper) - An exhaustive list of available mappers on your installation can be obtained on the admin console by going to Server Info -> Providers and looking under 'protocol-mapper'. @@ -146,10 +145,10 @@ options: config: description: - Dict specifying the configuration options for the protocol mapper; the - contents differ depending on the value of I(protocolMapper) and are not documented + contents differ depending on the value of O(protocol_mappers[].protocolMapper) and are not documented other than by the source of the mappers and its parent class(es). An example is given below. It is easiest to obtain valid config values by dumping an already-existing - protocol mapper configuration through check-mode in the I(existing) field. + protocol mapper configuration through check-mode in the RV(existing) field. type: dict attributes: @@ -160,9 +159,9 @@ options: type: dict notes: - - The Keycloak REST API defines further fields (namely I(bearerOnly), I(consentRequired), I(standardFlowEnabled), - I(implicitFlowEnabled), I(directAccessGrantsEnabled), I(serviceAccountsEnabled), I(publicClient), and - I(frontchannelLogout)) which, while available with keycloak_client, do not have any effect on + - The Keycloak REST API defines further fields (namely C(bearerOnly), C(consentRequired), C(standardFlowEnabled), + C(implicitFlowEnabled), C(directAccessGrantsEnabled), C(serviceAccountsEnabled), C(publicClient), and + C(frontchannelLogout)) which, while available with keycloak_client, do not have any effect on Keycloak client-templates and are discarded if supplied with an API request changing client-templates. As such, they are not available through this module. diff --git a/ansible_collections/community/general/plugins/modules/keycloak_component_info.py b/ansible_collections/community/general/plugins/modules/keycloak_component_info.py new file mode 100644 index 000000000..a788735d9 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/keycloak_component_info.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_component_info + +short_description: Retrive component info in Keycloak + +version_added: 8.2.0 + +description: + - This module retrive information on component from Keycloak. +options: + realm: + description: + - The name of the realm. + required: true + type: str + name: + description: + - Name of the Component. + type: str + provider_type: + description: + - Provider type of components. + - "Example: + V(org.keycloak.storage.UserStorageProvider), + V(org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy), + V(org.keycloak.keys.KeyProvider), + V(org.keycloak.userprofile.UserProfileProvider), + V(org.keycloak.storage.ldap.mappers.LDAPStorageMapper)." + type: str + parent_id: + description: + - Container ID of the components. + type: str + + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + - community.general.attributes.info_module + +author: + - Andre Desrosiers (@desand01) +''' + +EXAMPLES = ''' + - name: Retrive info of a UserStorageProvider named myldap + community.general.keycloak_component_info: + auth_keycloak_url: http://localhost:8080/auth + auth_sername: admin + auth_password: password + auth_realm: master + realm: myrealm + name: myldap + provider_type: org.keycloak.storage.UserStorageProvider + + - name: Retrive key info component + community.general.keycloak_component_info: + auth_keycloak_url: http://localhost:8080/auth + auth_sername: admin + auth_password: password + auth_realm: master + realm: myrealm + name: rsa-enc-generated + provider_type: org.keycloak.keys.KeyProvider + + - name: Retrive all component from realm master + community.general.keycloak_component_info: + auth_keycloak_url: http://localhost:8080/auth + auth_sername: admin + auth_password: password + auth_realm: master + realm: myrealm + + - name: Retrive all sub components of parent component filter by type + community.general.keycloak_component_info: + auth_keycloak_url: http://localhost:8080/auth + auth_sername: admin + auth_password: password + auth_realm: master + realm: myrealm + parent_id: "075ef2fa-19fc-4a6d-bf4c-249f57365fd2" + provider_type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" + + +''' + +RETURN = ''' +components: + description: JSON representation of components. + returned: always + type: list + elements: dict +''' + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \ + keycloak_argument_spec, get_token, KeycloakError +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six.moves.urllib.parse import quote + + +def main(): + """ + Module execution + + :return: + """ + argument_spec = keycloak_argument_spec() + + meta_args = dict( + name=dict(type='str'), + realm=dict(type='str', required=True), + parent_id=dict(type='str'), + provider_type=dict(type='str'), + ) + + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + result = dict(changed=False, components=[]) + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + realm = module.params.get('realm') + parentId = module.params.get('parent_id') + name = module.params.get('name') + providerType = module.params.get('provider_type') + + objRealm = kc.get_realm_by_id(realm) + if not objRealm: + module.fail_json(msg="Failed to retrive realm '{realm}'".format(realm=realm)) + + filters = [] + + if parentId: + filters.append("parent=%s" % (quote(parentId, safe=''))) + else: + filters.append("parent=%s" % (quote(objRealm['id'], safe=''))) + + if name: + filters.append("name=%s" % (quote(name, safe=''))) + if providerType: + filters.append("type=%s" % (quote(providerType, safe=''))) + + result['components'] = kc.get_components(filter="&".join(filters), realm=realm) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/keycloak_group.py b/ansible_collections/community/general/plugins/modules/keycloak_group.py index 399bc5b4f..5398a4b5d 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_group.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_group.py @@ -41,9 +41,9 @@ options: state: description: - State of the group. - - On C(present), the group will be created if it does not yet exist, or updated with the parameters you provide. + - On V(present), the group will be created if it does not yet exist, or updated with the parameters you provide. - >- - On C(absent), the group will be removed if it exists. Be aware that absenting + On V(absent), the group will be removed if it exists. Be aware that absenting a group with subgroups will automatically delete all its subgroups too. default: 'present' type: str @@ -93,7 +93,7 @@ options: type: str description: - Identify parent by ID. - - Needs less API calls than using I(name). + - Needs less API calls than using O(parents[].name). - A deep parent chain can be started at any point when first given parent is given as ID. - Note that in principle both ID and name can be specified at the same time but current implementation only always use just one of them, with ID @@ -102,14 +102,14 @@ options: type: str description: - Identify parent by name. - - Needs more internal API calls than using I(id) to map names to ID's under the hood. + - Needs more internal API calls than using O(parents[].id) to map names to ID's under the hood. - When giving a parent chain with only names it must be complete up to the top. - Note that in principle both ID and name can be specified at the same time but current implementation only always use just one of them, with ID being preferred. notes: - - Presently, the I(realmRoles), I(clientRoles) and I(access) attributes returned by the Keycloak API + - Presently, the RV(end_state.realmRoles), RV(end_state.clientRoles), and RV(end_state.access) attributes returned by the Keycloak API are read-only for groups. This limitation will be removed in a later version of this module. extends_documentation_fragment: diff --git a/ansible_collections/community/general/plugins/modules/keycloak_identity_provider.py b/ansible_collections/community/general/plugins/modules/keycloak_identity_provider.py index 0d12ae03a..588f553e8 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_identity_provider.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_identity_provider.py @@ -36,8 +36,8 @@ options: state: description: - State of the identity provider. - - On C(present), the identity provider will be created if it does not yet exist, or updated with the parameters you provide. - - On C(absent), the identity provider will be removed if it exists. + - On V(present), the identity provider will be created if it does not yet exist, or updated with the parameters you provide. + - On V(absent), the identity provider will be removed if it exists. default: 'present' type: str choices: @@ -120,16 +120,16 @@ options: provider_id: description: - - Protocol used by this provider (supported values are C(oidc) or C(saml)). + - Protocol used by this provider (supported values are V(oidc) or V(saml)). aliases: - providerId type: str config: description: - - Dict specifying the configuration options for the provider; the contents differ depending on the value of I(providerId). - Examples are given below for C(oidc) and C(saml). It is easiest to obtain valid config values by dumping an already-existing - identity provider configuration through check-mode in the I(existing) field. + - Dict specifying the configuration options for the provider; the contents differ depending on the value of O(provider_id). + Examples are given below for V(oidc) and V(saml). It is easiest to obtain valid config values by dumping an already-existing + identity provider configuration through check-mode in the RV(existing) field. type: dict suboptions: hide_on_login_page: @@ -271,7 +271,8 @@ options: config: description: - - Dict specifying the configuration options for the mapper; the contents differ depending on the value of I(identityProviderMapper). + - Dict specifying the configuration options for the mapper; the contents differ depending on the value of + O(mappers[].identityProviderMapper). type: dict extends_documentation_fragment: @@ -541,10 +542,14 @@ def main(): old_mapper = dict() new_mapper = old_mapper.copy() new_mapper.update(change) - if new_mapper != old_mapper: - if changeset.get('mappers') is None: - changeset['mappers'] = list() - changeset['mappers'].append(new_mapper) + + if changeset.get('mappers') is None: + changeset['mappers'] = list() + # eventually this holds all desired mappers, unchanged, modified and newly added + changeset['mappers'].append(new_mapper) + + # ensure idempotency in case module.params.mappers is not sorted by name + changeset['mappers'] = sorted(changeset['mappers'], key=lambda x: x.get('id') if x.get('name') is None else x['name']) # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) desired_idp = before_idp.copy() @@ -611,10 +616,17 @@ def main(): # do the update desired_idp = desired_idp.copy() updated_mappers = desired_idp.pop('mappers', []) + original_mappers = list(before_idp.get('mappers', [])) + kc.update_identity_provider(desired_idp, realm) for mapper in updated_mappers: if mapper.get('id') is not None: - kc.update_identity_provider_mapper(mapper, alias, realm) + # only update existing if there is a change + for i, orig in enumerate(original_mappers): + if mapper['id'] == orig['id']: + del original_mappers[i] + if mapper != orig: + kc.update_identity_provider_mapper(mapper, alias, realm) else: if mapper.get('identityProviderAlias') is None: mapper['identityProviderAlias'] = alias diff --git a/ansible_collections/community/general/plugins/modules/keycloak_realm.py b/ansible_collections/community/general/plugins/modules/keycloak_realm.py index 53f81be48..9f2e72b52 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_realm.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_realm.py @@ -42,8 +42,8 @@ options: state: description: - State of the realm. - - On C(present), the realm will be created (or updated if it exists already). - - On C(absent), the realm will be removed if it exists. + - On V(present), the realm will be created (or updated if it exists already). + - On V(absent), the realm will be removed if it exists. choices: ['present', 'absent'] default: 'present' type: str diff --git a/ansible_collections/community/general/plugins/modules/keycloak_realm_key.py b/ansible_collections/community/general/plugins/modules/keycloak_realm_key.py new file mode 100644 index 000000000..6e762fba9 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/keycloak_realm_key.py @@ -0,0 +1,475 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Eike Frost +# Copyright (c) 2021, Christophe Gilles +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_realm_key + +short_description: Allows administration of Keycloak realm keys via Keycloak API + +version_added: 7.5.0 + +description: + - This module allows the administration of Keycloak realm keys via the Keycloak REST API. It + requires access to the REST API via OpenID Connect; the user connecting and the realm being + used must have the requisite access rights. In a default Keycloak installation, admin-cli + and an admin user would work, as would a separate realm definition with the scope tailored + to your needs and a user having the expected roles. + + - The names of module options are snake_cased versions of the camelCase ones found in the + Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html). + Aliases are provided so camelCased versions can be used as well. + + - This module is unable to detect changes to the actual cryptographic key after importing it. + However, if some other property is changed alongside the cryptographic key, then the key + will also get changed as a side-effect, as the JSON payload needs to include the private key. + This can be considered either a bug or a feature, as the alternative would be to always + update the realm key whether it has changed or not. + + - If certificate is not explicitly provided it will be dynamically created by Keycloak. + Therefore comparing the current state of the certificate to the desired state (which may be + empty) is not possible. + +attributes: + check_mode: + support: full + diff_mode: + support: partial + +options: + state: + description: + - State of the keycloak realm key. + - On V(present), the realm key will be created (or updated if it exists already). + - On V(absent), the realm key will be removed if it exists. + choices: ['present', 'absent'] + default: 'present' + type: str + name: + description: + - Name of the realm key to create. + type: str + required: true + force: + description: + - Enforce the state of the private key and certificate. This is not automatically the + case as this module is unable to determine the current state of the private key and + thus cannot trigger an update based on an actual divergence. That said, a private key + update may happen even if force is false as a side-effect of other changes. + default: false + type: bool + parent_id: + description: + - The parent_id of the realm key. In practice the ID (name) of the realm. + type: str + required: true + provider_id: + description: + - The name of the "provider ID" for the key. + - The value V(rsa-enc) has been added in community.general 8.2.0. + choices: ['rsa', 'rsa-enc'] + default: 'rsa' + type: str + config: + description: + - Dict specifying the key and its properties. + type: dict + suboptions: + active: + description: + - Whether they key is active or inactive. Not to be confused with the state + of the Ansible resource managed by the O(state) parameter. + default: true + type: bool + enabled: + description: + - Whether the key is enabled or disabled. Not to be confused with the state + of the Ansible resource managed by the O(state) parameter. + default: true + type: bool + priority: + description: + - The priority of the key. + type: int + required: true + algorithm: + description: + - Key algorithm. + - The values V(RS384), V(RS512), V(PS256), V(PS384), V(PS512), V(RSA1_5), + V(RSA-OAEP), V(RSA-OAEP-256) have been added in community.general 8.2.0. + default: RS256 + choices: ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256'] + type: str + private_key: + description: + - The private key as an ASCII string. Contents of the key must match O(config.algorithm) + and O(provider_id). + - Please note that the module cannot detect whether the private key specified differs from the + current state's private key. Use O(force=true) to force the module to update the private key + if you expect it to be updated. + required: true + type: str + certificate: + description: + - A certificate signed with the private key as an ASCII string. Contents of the + key must match O(config.algorithm) and O(provider_id). + - If you want Keycloak to automatically generate a certificate using your private key + then set this to an empty string. + required: true + type: str +notes: + - Current value of the private key cannot be fetched from Keycloak. + Therefore comparing its desired state to the current state is not + possible. + - If certificate is not explicitly provided it will be dynamically created + by Keycloak. Therefore comparing the current state of the certificate to + the desired state (which may be empty) is not possible. + - Due to the private key and certificate options the module is + B(not fully idempotent). You can use O(force=true) to force the module + to always update if you know that the private key might have changed. + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + +author: + - Samuli Seppänen (@mattock) +''' + +EXAMPLES = ''' +- name: Manage Keycloak realm key (certificate autogenerated by Keycloak) + community.general.keycloak_realm_key: + name: custom + state: present + parent_id: master + provider_id: rsa + auth_keycloak_url: http://localhost:8080/auth + auth_username: keycloak + auth_password: keycloak + auth_realm: master + config: + private_key: "{{ private_key }}" + certificate: "" + enabled: true + active: true + priority: 120 + algorithm: RS256 +- name: Manage Keycloak realm key and certificate + community.general.keycloak_realm_key: + name: custom + state: present + parent_id: master + provider_id: rsa + auth_keycloak_url: http://localhost:8080/auth + auth_username: keycloak + auth_password: keycloak + auth_realm: master + config: + private_key: "{{ private_key }}" + certificate: "{{ certificate }}" + enabled: true + active: true + priority: 120 + algorithm: RS256 +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + +end_state: + description: Representation of the keycloak_realm_key after module execution. + returned: on success + type: dict + contains: + id: + description: ID of the realm key. + type: str + returned: when O(state=present) + sample: 5b7ec13f-99da-46ad-8326-ab4c73cf4ce4 + name: + description: Name of the realm key. + type: str + returned: when O(state=present) + sample: mykey + parentId: + description: ID of the realm this key belongs to. + type: str + returned: when O(state=present) + sample: myrealm + providerId: + description: The ID of the key provider. + type: str + returned: when O(state=present) + sample: rsa + providerType: + description: The type of provider. + type: str + returned: when O(state=present) + config: + description: Realm key configuration. + type: dict + returned: when O(state=present) + sample: { + "active": ["true"], + "algorithm": ["RS256"], + "enabled": ["true"], + "priority": ["140"] + } +''' + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ + keycloak_argument_spec, get_token, KeycloakError +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six.moves.urllib.parse import urlencode +from copy import deepcopy + + +def main(): + """ + Module execution + + :return: + """ + argument_spec = keycloak_argument_spec() + + meta_args = dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + name=dict(type='str', required=True), + force=dict(type='bool', default=False), + parent_id=dict(type='str', required=True), + provider_id=dict(type='str', default='rsa', choices=['rsa', 'rsa-enc']), + config=dict( + type='dict', + options=dict( + active=dict(type='bool', default=True), + enabled=dict(type='bool', default=True), + priority=dict(type='int', required=True), + algorithm=dict( + type="str", + default="RS256", + choices=[ + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "RSA1_5", + "RSA-OAEP", + "RSA-OAEP-256", + ], + ), + private_key=dict(type='str', required=True, no_log=True), + certificate=dict(type='str', required=True) + ) + ) + ) + + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=([['token', 'auth_realm', 'auth_username', 'auth_password']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']])) + + # Initialize the result object. Only "changed" seems to have special + # meaning for Ansible. + result = dict(changed=False, msg='', end_state={}, diff=dict(before={}, after={})) + + # This will include the current state of the realm key if it is already + # present. This is only used for diff-mode. + before_realm_key = {} + before_realm_key['config'] = {} + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + params_to_ignore = list(keycloak_argument_spec().keys()) + ["state", "force"] + + # Filter and map the parameters names that apply to the role + component_params = [x for x in module.params + if x not in params_to_ignore and + module.params.get(x) is not None] + + # We only support one component provider type in this module + provider_type = 'org.keycloak.keys.KeyProvider' + + # Build a proposed changeset from parameters given to this module + changeset = {} + changeset['config'] = {} + + # Generate a JSON payload for Keycloak Admin API from the module + # parameters. Parameters that do not belong to the JSON payload (e.g. + # "state" or "auth_keycloal_url") have been filtered away earlier (see + # above). + # + # This loop converts Ansible module parameters (snake-case) into + # Keycloak-compatible format (camel-case). For example private_key + # becomes privateKey. + # + # It also converts bool, str and int parameters into lists with a single + # entry of 'str' type. Bool values are also lowercased. This is required + # by Keycloak. + # + for component_param in component_params: + if component_param == 'config': + for config_param in module.params.get('config'): + changeset['config'][camel(config_param)] = [] + raw_value = module.params.get('config')[config_param] + if isinstance(raw_value, bool): + value = str(raw_value).lower() + else: + value = str(raw_value) + + changeset['config'][camel(config_param)].append(value) + else: + # No need for camelcase in here as these are one word parameters + new_param_value = module.params.get(component_param) + changeset[camel(component_param)] = new_param_value + + # As provider_type is not a module parameter we have to add it to the + # changeset explicitly. + changeset['providerType'] = provider_type + + # Make a deep copy of the changeset. This is use when determining + # changes to the current state. + changeset_copy = deepcopy(changeset) + + # It is not possible to compare current keys to desired keys, because the + # certificate parameter is a base64-encoded binary blob created on the fly + # when a key is added. Moreover, the Keycloak Admin API does not seem to + # return the value of the private key for comparison. So, in effect, it we + # just have to ignore changes to the keys. However, as the privateKey + # parameter needs be present in the JSON payload, any changes done to any + # other parameters (e.g. config.priority) will trigger update of the keys + # as a side-effect. + del changeset_copy['config']['privateKey'] + del changeset_copy['config']['certificate'] + + # Make it easier to refer to current module parameters + name = module.params.get('name') + force = module.params.get('force') + state = module.params.get('state') + enabled = module.params.get('enabled') + provider_id = module.params.get('provider_id') + parent_id = module.params.get('parent_id') + + # Get a list of all Keycloak components that are of keyprovider type. + realm_keys = kc.get_components(urlencode(dict(type=provider_type, parent=parent_id)), parent_id) + + # If this component is present get its key ID. Confusingly the key ID is + # also known as the Provider ID. + key_id = None + + # Track individual parameter changes + changes = "" + + # This tells Ansible whether the key was changed (added, removed, modified) + result['changed'] = False + + # Loop through the list of components. If we encounter a component whose + # name matches the value of the name parameter then assume the key is + # already present. + for key in realm_keys: + if key['name'] == name: + key_id = key['id'] + changeset['id'] = key_id + changeset_copy['id'] = key_id + + # Compare top-level parameters + for param, value in changeset.items(): + before_realm_key[param] = key[param] + + if changeset_copy[param] != key[param] and param != 'config': + changes += "%s: %s -> %s, " % (param, key[param], changeset_copy[param]) + result['changed'] = True + + # Compare parameters under the "config" key + for p, v in changeset_copy['config'].items(): + before_realm_key['config'][p] = key['config'][p] + if changeset_copy['config'][p] != key['config'][p]: + changes += "config.%s: %s -> %s, " % (p, key['config'][p], changeset_copy['config'][p]) + result['changed'] = True + + # Sanitize linefeeds for the privateKey. Without this the JSON payload + # will be invalid. + changeset['config']['privateKey'][0] = changeset['config']['privateKey'][0].replace('\\n', '\n') + changeset['config']['certificate'][0] = changeset['config']['certificate'][0].replace('\\n', '\n') + + # Check all the possible states of the resource and do what is needed to + # converge current state with desired state (create, update or delete + # the key). + if key_id and state == 'present': + if result['changed']: + if module._diff: + del before_realm_key['config']['privateKey'] + del before_realm_key['config']['certificate'] + result['diff'] = dict(before=before_realm_key, after=changeset_copy) + + if module.check_mode: + result['msg'] = "Realm key %s would be changed: %s" % (name, changes.strip(", ")) + else: + kc.update_component(changeset, parent_id) + result['msg'] = "Realm key %s changed: %s" % (name, changes.strip(", ")) + elif not result['changed'] and force: + kc.update_component(changeset, parent_id) + result['changed'] = True + result['msg'] = "Realm key %s was forcibly updated" % (name) + else: + result['msg'] = "Realm key %s was in sync" % (name) + + result['end_state'] = changeset_copy + elif key_id and state == 'absent': + if module._diff: + del before_realm_key['config']['privateKey'] + del before_realm_key['config']['certificate'] + result['diff'] = dict(before=before_realm_key, after={}) + + if module.check_mode: + result['changed'] = True + result['msg'] = "Realm key %s would be deleted" % (name) + else: + kc.delete_component(key_id, parent_id) + result['changed'] = True + result['msg'] = "Realm key %s deleted" % (name) + + result['end_state'] = {} + elif not key_id and state == 'present': + if module._diff: + result['diff'] = dict(before={}, after=changeset_copy) + + if module.check_mode: + result['changed'] = True + result['msg'] = "Realm key %s would be created" % (name) + else: + kc.create_component(changeset, parent_id) + result['changed'] = True + result['msg'] = "Realm key %s created" % (name) + + result['end_state'] = changeset_copy + elif not key_id and state == 'absent': + result['changed'] = False + result['msg'] = "Realm key %s not present" % (name) + result['end_state'] = {} + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/keycloak_realm_rolemapping.py b/ansible_collections/community/general/plugins/modules/keycloak_realm_rolemapping.py new file mode 100644 index 000000000..693cf9894 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/keycloak_realm_rolemapping.py @@ -0,0 +1,391 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_realm_rolemapping + +short_description: Allows administration of Keycloak realm role mappings into groups with the Keycloak API + +version_added: 8.2.0 + +description: + - This module allows you to add, remove or modify Keycloak realm role + mappings into groups with the Keycloak REST API. It requires access to the + REST API via OpenID Connect; the user connecting and the client being used + must have the requisite access rights. In a default Keycloak installation, + admin-cli and an admin user would work, as would a separate client + definition with the scope tailored to your needs and a user having the + expected roles. + + - The names of module options are snake_cased versions of the camelCase ones found in the + Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/18.0/rest-api/index.html). + + - Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and will + be returned that way by this module. You may pass single values for attributes when calling the module, + and this will be translated into a list suitable for the API. + + - When updating a group_rolemapping, where possible provide the role ID to the module. This removes a lookup + to the API to translate the name into the role ID. + +attributes: + check_mode: + support: full + diff_mode: + support: full + +options: + state: + description: + - State of the realm_rolemapping. + - On C(present), the realm_rolemapping will be created if it does not yet exist, or updated with the parameters you provide. + - On C(absent), the realm_rolemapping will be removed if it exists. + default: 'present' + type: str + choices: + - present + - absent + + realm: + type: str + description: + - They Keycloak realm under which this role_representation resides. + default: 'master' + + group_name: + type: str + description: + - Name of the group to be mapped. + - This parameter is required (can be replaced by gid for less API call). + + parents: + type: list + description: + - List of parent groups for the group to handle sorted top to bottom. + - >- + Set this if your group is a subgroup and you do not provide the GID in O(gid). + elements: dict + suboptions: + id: + type: str + description: + - Identify parent by ID. + - Needs less API calls than using O(parents[].name). + - A deep parent chain can be started at any point when first given parent is given as ID. + - Note that in principle both ID and name can be specified at the same time + but current implementation only always use just one of them, with ID + being preferred. + name: + type: str + description: + - Identify parent by name. + - Needs more internal API calls than using O(parents[].id) to map names to ID's under the hood. + - When giving a parent chain with only names it must be complete up to the top. + - Note that in principle both ID and name can be specified at the same time + but current implementation only always use just one of them, with ID + being preferred. + gid: + type: str + description: + - ID of the group to be mapped. + - This parameter is not required for updating or deleting the rolemapping but + providing it will reduce the number of API calls required. + + roles: + description: + - Roles to be mapped to the group. + type: list + elements: dict + suboptions: + name: + type: str + description: + - Name of the role_representation. + - This parameter is required only when creating or updating the role_representation. + id: + type: str + description: + - The unique identifier for this role_representation. + - This parameter is not required for updating or deleting a role_representation but + providing it will reduce the number of API calls required. + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + +author: + - Gaëtan Daubresse (@Gaetan2907) + - Marius Huysamen (@mhuysamen) + - Alexander Groß (@agross) +''' + +EXAMPLES = ''' +- name: Map a client role to a group, authentication with credentials + community.general.keycloak_realm_rolemapping: + realm: MyCustomRealm + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + state: present + group_name: group1 + roles: + - name: role_name1 + id: role_id1 + - name: role_name2 + id: role_id2 + delegate_to: localhost + +- name: Map a client role to a group, authentication with token + community.general.keycloak_realm_rolemapping: + realm: MyCustomRealm + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + token: TOKEN + state: present + group_name: group1 + roles: + - name: role_name1 + id: role_id1 + - name: role_name2 + id: role_id2 + delegate_to: localhost + +- name: Map a client role to a subgroup, authentication with token + community.general.keycloak_realm_rolemapping: + realm: MyCustomRealm + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + token: TOKEN + state: present + group_name: subgroup1 + parents: + - name: parent-group + roles: + - name: role_name1 + id: role_id1 + - name: role_name2 + id: role_id2 + delegate_to: localhost + +- name: Unmap realm role from a group + community.general.keycloak_realm_rolemapping: + realm: MyCustomRealm + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + state: absent + group_name: group1 + roles: + - name: role_name1 + id: role_id1 + - name: role_name2 + id: role_id2 + delegate_to: localhost +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + sample: "Role role1 assigned to group group1." + +proposed: + description: Representation of proposed client role mapping. + returned: always + type: dict + sample: { + clientId: "test" + } + +existing: + description: + - Representation of existing client role mapping. + - The sample is truncated. + returned: always + type: dict + sample: { + "adminUrl": "http://www.example.com/admin_url", + "attributes": { + "request.object.signature.alg": "RS256", + } + } + +end_state: + description: + - Representation of client role mapping after module execution. + - The sample is truncated. + returned: on success + type: dict + sample: { + "adminUrl": "http://www.example.com/admin_url", + "attributes": { + "request.object.signature.alg": "RS256", + } + } +''' + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import ( + KeycloakAPI, keycloak_argument_spec, get_token, KeycloakError, +) +from ansible.module_utils.basic import AnsibleModule + + +def main(): + """ + Module execution + + :return: + """ + argument_spec = keycloak_argument_spec() + + roles_spec = dict( + name=dict(type='str'), + id=dict(type='str'), + ) + + meta_args = dict( + state=dict(default='present', choices=['present', 'absent']), + realm=dict(default='master'), + gid=dict(type='str'), + group_name=dict(type='str'), + parents=dict( + type='list', elements='dict', + options=dict( + id=dict(type='str'), + name=dict(type='str') + ), + ), + roles=dict(type='list', elements='dict', options=roles_spec), + ) + + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=([['token', 'auth_realm', 'auth_username', 'auth_password']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']])) + + result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={}) + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + realm = module.params.get('realm') + state = module.params.get('state') + gid = module.params.get('gid') + group_name = module.params.get('group_name') + roles = module.params.get('roles') + parents = module.params.get('parents') + + # Check the parameters + if gid is None and group_name is None: + module.fail_json(msg='Either the `group_name` or `gid` has to be specified.') + + # Get the potential missing parameters + if gid is None: + group_rep = kc.get_group_by_name(group_name, realm=realm, parents=parents) + if group_rep is not None: + gid = group_rep['id'] + else: + module.fail_json(msg='Could not fetch group %s:' % group_name) + else: + group_rep = kc.get_group_by_groupid(gid, realm=realm) + + if roles is None: + module.exit_json(msg="Nothing to do (no roles specified).") + else: + for role_index, role in enumerate(roles, start=0): + if role['name'] is None and role['id'] is None: + module.fail_json(msg='Either the `name` or `id` has to be specified on each role.') + # Fetch missing role_id + if role['id'] is None: + role_rep = kc.get_realm_role(role['name'], realm=realm) + if role_rep is not None: + role['id'] = role_rep['id'] + else: + module.fail_json(msg='Could not fetch realm role %s by name:' % (role['name'])) + # Fetch missing role_name + else: + for realm_role in kc.get_realm_roles(realm=realm): + if realm_role['id'] == role['id']: + role['name'] = realm_role['name'] + break + + if role['name'] is None: + module.fail_json(msg='Could not fetch realm role %s by ID' % (role['id'])) + + assigned_roles_before = group_rep.get('realmRoles', []) + + result['existing'] = assigned_roles_before + result['proposed'] = list(assigned_roles_before) if assigned_roles_before else [] + + update_roles = [] + for role_index, role in enumerate(roles, start=0): + # Fetch roles to assign if state present + if state == 'present': + if any(assigned == role['name'] for assigned in assigned_roles_before): + pass + else: + update_roles.append({ + 'id': role['id'], + 'name': role['name'], + }) + result['proposed'].append(role['name']) + # Fetch roles to remove if state absent + else: + if any(assigned == role['name'] for assigned in assigned_roles_before): + update_roles.append({ + 'id': role['id'], + 'name': role['name'], + }) + if role['name'] in result['proposed']: # Handle double removal + result['proposed'].remove(role['name']) + + if len(update_roles): + result['changed'] = True + if module._diff: + result['diff'] = dict(before=assigned_roles_before, after=result['proposed']) + if module.check_mode: + module.exit_json(**result) + + if state == 'present': + # Assign roles + kc.add_group_realm_rolemapping(gid=gid, role_rep=update_roles, realm=realm) + result['msg'] = 'Realm roles %s assigned to groupId %s.' % (update_roles, gid) + else: + # Remove mapping of role + kc.delete_group_realm_rolemapping(gid=gid, role_rep=update_roles, realm=realm) + result['msg'] = 'Realm roles %s removed from groupId %s.' % (update_roles, gid) + + if gid is None: + assigned_roles_after = kc.get_group_by_name(group_name, realm=realm, parents=parents).get('realmRoles', []) + else: + assigned_roles_after = kc.get_group_by_groupid(gid, realm=realm).get('realmRoles', []) + result['end_state'] = assigned_roles_after + module.exit_json(**result) + # Do nothing + else: + result['changed'] = False + result['msg'] = 'Nothing to do, roles %s are %s with group %s.' % (roles, 'mapped' if state == 'present' else 'not mapped', group_name) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/keycloak_role.py b/ansible_collections/community/general/plugins/modules/keycloak_role.py index bbec5f591..f3e01483f 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_role.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_role.py @@ -40,8 +40,8 @@ options: state: description: - State of the role. - - On C(present), the role will be created if it does not yet exist, or updated with the parameters you provide. - - On C(absent), the role will be removed if it exists. + - On V(present), the role will be created if it does not yet exist, or updated with the parameters you provide. + - On V(absent), the role will be removed if it exists. default: 'present' type: str choices: @@ -77,6 +77,42 @@ options: description: - A dict of key/value pairs to set as custom attributes for the role. - Values may be single values (e.g. a string) or a list of strings. + composite: + description: + - If V(true), the role is a composition of other realm and/or client role. + default: false + type: bool + version_added: 7.1.0 + composites: + description: + - List of roles to include to the composite realm role. + - If the composite role is a client role, the C(clientId) (not ID of the client) must be specified. + default: [] + type: list + elements: dict + version_added: 7.1.0 + suboptions: + name: + description: + - Name of the role. This can be the name of a REALM role or a client role. + type: str + required: true + client_id: + description: + - Client ID if the role is a client role. Do not include this option for a REALM role. + - Use the client ID you can see in the Keycloak console, not the technical ID of the client. + type: str + required: false + aliases: + - clientId + state: + description: + - Create the composite if present, remove it if absent. + type: str + choices: + - present + - absent + default: present extends_documentation_fragment: - community.general.keycloak @@ -198,8 +234,9 @@ end_state: ''' from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ - keycloak_argument_spec, get_token, KeycloakError + keycloak_argument_spec, get_token, KeycloakError, is_struct_included from ansible.module_utils.basic import AnsibleModule +import copy def main(): @@ -210,6 +247,12 @@ def main(): """ argument_spec = keycloak_argument_spec() + composites_spec = dict( + name=dict(type='str', required=True), + client_id=dict(type='str', aliases=['clientId'], required=False), + state=dict(type='str', default='present', choices=['present', 'absent']) + ) + meta_args = dict( state=dict(type='str', default='present', choices=['present', 'absent']), name=dict(type='str', required=True), @@ -217,6 +260,8 @@ def main(): realm=dict(type='str', default='master'), client_id=dict(type='str'), attributes=dict(type='dict'), + composites=dict(type='list', default=[], options=composites_spec, elements='dict'), + composite=dict(type='bool', default=False), ) argument_spec.update(meta_args) @@ -250,7 +295,7 @@ def main(): # Filter and map the parameters names that apply to the role role_params = [x for x in module.params - if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'client_id', 'composites'] and + if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'client_id'] and module.params.get(x) is not None] # See if it already exists in Keycloak @@ -269,10 +314,10 @@ def main(): new_param_value = module.params.get(param) old_value = before_role[param] if param in before_role else None if new_param_value != old_value: - changeset[camel(param)] = new_param_value + changeset[camel(param)] = copy.deepcopy(new_param_value) # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) - desired_role = before_role.copy() + desired_role = copy.deepcopy(before_role) desired_role.update(changeset) result['proposed'] = changeset @@ -309,6 +354,9 @@ def main(): kc.create_client_role(desired_role, clientid, realm) after_role = kc.get_client_role(name, clientid, realm) + if after_role['composite']: + after_role['composites'] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm) + result['end_state'] = after_role result['msg'] = 'Role {name} has been created'.format(name=name) @@ -316,10 +364,25 @@ def main(): else: if state == 'present': + compare_exclude = [] + if 'composites' in desired_role and isinstance(desired_role['composites'], list) and len(desired_role['composites']) > 0: + composites = kc.get_role_composites(rolerep=before_role, clientid=clientid, realm=realm) + before_role['composites'] = [] + for composite in composites: + before_composite = {} + if composite['clientRole']: + composite_client = kc.get_client_by_id(id=composite['containerId'], realm=realm) + before_composite['client_id'] = composite_client['clientId'] + else: + before_composite['client_id'] = None + before_composite['name'] = composite['name'] + before_composite['state'] = 'present' + before_role['composites'].append(before_composite) + else: + compare_exclude.append('composites') # Process an update - # no changes - if desired_role == before_role: + if is_struct_included(desired_role, before_role, exclude=compare_exclude): result['changed'] = False result['end_state'] = desired_role result['msg'] = "No changes required to role {name}.".format(name=name) @@ -341,6 +404,8 @@ def main(): else: kc.update_client_role(desired_role, clientid, realm) after_role = kc.get_client_role(name, clientid, realm) + if after_role['composite']: + after_role['composites'] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm) result['end_state'] = after_role diff --git a/ansible_collections/community/general/plugins/modules/keycloak_user.py b/ansible_collections/community/general/plugins/modules/keycloak_user.py new file mode 100644 index 000000000..1aeff0da5 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/keycloak_user.py @@ -0,0 +1,542 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2019, INSPQ (@elfelip) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: keycloak_user +short_description: Create and configure a user in Keycloak +description: + - This module creates, removes, or updates Keycloak users. +version_added: 7.1.0 +options: + auth_username: + aliases: [] + realm: + description: + - The name of the realm in which is the client. + default: master + type: str + username: + description: + - Username for the user. + required: true + type: str + id: + description: + - ID of the user on the Keycloak server if known. + type: str + enabled: + description: + - Enabled user. + type: bool + email_verified: + description: + - Check the validity of user email. + default: false + type: bool + aliases: + - emailVerified + first_name: + description: + - The user's first name. + required: false + type: str + aliases: + - firstName + last_name: + description: + - The user's last name. + required: false + type: str + aliases: + - lastName + email: + description: + - User email. + required: false + type: str + federation_link: + description: + - Federation Link. + required: false + type: str + aliases: + - federationLink + service_account_client_id: + description: + - Description of the client Application. + required: false + type: str + aliases: + - serviceAccountClientId + client_consents: + description: + - Client Authenticator Type. + type: list + elements: dict + default: [] + aliases: + - clientConsents + suboptions: + client_id: + description: + - Client ID of the client role. Not the technical ID of the client. + type: str + required: true + aliases: + - clientId + roles: + description: + - List of client roles to assign to the user. + type: list + required: true + elements: str + groups: + description: + - List of groups for the user. + type: list + elements: dict + default: [] + suboptions: + name: + description: + - Name of the group. + type: str + state: + description: + - Control whether the user must be member of this group or not. + choices: [ "present", "absent" ] + default: present + type: str + credentials: + description: + - User credentials. + default: [] + type: list + elements: dict + suboptions: + type: + description: + - Credential type. + type: str + required: true + value: + description: + - Value of the credential. + type: str + required: true + temporary: + description: + - If V(true), the users are required to reset their credentials at next login. + type: bool + default: false + required_actions: + description: + - RequiredActions user Auth. + default: [] + type: list + elements: str + aliases: + - requiredActions + federated_identities: + description: + - List of IDPs of user. + default: [] + type: list + elements: str + aliases: + - federatedIdentities + attributes: + description: + - List of user attributes. + required: false + type: list + elements: dict + suboptions: + name: + description: + - Name of the attribute. + type: str + values: + description: + - Values for the attribute as list. + type: list + elements: str + state: + description: + - Control whether the attribute must exists or not. + choices: [ "present", "absent" ] + default: present + type: str + access: + description: + - list user access. + required: false + type: dict + disableable_credential_types: + description: + - list user Credential Type. + default: [] + type: list + elements: str + aliases: + - disableableCredentialTypes + origin: + description: + - user origin. + required: false + type: str + self: + description: + - user self administration. + required: false + type: str + state: + description: + - Control whether the user should exists or not. + choices: [ "present", "absent" ] + default: present + type: str + force: + description: + - If V(true), allows to remove user and recreate it. + type: bool + default: false +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: full +notes: + - The module does not modify the user ID of an existing user. +author: + - Philippe Gauthier (@elfelip) +''' + +EXAMPLES = ''' +- name: Create a user user1 + community.general.keycloak_user: + auth_keycloak_url: http://localhost:8080/auth + auth_username: admin + auth_password: password + realm: master + username: user1 + firstName: user1 + lastName: user1 + email: user1 + enabled: true + emailVerified: false + credentials: + - type: password + value: password + temporary: false + attributes: + - name: attr1 + values: + - value1 + state: present + - name: attr2 + values: + - value2 + state: absent + groups: + - name: group1 + state: present + state: present + +- name: Re-create a User + community.general.keycloak_user: + auth_keycloak_url: http://localhost:8080/auth + auth_username: admin + auth_password: password + realm: master + username: user1 + firstName: user1 + lastName: user1 + email: user1 + enabled: true + emailVerified: false + credentials: + - type: password + value: password + temporary: false + attributes: + - name: attr1 + values: + - value1 + state: present + - name: attr2 + values: + - value2 + state: absent + groups: + - name: group1 + state: present + state: present + +- name: Re-create a User + community.general.keycloak_user: + auth_keycloak_url: http://localhost:8080/auth + auth_username: admin + auth_password: password + realm: master + username: user1 + firstName: user1 + lastName: user1 + email: user1 + enabled: true + emailVerified: false + credentials: + - type: password + value: password + temporary: false + attributes: + - name: attr1 + values: + - value1 + state: present + - name: attr2 + values: + - value2 + state: absent + groups: + - name: group1 + state: present + state: present + force: true + +- name: Remove User + community.general.keycloak_user: + auth_keycloak_url: http://localhost:8080/auth + auth_username: admin + auth_password: password + realm: master + username: user1 + state: absent +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + sample: User f18c709c-03d6-11ee-970b-c74bf2721112 created +proposed: + description: Representation of the proposed user. + returned: on success + type: dict +existing: + description: Representation of the existing user. + returned: on success + type: dict +end_state: + description: Representation of the user after module execution + returned: on success + type: dict +changed: + description: Return V(true) if the operation changed the user on the keycloak server, V(false) otherwise. + returned: always + type: bool +''' +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ + keycloak_argument_spec, get_token, KeycloakError, is_struct_included +from ansible.module_utils.basic import AnsibleModule +import copy + + +def main(): + argument_spec = keycloak_argument_spec() + argument_spec['auth_username']['aliases'] = [] + credential_spec = dict( + type=dict(type='str', required=True), + value=dict(type='str', required=True), + temporary=dict(type='bool', default=False) + ) + client_consents_spec = dict( + client_id=dict(type='str', required=True, aliases=['clientId']), + roles=dict(type='list', elements='str', required=True) + ) + attributes_spec = dict( + name=dict(type='str'), + values=dict(type='list', elements='str'), + state=dict(type='str', choices=['present', 'absent'], default='present') + ) + groups_spec = dict( + name=dict(type='str'), + state=dict(type='str', choices=['present', 'absent'], default='present') + ) + meta_args = dict( + realm=dict(type='str', default='master'), + self=dict(type='str'), + id=dict(type='str'), + username=dict(type='str', required=True), + first_name=dict(type='str', aliases=['firstName']), + last_name=dict(type='str', aliases=['lastName']), + email=dict(type='str'), + enabled=dict(type='bool'), + email_verified=dict(type='bool', default=False, aliases=['emailVerified']), + federation_link=dict(type='str', aliases=['federationLink']), + service_account_client_id=dict(type='str', aliases=['serviceAccountClientId']), + attributes=dict(type='list', elements='dict', options=attributes_spec), + access=dict(type='dict'), + groups=dict(type='list', default=[], elements='dict', options=groups_spec), + disableable_credential_types=dict(type='list', default=[], aliases=['disableableCredentialTypes'], elements='str'), + required_actions=dict(type='list', default=[], aliases=['requiredActions'], elements='str'), + credentials=dict(type='list', default=[], elements='dict', options=credential_spec), + federated_identities=dict(type='list', default=[], aliases=['federatedIdentities'], elements='str'), + client_consents=dict(type='list', default=[], aliases=['clientConsents'], elements='dict', options=client_consents_spec), + origin=dict(type='str'), + state=dict(choices=["absent", "present"], default='present'), + force=dict(type='bool', default=False), + ) + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=([['token', 'auth_realm', 'auth_username', 'auth_password']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']])) + + result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={}) + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + realm = module.params.get('realm') + state = module.params.get('state') + force = module.params.get('force') + username = module.params.get('username') + groups = module.params.get('groups') + + # Filter and map the parameters names that apply to the user + user_params = [x for x in module.params + if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'force', 'groups'] and + module.params.get(x) is not None] + + before_user = kc.get_user_by_username(username=username, realm=realm) + + if before_user is None: + before_user = {} + + changeset = {} + + for param in user_params: + new_param_value = module.params.get(param) + if param == 'attributes' and param in before_user: + old_value = kc.convert_keycloak_user_attributes_dict_to_module_list(attributes=before_user['attributes']) + else: + old_value = before_user[param] if param in before_user else None + if new_param_value != old_value: + if old_value is not None and param == 'attributes': + for old_attribute in old_value: + old_attribute_found = False + for new_attribute in new_param_value: + if new_attribute['name'] == old_attribute['name']: + old_attribute_found = True + if not old_attribute_found: + new_param_value.append(copy.deepcopy(old_attribute)) + if isinstance(new_param_value, dict): + changeset[camel(param)] = copy.deepcopy(new_param_value) + else: + changeset[camel(param)] = new_param_value + # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) + desired_user = copy.deepcopy(before_user) + desired_user.update(changeset) + + result['proposed'] = changeset + result['existing'] = before_user + + changed = False + + # Cater for when it doesn't exist (an empty dict) + if state == 'absent': + if not before_user: + # Do nothing and exit + if module._diff: + result['diff'] = dict(before='', after='') + result['changed'] = False + result['end_state'] = {} + result['msg'] = 'Role does not exist, doing nothing.' + module.exit_json(**result) + else: + # Delete user + kc.delete_user(user_id=before_user['id'], realm=realm) + result["msg"] = 'User %s deleted' % (before_user['username']) + changed = True + + else: + after_user = {} + if force and before_user: # If the force option is set to true + # Delete the existing user + kc.delete_user(user_id=before_user["id"], realm=realm) + + if not before_user or force: + # Process a creation + changed = True + + if username is None: + module.fail_json(msg='username must be specified when creating a new user') + + if module._diff: + result['diff'] = dict(before='', after=desired_user) + + if module.check_mode: + module.exit_json(**result) + # Create the user + after_user = kc.create_user(userrep=desired_user, realm=realm) + result["msg"] = 'User %s created' % (desired_user['username']) + # Add user ID to new representation + desired_user['id'] = after_user["id"] + else: + excludes = [ + "access", + "notBefore", + "createdTimestamp", + "totp", + "credentials", + "disableableCredentialTypes", + "groups", + "clientConsents", + "federatedIdentities", + "requiredActions"] + # Add user ID to new representation + desired_user['id'] = before_user["id"] + + # Compare users + if not (is_struct_included(desired_user, before_user, excludes)): # If the new user does not introduce a change to the existing user + # Update the user + after_user = kc.update_user(userrep=desired_user, realm=realm) + changed = True + + # set user groups + if kc.update_user_groups_membership(userrep=desired_user, groups=groups, realm=realm): + changed = True + # Get the user groups + after_user["groups"] = kc.get_user_groups(user_id=desired_user["id"], realm=realm) + result["end_state"] = after_user + if changed: + result["msg"] = 'User %s updated' % (desired_user['username']) + else: + result["msg"] = 'No changes made for user %s' % (desired_user['username']) + + result['changed'] = changed + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/keycloak_user_federation.py b/ansible_collections/community/general/plugins/modules/keycloak_user_federation.py index c0dc5d271..fee0d1265 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_user_federation.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_user_federation.py @@ -36,9 +36,9 @@ options: state: description: - State of the user federation. - - On C(present), the user federation will be created if it does not yet exist, or updated with + - On V(present), the user federation will be created if it does not yet exist, or updated with the parameters you provide. - - On C(absent), the user federation will be removed if it exists. + - On V(absent), the user federation will be removed if it exists. default: 'present' type: str choices: @@ -54,7 +54,7 @@ options: id: description: - The unique ID for this user federation. If left empty, the user federation will be searched - by its I(name). + by its O(name). type: str name: @@ -64,18 +64,15 @@ options: provider_id: description: - - Provider for this user federation. + - Provider for this user federation. Built-in providers are V(ldap), V(kerberos), and V(sssd). + Custom user storage providers can also be used. aliases: - providerId type: str - choices: - - ldap - - kerberos - - sssd provider_type: description: - - Component type for user federation (only supported value is C(org.keycloak.storage.UserStorageProvider)). + - Component type for user federation (only supported value is V(org.keycloak.storage.UserStorageProvider)). aliases: - providerType default: org.keycloak.storage.UserStorageProvider @@ -91,10 +88,10 @@ options: config: description: - Dict specifying the configuration options for the provider; the contents differ depending on - the value of I(provider_id). Examples are given below for C(ldap), C(kerberos) and C(sssd). + the value of O(provider_id). Examples are given below for V(ldap), V(kerberos) and V(sssd). It is easiest to obtain valid config values by dumping an already-existing user federation - configuration through check-mode in the I(existing) field. - - The value C(sssd) has been supported since community.general 4.2.0. + configuration through check-mode in the RV(existing) field. + - The value V(sssd) has been supported since community.general 4.2.0. type: dict suboptions: enabled: @@ -111,15 +108,15 @@ options: importEnabled: description: - - If C(true), LDAP users will be imported into Keycloak DB and synced by the configured + - If V(true), LDAP users will be imported into Keycloak DB and synced by the configured sync policies. default: true type: bool editMode: description: - - C(READ_ONLY) is a read-only LDAP store. C(WRITABLE) means data will be synced back to LDAP - on demand. C(UNSYNCED) means user data will be imported, but not synced back to LDAP. + - V(READ_ONLY) is a read-only LDAP store. V(WRITABLE) means data will be synced back to LDAP + on demand. V(UNSYNCED) means user data will be imported, but not synced back to LDAP. type: str choices: - READ_ONLY @@ -136,13 +133,13 @@ options: vendor: description: - LDAP vendor (provider). - - Use short name. For instance, write C(rhds) for "Red Hat Directory Server". + - Use short name. For instance, write V(rhds) for "Red Hat Directory Server". type: str usernameLDAPAttribute: description: - Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server - vendors it can be C(uid). For Active directory it can be C(sAMAccountName) or C(cn). + vendors it can be V(uid). For Active directory it can be V(sAMAccountName) or V(cn). The attribute should be filled for all LDAP user records you want to import from LDAP to Keycloak. type: str @@ -151,15 +148,15 @@ options: description: - Name of LDAP attribute, which is used as RDN (top attribute) of typical user DN. Usually it's the same as Username LDAP attribute, however it is not required. For - example for Active directory, it is common to use C(cn) as RDN attribute when - username attribute might be C(sAMAccountName). + example for Active directory, it is common to use V(cn) as RDN attribute when + username attribute might be V(sAMAccountName). type: str uuidLDAPAttribute: description: - Name of LDAP attribute, which is used as unique object identifier (UUID) for objects - in LDAP. For many LDAP server vendors, it is C(entryUUID); however some are different. - For example for Active directory it should be C(objectGUID). If your LDAP server does + in LDAP. For many LDAP server vendors, it is V(entryUUID); however some are different. + For example for Active directory it should be V(objectGUID). If your LDAP server does not support the notion of UUID, you can use any other attribute that is supposed to be unique among LDAP users in tree. type: str @@ -167,7 +164,7 @@ options: userObjectClasses: description: - All values of LDAP objectClass attribute for users in LDAP divided by comma. - For example C(inetOrgPerson, organizationalPerson). Newly created Keycloak users + For example V(inetOrgPerson, organizationalPerson). Newly created Keycloak users will be written to LDAP with all those object classes and existing LDAP user records are found just if they contain all those object classes. type: str @@ -251,8 +248,8 @@ options: useTruststoreSpi: description: - Specifies whether LDAP connection will use the truststore SPI with the truststore - configured in standalone.xml/domain.xml. C(Always) means that it will always use it. - C(Never) means that it will not use it. C(Only for ldaps) means that it will use if + configured in standalone.xml/domain.xml. V(always) means that it will always use it. + V(never) means that it will not use it. V(ldapsOnly) means that it will use if your connection URL use ldaps. Note even if standalone.xml/domain.xml is not configured, the default Java cacerts or certificate specified by C(javax.net.ssl.trustStore) property will be used. @@ -297,7 +294,7 @@ options: connectionPoolingDebug: description: - A string that indicates the level of debug output to produce. Example valid values are - C(fine) (trace connection creation and removal) and C(all) (all debugging information). + V(fine) (trace connection creation and removal) and V(all) (all debugging information). type: str connectionPoolingInitSize: @@ -321,7 +318,7 @@ options: connectionPoolingProtocol: description: - A list of space-separated protocol types of connections that may be pooled. - Valid types are C(plain) and C(ssl). + Valid types are V(plain) and V(ssl). type: str connectionPoolingTimeout: @@ -342,17 +339,27 @@ options: - Name of kerberos realm. type: str + krbPrincipalAttribute: + description: + - Name of the LDAP attribute, which refers to Kerberos principal. + This is used to lookup appropriate LDAP user after successful Kerberos/SPNEGO authentication in Keycloak. + When this is empty, the LDAP user will be looked based on LDAP username corresponding + to the first part of his Kerberos principal. For instance, for principal C(john@KEYCLOAK.ORG), + it will assume that LDAP username is V(john). + type: str + version_added: 8.1.0 + serverPrincipal: description: - Full name of server principal for HTTP service including server and domain name. For - example C(HTTP/host.foo.org@FOO.ORG). Use C(*) to accept any service principal in the + example V(HTTP/host.foo.org@FOO.ORG). Use V(*) to accept any service principal in the KeyTab file. type: str keyTab: description: - Location of Kerberos KeyTab file containing the credentials of server principal. For - example C(/etc/krb5.keytab). + example V(/etc/krb5.keytab). type: str debug: @@ -451,7 +458,7 @@ options: providerId: description: - - The mapper type for this mapper (for instance C(user-attribute-ldap-mapper)). + - The mapper type for this mapper (for instance V(user-attribute-ldap-mapper)). type: str providerType: @@ -464,6 +471,7 @@ options: description: - Dict specifying the configuration options for the mapper; the contents differ depending on the value of I(identityProviderMapper). + # TODO: what is identityProviderMapper above??? type: dict extends_documentation_fragment: @@ -763,6 +771,7 @@ def main(): readTimeout=dict(type='int'), searchScope=dict(type='str', choices=['1', '2'], default='1'), serverPrincipal=dict(type='str'), + krbPrincipalAttribute=dict(type='str'), startTls=dict(type='bool', default=False), syncRegistrations=dict(type='bool', default=False), trustEmail=dict(type='bool', default=False), @@ -793,7 +802,7 @@ def main(): realm=dict(type='str', default='master'), id=dict(type='str'), name=dict(type='str'), - provider_id=dict(type='str', aliases=['providerId'], choices=['ldap', 'kerberos', 'sssd']), + provider_id=dict(type='str', aliases=['providerId']), provider_type=dict(type='str', aliases=['providerType'], default='org.keycloak.storage.UserStorageProvider'), parent_id=dict(type='str', aliases=['parentId']), mappers=dict(type='list', elements='dict', options=mapper_spec), diff --git a/ansible_collections/community/general/plugins/modules/keycloak_user_rolemapping.py b/ansible_collections/community/general/plugins/modules/keycloak_user_rolemapping.py index d754e313a..59727a346 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_user_rolemapping.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_user_rolemapping.py @@ -42,8 +42,8 @@ options: state: description: - State of the user_rolemapping. - - On C(present), the user_rolemapping will be created if it does not yet exist, or updated with the parameters you provide. - - On C(absent), the user_rolemapping will be removed if it exists. + - On V(present), the user_rolemapping will be created if it does not yet exist, or updated with the parameters you provide. + - On V(absent), the user_rolemapping will be removed if it exists. default: 'present' type: str choices: @@ -79,8 +79,8 @@ options: client_id: type: str description: - - Name of the client to be mapped (different than I(cid)). - - This parameter is required if I(cid) is not provided (can be replaced by I(cid) + - Name of the client to be mapped (different than O(cid)). + - This parameter is required if O(cid) is not provided (can be replaced by O(cid) to reduce the number of API calls that must be made). cid: diff --git a/ansible_collections/community/general/plugins/modules/keyring.py b/ansible_collections/community/general/plugins/modules/keyring.py index ada22ed58..8329b727b 100644 --- a/ansible_collections/community/general/plugins/modules/keyring.py +++ b/ansible_collections/community/general/plugins/modules/keyring.py @@ -106,7 +106,7 @@ def del_passphrase(module): try: keyring.delete_password(module.params["service"], module.params["username"]) return None - except keyring.errors.KeyringLocked as keyring_locked_err: # pylint: disable=unused-variable + except keyring.errors.KeyringLocked: delete_argument = ( 'echo "%s" | gnome-keyring-daemon --unlock\nkeyring del %s %s\n' % ( @@ -140,7 +140,7 @@ def set_passphrase(module): module.params["user_password"], ) return None - except keyring.errors.KeyringLocked as keyring_locked_err: # pylint: disable=unused-variable + except keyring.errors.KeyringLocked: set_argument = ( 'echo "%s" | gnome-keyring-daemon --unlock\nkeyring set %s %s\n%s\n' % ( diff --git a/ansible_collections/community/general/plugins/modules/kibana_plugin.py b/ansible_collections/community/general/plugins/modules/kibana_plugin.py index a52eda2fd..f6744b396 100644 --- a/ansible_collections/community/general/plugins/modules/kibana_plugin.py +++ b/ansible_collections/community/general/plugins/modules/kibana_plugin.py @@ -60,7 +60,7 @@ options: version: description: - Version of the plugin to be installed. - - If plugin exists with previous version, plugin will NOT be updated unless C(force) is set to yes. + - If plugin exists with previous version, plugin will B(not) be updated unless O(force) is set to V(true). type: str force: description: diff --git a/ansible_collections/community/general/plugins/modules/launchd.py b/ansible_collections/community/general/plugins/modules/launchd.py index 13a8ce086..e5942ea7c 100644 --- a/ansible_collections/community/general/plugins/modules/launchd.py +++ b/ansible_collections/community/general/plugins/modules/launchd.py @@ -32,14 +32,14 @@ options: required: true state: description: - - C(started)/C(stopped) are idempotent actions that will not run + - V(started)/V(stopped) are idempotent actions that will not run commands unless necessary. - - Launchd does not support C(restarted) nor C(reloaded) natively. + - Launchd does not support V(restarted) nor V(reloaded) natively. These will trigger a stop/start (restarted) or an unload/load (reloaded). - - C(restarted) unloads and loads the service before start to ensure + - V(restarted) unloads and loads the service before start to ensure that the latest job definition (plist) is used. - - C(reloaded) unloads and loads the service to ensure that the latest + - V(reloaded) unloads and loads the service to ensure that the latest job definition (plist) is used. Whether a service is started or stopped depends on the content of the definition file. type: str @@ -54,7 +54,7 @@ options: - Whether the service should not be restarted automatically by launchd. - Services might have the 'KeepAlive' attribute set to true in a launchd configuration. In case this is set to true, stopping a service will cause that launchd starts the service again. - - Set this option to C(true) to let this module change the 'KeepAlive' attribute to false. + - Set this option to V(true) to let this module change the 'KeepAlive' attribute to V(false). type: bool default: false notes: diff --git a/ansible_collections/community/general/plugins/modules/layman.py b/ansible_collections/community/general/plugins/modules/layman.py index 940ac30d1..13d514274 100644 --- a/ansible_collections/community/general/plugins/modules/layman.py +++ b/ansible_collections/community/general/plugins/modules/layman.py @@ -19,7 +19,6 @@ description: - Uses Layman to manage an additional repositories for the Portage package manager on Gentoo Linux. Please note that Layman must be installed on a managed node prior using this module. requirements: - - "python >= 2.6" - layman python module extends_documentation_fragment: - community.general.attributes @@ -32,27 +31,27 @@ options: name: description: - The overlay id to install, synchronize, or uninstall. - Use 'ALL' to sync all of the installed overlays (can be used only when I(state=updated)). + Use 'ALL' to sync all of the installed overlays (can be used only when O(state=updated)). required: true type: str list_url: description: - An URL of the alternative overlays list that defines the overlay to install. - This list will be fetched and saved under C(${overlay_defs})/${name}.xml), where - C(overlay_defs) is readed from the Layman's configuration. + This list will be fetched and saved under C(${overlay_defs}/${name}.xml), where + C(overlay_defs) is read from the Layman's configuration. aliases: [url] type: str state: description: - - Whether to install (C(present)), sync (C(updated)), or uninstall (C(absent)) the overlay. + - Whether to install (V(present)), sync (V(updated)), or uninstall (V(absent)) the overlay. default: present choices: [present, absent, updated] type: str validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be - set to C(false) when no other option exists. Prior to 1.9.3 the code - defaulted to C(false). + - If V(false), SSL certificates will not be validated. This should only be + set to V(false) when no other option exists. Prior to 1.9.3 the code + defaulted to V(false). type: bool default: true ''' diff --git a/ansible_collections/community/general/plugins/modules/ldap_attrs.py b/ansible_collections/community/general/plugins/modules/ldap_attrs.py index c2cac8644..7986833a6 100644 --- a/ansible_collections/community/general/plugins/modules/ldap_attrs.py +++ b/ansible_collections/community/general/plugins/modules/ldap_attrs.py @@ -25,10 +25,10 @@ notes: bind over a UNIX domain socket. This works well with the default Ubuntu install for example, which includes a cn=peercred,cn=external,cn=auth ACL rule allowing root to modify the server configuration. If you need to use - a simple bind to access your server, pass the credentials in I(bind_dn) - and I(bind_pw). - - For I(state=present) and I(state=absent), all value comparisons are - performed on the server for maximum accuracy. For I(state=exact), values + a simple bind to access your server, pass the credentials in O(bind_dn) + and O(bind_pw). + - For O(state=present) and O(state=absent), all value comparisons are + performed on the server for maximum accuracy. For O(state=exact), values have to be compared in Python, which obviously ignores LDAP matching rules. This should work out in most cases, but it is theoretically possible to see spurious changes when target and actual values are @@ -44,7 +44,8 @@ attributes: check_mode: support: full diff_mode: - support: none + support: full + version_added: 8.5.0 options: state: required: false @@ -52,11 +53,11 @@ options: choices: [present, absent, exact] default: present description: - - The state of the attribute values. If C(present), all given attribute - values will be added if they're missing. If C(absent), all given - attribute values will be removed if present. If C(exact), the set of + - The state of the attribute values. If V(present), all given attribute + values will be added if they're missing. If V(absent), all given + attribute values will be removed if present. If V(exact), the set of attribute values will be forced to exactly those provided and no others. - If I(state=exact) and the attribute I(value) is empty, all values for + If O(state=exact) and the attribute value is empty, all values for this attribute will be removed. attributes: required: true @@ -69,16 +70,16 @@ options: readability for long string values by using YAML block modifiers as seen in the examples for this module. - Note that when using values that YAML/ansible-core interprets as other types, - like C(yes), C(no) (booleans), or C(2.10) (float), make sure to quote them if + like V(yes), V(no) (booleans), or V(2.10) (float), make sure to quote them if these are meant to be strings. Otherwise the wrong values may be sent to LDAP. ordered: required: false type: bool default: false description: - - If C(true), prepend list values with X-ORDERED index numbers in all + - If V(true), prepend list values with X-ORDERED index numbers in all attributes specified in the current task. This is useful mostly with - I(olcAccess) attribute to easily manage LDAP Access Control Lists. + C(olcAccess) attribute to easily manage LDAP Access Control Lists. extends_documentation_fragment: - community.general.ldap.documentation - community.general.attributes @@ -182,7 +183,7 @@ import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text -from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs, ldap_required_together import re @@ -207,7 +208,7 @@ class LdapAttrs(LdapGeneric): self.ordered = self.module.params['ordered'] def _order_values(self, values): - """ Preprend X-ORDERED index numbers to attribute's values. """ + """ Prepend X-ORDERED index numbers to attribute's values. """ ordered_values = [] if isinstance(values, list): @@ -235,26 +236,38 @@ class LdapAttrs(LdapGeneric): def add(self): modlist = [] + new_attrs = {} for name, values in self.module.params['attributes'].items(): norm_values = self._normalize_values(values) + added_values = [] for value in norm_values: if self._is_value_absent(name, value): modlist.append((ldap.MOD_ADD, name, value)) - - return modlist + added_values.append(value) + if added_values: + new_attrs[name] = norm_values + return modlist, {}, new_attrs def delete(self): modlist = [] + old_attrs = {} + new_attrs = {} for name, values in self.module.params['attributes'].items(): norm_values = self._normalize_values(values) + removed_values = [] for value in norm_values: if self._is_value_present(name, value): + removed_values.append(value) modlist.append((ldap.MOD_DELETE, name, value)) - - return modlist + if removed_values: + old_attrs[name] = norm_values + new_attrs[name] = [value for value in norm_values if value not in removed_values] + return modlist, old_attrs, new_attrs def exact(self): modlist = [] + old_attrs = {} + new_attrs = {} for name, values in self.module.params['attributes'].items(): norm_values = self._normalize_values(values) try: @@ -272,8 +285,13 @@ class LdapAttrs(LdapGeneric): modlist.append((ldap.MOD_DELETE, name, None)) else: modlist.append((ldap.MOD_REPLACE, name, norm_values)) + old_attrs[name] = current + new_attrs[name] = norm_values + if len(current) == 1 and len(norm_values) == 1: + old_attrs[name] = current[0] + new_attrs[name] = norm_values[0] - return modlist + return modlist, old_attrs, new_attrs def _is_value_present(self, name, value): """ True if the target attribute has the given value. """ @@ -300,6 +318,7 @@ def main(): state=dict(type='str', default='present', choices=['absent', 'exact', 'present']), ), supports_check_mode=True, + required_together=ldap_required_together(), ) if not HAS_LDAP: @@ -308,16 +327,18 @@ def main(): # Instantiate the LdapAttr object ldap = LdapAttrs(module) + old_attrs = None + new_attrs = None state = module.params['state'] # Perform action if state == 'present': - modlist = ldap.add() + modlist, old_attrs, new_attrs = ldap.add() elif state == 'absent': - modlist = ldap.delete() + modlist, old_attrs, new_attrs = ldap.delete() elif state == 'exact': - modlist = ldap.exact() + modlist, old_attrs, new_attrs = ldap.exact() changed = False @@ -330,7 +351,7 @@ def main(): except Exception as e: module.fail_json(msg="Attribute action failed.", details=to_native(e)) - module.exit_json(changed=changed, modlist=modlist) + module.exit_json(changed=changed, modlist=modlist, diff={"before": old_attrs, "after": new_attrs}) if __name__ == '__main__': diff --git a/ansible_collections/community/general/plugins/modules/ldap_entry.py b/ansible_collections/community/general/plugins/modules/ldap_entry.py index 619bbf927..5deaf7c4c 100644 --- a/ansible_collections/community/general/plugins/modules/ldap_entry.py +++ b/ansible_collections/community/general/plugins/modules/ldap_entry.py @@ -24,8 +24,8 @@ notes: bind over a UNIX domain socket. This works well with the default Ubuntu install for example, which includes a cn=peercred,cn=external,cn=auth ACL rule allowing root to modify the server configuration. If you need to use - a simple bind to access your server, pass the credentials in I(bind_dn) - and I(bind_pw). + a simple bind to access your server, pass the credentials in O(bind_dn) + and O(bind_pw). author: - Jiri Tyr (@jtyr) requirements: @@ -38,7 +38,7 @@ attributes: options: attributes: description: - - If I(state=present), attributes necessary to create an entry. Existing + - If O(state=present), attributes necessary to create an entry. Existing entries are never modified. To assert specific attribute values on an existing entry, use M(community.general.ldap_attrs) module instead. - Each attribute value can be a string for single-valued attributes or @@ -47,13 +47,13 @@ options: readability for long string values by using YAML block modifiers as seen in the examples for this module. - Note that when using values that YAML/ansible-core interprets as other types, - like C(yes), C(no) (booleans), or C(2.10) (float), make sure to quote them if + like V(yes), V(no) (booleans), or V(2.10) (float), make sure to quote them if these are meant to be strings. Otherwise the wrong values may be sent to LDAP. type: dict default: {} objectClass: description: - - If I(state=present), value or list of values to use when creating + - If O(state=present), value or list of values to use when creating the entry. It can either be a string or an actual list of strings. type: list @@ -66,7 +66,7 @@ options: type: str recursive: description: - - If I(state=delete), a flag indicating whether a single entry or the + - If O(state=delete), a flag indicating whether a single entry or the whole branch must be deleted. type: bool default: false @@ -151,7 +151,7 @@ import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.common.text.converters import to_native, to_bytes -from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs, ldap_required_together LDAP_IMP_ERR = None try: @@ -213,7 +213,7 @@ class LdapEntry(LdapGeneric): self.connection.delete_s(self.dn) def _delete_recursive(): - """ Attempt recurive deletion using the subtree-delete control. + """ Attempt recursive deletion using the subtree-delete control. If that fails, do it manually. """ try: subtree_delete = ldap.controls.ValueLessRequestControl('1.2.840.113556.1.4.805') @@ -255,6 +255,7 @@ def main(): ), required_if=[('state', 'present', ['objectClass'])], supports_check_mode=True, + required_together=ldap_required_together(), ) if not HAS_LDAP: diff --git a/ansible_collections/community/general/plugins/modules/ldap_passwd.py b/ansible_collections/community/general/plugins/modules/ldap_passwd.py index f47fa330e..5044586b0 100644 --- a/ansible_collections/community/general/plugins/modules/ldap_passwd.py +++ b/ansible_collections/community/general/plugins/modules/ldap_passwd.py @@ -20,10 +20,10 @@ description: notes: - The default authentication settings will attempt to use a SASL EXTERNAL bind over a UNIX domain socket. This works well with the default Ubuntu - install for example, which includes a cn=peercred,cn=external,cn=auth ACL + install for example, which includes a C(cn=peercred,cn=external,cn=auth) ACL rule allowing root to modify the server configuration. If you need to use - a simple bind to access your server, pass the credentials in I(bind_dn) - and I(bind_pw). + a simple bind to access your server, pass the credentials in O(bind_dn) + and O(bind_pw). author: - Keller Fuchs (@KellerFuchs) requirements: @@ -36,7 +36,7 @@ attributes: options: passwd: description: - - The (plaintext) password to be set for I(dn). + - The (plaintext) password to be set for O(dn). type: str extends_documentation_fragment: - community.general.ldap.documentation @@ -72,7 +72,7 @@ modlist: import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs, ldap_required_together LDAP_IMP_ERR = None try: @@ -133,6 +133,7 @@ def main(): module = AnsibleModule( argument_spec=gen_specs(passwd=dict(no_log=True)), supports_check_mode=True, + required_together=ldap_required_together(), ) if not HAS_LDAP: diff --git a/ansible_collections/community/general/plugins/modules/ldap_search.py b/ansible_collections/community/general/plugins/modules/ldap_search.py index ad79a2d73..45744e634 100644 --- a/ansible_collections/community/general/plugins/modules/ldap_search.py +++ b/ansible_collections/community/general/plugins/modules/ldap_search.py @@ -21,8 +21,8 @@ notes: bind over a UNIX domain socket. This works well with the default Ubuntu install for example, which includes a C(cn=peercred,cn=external,cn=auth) ACL rule allowing root to modify the server configuration. If you need to use - a simple bind to access your server, pass the credentials in I(bind_dn) - and I(bind_pw). + a simple bind to access your server, pass the credentials in O(bind_dn) + and O(bind_pw). author: - Sebastian Pfahl (@eryx12o45) requirements: @@ -59,8 +59,27 @@ options: default: false type: bool description: - - Set to C(true) to return the full attribute schema of entries, not - their attribute values. Overrides I(attrs) when provided. + - Set to V(true) to return the full attribute schema of entries, not + their attribute values. Overrides O(attrs) when provided. + page_size: + default: 0 + type: int + description: + - The page size when performing a simple paged result search (RFC 2696). + This setting can be tuned to reduce issues with timeouts and server limits. + - Setting the page size to V(0) (default) disables paged searching. + version_added: 7.1.0 + base64_attributes: + description: + - If provided, all attribute values returned that are listed in this option + will be Base64 encoded. + - If the special value V(*) appears in this list, all attributes will be + Base64 encoded. + - All other attribute values will be converted to UTF-8 strings. If they + contain binary data, please note that invalid UTF-8 bytes will be omitted. + type: list + elements: str + version_added: 7.0.0 extends_documentation_fragment: - community.general.ldap.documentation - community.general.attributes @@ -81,11 +100,28 @@ EXAMPLES = r""" register: ldap_group_gids """ +RESULTS = """ +results: + description: + - For every entry found, one dictionary will be returned. + - Every dictionary contains a key C(dn) with the entry's DN as a value. + - Every attribute of the entry found is added to the dictionary. If the key + has precisely one value, that value is taken directly, otherwise the key's + value is a list. + - Note that all values (for single-element lists) and list elements (for multi-valued + lists) will be UTF-8 strings. Some might contain Base64-encoded binary data; which + ones is determined by the O(base64_attributes) option. + type: list + elements: dict +""" + +import base64 import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.common.text.converters import to_native -from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text +from ansible.module_utils.six import binary_type, string_types, text_type +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs, ldap_required_together LDAP_IMP_ERR = None try: @@ -105,8 +141,11 @@ def main(): filter=dict(type='str', default='(objectClass=*)'), attrs=dict(type='list', elements='str'), schema=dict(type='bool', default=False), + page_size=dict(type='int', default=0), + base64_attributes=dict(type='list', elements='str'), ), supports_check_mode=True, + required_together=ldap_required_together(), ) if not HAS_LDAP: @@ -118,16 +157,30 @@ def main(): except Exception as exception: module.fail_json(msg="Attribute action failed.", details=to_native(exception)) - module.exit_json(changed=False) + +def _normalize_string(val, convert_to_base64): + if isinstance(val, (string_types, binary_type)): + if isinstance(val, text_type): + val = to_bytes(val, encoding='utf-8') + if convert_to_base64: + val = to_text(base64.b64encode(val)) + else: + # See https://github.com/ansible/ansible/issues/80258#issuecomment-1477038952 for details. + # We want to make sure that all strings are properly UTF-8 encoded, even if they were not, + # or happened to be byte strings. + val = to_text(val, 'utf-8', errors='replace') + # See also https://github.com/ansible-collections/community.general/issues/5704. + return val -def _extract_entry(dn, attrs): +def _extract_entry(dn, attrs, base64_attributes): extracted = {'dn': dn} for attr, val in list(attrs.items()): + convert_to_base64 = '*' in base64_attributes or attr in base64_attributes if len(val) == 1: - extracted[attr] = val[0] + extracted[attr] = _normalize_string(val[0], convert_to_base64) else: - extracted[attr] = val + extracted[attr] = [_normalize_string(v, convert_to_base64) for v in val] return extracted @@ -137,12 +190,14 @@ class LdapSearch(LdapGeneric): self.filterstr = self.module.params['filter'] self.attrlist = [] + self.page_size = self.module.params['page_size'] self._load_scope() self._load_attrs() self._load_schema() + self._base64_attributes = set(self.module.params['base64_attributes'] or []) def _load_schema(self): - self.schema = self.module.boolean(self.module.params['schema']) + self.schema = self.module.params['schema'] if self.schema: self.attrsonly = 1 else: @@ -165,22 +220,32 @@ class LdapSearch(LdapGeneric): self.module.exit_json(changed=False, results=results) def perform_search(self): + ldap_entries = [] + controls = [] + if self.page_size > 0: + controls.append(ldap.controls.libldap.SimplePagedResultsControl(True, size=self.page_size, cookie='')) try: - results = self.connection.search_s( - self.dn, - self.scope, - filterstr=self.filterstr, - attrlist=self.attrlist, - attrsonly=self.attrsonly - ) - ldap_entries = [] - for result in results: - if isinstance(result[1], dict): - if self.schema: - ldap_entries.append(dict(dn=result[0], attrs=list(result[1].keys()))) - else: - ldap_entries.append(_extract_entry(result[0], result[1])) - return ldap_entries + while True: + response = self.connection.search_ext( + self.dn, + self.scope, + filterstr=self.filterstr, + attrlist=self.attrlist, + attrsonly=self.attrsonly, + serverctrls=controls, + ) + rtype, results, rmsgid, serverctrls = self.connection.result3(response) + for result in results: + if isinstance(result[1], dict): + if self.schema: + ldap_entries.append(dict(dn=result[0], attrs=list(result[1].keys()))) + else: + ldap_entries.append(_extract_entry(result[0], result[1], self._base64_attributes)) + cookies = [c.cookie for c in serverctrls if c.controlType == ldap.controls.libldap.SimplePagedResultsControl.controlType] + if self.page_size > 0 and cookies and cookies[0]: + controls[0].cookie = cookies[0] + else: + return ldap_entries except ldap.NO_SUCH_OBJECT: self.module.fail_json(msg="Base not found: {0}".format(self.dn)) diff --git a/ansible_collections/community/general/plugins/modules/linode.py b/ansible_collections/community/general/plugins/modules/linode.py index 404e7a393..9e04ac63d 100644 --- a/ansible_collections/community/general/plugins/modules/linode.py +++ b/ansible_collections/community/general/plugins/modules/linode.py @@ -31,7 +31,7 @@ options: api_key: description: - Linode API key. - - C(LINODE_API_KEY) env variable can be used instead. + - E(LINODE_API_KEY) environment variable can be used instead. type: str required: true name: @@ -124,7 +124,7 @@ options: private_ip: description: - Add private IPv4 address when Linode is created. - - Default is C(false). + - Default is V(false). type: bool ssh_pub_key: description: @@ -149,7 +149,7 @@ options: type: int wait: description: - - wait for the instance to be in state C(running) before returning + - wait for the instance to be in state V(running) before returning type: bool default: true wait_timeout: @@ -163,7 +163,6 @@ options: type: bool default: true requirements: - - python >= 2.6 - linode-python author: - Vincent Viallet (@zbal) diff --git a/ansible_collections/community/general/plugins/modules/linode_v4.py b/ansible_collections/community/general/plugins/modules/linode_v4.py index f213af125..da885f3a5 100644 --- a/ansible_collections/community/general/plugins/modules/linode_v4.py +++ b/ansible_collections/community/general/plugins/modules/linode_v4.py @@ -14,7 +14,6 @@ module: linode_v4 short_description: Manage instances on the Linode cloud description: Manage instances on the Linode cloud. requirements: - - python >= 2.7 - linode_api4 >= 2.0.0 author: - Luke Murphy (@decentral1se) @@ -62,7 +61,7 @@ options: type: str private_ip: description: - - If C(true), the created Linode will have private networking enabled and + - If V(true), the created Linode will have private networking enabled and assigned a private IPv4 address. type: bool default: false @@ -95,7 +94,7 @@ options: access_token: description: - The Linode API v4 access token. It may also be specified by exposing - the C(LINODE_ACCESS_TOKEN) environment variable. See + the E(LINODE_ACCESS_TOKEN) environment variable. See U(https://www.linode.com/docs/api#access-and-authentication). required: true type: str diff --git a/ansible_collections/community/general/plugins/modules/listen_ports_facts.py b/ansible_collections/community/general/plugins/modules/listen_ports_facts.py index bc630e1d2..08030a8b3 100644 --- a/ansible_collections/community/general/plugins/modules/listen_ports_facts.py +++ b/ansible_collections/community/general/plugins/modules/listen_ports_facts.py @@ -40,7 +40,8 @@ options: include_non_listening: description: - Show both listening and non-listening sockets (for TCP this means established connections). - - Adds the return values C(state) and C(foreign_address) to the returned facts. + - Adds the return values RV(ansible_facts.tcp_listen[].state), RV(ansible_facts.udp_listen[].state), + RV(ansible_facts.tcp_listen[].foreign_address), and RV(ansible_facts.udp_listen[].foreign_address) to the returned facts. type: bool default: false version_added: 5.4.0 @@ -96,13 +97,13 @@ ansible_facts: sample: "0.0.0.0" foreign_address: description: The address of the remote end of the socket. - returned: if I(include_non_listening=true) + returned: if O(include_non_listening=true) type: str sample: "10.80.0.1" version_added: 5.4.0 state: description: The state of the socket. - returned: if I(include_non_listening=true) + returned: if O(include_non_listening=true) type: str sample: "ESTABLISHED" version_added: 5.4.0 @@ -148,13 +149,13 @@ ansible_facts: sample: "0.0.0.0" foreign_address: description: The address of the remote end of the socket. - returned: if I(include_non_listening=true) + returned: if O(include_non_listening=true) type: str sample: "10.80.0.1" version_added: 5.4.0 state: description: The state of the socket. UDP is a connectionless protocol. Shows UCONN or ESTAB. - returned: if I(include_non_listening=true) + returned: if O(include_non_listening=true) type: str sample: "UCONN" version_added: 5.4.0 @@ -199,7 +200,7 @@ from ansible.module_utils.basic import AnsibleModule def split_pid_name(pid_name): """ Split the entry PID/Program name into the PID (int) and the name (str) - :param pid_name: PID/Program String seperated with a dash. E.g 51/sshd: returns pid = 51 and name = sshd + :param pid_name: PID/Program String separated with a dash. E.g 51/sshd: returns pid = 51 and name = sshd :return: PID (int) and the program name (str) """ try: diff --git a/ansible_collections/community/general/plugins/modules/locale_gen.py b/ansible_collections/community/general/plugins/modules/locale_gen.py index fccdf977a..0dd76c9ab 100644 --- a/ansible_collections/community/general/plugins/modules/locale_gen.py +++ b/ansible_collections/community/general/plugins/modules/locale_gen.py @@ -35,6 +35,8 @@ options: - Whether the locale shall be present. choices: [ absent, present ] default: present +notes: + - This module does not support RHEL-based systems. ''' EXAMPLES = ''' @@ -46,154 +48,31 @@ EXAMPLES = ''' import os import re -from subprocess import Popen, PIPE, call - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.common.text.converters import to_native - -LOCALE_NORMALIZATION = { - ".utf8": ".UTF-8", - ".eucjp": ".EUC-JP", - ".iso885915": ".ISO-8859-15", - ".cp1251": ".CP1251", - ".koi8r": ".KOI8-R", - ".armscii8": ".ARMSCII-8", - ".euckr": ".EUC-KR", - ".gbk": ".GBK", - ".gb18030": ".GB18030", - ".euctw": ".EUC-TW", -} - - -# =========================================== -# location module specific support methods. -# - -def is_available(name, ubuntuMode): - """Check if the given locale is available on the system. This is done by - checking either : - * if the locale is present in /etc/locales.gen - * or if the locale is present in /usr/share/i18n/SUPPORTED""" - if ubuntuMode: - __regexp = r'^(?P\S+_\S+) (?P\S+)\s*$' - __locales_available = '/usr/share/i18n/SUPPORTED' - else: - __regexp = r'^#{0,1}\s*(?P\S+_\S+) (?P\S+)\s*$' - __locales_available = '/etc/locale.gen' - - re_compiled = re.compile(__regexp) - fd = open(__locales_available, 'r') - for line in fd: - result = re_compiled.match(line) - if result and result.group('locale') == name: - return True - fd.close() - return False - - -def is_present(name): - """Checks if the given locale is currently installed.""" - output = Popen(["locale", "-a"], stdout=PIPE).communicate()[0] - output = to_native(output) - return any(fix_case(name) == fix_case(line) for line in output.splitlines()) - - -def fix_case(name): - """locale -a might return the encoding in either lower or upper case. - Passing through this function makes them uniform for comparisons.""" - for s, r in LOCALE_NORMALIZATION.items(): - name = name.replace(s, r) - return name - - -def replace_line(existing_line, new_line): - """Replaces lines in /etc/locale.gen""" - try: - f = open("/etc/locale.gen", "r") - lines = [line.replace(existing_line, new_line) for line in f] - finally: - f.close() - try: - f = open("/etc/locale.gen", "w") - f.write("".join(lines)) - finally: - f.close() - - -def set_locale(name, enabled=True): - """ Sets the state of the locale. Defaults to enabled. """ - search_string = r'#{0,1}\s*%s (?P.+)' % name - if enabled: - new_string = r'%s \g' % (name) - else: - new_string = r'# %s \g' % (name) - try: - f = open("/etc/locale.gen", "r") - lines = [re.sub(search_string, new_string, line) for line in f] - finally: - f.close() - try: - f = open("/etc/locale.gen", "w") - f.write("".join(lines)) - finally: - f.close() - - -def apply_change(targetState, name): - """Create or remove locale. - - Keyword arguments: - targetState -- Desired state, either present or absent. - name -- Name including encoding such as de_CH.UTF-8. - """ - if targetState == "present": - # Create locale. - set_locale(name, enabled=True) - else: - # Delete locale. - set_locale(name, enabled=False) - - localeGenExitValue = call("locale-gen") - if localeGenExitValue != 0: - raise EnvironmentError(localeGenExitValue, "locale.gen failed to execute, it returned " + str(localeGenExitValue)) - - -def apply_change_ubuntu(targetState, name): - """Create or remove locale. - - Keyword arguments: - targetState -- Desired state, either present or absent. - name -- Name including encoding such as de_CH.UTF-8. - """ - if targetState == "present": - # Create locale. - # Ubuntu's patched locale-gen automatically adds the new locale to /var/lib/locales/supported.d/local - localeGenExitValue = call(["locale-gen", name]) - else: - # Delete locale involves discarding the locale from /var/lib/locales/supported.d/local and regenerating all locales. - try: - f = open("/var/lib/locales/supported.d/local", "r") - content = f.readlines() - finally: - f.close() - try: - f = open("/var/lib/locales/supported.d/local", "w") - for line in content: - locale, charset = line.split(' ') - if locale != name: - f.write(line) - finally: - f.close() - # Purge locales and regenerate. - # Please provide a patch if you know how to avoid regenerating the locales to keep! - localeGenExitValue = call(["locale-gen", "--purge"]) - - if localeGenExitValue != 0: - raise EnvironmentError(localeGenExitValue, "locale.gen failed to execute, it returned " + str(localeGenExitValue)) - -def main(): - module = AnsibleModule( +from ansible_collections.community.general.plugins.module_utils.module_helper import StateModuleHelper +from ansible_collections.community.general.plugins.module_utils.mh.deco import check_mode_skip + +from ansible_collections.community.general.plugins.module_utils.locale_gen import locale_runner, locale_gen_runner + + +class LocaleGen(StateModuleHelper): + LOCALE_NORMALIZATION = { + ".utf8": ".UTF-8", + ".eucjp": ".EUC-JP", + ".iso885915": ".ISO-8859-15", + ".cp1251": ".CP1251", + ".koi8r": ".KOI8-R", + ".armscii8": ".ARMSCII-8", + ".euckr": ".EUC-KR", + ".gbk": ".GBK", + ".gb18030": ".GB18030", + ".euctw": ".EUC-TW", + } + LOCALE_GEN = "/etc/locale.gen" + LOCALE_SUPPORTED = "/var/lib/locales/supported.d/" + + output_params = ["name"] + module = dict( argument_spec=dict( name=dict(type='str', required=True), state=dict(type='str', default='present', choices=['absent', 'present']), @@ -201,42 +80,133 @@ def main(): supports_check_mode=True, ) - name = module.params['name'] - state = module.params['state'] - - if not os.path.exists("/var/lib/locales/supported.d/"): - if os.path.exists("/etc/locale.gen"): - # We found the common way to manage locales. - ubuntuMode = False + def __init_module__(self): + self.vars.set("ubuntu_mode", False) + if os.path.exists(self.LOCALE_SUPPORTED): + self.vars.ubuntu_mode = True + else: + if not os.path.exists(self.LOCALE_GEN): + self.do_raise("{0} and {1} are missing. Is the package \"locales\" installed?".format( + self.LOCALE_SUPPORTED, self.LOCALE_GEN + )) + + if not self.is_available(): + self.do_raise("The locale you've entered is not available on your system.") + + self.vars.set("is_present", self.is_present(), output=False) + self.vars.set("state_tracking", self._state_name(self.vars.is_present), output=False, change=True) + + def __quit_module__(self): + self.vars.state_tracking = self._state_name(self.is_present()) + + @staticmethod + def _state_name(present): + return "present" if present else "absent" + + def is_available(self): + """Check if the given locale is available on the system. This is done by + checking either : + * if the locale is present in /etc/locales.gen + * or if the locale is present in /usr/share/i18n/SUPPORTED""" + __regexp = r'^#?\s*(?P\S+[\._\S]+) (?P\S+)\s*$' + if self.vars.ubuntu_mode: + __locales_available = '/usr/share/i18n/SUPPORTED' + else: + __locales_available = '/etc/locale.gen' + + re_compiled = re.compile(__regexp) + with open(__locales_available, 'r') as fd: + lines = fd.readlines() + res = [re_compiled.match(line) for line in lines] + if self.verbosity >= 4: + self.vars.available_lines = lines + if any(r.group("locale") == self.vars.name for r in res if r): + return True + # locale may be installed but not listed in the file, for example C.UTF-8 in some systems + return self.is_present() + + def is_present(self): + runner = locale_runner(self.module) + with runner() as ctx: + rc, out, err = ctx.run() + if self.verbosity >= 4: + self.vars.locale_run_info = ctx.run_info + return any(self.fix_case(self.vars.name) == self.fix_case(line) for line in out.splitlines()) + + def fix_case(self, name): + """locale -a might return the encoding in either lower or upper case. + Passing through this function makes them uniform for comparisons.""" + for s, r in self.LOCALE_NORMALIZATION.items(): + name = name.replace(s, r) + return name + + def set_locale(self, name, enabled=True): + """ Sets the state of the locale. Defaults to enabled. """ + search_string = r'#?\s*%s (?P.+)' % re.escape(name) + if enabled: + new_string = r'%s \g' % (name) else: - module.fail_json(msg="/etc/locale.gen and /var/lib/locales/supported.d/local are missing. Is the package \"locales\" installed?") - else: - # Ubuntu created its own system to manage locales. - ubuntuMode = True - - if not is_available(name, ubuntuMode): - module.fail_json(msg="The locale you've entered is not available " - "on your system.") - - if is_present(name): - prev_state = "present" - else: - prev_state = "absent" - changed = (prev_state != state) - - if module.check_mode: - module.exit_json(changed=changed) - else: - if changed: - try: - if ubuntuMode is False: - apply_change(state, name) - else: - apply_change_ubuntu(state, name) - except EnvironmentError as e: - module.fail_json(msg=to_native(e), exitValue=e.errno) - - module.exit_json(name=name, changed=changed, msg="OK") + new_string = r'# %s \g' % (name) + re_search = re.compile(search_string) + with open("/etc/locale.gen", "r") as fr: + lines = [re_search.sub(new_string, line) for line in fr] + with open("/etc/locale.gen", "w") as fw: + fw.write("".join(lines)) + + def apply_change(self, targetState, name): + """Create or remove locale. + + Keyword arguments: + targetState -- Desired state, either present or absent. + name -- Name including encoding such as de_CH.UTF-8. + """ + + self.set_locale(name, enabled=(targetState == "present")) + + runner = locale_gen_runner(self.module) + with runner() as ctx: + ctx.run() + + def apply_change_ubuntu(self, targetState, name): + """Create or remove locale. + + Keyword arguments: + targetState -- Desired state, either present or absent. + name -- Name including encoding such as de_CH.UTF-8. + """ + runner = locale_gen_runner(self.module) + + if targetState == "present": + # Create locale. + # Ubuntu's patched locale-gen automatically adds the new locale to /var/lib/locales/supported.d/local + with runner() as ctx: + ctx.run() + else: + # Delete locale involves discarding the locale from /var/lib/locales/supported.d/local and regenerating all locales. + with open("/var/lib/locales/supported.d/local", "r") as fr: + content = fr.readlines() + with open("/var/lib/locales/supported.d/local", "w") as fw: + for line in content: + locale, charset = line.split(' ') + if locale != name: + fw.write(line) + # Purge locales and regenerate. + # Please provide a patch if you know how to avoid regenerating the locales to keep! + with runner("purge") as ctx: + ctx.run() + + @check_mode_skip + def __state_fallback__(self): + if self.vars.state_tracking == self.vars.state: + return + if self.vars.ubuntu_mode: + self.apply_change_ubuntu(self.vars.state, self.vars.name) + else: + self.apply_change(self.vars.state, self.vars.name) + + +def main(): + LocaleGen.execute() if __name__ == '__main__': diff --git a/ansible_collections/community/general/plugins/modules/lvg.py b/ansible_collections/community/general/plugins/modules/lvg.py index 60eaaa42b..8a6384369 100644 --- a/ansible_collections/community/general/plugins/modules/lvg.py +++ b/ansible_collections/community/general/plugins/modules/lvg.py @@ -39,10 +39,10 @@ options: elements: str pesize: description: - - "The size of the physical extent. I(pesize) must be a power of 2 of at least 1 sector + - "The size of the physical extent. O(pesize) must be a power of 2 of at least 1 sector (where the sector size is the largest sector size of the PVs currently used in the VG), or at least 128KiB." - - Since Ansible 2.6, pesize can be optionally suffixed by a UNIT (k/K/m/M/g/G), default unit is megabyte. + - O(pesize) can be optionally suffixed by a UNIT (k/K/m/M/g/G), default unit is megabyte. type: str default: "4" pv_options: @@ -52,7 +52,7 @@ options: default: '' pvresize: description: - - If C(true), resize the physical volume to the maximum available size. + - If V(true), resize the physical volume to the maximum available size. type: bool default: false version_added: '0.2.0' @@ -63,15 +63,33 @@ options: default: '' state: description: - - Control if the volume group exists. + - Control if the volume group exists and it's state. + - The states V(active) and V(inactive) implies V(present) state. Added in 7.1.0 + - "If V(active) or V(inactive), the module manages the VG's logical volumes current state. + The module also handles the VG's autoactivation state if supported + unless when creating a volume group and the autoactivation option specified in O(vg_options)." type: str - choices: [ absent, present ] + choices: [ absent, present, active, inactive ] default: present force: description: - - If C(true), allows to remove volume group with logical volumes. + - If V(true), allows to remove volume group with logical volumes. type: bool default: false + reset_vg_uuid: + description: + - Whether the volume group's UUID is regenerated. + - This is B(not idempotent). Specifying this parameter always results in a change. + type: bool + default: false + version_added: 7.1.0 + reset_pv_uuid: + description: + - Whether the volume group's physical volumes' UUIDs are regenerated. + - This is B(not idempotent). Specifying this parameter always results in a change. + type: bool + default: false + version_added: 7.1.0 seealso: - module: community.general.filesystem - module: community.general.lvol @@ -112,6 +130,30 @@ EXAMPLES = r''' vg: resizableVG pvs: /dev/sda3 pvresize: true + +- name: Deactivate a volume group + community.general.lvg: + state: inactive + vg: vg.services + +- name: Activate a volume group + community.general.lvg: + state: active + vg: vg.services + +- name: Reset a volume group UUID + community.general.lvg: + state: inactive + vg: vg.services + reset_vg_uuid: true + +- name: Reset both volume group and pv UUID + community.general.lvg: + state: inactive + vg: vg.services + pvs: /dev/sdb1,/dev/sdc5 + reset_vg_uuid: true + reset_pv_uuid: true ''' import itertools @@ -119,6 +161,8 @@ import os from ansible.module_utils.basic import AnsibleModule +VG_AUTOACTIVATION_OPT = '--setautoactivation' + def parse_vgs(data): vgs = [] @@ -156,6 +200,178 @@ def parse_pvs(module, data): return pvs +def find_vg(module, vg): + if not vg: + return None + vgs_cmd = module.get_bin_path('vgs', True) + dummy, current_vgs, dummy = module.run_command("%s --noheadings -o vg_name,pv_count,lv_count --separator ';'" % vgs_cmd, check_rc=True) + + vgs = parse_vgs(current_vgs) + + for test_vg in vgs: + if test_vg['name'] == vg: + this_vg = test_vg + break + else: + this_vg = None + + return this_vg + + +def is_autoactivation_supported(module, vg_cmd): + autoactivation_supported = False + dummy, vgchange_opts, dummy = module.run_command([vg_cmd, '--help'], check_rc=True) + + if VG_AUTOACTIVATION_OPT in vgchange_opts: + autoactivation_supported = True + + return autoactivation_supported + + +def activate_vg(module, vg, active): + changed = False + vgchange_cmd = module.get_bin_path('vgchange', True) + vgs_cmd = module.get_bin_path('vgs', True) + vgs_fields = ['lv_attr'] + + autoactivation_enabled = False + autoactivation_supported = is_autoactivation_supported(module=module, vg_cmd=vgchange_cmd) + + if autoactivation_supported: + vgs_fields.append('autoactivation') + + vgs_cmd_with_opts = [vgs_cmd, '--noheadings', '-o', ','.join(vgs_fields), '--separator', ';', vg] + dummy, current_vg_lv_states, dummy = module.run_command(vgs_cmd_with_opts, check_rc=True) + + lv_active_count = 0 + lv_inactive_count = 0 + + for line in current_vg_lv_states.splitlines(): + parts = line.strip().split(';') + if parts[0][4] == 'a': + lv_active_count += 1 + else: + lv_inactive_count += 1 + if autoactivation_supported: + autoactivation_enabled = autoactivation_enabled or parts[1] == 'enabled' + + activate_flag = None + if active and lv_inactive_count > 0: + activate_flag = 'y' + elif not active and lv_active_count > 0: + activate_flag = 'n' + + # Extra logic necessary because vgchange returns error when autoactivation is already set + if autoactivation_supported: + if active and not autoactivation_enabled: + if module.check_mode: + changed = True + else: + module.run_command([vgchange_cmd, VG_AUTOACTIVATION_OPT, 'y', vg], check_rc=True) + changed = True + elif not active and autoactivation_enabled: + if module.check_mode: + changed = True + else: + module.run_command([vgchange_cmd, VG_AUTOACTIVATION_OPT, 'n', vg], check_rc=True) + changed = True + + if activate_flag is not None: + if module.check_mode: + changed = True + else: + module.run_command([vgchange_cmd, '--activate', activate_flag, vg], check_rc=True) + changed = True + + return changed + + +def append_vgcreate_options(module, state, vgoptions): + vgcreate_cmd = module.get_bin_path('vgcreate', True) + + autoactivation_supported = is_autoactivation_supported(module=module, vg_cmd=vgcreate_cmd) + + if autoactivation_supported and state in ['active', 'inactive']: + if VG_AUTOACTIVATION_OPT not in vgoptions: + if state == 'active': + vgoptions += [VG_AUTOACTIVATION_OPT, 'y'] + else: + vgoptions += [VG_AUTOACTIVATION_OPT, 'n'] + + +def get_pv_values_for_resize(module, device): + pvdisplay_cmd = module.get_bin_path('pvdisplay', True) + pvdisplay_ops = ["--units", "b", "--columns", "--noheadings", "--nosuffix", "--separator", ";", "-o", "dev_size,pv_size,pe_start,vg_extent_size"] + pvdisplay_cmd_device_options = [pvdisplay_cmd, device] + pvdisplay_ops + + dummy, pv_values, dummy = module.run_command(pvdisplay_cmd_device_options, check_rc=True) + + values = pv_values.strip().split(';') + + dev_size = int(values[0]) + pv_size = int(values[1]) + pe_start = int(values[2]) + vg_extent_size = int(values[3]) + + return (dev_size, pv_size, pe_start, vg_extent_size) + + +def resize_pv(module, device): + changed = False + pvresize_cmd = module.get_bin_path('pvresize', True) + + dev_size, pv_size, pe_start, vg_extent_size = get_pv_values_for_resize(module=module, device=device) + if (dev_size - (pe_start + pv_size)) > vg_extent_size: + if module.check_mode: + changed = True + else: + # If there is a missing pv on the machine, versions of pvresize rc indicates failure. + rc, out, err = module.run_command([pvresize_cmd, device]) + dummy, new_pv_size, dummy, dummy = get_pv_values_for_resize(module=module, device=device) + if pv_size == new_pv_size: + module.fail_json(msg="Failed executing pvresize command.", rc=rc, err=err, out=out) + else: + changed = True + + return changed + + +def reset_uuid_pv(module, device): + changed = False + pvs_cmd = module.get_bin_path('pvs', True) + pvs_cmd_with_opts = [pvs_cmd, '--noheadings', '-o', 'uuid', device] + pvchange_cmd = module.get_bin_path('pvchange', True) + pvchange_cmd_with_opts = [pvchange_cmd, '-u', device] + + dummy, orig_uuid, dummy = module.run_command(pvs_cmd_with_opts, check_rc=True) + + if module.check_mode: + changed = True + else: + # If there is a missing pv on the machine, pvchange rc indicates failure. + pvchange_rc, pvchange_out, pvchange_err = module.run_command(pvchange_cmd_with_opts) + dummy, new_uuid, dummy = module.run_command(pvs_cmd_with_opts, check_rc=True) + if orig_uuid.strip() == new_uuid.strip(): + module.fail_json(msg="PV (%s) UUID change failed" % (device), rc=pvchange_rc, err=pvchange_err, out=pvchange_out) + else: + changed = True + + return changed + + +def reset_uuid_vg(module, vg): + changed = False + vgchange_cmd = module.get_bin_path('vgchange', True) + vgchange_cmd_with_opts = [vgchange_cmd, '-u', vg] + if module.check_mode: + changed = True + else: + module.run_command(vgchange_cmd_with_opts, check_rc=True) + changed = True + + return changed + + def main(): module = AnsibleModule( argument_spec=dict( @@ -165,9 +381,14 @@ def main(): pv_options=dict(type='str', default=''), pvresize=dict(type='bool', default=False), vg_options=dict(type='str', default=''), - state=dict(type='str', default='present', choices=['absent', 'present']), + state=dict(type='str', default='present', choices=['absent', 'present', 'active', 'inactive']), force=dict(type='bool', default=False), + reset_vg_uuid=dict(type='bool', default=False), + reset_pv_uuid=dict(type='bool', default=False), ), + required_if=[ + ['reset_pv_uuid', True, ['pvs']], + ], supports_check_mode=True, ) @@ -178,18 +399,25 @@ def main(): pesize = module.params['pesize'] pvoptions = module.params['pv_options'].split() vgoptions = module.params['vg_options'].split() + reset_vg_uuid = module.boolean(module.params['reset_vg_uuid']) + reset_pv_uuid = module.boolean(module.params['reset_pv_uuid']) + + this_vg = find_vg(module=module, vg=vg) + present_state = state in ['present', 'active', 'inactive'] + pvs_required = present_state and this_vg is None + changed = False dev_list = [] if module.params['pvs']: dev_list = list(module.params['pvs']) - elif state == 'present': + elif pvs_required: module.fail_json(msg="No physical volumes given.") # LVM always uses real paths not symlinks so replace symlinks with actual path for idx, dev in enumerate(dev_list): dev_list[idx] = os.path.realpath(dev) - if state == 'present': + if present_state: # check given devices for test_dev in dev_list: if not os.path.exists(test_dev): @@ -216,25 +444,9 @@ def main(): if used_pvs: module.fail_json(msg="Device %s is already in %s volume group." % (used_pvs[0]['name'], used_pvs[0]['vg_name'])) - vgs_cmd = module.get_bin_path('vgs', True) - rc, current_vgs, err = module.run_command("%s --noheadings -o vg_name,pv_count,lv_count --separator ';'" % vgs_cmd) - - if rc != 0: - module.fail_json(msg="Failed executing vgs command.", rc=rc, err=err) - - changed = False - - vgs = parse_vgs(current_vgs) - - for test_vg in vgs: - if test_vg['name'] == vg: - this_vg = test_vg - break - else: - this_vg = None - if this_vg is None: - if state == 'present': + if present_state: + append_vgcreate_options(module=module, state=state, vgoptions=vgoptions) # create VG if module.check_mode: changed = True @@ -268,68 +480,61 @@ def main(): module.fail_json(msg="Failed to remove volume group %s" % (vg), rc=rc, err=err) else: module.fail_json(msg="Refuse to remove non-empty volume group %s without force=true" % (vg)) + # activate/deactivate existing VG + elif state == 'active': + changed = activate_vg(module=module, vg=vg, active=True) + elif state == 'inactive': + changed = activate_vg(module=module, vg=vg, active=False) + + # reset VG uuid + if reset_vg_uuid: + changed = reset_uuid_vg(module=module, vg=vg) or changed # resize VG - current_devs = [os.path.realpath(pv['name']) for pv in pvs if pv['vg_name'] == vg] - devs_to_remove = list(set(current_devs) - set(dev_list)) - devs_to_add = list(set(dev_list) - set(current_devs)) - - if current_devs: - if state == 'present' and pvresize: - for device in current_devs: - pvresize_cmd = module.get_bin_path('pvresize', True) - pvdisplay_cmd = module.get_bin_path('pvdisplay', True) - pvdisplay_ops = ["--units", "b", "--columns", "--noheadings", "--nosuffix"] - pvdisplay_cmd_device_options = [pvdisplay_cmd, device] + pvdisplay_ops - rc, dev_size, err = module.run_command(pvdisplay_cmd_device_options + ["-o", "dev_size"]) - dev_size = int(dev_size.replace(" ", "")) - rc, pv_size, err = module.run_command(pvdisplay_cmd_device_options + ["-o", "pv_size"]) - pv_size = int(pv_size.replace(" ", "")) - rc, pe_start, err = module.run_command(pvdisplay_cmd_device_options + ["-o", "pe_start"]) - pe_start = int(pe_start.replace(" ", "")) - rc, vg_extent_size, err = module.run_command(pvdisplay_cmd_device_options + ["-o", "vg_extent_size"]) - vg_extent_size = int(vg_extent_size.replace(" ", "")) - if (dev_size - (pe_start + pv_size)) > vg_extent_size: - if module.check_mode: + if dev_list: + current_devs = [os.path.realpath(pv['name']) for pv in pvs if pv['vg_name'] == vg] + devs_to_remove = list(set(current_devs) - set(dev_list)) + devs_to_add = list(set(dev_list) - set(current_devs)) + + if current_devs: + if present_state: + for device in current_devs: + if pvresize: + changed = resize_pv(module=module, device=device) or changed + if reset_pv_uuid: + changed = reset_uuid_pv(module=module, device=device) or changed + + if devs_to_add or devs_to_remove: + if module.check_mode: + changed = True + else: + if devs_to_add: + devs_to_add_string = ' '.join(devs_to_add) + # create PV + pvcreate_cmd = module.get_bin_path('pvcreate', True) + for current_dev in devs_to_add: + rc, dummy, err = module.run_command([pvcreate_cmd] + pvoptions + ['-f', str(current_dev)]) + if rc == 0: + changed = True + else: + module.fail_json(msg="Creating physical volume '%s' failed" % current_dev, rc=rc, err=err) + # add PV to our VG + vgextend_cmd = module.get_bin_path('vgextend', True) + rc, dummy, err = module.run_command("%s %s %s" % (vgextend_cmd, vg, devs_to_add_string)) + if rc == 0: changed = True else: - rc, dummy, err = module.run_command([pvresize_cmd, device]) - if rc != 0: - module.fail_json(msg="Failed executing pvresize command.", rc=rc, err=err) - else: - changed = True + module.fail_json(msg="Unable to extend %s by %s." % (vg, devs_to_add_string), rc=rc, err=err) - if devs_to_add or devs_to_remove: - if module.check_mode: - changed = True - else: - if devs_to_add: - devs_to_add_string = ' '.join(devs_to_add) - # create PV - pvcreate_cmd = module.get_bin_path('pvcreate', True) - for current_dev in devs_to_add: - rc, dummy, err = module.run_command([pvcreate_cmd] + pvoptions + ['-f', str(current_dev)]) + # remove some PV from our VG + if devs_to_remove: + devs_to_remove_string = ' '.join(devs_to_remove) + vgreduce_cmd = module.get_bin_path('vgreduce', True) + rc, dummy, err = module.run_command("%s --force %s %s" % (vgreduce_cmd, vg, devs_to_remove_string)) if rc == 0: changed = True else: - module.fail_json(msg="Creating physical volume '%s' failed" % current_dev, rc=rc, err=err) - # add PV to our VG - vgextend_cmd = module.get_bin_path('vgextend', True) - rc, dummy, err = module.run_command("%s %s %s" % (vgextend_cmd, vg, devs_to_add_string)) - if rc == 0: - changed = True - else: - module.fail_json(msg="Unable to extend %s by %s." % (vg, devs_to_add_string), rc=rc, err=err) - - # remove some PV from our VG - if devs_to_remove: - devs_to_remove_string = ' '.join(devs_to_remove) - vgreduce_cmd = module.get_bin_path('vgreduce', True) - rc, dummy, err = module.run_command("%s --force %s %s" % (vgreduce_cmd, vg, devs_to_remove_string)) - if rc == 0: - changed = True - else: - module.fail_json(msg="Unable to reduce %s by %s." % (vg, devs_to_remove_string), rc=rc, err=err) + module.fail_json(msg="Unable to reduce %s by %s." % (vg, devs_to_remove_string), rc=rc, err=err) module.exit_json(changed=changed) diff --git a/ansible_collections/community/general/plugins/modules/lvg_rename.py b/ansible_collections/community/general/plugins/modules/lvg_rename.py new file mode 100644 index 000000000..bd48ffa62 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/lvg_rename.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) Contributors to the Ansible project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +author: + - Laszlo Szomor (@lszomor) +module: lvg_rename +short_description: Renames LVM volume groups +description: + - This module renames volume groups using the C(vgchange) command. +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: full +version_added: 7.1.0 +options: + vg: + description: + - The name or UUID of the source VG. + - See V(vgrename(8\)) for valid values. + type: str + required: true + vg_new: + description: + - The new name of the VG. + - See V(lvm(8\)) for valid names. + type: str + required: true +seealso: +- module: community.general.lvg +notes: + - This module does not modify VG renaming-related configurations like C(fstab) entries or boot parameters. +''' + +EXAMPLES = r''' +- name: Rename a VG by name + community.general.lvg_rename: + vg: vg_orig_name + vg_new: vg_new_name + +- name: Rename a VG by UUID + community.general.lvg_rename: + vg_uuid: SNgd0Q-rPYa-dPB8-U1g6-4WZI-qHID-N7y9Vj + vg_new: vg_new_name +''' + +from ansible.module_utils.basic import AnsibleModule + +argument_spec = dict( + vg=dict(type='str', required=True,), + vg_new=dict(type='str', required=True,), +) + + +class LvgRename(object): + def __init__(self, module): + ''' + Orchestrates the lvg_rename module logic. + + :param module: An AnsibleModule instance. + ''' + self.module = module + self.result = {'changed': False} + self.vg_list = [] + self._load_params() + + def run(self): + """Performs the module logic.""" + + self._load_vg_list() + + old_vg_exists = self._is_vg_exists(vg=self.vg) + new_vg_exists = self._is_vg_exists(vg=self.vg_new) + + if old_vg_exists: + if new_vg_exists: + self.module.fail_json(msg='The new VG name (%s) is already in use.' % (self.vg_new)) + else: + self._rename_vg() + else: + if new_vg_exists: + self.result['msg'] = 'The new VG (%s) already exists, nothing to do.' % (self.vg_new) + self.module.exit_json(**self.result) + else: + self.module.fail_json(msg='Both current (%s) and new (%s) VG are missing.' % (self.vg, self.vg_new)) + + self.module.exit_json(**self.result) + + def _load_params(self): + """Load the parameters from the module.""" + + self.vg = self.module.params['vg'] + self.vg_new = self.module.params['vg_new'] + + def _load_vg_list(self): + """Load the VGs from the system.""" + + vgs_cmd = self.module.get_bin_path('vgs', required=True) + vgs_cmd_with_opts = [vgs_cmd, '--noheadings', '--separator', ';', '-o', 'vg_name,vg_uuid'] + dummy, vg_raw_list, dummy = self.module.run_command(vgs_cmd_with_opts, check_rc=True) + + for vg_info in vg_raw_list.splitlines(): + vg_name, vg_uuid = vg_info.strip().split(';') + self.vg_list.append(vg_name) + self.vg_list.append(vg_uuid) + + def _is_vg_exists(self, vg): + ''' + Checks VG existence by name or UUID. It removes the '/dev/' prefix before checking. + + :param vg: A string with the name or UUID of the VG. + :returns: A boolean indicates whether the VG exists or not. + ''' + + vg_found = False + dev_prefix = '/dev/' + + if vg.startswith(dev_prefix): + vg_id = vg[len(dev_prefix):] + else: + vg_id = vg + + vg_found = vg_id in self.vg_list + + return vg_found + + def _rename_vg(self): + """Renames the volume group.""" + + vgrename_cmd = self.module.get_bin_path('vgrename', required=True) + + if self.module._diff: + self.result['diff'] = {'before': {'vg': self.vg}, 'after': {'vg': self.vg_new}} + + if self.module.check_mode: + self.result['msg'] = "Running in check mode. The module would rename VG %s to %s." % (self.vg, self.vg_new) + self.result['changed'] = True + else: + vgrename_cmd_with_opts = [vgrename_cmd, self.vg, self.vg_new] + dummy, vg_rename_out, dummy = self.module.run_command(vgrename_cmd_with_opts, check_rc=True) + + self.result['msg'] = vg_rename_out + self.result['changed'] = True + + +def setup_module_object(): + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + return module + + +def main(): + module = setup_module_object() + lvg_rename = LvgRename(module=module) + lvg_rename.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/lvol.py b/ansible_collections/community/general/plugins/modules/lvol.py index d193a4e83..a2a870260 100644 --- a/ansible_collections/community/general/plugins/modules/lvol.py +++ b/ansible_collections/community/general/plugins/modules/lvol.py @@ -41,18 +41,18 @@ options: description: - The size of the logical volume, according to lvcreate(8) --size, by default in megabytes or optionally with one of [bBsSkKmMgGtTpPeE] units; or - according to lvcreate(8) --extents as a percentage of [VG|PVS|FREE]; + according to lvcreate(8) --extents as a percentage of [VG|PVS|FREE|ORIGIN]; Float values must begin with a digit. - When resizing, apart from specifying an absolute size you may, according to lvextend(8)|lvreduce(8) C(--size), specify the amount to extend the logical volume with - the prefix C(+) or the amount to reduce the logical volume by with prefix C(-). - - Resizing using C(+) or C(-) was not supported prior to community.general 3.0.0. - - Please note that when using C(+) or C(-), the module is B(not idempotent). + the prefix V(+) or the amount to reduce the logical volume by with prefix V(-). + - Resizing using V(+) or V(-) was not supported prior to community.general 3.0.0. + - Please note that when using V(+), V(-), or percentage of FREE, the module is B(not idempotent). state: type: str description: - - Control if the logical volume exists. If C(present) and the - volume does not already exist then the C(size) option is required. + - Control if the logical volume exists. If V(present) and the + volume does not already exist then the O(size) option is required. choices: [ absent, present ] default: present active: @@ -73,11 +73,12 @@ options: snapshot: type: str description: - - The name of the snapshot volume + - The name of a snapshot volume to be configured. When creating a snapshot volume, the O(lv) parameter specifies the origin volume. pvs: - type: str + type: list + elements: str description: - - Comma separated list of physical volumes (e.g. /dev/sda,/dev/sdb). + - List of physical volumes (for example V(/dev/sda, /dev/sdb)). thinpool: type: str description: @@ -110,7 +111,9 @@ EXAMPLES = ''' vg: firefly lv: test size: 512 - pvs: /dev/sda,/dev/sdb + pvs: + - /dev/sda + - /dev/sdb - name: Create cache pool logical volume community.general.lvol: @@ -299,7 +302,7 @@ def main(): shrink=dict(type='bool', default=True), active=dict(type='bool', default=True), snapshot=dict(type='str'), - pvs=dict(type='str'), + pvs=dict(type='list', elements='str'), resizefs=dict(type='bool', default=False), thinpool=dict(type='str'), ), @@ -340,7 +343,7 @@ def main(): if pvs is None: pvs = "" else: - pvs = pvs.replace(",", " ") + pvs = " ".join(pvs) if opts is None: opts = "" @@ -368,10 +371,10 @@ def main(): if size_percent > 100: module.fail_json(msg="Size percentage cannot be larger than 100%") size_whole = size_parts[1] - if size_whole == 'ORIGIN': - module.fail_json(msg="Snapshot Volumes are not supported") - elif size_whole not in ['VG', 'PVS', 'FREE']: - module.fail_json(msg="Specify extents as a percentage of VG|PVS|FREE") + if size_whole == 'ORIGIN' and snapshot is None: + module.fail_json(msg="Percentage of ORIGIN supported only for snapshot volumes") + elif size_whole not in ['VG', 'PVS', 'FREE', 'ORIGIN']: + module.fail_json(msg="Specify extents as a percentage of VG|PVS|FREE|ORIGIN") size_opt = 'l' size_unit = '' @@ -552,9 +555,9 @@ def main(): elif rc == 0: changed = True msg = "Volume %s resized to %s%s" % (this_lv['name'], size_requested, unit) - elif "matches existing size" in err: + elif "matches existing size" in err or "matches existing size" in out: module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size']) - elif "not larger than existing size" in err: + elif "not larger than existing size" in err or "not larger than existing size" in out: module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size'], msg="Original size is larger than requested size", err=err) else: module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err) @@ -585,9 +588,9 @@ def main(): module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out) elif rc == 0: changed = True - elif "matches existing size" in err: + elif "matches existing size" in err or "matches existing size" in out: module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size']) - elif "not larger than existing size" in err: + elif "not larger than existing size" in err or "not larger than existing size" in out: module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size'], msg="Original size is larger than requested size", err=err) else: module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err) diff --git a/ansible_collections/community/general/plugins/modules/lxc_container.py b/ansible_collections/community/general/plugins/modules/lxc_container.py index aec8f12dc..7ded041e9 100644 --- a/ansible_collections/community/general/plugins/modules/lxc_container.py +++ b/ansible_collections/community/general/plugins/modules/lxc_container.py @@ -92,7 +92,7 @@ options: type: str lxc_path: description: - - Place container under C(PATH). + - Place container under E(PATH). type: path container_log: description: @@ -111,7 +111,7 @@ options: - debug - DEBUG description: - - Set the log level for a container where I(container_log) was set. + - Set the log level for a container where O(container_log) was set. type: str required: false default: INFO @@ -158,7 +158,7 @@ options: - clone description: - Define the state of a container. - - If you clone a container using I(clone_name) the newly cloned + - If you clone a container using O(clone_name) the newly cloned container created in a stopped state. - The running container will be stopped while the clone operation is happening and upon completion of the clone the original container @@ -178,17 +178,17 @@ notes: - Containers must have a unique name. If you attempt to create a container with a name that already exists in the users namespace the module will simply return as "unchanged". - - The I(container_command) can be used with any state except C(absent). If - used with state C(stopped) the container will be C(started), the command - executed, and then the container C(stopped) again. Likewise if I(state=stopped) + - The O(container_command) can be used with any state except V(absent). If + used with state V(stopped) the container will be V(started), the command + executed, and then the container V(stopped) again. Likewise if O(state=stopped) and the container does not exist it will be first created, - C(started), the command executed, and then C(stopped). If you use a "|" + V(started), the command executed, and then V(stopped). If you use a "|" in the variable you can use common script formatting within the variable - itself. The I(container_command) option will always execute as BASH. - When using I(container_command), a log file is created in the C(/tmp/) directory + itself. The O(container_command) option will always execute as BASH. + When using O(container_command), a log file is created in the C(/tmp/) directory which contains both C(stdout) and C(stderr) of any command executed. - - If I(archive=true) the system will attempt to create a compressed - tarball of the running container. The I(archive) option supports LVM backed + - If O(archive=true) the system will attempt to create a compressed + tarball of the running container. The O(archive) option supports LVM backed containers and will create a snapshot of the running container when creating the archive. - If your distro does not have a package for C(python3-lxc), which is a @@ -1277,7 +1277,7 @@ class LxcContainerManagement(object): """ vg = self._get_lxc_vg() - free_space, messurement = self._get_vg_free_pe(vg_name=vg) + free_space, measurement = self._get_vg_free_pe(vg_name=vg) if free_space < float(snapshot_size_gb): message = ( diff --git a/ansible_collections/community/general/plugins/modules/lxd_container.py b/ansible_collections/community/general/plugins/modules/lxd_container.py index f10fc4872..9fd1b183b 100644 --- a/ansible_collections/community/general/plugins/modules/lxd_container.py +++ b/ansible_collections/community/general/plugins/modules/lxd_container.py @@ -34,32 +34,33 @@ options: project: description: - 'Project of an instance. - See U(https://github.com/lxc/lxd/blob/master/doc/projects.md).' + See U(https://documentation.ubuntu.com/lxd/en/latest/projects/).' required: false type: str version_added: 4.8.0 architecture: description: - - 'The architecture for the instance (for example C(x86_64) or C(i686)). - See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).' + - 'The architecture for the instance (for example V(x86_64) or V(i686)). + See U(https://documentation.ubuntu.com/lxd/en/latest/api/#/instances/instance_get).' type: str required: false config: description: - - 'The config for the instance (for example C({"limits.cpu": "2"})). - See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).' + - 'The config for the instance (for example V({"limits.cpu": "2"})). + See U(https://documentation.ubuntu.com/lxd/en/latest/api/#/instances/instance_get).' - If the instance already exists and its "config" values in metadata - obtained from the LXD API U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#instances-containers-and-virtual-machines) - are different, this module tries to apply the configurations. - - The keys starting with C(volatile.) are ignored for this comparison when I(ignore_volatile_options=true). + obtained from the LXD API U(https://documentation.ubuntu.com/lxd/en/latest/api/#/instances/instance_get) + are different, then this module tries to apply the configurations + U(https://documentation.ubuntu.com/lxd/en/latest/api/#/instances/instance_put). + - The keys starting with C(volatile.) are ignored for this comparison when O(ignore_volatile_options=true). type: dict required: false ignore_volatile_options: description: - - If set to C(true), options starting with C(volatile.) are ignored. As a result, + - If set to V(true), options starting with C(volatile.) are ignored. As a result, they are reapplied for each execution. - - This default behavior can be changed by setting this option to C(false). - - The default value changed from C(true) to C(false) in community.general 6.0.0. + - This default behavior can be changed by setting this option to V(false). + - The default value changed from V(true) to V(false) in community.general 6.0.0. type: bool required: false default: false @@ -72,26 +73,23 @@ options: devices: description: - 'The devices for the instance - (for example C({ "rootfs": { "path": "/dev/kvm", "type": "unix-char" }})). - See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).' + (for example V({ "rootfs": { "path": "/dev/kvm", "type": "unix-char" }})). + See U(https://documentation.ubuntu.com/lxd/en/latest/api/#/instances/instance_get).' type: dict required: false ephemeral: description: - - Whether or not the instance is ephemeral (for example C(true) or C(false)). - See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1). + - Whether or not the instance is ephemeral (for example V(true) or V(false)). + See U(https://documentation.ubuntu.com/lxd/en/latest/api/#/instances/instance_get). required: false type: bool source: description: - 'The source for the instance - (e.g. { "type": "image", - "mode": "pull", - "server": "https://images.linuxcontainers.org", - "protocol": "lxd", - "alias": "ubuntu/xenial/amd64" }).' - - 'See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1) for complete API documentation.' - - 'Note that C(protocol) accepts two choices: C(lxd) or C(simplestreams).' + (for example V({ "type": "image", "mode": "pull", "server": "https://images.linuxcontainers.org", + "protocol": "lxd", "alias": "ubuntu/xenial/amd64" })).' + - 'See U(https://documentation.ubuntu.com/lxd/en/latest/api/) for complete API documentation.' + - 'Note that C(protocol) accepts two choices: V(lxd) or V(simplestreams).' required: false type: dict state: @@ -125,7 +123,7 @@ options: type: int type: description: - - Instance type can be either C(virtual-machine) or C(container). + - Instance type can be either V(virtual-machine) or V(container). required: false default: container choices: @@ -135,7 +133,7 @@ options: version_added: 4.1.0 wait_for_ipv4_addresses: description: - - If this is true, the C(lxd_container) waits until IPv4 addresses + - If this is V(true), the C(lxd_container) waits until IPv4 addresses are set to the all network interfaces in the instance after starting or restarting. required: false @@ -143,14 +141,14 @@ options: type: bool wait_for_container: description: - - If set to C(true), the tasks will wait till the task reports a + - If set to V(true), the tasks will wait till the task reports a success status when performing container operations. default: false type: bool version_added: 4.4.0 force_stop: description: - - If this is true, the C(lxd_container) forces to stop the instance + - If this is V(true), the C(lxd_container) forces to stop the instance when it stops or restarts the instance. required: false default: false @@ -201,7 +199,8 @@ notes: 2.1, the later requires python to be installed in the instance which can be done with the command module. - You can copy a file from the host to the instance - with the Ansible M(ansible.builtin.copy) and M(ansible.builtin.template) module and the C(community.general.lxd) connection plugin. + with the Ansible M(ansible.builtin.copy) and M(ansible.builtin.template) module + and the P(community.general.lxd#connection) connection plugin. See the example below. - You can copy a file in the created instance to the localhost with C(command=lxc file pull instance_name/dir/filename filename). @@ -437,12 +436,12 @@ ANSIBLE_LXD_DEFAULT_URL = 'unix:/var/lib/lxd/unix.socket' # CONFIG_PARAMS is a list of config attribute names. CONFIG_PARAMS = [ - 'architecture', 'config', 'devices', 'ephemeral', 'profiles', 'source' + 'architecture', 'config', 'devices', 'ephemeral', 'profiles', 'source', 'type' ] # CONFIG_CREATION_PARAMS is a list of attribute names that are only applied # on instance creation. -CONFIG_CREATION_PARAMS = ['source'] +CONFIG_CREATION_PARAMS = ['source', 'type'] class LXDContainerManagement(object): @@ -468,13 +467,6 @@ class LXDContainerManagement(object): self.type = self.module.params['type'] - # LXD Rest API provides additional endpoints for creating containers and virtual-machines. - self.api_endpoint = None - if self.type == 'container': - self.api_endpoint = '/1.0/containers' - elif self.type == 'virtual-machine': - self.api_endpoint = '/1.0/virtual-machines' - self.key_file = self.module.params.get('client_key') if self.key_file is None: self.key_file = '{0}/.config/lxc/client.key'.format(os.environ['HOME']) @@ -500,6 +492,18 @@ class LXDContainerManagement(object): ) except LXDClientException as e: self.module.fail_json(msg=e.msg) + + # LXD (3.19) Rest API provides instances endpoint, failback to containers and virtual-machines + # https://documentation.ubuntu.com/lxd/en/latest/rest-api/#instances-containers-and-virtual-machines + self.api_endpoint = '/1.0/instances' + check_api_endpoint = self.client.do('GET', '{0}?project='.format(self.api_endpoint), ok_error_codes=[404]) + + if check_api_endpoint['error_code'] == 404: + if self.type == 'container': + self.api_endpoint = '/1.0/containers' + elif self.type == 'virtual-machine': + self.api_endpoint = '/1.0/virtual-machines' + self.trust_password = self.module.params.get('trust_password', None) self.actions = [] self.diff = {'before': {}, 'after': {}} @@ -552,6 +556,8 @@ class LXDContainerManagement(object): url = '{0}?{1}'.format(url, urlencode(url_params)) config = self.config.copy() config['name'] = self.name + if self.type not in self.api_endpoint: + config['type'] = self.type if not self.module.check_mode: self.client.do('POST', url, config, wait_for_container=self.wait_for_container) self.actions.append('create') diff --git a/ansible_collections/community/general/plugins/modules/lxd_profile.py b/ansible_collections/community/general/plugins/modules/lxd_profile.py index 45f499b78..13660fd91 100644 --- a/ansible_collections/community/general/plugins/modules/lxd_profile.py +++ b/ansible_collections/community/general/plugins/modules/lxd_profile.py @@ -32,7 +32,7 @@ options: project: description: - 'Project of a profile. - See U(https://github.com/lxc/lxd/blob/master/doc/projects.md).' + See U(https://documentation.ubuntu.com/lxd/en/latest/projects/).' type: str required: false version_added: 4.8.0 @@ -43,12 +43,13 @@ options: config: description: - 'The config for the instance (e.g. {"limits.memory": "4GB"}). - See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#patch-3)' + See U(https://documentation.ubuntu.com/lxd/en/latest/api/#/profiles/profile_get).' - If the profile already exists and its "config" value in metadata obtained from GET /1.0/profiles/ - U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#get-19) - are different, they this module tries to apply the configurations. + U(https://documentation.ubuntu.com/lxd/en/latest/api/#/profiles/profile_get) + are different, then this module tries to apply the configurations + U(https://documentation.ubuntu.com/lxd/en/latest/api/#/profiles/profile_put). - Not all config values are supported to apply the existing profile. Maybe you need to delete and recreate a profile. required: false @@ -57,14 +58,14 @@ options: description: - 'The devices for the profile (e.g. {"rootfs": {"path": "/dev/kvm", "type": "unix-char"}). - See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#patch-3)' + See U(https://documentation.ubuntu.com/lxd/en/latest/api/#/profiles/profile_get).' required: false type: dict new_name: description: - A new name of a profile. - If this parameter is specified a profile will be renamed to this name. - See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-11) + See U(https://documentation.ubuntu.com/lxd/en/latest/api/#/profiles/profile_post). required: false type: str merge_profile: @@ -419,7 +420,7 @@ class LXDProfileManagement(object): Rebuild the Profile by the configuration provided in the play. Existing configurations are discarded. - This ist the default behavior. + This is the default behavior. Args: dict(config): Dict with the old config in 'metadata' and new config in 'config' diff --git a/ansible_collections/community/general/plugins/modules/lxd_project.py b/ansible_collections/community/general/plugins/modules/lxd_project.py index 983531fa0..0d321808a 100644 --- a/ansible_collections/community/general/plugins/modules/lxd_project.py +++ b/ansible_collections/community/general/plugins/modules/lxd_project.py @@ -34,19 +34,20 @@ options: type: str config: description: - - 'The config for the project (for example C({"features.profiles": "true"})). - See U(https://linuxcontainers.org/lxd/docs/master/projects/).' + - 'The config for the project (for example V({"features.profiles": "true"})). + See U(https://documentation.ubuntu.com/lxd/en/latest/api/#/projects/project_get).' - If the project already exists and its "config" value in metadata obtained from C(GET /1.0/projects/) - U(https://linuxcontainers.org/lxd/docs/master/api/#/projects/project_get) - are different, then this module tries to apply the configurations. + U(https://documentation.ubuntu.com/lxd/en/latest/api/#/projects/project_get) + are different, then this module tries to apply the configurations + U(https://documentation.ubuntu.com/lxd/en/latest/api/#/projects/project_put). type: dict new_name: description: - A new name of a project. - If this parameter is specified a project will be renamed to this name. - See U(https://linuxcontainers.org/lxd/docs/master/api/#/projects/project_post). + See U(https://documentation.ubuntu.com/lxd/en/latest/api/#/projects/project_post). required: false type: str merge_project: @@ -98,7 +99,7 @@ options: running this module using the following command: C(lxc config set core.trust_password ) See U(https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/).' - - If I(trust_password) is set, this module send a request for + - If O(trust_password) is set, this module send a request for authentication before sending any requests. required: false type: str @@ -146,7 +147,7 @@ logs: elements: dict contains: type: - description: Type of actions performed, currently only C(sent request). + description: Type of actions performed, currently only V(sent request). type: str sample: "sent request" request: @@ -166,7 +167,7 @@ logs: type: str sample: "(too long to be placed here)" timeout: - description: Timeout of HTTP request, C(null) if unset. + description: Timeout of HTTP request, V(null) if unset. type: int sample: null response: diff --git a/ansible_collections/community/general/plugins/modules/macports.py b/ansible_collections/community/general/plugins/modules/macports.py index 6f40d0938..e81fb9142 100644 --- a/ansible_collections/community/general/plugins/modules/macports.py +++ b/ansible_collections/community/general/plugins/modules/macports.py @@ -55,7 +55,7 @@ options: variant: description: - A port variant specification. - - 'C(variant) is only supported with state: I(installed)/I(present).' + - 'O(variant) is only supported with O(state=installed) and O(state=present).' aliases: ['variants'] type: str ''' diff --git a/ansible_collections/community/general/plugins/modules/mail.py b/ansible_collections/community/general/plugins/modules/mail.py index feaac6923..1916c140c 100644 --- a/ansible_collections/community/general/plugins/modules/mail.py +++ b/ansible_collections/community/general/plugins/modules/mail.py @@ -114,18 +114,18 @@ options: default: utf-8 subtype: description: - - The minor mime type, can be either C(plain) or C(html). - - The major type is always C(text). + - The minor mime type, can be either V(plain) or V(html). + - The major type is always V(text). type: str choices: [ html, plain ] default: plain secure: description: - - If C(always), the connection will only send email if the connection is Encrypted. + - If V(always), the connection will only send email if the connection is Encrypted. If the server doesn't accept the encrypted connection it will fail. - - If C(try), the connection will attempt to setup a secure SSL/TLS session, before trying to send. - - If C(never), the connection will not attempt to setup a secure SSL/TLS session, before sending - - If C(starttls), the connection will try to upgrade to a secure SSL/TLS connection, before sending. + - If V(try), the connection will attempt to setup a secure SSL/TLS session, before trying to send. + - If V(never), the connection will not attempt to setup a secure SSL/TLS session, before sending + - If V(starttls), the connection will try to upgrade to a secure SSL/TLS connection, before sending. If it is unable to do so it will fail. type: str choices: [ always, never, starttls, try ] @@ -140,6 +140,13 @@ options: - Allows for manual specification of host for EHLO. type: str version_added: 3.8.0 + message_id_domain: + description: + - The domain name to use for the L(Message-ID header, https://en.wikipedia.org/wiki/Message-ID). + - Note that this is only available on Python 3+. On Python 2, this value will be ignored. + type: str + default: ansible + version_added: 8.2.0 ''' EXAMPLES = r''' @@ -205,10 +212,11 @@ EXAMPLES = r''' body: System {{ ansible_hostname }} has been successfully provisioned. secure: starttls -- name: Sending an e-mail using StartTLS, remote server, custom EHLO +- name: Sending an e-mail using StartTLS, remote server, custom EHLO, and timeout of 10 seconds community.general.mail: host: some.smtp.host.tld port: 25 + timeout: 10 ehlohost: my-resolvable-hostname.tld to: John Smith subject: Ansible-report @@ -221,7 +229,7 @@ import smtplib import ssl import traceback from email import encoders -from email.utils import parseaddr, formataddr, formatdate +from email.utils import parseaddr, formataddr, formatdate, make_msgid from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -253,6 +261,7 @@ def main(): subtype=dict(type='str', default='plain', choices=['html', 'plain']), secure=dict(type='str', default='try', choices=['always', 'never', 'starttls', 'try']), timeout=dict(type='int', default=20), + message_id_domain=dict(type='str', default='ansible'), ), required_together=[['password', 'username']], ) @@ -274,6 +283,7 @@ def main(): subtype = module.params.get('subtype') secure = module.params.get('secure') timeout = module.params.get('timeout') + message_id_domain = module.params['message_id_domain'] code = 0 secure_state = False @@ -348,13 +358,19 @@ def main(): msg['From'] = formataddr((sender_phrase, sender_addr)) msg['Date'] = formatdate(localtime=True) msg['Subject'] = Header(subject, charset) + try: + msg['Message-ID'] = make_msgid(domain=message_id_domain) + except TypeError: + # `domain` is only available in Python 3 + msg['Message-ID'] = make_msgid() + module.warn("The Message-ID domain cannot be set on Python 2; the system's hostname is used") msg.preamble = "Multipart message" for header in headers: # NOTE: Backward compatible with old syntax using '|' as delimiter for hdr in [x.strip() for x in header.split('|')]: try: - h_key, h_val = hdr.split('=') + h_key, h_val = hdr.split('=', 1) h_val = to_native(Header(h_val, charset)) msg.add_header(h_key, h_val) except Exception: @@ -382,7 +398,7 @@ def main(): part = MIMEText(body + "\n\n", _subtype=subtype, _charset=charset) msg.attach(part) - # NOTE: Backware compatibility with old syntax using space as delimiter is not retained + # NOTE: Backward compatibility with old syntax using space as delimiter is not retained # This breaks files with spaces in it :-( for filename in attach_files: try: diff --git a/ansible_collections/community/general/plugins/modules/make.py b/ansible_collections/community/general/plugins/modules/make.py index ebff6cfe1..39392afca 100644 --- a/ansible_collections/community/general/plugins/modules/make.py +++ b/ansible_collections/community/general/plugins/modules/make.py @@ -49,12 +49,22 @@ options: params: description: - Any extra parameters to pass to make. + - If the value is empty, only the key will be used. For example, V(FOO:) will produce V(FOO), not V(FOO=). type: dict target: description: - The target to run. - - Typically this would be something like C(install), C(test), or C(all). + - Typically this would be something like V(install), V(test), or V(all). + - O(target) and O(targets) are mutually exclusive. type: str + targets: + description: + - The list of targets to run. + - Typically this would be something like V(install), V(test), or V(all). + - O(target) and O(targets) are mutually exclusive. + type: list + elements: str + version_added: 7.2.0 ''' EXAMPLES = r''' @@ -81,12 +91,24 @@ EXAMPLES = r''' chdir: /home/ubuntu/cool-project target: all file: /some-project/Makefile + +- name: build arm64 kernel on FreeBSD, with 16 parallel jobs + community.general.make: + chdir: /usr/src + jobs: 16 + target: buildkernel + params: + # This adds -DWITH_FDT to the command line: + -DWITH_FDT: + # The following adds TARGET=arm64 TARGET_ARCH=aarch64 to the command line: + TARGET: arm64 + TARGET_ARCH: aarch64 ''' RETURN = r''' chdir: description: - - The value of the module parameter I(chdir). + - The value of the module parameter O(chdir). type: str returned: success command: @@ -97,24 +119,30 @@ command: version_added: 6.5.0 file: description: - - The value of the module parameter I(file). + - The value of the module parameter O(file). type: str returned: success jobs: description: - - The value of the module parameter I(jobs). + - The value of the module parameter O(jobs). type: int returned: success params: description: - - The value of the module parameter I(params). + - The value of the module parameter O(params). type: dict returned: success target: description: - - The value of the module parameter I(target). + - The value of the module parameter O(target). + type: str + returned: success +targets: + description: + - The value of the module parameter O(targets). type: str returned: success + version_added: 7.2.0 ''' from ansible.module_utils.six import iteritems @@ -155,12 +183,14 @@ def main(): module = AnsibleModule( argument_spec=dict( target=dict(type='str'), + targets=dict(type='list', elements='str'), params=dict(type='dict'), chdir=dict(type='path', required=True), file=dict(type='path'), make=dict(type='path'), jobs=dict(type='int'), ), + mutually_exclusive=[('target', 'targets')], supports_check_mode=True, ) @@ -172,9 +202,8 @@ def main(): if not make_path: # Fall back to system make make_path = module.get_bin_path('make', required=True) - make_target = module.params['target'] if module.params['params'] is not None: - make_parameters = [k + '=' + str(v) for k, v in iteritems(module.params['params'])] + make_parameters = [k + (('=' + str(v)) if v is not None else '') for k, v in iteritems(module.params['params'])] else: make_parameters = [] @@ -188,7 +217,10 @@ def main(): base_command.extend(["-f", module.params['file']]) # add make target - base_command.append(make_target) + if module.params['target']: + base_command.append(module.params['target']) + elif module.params['targets']: + base_command.extend(module.params['targets']) # add makefile parameters base_command.extend(make_parameters) @@ -206,8 +238,7 @@ def main(): changed = False else: # The target isn't up to date, so we need to run it - rc, out, err = run_command(base_command, module, - check_rc=True) + rc, out, err = run_command(base_command, module, check_rc=True) changed = True # We don't report the return code, as if this module failed @@ -221,6 +252,7 @@ def main(): stdout=out, stderr=err, target=module.params['target'], + targets=module.params['targets'], params=module.params['params'], chdir=module.params['chdir'], file=module.params['file'], diff --git a/ansible_collections/community/general/plugins/modules/manageiq_alert_profiles.py b/ansible_collections/community/general/plugins/modules/manageiq_alert_profiles.py index c6cefad6a..eb6424bcd 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_alert_profiles.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_alert_profiles.py @@ -72,7 +72,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Delete an alert profile from ManageIQ community.general.manageiq_alert_profiles: @@ -82,7 +82,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! ''' RETURN = ''' diff --git a/ansible_collections/community/general/plugins/modules/manageiq_alerts.py b/ansible_collections/community/general/plugins/modules/manageiq_alerts.py index 518b29f1f..53f40fb00 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_alerts.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_alerts.py @@ -91,7 +91,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Add an alert with a "miq expression" to ManageIQ community.general.manageiq_alerts: @@ -118,7 +118,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Delete an alert from ManageIQ community.general.manageiq_alerts: @@ -128,7 +128,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! ''' RETURN = ''' diff --git a/ansible_collections/community/general/plugins/modules/manageiq_group.py b/ansible_collections/community/general/plugins/modules/manageiq_group.py index a142a939f..e060b9a01 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_group.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_group.py @@ -52,7 +52,7 @@ options: type: str description: - The the group role name - - The C(role_id) has precedence over the C(role) when supplied. + - The O(role_id) has precedence over the O(role) when supplied. required: false default: null tenant_id: @@ -65,7 +65,7 @@ options: type: str description: - The tenant for the group identified by the tenant name. - - The C(tenant_id) has precedence over the C(tenant) when supplied. + - The O(tenant_id) has precedence over the O(tenant) when supplied. - Tenant names are case sensitive. required: false default: null @@ -78,7 +78,7 @@ options: type: str description: - In merge mode existing categories are kept or updated, new categories are added. - - In replace mode all categories will be replaced with the supplied C(managed_filters). + - In replace mode all categories will be replaced with the supplied O(managed_filters). choices: [ merge, replace ] default: replace belongsto_filters: @@ -90,8 +90,8 @@ options: belongsto_filters_merge_mode: type: str description: - - In merge mode existing settings are merged with the supplied C(belongsto_filters). - - In replace mode current values are replaced with the supplied C(belongsto_filters). + - In merge mode existing settings are merged with the supplied O(belongsto_filters). + - In replace mode current values are replaced with the supplied O(belongsto_filters). choices: [ merge, replace ] default: replace ''' @@ -103,10 +103,10 @@ EXAMPLES = ''' role: 'EvmRole-user' tenant: 'my_tenant' manageiq_connection: - url: 'https://manageiq_server' + url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Create a group in ManageIQ with the role EvmRole-user and tenant with tenant_id 4 community.general.manageiq_group: @@ -114,10 +114,10 @@ EXAMPLES = ''' role: 'EvmRole-user' tenant_id: 4 manageiq_connection: - url: 'https://manageiq_server' + url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: - Create or update a group in ManageIQ with the role EvmRole-user and tenant my_tenant. @@ -140,10 +140,10 @@ EXAMPLES = ''' - "/belongsto/ExtManagementSystem|ProviderName/EmsFolder|Datacenters/EmsFolder|dc_name/EmsFolder|host/EmsCluster|Cluster name" belongsto_filters_merge_mode: merge manageiq_connection: - url: 'https://manageiq_server' + url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Delete a group in ManageIQ community.general.manageiq_group: diff --git a/ansible_collections/community/general/plugins/modules/manageiq_policies.py b/ansible_collections/community/general/plugins/modules/manageiq_policies.py index 061168f7f..f2101ad28 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_policies.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_policies.py @@ -32,20 +32,16 @@ options: state: type: str description: - - C(absent) - policy_profiles should not exist, - - C(present) - policy_profiles should exist, - - > - C(list) - list current policy_profiles and policies. - This state is deprecated and will be removed 8.0.0. - Please use the module M(community.general.manageiq_policies_info) instead. - choices: ['absent', 'present', 'list'] + - V(absent) - policy_profiles should not exist, + - V(present) - policy_profiles should exist, + choices: ['absent', 'present'] default: 'present' policy_profiles: type: list elements: dict description: - - List of dictionaries, each includes the policy_profile C(name) key. - - Required if I(state) is C(present) or C(absent). + - List of dictionaries, each includes the policy_profile V(name) key. + - Required if O(state) is V(present) or V(absent). resource_type: type: str description: @@ -58,12 +54,12 @@ options: type: str description: - The name of the resource to which the profile should be [un]assigned. - - Must be specified if I(resource_id) is not set. Both options are mutually exclusive. + - Must be specified if O(resource_id) is not set. Both options are mutually exclusive. resource_id: type: int description: - The ID of the resource to which the profile should be [un]assigned. - - Must be specified if I(resource_name) is not set. Both options are mutually exclusive. + - Must be specified if O(resource_name) is not set. Both options are mutually exclusive. version_added: 2.2.0 ''' @@ -78,7 +74,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Unassign a policy_profile for a provider in ManageIQ community.general.manageiq_policies: @@ -91,18 +87,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false - -- name: List current policy_profile and policies for a provider in ManageIQ - community.general.manageiq_policies: - state: list - resource_name: 'EngLab' - resource_type: 'provider' - manageiq_connection: - url: 'http://127.0.0.1:3000' - username: 'admin' - password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! ''' RETURN = ''' @@ -144,7 +129,7 @@ from ansible_collections.community.general.plugins.module_utils.manageiq import def main(): - actions = {'present': 'assign', 'absent': 'unassign', 'list': 'list'} + actions = {'present': 'assign', 'absent': 'unassign'} argument_spec = dict( policy_profiles=dict(type='list', elements='dict'), resource_id=dict(type='int'), @@ -152,7 +137,7 @@ def main(): resource_type=dict(required=True, type='str', choices=list(manageiq_entities().keys())), state=dict(required=False, type='str', - choices=['present', 'absent', 'list'], default='present'), + choices=['present', 'absent'], default='present'), ) # add the manageiq connection arguments to the arguments argument_spec.update(manageiq_argument_spec()) @@ -173,13 +158,6 @@ def main(): resource_name = module.params['resource_name'] state = module.params['state'] - if state == "list": - module.deprecate( - 'The value "list" for "state" is deprecated. Please use community.general.manageiq_policies_info instead.', - version='8.0.0', - collection_name='community.general' - ) - # get the action and resource type action = actions[state] resource_type = manageiq_entities()[resource_type_key] @@ -187,13 +165,8 @@ def main(): manageiq = ManageIQ(module) manageiq_policies = manageiq.policies(resource_id, resource_type, resource_name) - if action == 'list': - # return a list of current profiles for this object - current_profiles = manageiq_policies.query_resource_profiles() - res_args = dict(changed=False, profiles=current_profiles) - else: - # assign or unassign the profiles - res_args = manageiq_policies.assign_or_unassign_profiles(policy_profiles, action) + # assign or unassign the profiles + res_args = manageiq_policies.assign_or_unassign_profiles(policy_profiles, action) module.exit_json(**res_args) diff --git a/ansible_collections/community/general/plugins/modules/manageiq_policies_info.py b/ansible_collections/community/general/plugins/modules/manageiq_policies_info.py index 8a75ef646..fda7dcadf 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_policies_info.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_policies_info.py @@ -38,12 +38,12 @@ options: type: str description: - The name of the resource to obtain the profile for. - - Must be specified if I(resource_id) is not set. Both options are mutually exclusive. + - Must be specified if O(resource_id) is not set. Both options are mutually exclusive. resource_id: type: int description: - The ID of the resource to obtain the profile for. - - Must be specified if I(resource_name) is not set. Both options are mutually exclusive. + - Must be specified if O(resource_name) is not set. Both options are mutually exclusive. ''' EXAMPLES = ''' diff --git a/ansible_collections/community/general/plugins/modules/manageiq_provider.py b/ansible_collections/community/general/plugins/modules/manageiq_provider.py index bbc27214b..e6ded9ea7 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_provider.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_provider.py @@ -75,6 +75,7 @@ options: provider: description: Default endpoint connection information, required if state is true. + type: dict suboptions: hostname: type: str @@ -104,9 +105,30 @@ options: certificate_authority: type: str description: The CA bundle string with custom certificates. defaults to None. + path: + type: str + description: + - TODO needs documentation. + project: + type: str + description: + - TODO needs documentation. + role: + type: str + description: + - TODO needs documentation. + subscription: + type: str + description: + - TODO needs documentation. + uid_ems: + type: str + description: + - TODO needs documentation. metrics: description: Metrics endpoint connection information. + type: dict suboptions: hostname: type: str @@ -138,10 +160,27 @@ options: description: The CA bundle string with custom certificates. defaults to None. path: type: str - description: Database name for oVirt metrics. Defaults to C(ovirt_engine_history). + description: Database name for oVirt metrics. Defaults to V(ovirt_engine_history). + project: + type: str + description: + - TODO needs documentation. + role: + type: str + description: + - TODO needs documentation. + subscription: + type: str + description: + - TODO needs documentation. + uid_ems: + type: str + description: + - TODO needs documentation. alerts: description: Alerts endpoint connection information. + type: dict suboptions: hostname: type: str @@ -171,9 +210,30 @@ options: certificate_authority: type: str description: The CA bundle string with custom certificates. defaults to None. + path: + type: str + description: + - TODO needs documentation. + project: + type: str + description: + - TODO needs documentation. + role: + type: str + description: + - TODO needs documentation. + subscription: + type: str + description: + - TODO needs documentation. + uid_ems: + type: str + description: + - TODO needs documentation. ssh_keypair: description: SSH key pair used for SSH connections to all hosts in this provider. + type: dict suboptions: hostname: type: str @@ -191,6 +251,43 @@ options: type: bool default: true aliases: [ verify_ssl ] + security_protocol: + type: str + choices: ['ssl-with-validation','ssl-with-validation-custom-ca','ssl-without-validation', 'non-ssl'] + description: + - TODO needs documentation. + certificate_authority: + type: str + description: + - TODO needs documentation. + password: + type: str + description: + - TODO needs documentation. + path: + type: str + description: + - TODO needs documentation. + project: + type: str + description: + - TODO needs documentation. + role: + type: str + description: + - TODO needs documentation. + subscription: + type: str + description: + - TODO needs documentation. + uid_ems: + type: str + description: + - TODO needs documentation. + port: + type: int + description: + - TODO needs documentation. ''' EXAMPLES = ''' @@ -438,7 +535,7 @@ EXAMPLES = ''' url: 'https://cf-6af0.rhpds.opentlc.com' username: 'admin' password: 'password' - validate_certs: false + validate_certs: true - name: Create a new OpenStack Director provider in ManageIQ with rsa keypair community.general.manageiq_provider: diff --git a/ansible_collections/community/general/plugins/modules/manageiq_tags.py b/ansible_collections/community/general/plugins/modules/manageiq_tags.py index 7e190d49c..3ab5eca4f 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_tags.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_tags.py @@ -32,17 +32,16 @@ options: state: type: str description: - - C(absent) - tags should not exist. - - C(present) - tags should exist. - - C(list) - list current tags. - choices: ['absent', 'present', 'list'] + - V(absent) - tags should not exist. + - V(present) - tags should exist. + choices: ['absent', 'present'] default: 'present' tags: type: list elements: dict description: - - C(tags) - list of dictionaries, each includes C(name) and c(category) keys. - - Required if I(state) is C(present) or C(absent). + - V(tags) - list of dictionaries, each includes C(name) and C(category) keys. + - Required if O(state) is V(present) or V(absent). resource_type: type: str description: @@ -55,11 +54,11 @@ options: type: str description: - The name of the resource at which tags will be controlled. - - Must be specified if I(resource_id) is not set. Both options are mutually exclusive. + - Must be specified if O(resource_id) is not set. Both options are mutually exclusive. resource_id: description: - The ID of the resource at which tags will be controlled. - - Must be specified if I(resource_name) is not set. Both options are mutually exclusive. + - Must be specified if O(resource_name) is not set. Both options are mutually exclusive. type: int version_added: 2.2.0 ''' @@ -78,7 +77,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when connecting to localhost! - name: Create new tags for a provider in ManageIQ. community.general.manageiq_tags: @@ -93,7 +92,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when connecting to localhost! - name: Remove tags for a provider in ManageIQ. community.general.manageiq_tags: @@ -109,18 +108,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false - -- name: List current tags for a provider in ManageIQ. - community.general.manageiq_tags: - state: list - resource_name: 'EngLab' - resource_type: 'provider' - manageiq_connection: - url: 'http://127.0.0.1:3000' - username: 'admin' - password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when connecting to localhost! ''' RETURN = ''' @@ -133,7 +121,7 @@ from ansible_collections.community.general.plugins.module_utils.manageiq import def main(): - actions = {'present': 'assign', 'absent': 'unassign', 'list': 'list'} + actions = {'present': 'assign', 'absent': 'unassign'} argument_spec = dict( tags=dict(type='list', elements='dict'), resource_id=dict(type='int'), @@ -141,7 +129,7 @@ def main(): resource_type=dict(required=True, type='str', choices=list(manageiq_entities().keys())), state=dict(required=False, type='str', - choices=['present', 'absent', 'list'], default='present'), + choices=['present', 'absent'], default='present'), ) # add the manageiq connection arguments to the arguments argument_spec.update(manageiq_argument_spec()) @@ -174,13 +162,8 @@ def main(): manageiq_tags = ManageIQTags(manageiq, resource_type, resource_id) - if action == 'list': - # return a list of current tags for this object - current_tags = manageiq_tags.query_resource_tags() - res_args = dict(changed=False, tags=current_tags) - else: - # assign or unassign the tags - res_args = manageiq_tags.assign_or_unassign_tags(tags, action) + # assign or unassign the tags + res_args = manageiq_tags.assign_or_unassign_tags(tags, action) module.exit_json(**res_args) diff --git a/ansible_collections/community/general/plugins/modules/manageiq_tags_info.py b/ansible_collections/community/general/plugins/modules/manageiq_tags_info.py index af71e150c..75e111540 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_tags_info.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_tags_info.py @@ -36,11 +36,11 @@ options: type: str description: - The name of the resource at which tags will be controlled. - - Must be specified if I(resource_id) is not set. Both options are mutually exclusive. + - Must be specified if O(resource_id) is not set. Both options are mutually exclusive. resource_id: description: - The ID of the resource at which tags will be controlled. - - Must be specified if I(resource_name) is not set. Both options are mutually exclusive. + - Must be specified if O(resource_name) is not set. Both options are mutually exclusive. type: int ''' diff --git a/ansible_collections/community/general/plugins/modules/manageiq_tenant.py b/ansible_collections/community/general/plugins/modules/manageiq_tenant.py index d68e26a73..a5a56191e 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_tenant.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_tenant.py @@ -50,13 +50,13 @@ options: type: int description: - The id of the parent tenant. If not supplied the root tenant is used. - - The C(parent_id) takes president over C(parent) when supplied + - The O(parent_id) takes president over O(parent) when supplied required: false default: null parent: type: str description: - - The name of the parent tenant. If not supplied and no C(parent_id) is supplied the root tenant is used. + - The name of the parent tenant. If not supplied and no O(parent_id) is supplied the root tenant is used. required: false default: null quotas: @@ -83,7 +83,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Create a tenant in ManageIQ community.general.manageiq_tenant: @@ -94,7 +94,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Delete a tenant in ManageIQ community.general.manageiq_tenant: @@ -105,7 +105,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Set tenant quota for cpu_allocated, mem_allocated, remove quota for vms_allocated community.general.manageiq_tenant: @@ -119,7 +119,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Delete a tenant in ManageIQ using a token @@ -130,7 +130,7 @@ EXAMPLES = ''' manageiq_connection: url: 'http://127.0.0.1:3000' token: 'sometoken' - validate_certs: false + validate_certs: false # only do this when you trust the network! ''' RETURN = ''' diff --git a/ansible_collections/community/general/plugins/modules/manageiq_user.py b/ansible_collections/community/general/plugins/modules/manageiq_user.py index 0d3d8718b..0d8a81984 100644 --- a/ansible_collections/community/general/plugins/modules/manageiq_user.py +++ b/ansible_collections/community/general/plugins/modules/manageiq_user.py @@ -60,7 +60,7 @@ options: default: always choices: ['always', 'on_create'] description: - - C(always) will update passwords unconditionally. C(on_create) will only set the password for a newly created user. + - V(always) will update passwords unconditionally. V(on_create) will only set the password for a newly created user. ''' EXAMPLES = ''' @@ -75,7 +75,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Create a new user in ManageIQ using a token community.general.manageiq_user: @@ -87,7 +87,7 @@ EXAMPLES = ''' manageiq_connection: url: 'http://127.0.0.1:3000' token: 'sometoken' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Delete a user in ManageIQ community.general.manageiq_user: @@ -97,7 +97,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Delete a user in ManageIQ using a token community.general.manageiq_user: @@ -106,7 +106,7 @@ EXAMPLES = ''' manageiq_connection: url: 'http://127.0.0.1:3000' token: 'sometoken' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Update email of user in ManageIQ community.general.manageiq_user: @@ -116,7 +116,7 @@ EXAMPLES = ''' url: 'http://127.0.0.1:3000' username: 'admin' password: 'smartvm' - validate_certs: false + validate_certs: false # only do this when you trust the network! - name: Update email of user in ManageIQ using a token community.general.manageiq_user: @@ -125,7 +125,7 @@ EXAMPLES = ''' manageiq_connection: url: 'http://127.0.0.1:3000' token: 'sometoken' - validate_certs: false + validate_certs: false # only do this when you trust the network! ''' RETURN = ''' diff --git a/ansible_collections/community/general/plugins/modules/mas.py b/ansible_collections/community/general/plugins/modules/mas.py index 5b8958beb..8bb80840c 100644 --- a/ansible_collections/community/general/plugins/modules/mas.py +++ b/ansible_collections/community/general/plugins/modules/mas.py @@ -36,7 +36,7 @@ options: state: description: - Desired state of the app installation. - - The C(absent) value requires root permissions, also see the examples. + - The V(absent) value requires root permissions, also see the examples. type: str choices: - absent @@ -53,6 +53,8 @@ requirements: - macOS 10.11+ - "mas-cli (U(https://github.com/mas-cli/mas)) 1.5.0+ available as C(mas) in the bin path" - The Apple ID to use already needs to be signed in to the Mac App Store (check with C(mas account)). + - The feature of "checking if user is signed in" is disabled for anyone using macOS 12.0+. + - Users need to sign in via the Mac App Store GUI beforehand for anyone using macOS 12.0+ due to U(https://github.com/mas-cli/mas/issues/417). ''' EXAMPLES = ''' @@ -106,6 +108,9 @@ import os from ansible_collections.community.general.plugins.module_utils.version import LooseVersion +import platform +NOT_WORKING_MAC_VERSION_MAS_ACCOUNT = '12.0' + class Mas(object): @@ -115,6 +120,7 @@ class Mas(object): # Initialize data properties self.mas_path = self.module.get_bin_path('mas') self._checked_signin = False + self._mac_version = platform.mac_ver()[0] or '0.0' self._installed = None # Populated only if needed self._outdated = None # Populated only if needed self.count_install = 0 @@ -156,14 +162,16 @@ class Mas(object): def check_signin(self): ''' Verifies that the user is signed in to the Mac App Store ''' - # Only check this once per execution if self._checked_signin: return - - rc, out, err = self.run(['account']) - if out.split("\n", 1)[0].rstrip() == 'Not signed in': - self.module.fail_json(msg='You must be signed in to the Mac App Store') + if LooseVersion(self._mac_version) >= LooseVersion(NOT_WORKING_MAC_VERSION_MAS_ACCOUNT): + # Checking if user is signed-in is disabled due to https://github.com/mas-cli/mas/issues/417 + self.module.log('WARNING: You must be signed in via the Mac App Store GUI beforehand else error will occur') + else: + rc, out, err = self.run(['account']) + if out.split("\n", 1)[0].rstrip() == 'Not signed in': + self.module.fail_json(msg='You must be signed in to the Mac App Store') self._checked_signin = True diff --git a/ansible_collections/community/general/plugins/modules/mattermost.py b/ansible_collections/community/general/plugins/modules/mattermost.py index 29894c3a7..154040a8f 100644 --- a/ansible_collections/community/general/plugins/modules/mattermost.py +++ b/ansible_collections/community/general/plugins/modules/mattermost.py @@ -39,26 +39,26 @@ options: description: - Mattermost webhook api key. Log into your mattermost site, go to Menu -> Integration -> Incoming Webhook -> Add Incoming Webhook. - This will give you full URL. api_key is the last part. + This will give you full URL. O(api_key) is the last part. http://mattermost.example.com/hooks/C(API_KEY) required: true text: type: str description: - Text to send. Note that the module does not handle escaping characters. - - Required when I(attachments) is not set. + - Required when O(attachments) is not set. attachments: type: list elements: dict description: - Define a list of attachments. - For more information, see U(https://developers.mattermost.com/integrate/admin-guide/admin-message-attachments/). - - Required when I(text) is not set. + - Required when O(text) is not set. version_added: 4.3.0 channel: type: str description: - - Channel to send the message to. If absent, the message goes to the channel selected for the I(api_key). + - Channel to send the message to. If absent, the message goes to the channel selected for the O(api_key). username: type: str description: @@ -71,7 +71,7 @@ options: default: https://docs.ansible.com/favicon.ico validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be used + - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. default: true type: bool diff --git a/ansible_collections/community/general/plugins/modules/maven_artifact.py b/ansible_collections/community/general/plugins/modules/maven_artifact.py index 3f9defa52..0dc020c37 100644 --- a/ansible_collections/community/general/plugins/modules/maven_artifact.py +++ b/ansible_collections/community/general/plugins/modules/maven_artifact.py @@ -43,14 +43,14 @@ options: type: str description: - The maven version coordinate - - Mutually exclusive with I(version_by_spec). + - Mutually exclusive with O(version_by_spec). version_by_spec: type: str description: - The maven dependency version ranges. - See supported version ranges on U(https://cwiki.apache.org/confluence/display/MAVENOLD/Dependency+Mediation+and+Conflict+Resolution) - The range type "(,1.0],[1.2,)" and "(,1.1),(1.1,)" is not supported. - - Mutually exclusive with I(version). + - Mutually exclusive with O(version). version_added: '0.2.0' classifier: type: str @@ -111,48 +111,48 @@ options: default: 10 validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be set to C(false) when no other option exists. + - If V(false), SSL certificates will not be validated. This should only be set to V(false) when no other option exists. type: bool default: true client_cert: description: - PEM formatted certificate chain file to be used for SSL client authentication. - - This file can also include the key as well, and if the key is included, I(client_key) is not required. + - This file can also include the key as well, and if the key is included, O(client_key) is not required. type: path version_added: '1.3.0' client_key: description: - PEM formatted file that contains your private key to be used for SSL client authentication. - - If I(client_cert) contains both the certificate and key, this option is not required. + - If O(client_cert) contains both the certificate and key, this option is not required. type: path version_added: '1.3.0' keep_name: description: - - If C(true), the downloaded artifact's name is preserved, i.e the version number remains part of it. - - This option only has effect when C(dest) is a directory and C(version) is set to C(latest) or C(version_by_spec) + - If V(true), the downloaded artifact's name is preserved, i.e the version number remains part of it. + - This option only has effect when O(dest) is a directory and O(version) is set to V(latest) or O(version_by_spec) is defined. type: bool default: false verify_checksum: type: str description: - - If C(never), the MD5/SHA1 checksum will never be downloaded and verified. - - If C(download), the MD5/SHA1 checksum will be downloaded and verified only after artifact download. This is the default. - - If C(change), the MD5/SHA1 checksum will be downloaded and verified if the destination already exist, + - If V(never), the MD5/SHA1 checksum will never be downloaded and verified. + - If V(download), the MD5/SHA1 checksum will be downloaded and verified only after artifact download. This is the default. + - If V(change), the MD5/SHA1 checksum will be downloaded and verified if the destination already exist, to verify if they are identical. This was the behaviour before 2.6. Since it downloads the checksum before (maybe) downloading the artifact, and since some repository software, when acting as a proxy/cache, return a 404 error if the artifact has not been cached yet, it may fail unexpectedly. - If you still need it, you should consider using C(always) instead - if you deal with a checksum, it is better to + If you still need it, you should consider using V(always) instead - if you deal with a checksum, it is better to use it to verify integrity after download. - - C(always) combines C(download) and C(change). + - V(always) combines V(download) and V(change). required: false default: 'download' choices: ['never', 'download', 'change', 'always'] checksum_alg: type: str description: - - If C(md5), checksums will use the MD5 algorithm. This is the default. - - If C(sha1), checksums will use the SHA1 algorithm. This can be used on systems configured to use + - If V(md5), checksums will use the MD5 algorithm. This is the default. + - If V(sha1), checksums will use the SHA1 algorithm. This can be used on systems configured to use FIPS-compliant algorithms, since MD5 will be blocked on such systems. default: 'md5' choices: ['md5', 'sha1'] @@ -162,14 +162,14 @@ options: elements: str version_added: 5.2.0 description: - - A list of headers that should not be included in the redirection. This headers are sent to the fetch_url C(fetch_url) function. - - On ansible-core version 2.12 or later, the default of this option is C([Authorization, Cookie]). + - A list of headers that should not be included in the redirection. This headers are sent to the C(fetch_url) function. + - On ansible-core version 2.12 or later, the default of this option is V([Authorization, Cookie]). - Useful if the redirection URL does not need to have sensitive headers in the request. - Requires ansible-core version 2.12 or later. directory_mode: type: str description: - - Filesystem permission mode applied recursively to I(dest) when it is a directory. + - Filesystem permission mode applied recursively to O(dest) when it is a directory. extends_documentation_fragment: - ansible.builtin.files - community.general.attributes diff --git a/ansible_collections/community/general/plugins/modules/memset_dns_reload.py b/ansible_collections/community/general/plugins/modules/memset_dns_reload.py index a1168724f..668c8c0bf 100644 --- a/ansible_collections/community/general/plugins/modules/memset_dns_reload.py +++ b/ansible_collections/community/general/plugins/modules/memset_dns_reload.py @@ -18,8 +18,8 @@ notes: happen every 15 minutes by default, however you can request an immediate reload if later tasks rely on the records being created. An API key generated via the Memset customer control panel is required with the following minimum scope - - I(dns.reload). If you wish to poll the job status to wait until the reload has - completed, then I(job.status) is also required. + C(dns.reload). If you wish to poll the job status to wait until the reload has + completed, then C(job.status) is also required. description: - Request a reload of Memset's DNS infrastructure, and optionally poll until it finishes. extends_documentation_fragment: diff --git a/ansible_collections/community/general/plugins/modules/memset_memstore_info.py b/ansible_collections/community/general/plugins/modules/memset_memstore_info.py index 5fc9d79e1..c00ef15eb 100644 --- a/ansible_collections/community/general/plugins/modules/memset_memstore_info.py +++ b/ansible_collections/community/general/plugins/modules/memset_memstore_info.py @@ -15,10 +15,9 @@ author: "Simon Weald (@glitchcrab)" short_description: Retrieve Memstore product usage information notes: - An API key generated via the Memset customer control panel is needed with the - following minimum scope - I(memstore.usage). + following minimum scope - C(memstore.usage). description: - Retrieve Memstore product usage information. - - This module was called C(memset_memstore_facts) before Ansible 2.9. The usage did not change. extends_documentation_fragment: - community.general.attributes - community.general.attributes.info_module @@ -36,7 +35,7 @@ options: required: true type: str description: - - The Memstore product name (i.e. C(mstestyaa1)). + - The Memstore product name (that is, C(mstestyaa1)). ''' EXAMPLES = ''' diff --git a/ansible_collections/community/general/plugins/modules/memset_server_info.py b/ansible_collections/community/general/plugins/modules/memset_server_info.py index ecc0375eb..78ea99df3 100644 --- a/ansible_collections/community/general/plugins/modules/memset_server_info.py +++ b/ansible_collections/community/general/plugins/modules/memset_server_info.py @@ -15,10 +15,9 @@ author: "Simon Weald (@glitchcrab)" short_description: Retrieve server information notes: - An API key generated via the Memset customer control panel is needed with the - following minimum scope - I(server.info). + following minimum scope - C(server.info). description: - Retrieve server information. - - This module was called C(memset_server_facts) before Ansible 2.9. The usage did not change. extends_documentation_fragment: - community.general.attributes - community.general.attributes.info_module @@ -36,7 +35,7 @@ options: required: true type: str description: - - The server product name (i.e. C(testyaa1)). + - The server product name (that is, C(testyaa1)). ''' EXAMPLES = ''' diff --git a/ansible_collections/community/general/plugins/modules/memset_zone.py b/ansible_collections/community/general/plugins/modules/memset_zone.py index e17472e39..f520d5446 100644 --- a/ansible_collections/community/general/plugins/modules/memset_zone.py +++ b/ansible_collections/community/general/plugins/modules/memset_zone.py @@ -17,7 +17,7 @@ notes: - Zones can be thought of as a logical group of domains, all of which share the same DNS records (i.e. they point to the same IP). An API key generated via the Memset customer control panel is needed with the following minimum scope - - I(dns.zone_create), I(dns.zone_delete), I(dns.zone_list). + C(dns.zone_create), C(dns.zone_delete), C(dns.zone_list). description: - Manage DNS zones in a Memset account. extends_documentation_fragment: diff --git a/ansible_collections/community/general/plugins/modules/memset_zone_domain.py b/ansible_collections/community/general/plugins/modules/memset_zone_domain.py index 172a48be2..e07ac1ff0 100644 --- a/ansible_collections/community/general/plugins/modules/memset_zone_domain.py +++ b/ansible_collections/community/general/plugins/modules/memset_zone_domain.py @@ -17,9 +17,9 @@ notes: - Zone domains can be thought of as a collection of domains, all of which share the same DNS records (i.e. they point to the same IP). An API key generated via the Memset customer control panel is needed with the following minimum scope - - I(dns.zone_domain_create), I(dns.zone_domain_delete), I(dns.zone_domain_list). + C(dns.zone_domain_create), C(dns.zone_domain_delete), C(dns.zone_domain_list). - Currently this module can only create one domain at a time. Multiple domains should - be created using C(with_items). + be created using C(loop). description: - Manage DNS zone domains in a Memset account. extends_documentation_fragment: diff --git a/ansible_collections/community/general/plugins/modules/memset_zone_record.py b/ansible_collections/community/general/plugins/modules/memset_zone_record.py index 4e56a11ca..8406d93d2 100644 --- a/ansible_collections/community/general/plugins/modules/memset_zone_record.py +++ b/ansible_collections/community/general/plugins/modules/memset_zone_record.py @@ -17,9 +17,9 @@ notes: - Zones can be thought of as a logical group of domains, all of which share the same DNS records (i.e. they point to the same IP). An API key generated via the Memset customer control panel is needed with the following minimum scope - - I(dns.zone_create), I(dns.zone_delete), I(dns.zone_list). + C(dns.zone_create), C(dns.zone_delete), C(dns.zone_list). - Currently this module can only create one DNS record at a time. Multiple records - should be created using C(with_items). + should be created using C(loop). description: - Manage DNS records in a Memset account. extends_documentation_fragment: diff --git a/ansible_collections/community/general/plugins/modules/modprobe.py b/ansible_collections/community/general/plugins/modules/modprobe.py index 6389d758d..f271b3946 100644 --- a/ansible_collections/community/general/plugins/modules/modprobe.py +++ b/ansible_collections/community/general/plugins/modules/modprobe.py @@ -49,14 +49,14 @@ options: description: - Persistency between reboots for configured module. - This option creates files in C(/etc/modules-load.d/) and C(/etc/modprobe.d/) that make your module configuration persistent during reboots. - - If C(present), adds module name to C(/etc/modules-load.d/) and params to C(/etc/modprobe.d/) so the module will be loaded on next reboot. - - If C(absent), will comment out module name from C(/etc/modules-load.d/) and comment out params from C(/etc/modprobe.d/) so the module will not be + - If V(present), adds module name to C(/etc/modules-load.d/) and params to C(/etc/modprobe.d/) so the module will be loaded on next reboot. + - If V(absent), will comment out module name from C(/etc/modules-load.d/) and comment out params from C(/etc/modprobe.d/) so the module will not be loaded on next reboot. - - If C(disabled), will not touch anything and leave C(/etc/modules-load.d/) and C(/etc/modprobe.d/) as it is. + - If V(disabled), will not touch anything and leave C(/etc/modules-load.d/) and C(/etc/modprobe.d/) as it is. - Note that it is usually a better idea to rely on the automatic module loading by PCI IDs, USB IDs, DMI IDs or similar triggers encoded in the kernel modules themselves instead of configuration like this. - In fact, most modern kernel modules are prepared for automatic loading already. - - "B(Note:) This option works only with distributions that use C(systemd) when set to values other than C(disabled)." + - "B(Note:) This option works only with distributions that use C(systemd) when set to values other than V(disabled)." ''' EXAMPLES = ''' @@ -232,12 +232,16 @@ class Modprobe(object): @property def modules_files(self): + if not os.path.isdir(MODULES_LOAD_LOCATION): + return [] modules_paths = [os.path.join(MODULES_LOAD_LOCATION, path) for path in os.listdir(MODULES_LOAD_LOCATION)] return [path for path in modules_paths if os.path.isfile(path)] @property def modprobe_files(self): + if not os.path.isdir(PARAMETERS_FILES_LOCATION): + return [] modules_paths = [os.path.join(PARAMETERS_FILES_LOCATION, path) for path in os.listdir(PARAMETERS_FILES_LOCATION)] return [path for path in modules_paths if os.path.isfile(path)] diff --git a/ansible_collections/community/general/plugins/modules/monit.py b/ansible_collections/community/general/plugins/modules/monit.py index d2a160678..5475ab1e5 100644 --- a/ansible_collections/community/general/plugins/modules/monit.py +++ b/ansible_collections/community/general/plugins/modules/monit.py @@ -14,7 +14,7 @@ DOCUMENTATION = ''' module: monit short_description: Manage the state of a program monitored via Monit description: - - Manage the state of a program monitored via I(Monit). + - Manage the state of a program monitored via Monit. extends_documentation_fragment: - community.general.attributes attributes: @@ -25,7 +25,7 @@ attributes: options: name: description: - - The name of the I(monit) program/process to manage. + - The name of the C(monit) program/process to manage. required: true type: str state: diff --git a/ansible_collections/community/general/plugins/modules/mqtt.py b/ansible_collections/community/general/plugins/modules/mqtt.py index 389382649..f8d64e6a0 100644 --- a/ansible_collections/community/general/plugins/modules/mqtt.py +++ b/ansible_collections/community/general/plugins/modules/mqtt.py @@ -40,7 +40,7 @@ options: password: type: str description: - - Password for C(username) to authenticate against the broker. + - Password for O(username) to authenticate against the broker. client_id: type: str description: @@ -54,8 +54,8 @@ options: payload: type: str description: - - Payload. The special string C("None") may be used to send a NULL - (i.e. empty) payload which is useful to simply notify with the I(topic) + - Payload. The special string V("None") may be used to send a NULL + (that is, empty) payload which is useful to simply notify with the O(topic) or to clear previously retained messages. required: true qos: diff --git a/ansible_collections/community/general/plugins/modules/mssql_db.py b/ansible_collections/community/general/plugins/modules/mssql_db.py index 4006033cf..a85f721fc 100644 --- a/ansible_collections/community/general/plugins/modules/mssql_db.py +++ b/ansible_collections/community/general/plugins/modules/mssql_db.py @@ -71,7 +71,6 @@ notes: - Requires the pymssql Python package on the remote host. For Ubuntu, this is as easy as pip install pymssql (See M(ansible.builtin.pip).) requirements: - - python >= 2.7 - pymssql author: Vedit Firat Arig (@vedit) ''' diff --git a/ansible_collections/community/general/plugins/modules/mssql_script.py b/ansible_collections/community/general/plugins/modules/mssql_script.py index 1696000db..b1713092c 100644 --- a/ansible_collections/community/general/plugins/modules/mssql_script.py +++ b/ansible_collections/community/general/plugins/modules/mssql_script.py @@ -46,33 +46,41 @@ options: type: str required: true login_port: - description: Port of the MSSQL server. Requires I(login_host) be defined as well. + description: Port of the MSSQL server. Requires O(login_host) be defined as well. default: 1433 type: int script: description: - The SQL script to be executed. - - Script can contain multiple SQL statements. Multiple Batches can be separated by C(GO) command. + - Script can contain multiple SQL statements. Multiple Batches can be separated by V(GO) command. - Each batch must return at least one result set. required: true type: str + transaction: + description: + - If transactional mode is requested, start a transaction and commit the change only if the script succeed. + Otherwise, rollback the transaction. + - If transactional mode is not requested (default), automatically commit the change. + type: bool + default: false + version_added: 8.4.0 output: description: - - With C(default) each row will be returned as a list of values. See C(query_results). - - Output format C(dict) will return dictionary with the column names as keys. See C(query_results_dict). - - C(dict) requires named columns to be returned by each query otherwise an error is thrown. + - With V(default) each row will be returned as a list of values. See RV(query_results). + - Output format V(dict) will return dictionary with the column names as keys. See RV(query_results_dict). + - V(dict) requires named columns to be returned by each query otherwise an error is thrown. choices: [ "dict", "default" ] default: 'default' type: str params: description: | - Parameters passed to the script as SQL parameters. ('SELECT %(name)s"' with C(example: '{"name": "John Doe"}).)' + Parameters passed to the script as SQL parameters. + (Query V('SELECT %(name\)s"') with V(example: '{"name": "John Doe"}).)' type: dict notes: - Requires the pymssql Python package on the remote host. For Ubuntu, this is as easy as C(pip install pymssql) (See M(ansible.builtin.pip).) requirements: - - python >= 2.7 - pymssql author: @@ -105,6 +113,19 @@ EXAMPLES = r''' - result_params.query_results[0][0][0][0] == 'msdb' - result_params.query_results[0][0][0][1] == 'ONLINE' +- name: Query within a transaction + community.general.mssql_script: + login_user: "{{ mssql_login_user }}" + login_password: "{{ mssql_login_password }}" + login_host: "{{ mssql_host }}" + login_port: "{{ mssql_port }}" + script: | + UPDATE sys.SomeTable SET desc = 'some_table_desc' WHERE name = %(dbname)s + UPDATE sys.AnotherTable SET desc = 'another_table_desc' WHERE name = %(dbname)s + transaction: true + params: + dbname: msdb + - name: two batches with default output community.general.mssql_script: login_user: "{{ mssql_login_user }}" @@ -148,17 +169,17 @@ EXAMPLES = r''' RETURN = r''' query_results: - description: List of batches (queries separated by C(GO) keyword). + description: List of batches (queries separated by V(GO) keyword). type: list elements: list - returned: success and I(output=default) + returned: success and O(output=default) sample: [[[["Batch 0 - Select 0"]], [["Batch 0 - Select 1"]]], [[["Batch 1 - Select 0"]]]] contains: queries: description: - List of result sets of each query. - If a query returns no results, the results of this and all the following queries will not be included in the output. - - Use the C(GO) keyword in I(script) to separate queries. + - Use the V(GO) keyword in O(script) to separate queries. type: list elements: list contains: @@ -175,10 +196,10 @@ query_results: example: ["Batch 0 - Select 0"] returned: success, if output is default query_results_dict: - description: List of batches (queries separated by C(GO) keyword). + description: List of batches (queries separated by V(GO) keyword). type: list elements: list - returned: success and I(output=dict) + returned: success and O(output=dict) sample: [[[["Batch 0 - Select 0"]], [["Batch 0 - Select 1"]]], [[["Batch 1 - Select 0"]]]] contains: queries: @@ -230,6 +251,7 @@ def run_module(): script=dict(required=True), output=dict(default='default', choices=['dict', 'default']), params=dict(type='dict'), + transaction=dict(type='bool', default=False), ) result = dict( @@ -252,6 +274,8 @@ def run_module(): script = module.params['script'] output = module.params['output'] sql_params = module.params['params'] + # Added param to set the transactional mode (true/false) + transaction = module.params['transaction'] login_querystring = login_host if login_port != 1433: @@ -273,21 +297,40 @@ def run_module(): module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check your " "@sysconfdir@/freetds.conf / ${HOME}/.freetds.conf") - conn.autocommit(True) + # If transactional mode is requested, start a transaction + conn.autocommit(not transaction) query_results_key = 'query_results' if output == 'dict': cursor = conn.cursor(as_dict=True) query_results_key = 'query_results_dict' - queries = script.split('\nGO\n') + # Process the script into batches + queries = [] + current_batch = [] + for statement in script.splitlines(True): + # Ignore the Byte Order Mark, if found + if statement.strip() == '\uFEFF': + continue + + # Assume each 'GO' is on its own line but may have leading/trailing whitespace + # and be of mixed-case + if statement.strip().upper() != 'GO': + current_batch.append(statement) + else: + queries.append(''.join(current_batch)) + current_batch = [] + if len(current_batch) > 0: + queries.append(''.join(current_batch)) + result['changed'] = True if module.check_mode: module.exit_json(**result) query_results = [] - try: - for query in queries: + for query in queries: + # Catch and exit on any bad query errors + try: cursor.execute(query, sql_params) qry_result = [] rows = cursor.fetchall() @@ -295,8 +338,24 @@ def run_module(): qry_result.append(rows) rows = cursor.fetchall() query_results.append(qry_result) - except Exception as e: - return module.fail_json(msg="query failed", query=query, error=str(e), **result) + except Exception as e: + # We know we executed the statement so this error just means we have no resultset + # which is ok (eg UPDATE/INSERT) + if ( + type(e).__name__ == 'OperationalError' and + str(e) == 'Statement not executed or executed statement has no resultset' + ): + query_results.append([]) + else: + # Rollback transaction before failing the module in case of error + if transaction: + conn.rollback() + error_msg = '%s: %s' % (type(e).__name__, str(e)) + module.fail_json(msg="query failed", query=query, error=error_msg, **result) + + # Commit transaction before exiting the module in case of no error + if transaction: + conn.commit() # ensure that the result is json serializable qry_results = json.loads(json.dumps(query_results, default=clean_output)) diff --git a/ansible_collections/community/general/plugins/modules/nagios.py b/ansible_collections/community/general/plugins/modules/nagios.py index 1831d0496..783aa88e2 100644 --- a/ansible_collections/community/general/plugins/modules/nagios.py +++ b/ansible_collections/community/general/plugins/modules/nagios.py @@ -21,13 +21,13 @@ short_description: Perform common tasks in Nagios related to downtime and notifi description: - "The C(nagios) module has two basic functions: scheduling downtime and toggling alerts for services or hosts." - The C(nagios) module is not idempotent. - - All actions require the I(host) parameter to be given explicitly. In playbooks you can use the C({{inventory_hostname}}) variable to refer + - All actions require the O(host) parameter to be given explicitly. In playbooks you can use the C({{inventory_hostname}}) variable to refer to the host the playbook is currently running on. - - You can specify multiple services at once by separating them with commas, .e.g. I(services=httpd,nfs,puppet). - - When specifying what service to handle there is a special service value, I(host), which will handle alerts/downtime/acknowledge for the I(host itself), - e.g., I(service=host). This keyword may not be given with other services at the same time. - I(Setting alerts/downtime/acknowledge for a host does not affect alerts/downtime/acknowledge for any of the services running on it.) - To schedule downtime for all services on particular host use keyword "all", e.g., I(service=all). + - You can specify multiple services at once by separating them with commas, .e.g. O(services=httpd,nfs,puppet). + - When specifying what service to handle there is a special service value, O(host), which will handle alerts/downtime/acknowledge for the I(host itself), + for example O(services=host). This keyword may not be given with other services at the same time. + B(Setting alerts/downtime/acknowledge for a host does not affect alerts/downtime/acknowledge for any of the services running on it.) + To schedule downtime for all services on particular host use keyword "all", for example O(services=all). extends_documentation_fragment: - community.general.attributes attributes: @@ -41,7 +41,7 @@ options: - Action to take. - servicegroup options were added in 2.0. - delete_downtime options were added in 2.2. - - The C(acknowledge) and C(forced_check) actions were added in community.general 1.2.0. + - The V(acknowledge) and V(forced_check) actions were added in community.general 1.2.0. required: true choices: [ "downtime", "delete_downtime", "enable_alerts", "disable_alerts", "silence", "unsilence", "silence_nagios", "unsilence_nagios", "command", "servicegroup_service_downtime", @@ -59,12 +59,12 @@ options: author: description: - Author to leave downtime comments as. - Only used when I(action) is C(downtime) or C(acknowledge). + Only used when O(action) is V(downtime) or V(acknowledge). type: str default: Ansible comment: description: - - Comment when I(action) is C(downtime) or C(acknowledge). + - Comment when O(action) is V(downtime) or V(acknowledge). type: str default: Scheduling downtime start: @@ -75,27 +75,24 @@ options: minutes: description: - Minutes to schedule downtime for. - - Only usable with the C(downtime) action. + - Only usable with O(action=downtime). type: int default: 30 services: description: - - > - What to manage downtime/alerts for. Separate multiple services with commas. - I(service) is an alias for I(services). - B(Required) option when I(action) is one of: C(downtime), C(acknowledge), C(forced_check), C(enable_alerts), C(disable_alerts). + - What to manage downtime/alerts for. Separate multiple services with commas. + - "B(Required) option when O(action) is one of: V(downtime), V(acknowledge), V(forced_check), V(enable_alerts), V(disable_alerts)." aliases: [ "service" ] type: str servicegroup: description: - The Servicegroup we want to set downtimes/alerts for. - B(Required) option when using the C(servicegroup_service_downtime) amd C(servicegroup_host_downtime). + - B(Required) option when using the V(servicegroup_service_downtime) and V(servicegroup_host_downtime) O(action). type: str command: description: - - The raw command to send to nagios, which - should not include the submitted time header or the line-feed - B(Required) option when using the C(command) action. + - The raw command to send to nagios, which should not include the submitted time header or the line-feed. + - B(Required) option when using the V(command) O(action). type: str author: "Tim Bielawa (@tbielawa)" diff --git a/ansible_collections/community/general/plugins/modules/netcup_dns.py b/ansible_collections/community/general/plugins/modules/netcup_dns.py index 77be50b2c..cba70c0fa 100644 --- a/ansible_collections/community/general/plugins/modules/netcup_dns.py +++ b/ansible_collections/community/general/plugins/modules/netcup_dns.py @@ -46,14 +46,17 @@ options: type: str record: description: - - Record to add or delete, supports wildcard (*). Default is C(@) (e.g. the zone name). + - Record to add or delete, supports wildcard (V(*)). Default is V(@) (that is, the zone name). default: "@" aliases: [ name ] type: str type: description: - Record type. - choices: ['A', 'AAAA', 'MX', 'CNAME', 'CAA', 'SRV', 'TXT', 'TLSA', 'NS', 'DS'] + - Support for V(OPENPGPKEY), V(SMIMEA) and V(SSHFP) was added in community.general 8.1.0. + - Record types V(OPENPGPKEY) and V(SMIMEA) require nc-dnsapi >= 0.1.5. + - Record type V(SSHFP) requires nc-dnsapi >= 0.1.6. + choices: ['A', 'AAAA', 'MX', 'CNAME', 'CAA', 'SRV', 'TXT', 'TLSA', 'NS', 'DS', 'OPENPGPKEY', 'SMIMEA', 'SSHFP'] required: true type: str value: @@ -65,11 +68,11 @@ options: type: bool default: false description: - - Whether the record should be the only one for that record type and record name. Only use with I(state=present). + - Whether the record should be the only one for that record type and record name. Only use with O(state=present). - This will delete all other records with the same record name and type. priority: description: - - Record priority. Required for I(type=MX). + - Record priority. Required for O(type=MX). required: false type: int state: @@ -169,7 +172,7 @@ records: sample: fancy-hostname type: description: the record type - returned: succcess + returned: success type: str sample: A value: @@ -213,7 +216,9 @@ def main(): domain=dict(required=True), record=dict(required=False, default='@', aliases=['name']), - type=dict(required=True, choices=['A', 'AAAA', 'MX', 'CNAME', 'CAA', 'SRV', 'TXT', 'TLSA', 'NS', 'DS']), + type=dict(required=True, choices=['A', 'AAAA', 'MX', 'CNAME', 'CAA', 'SRV', 'TXT', + 'TLSA', 'NS', 'DS', 'OPENPGPKEY', 'SMIMEA', + 'SSHFP']), value=dict(required=True), priority=dict(required=False, type='int'), solo=dict(required=False, type='bool', default=False), diff --git a/ansible_collections/community/general/plugins/modules/newrelic_deployment.py b/ansible_collections/community/general/plugins/modules/newrelic_deployment.py index ac9903b57..e5a116082 100644 --- a/ansible_collections/community/general/plugins/modules/newrelic_deployment.py +++ b/ansible_collections/community/general/plugins/modules/newrelic_deployment.py @@ -32,14 +32,14 @@ options: app_name: type: str description: - - The value of app_name in the newrelic.yml file used by the application. - - One of I(app_name) or I(application_id) is required. + - The value of C(app_name) in the C(newrelic.yml) file used by the application. + - One of O(app_name) or O(application_id) is required. required: false application_id: type: str description: - The application ID found in the metadata of the application in APM. - - One of I(app_name) or I(application_id) is required. + - One of O(app_name) or O(application_id) is required. required: false changelog: type: str @@ -61,25 +61,21 @@ options: description: - The name of the user/process that triggered this deployment required: false - appname: - type: str - description: - - Name of the application. - - This option has been deprecated and will be removed in community.general 7.0.0. Please do not use. - required: false - environment: - type: str - description: - - The environment for this deployment. - - This option has been deprecated and will be removed community.general 7.0.0. Please do not use. - required: false validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be used + - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. required: false default: true type: bool + app_name_exact_match: + type: bool + description: + - If this flag is set to V(true) then the application ID lookup by name would only work for an exact match. + If set to V(false) it returns the first result. + required: false + default: false + version_added: 7.5.0 requirements: [] ''' @@ -113,11 +109,11 @@ def main(): description=dict(required=False), revision=dict(required=True), user=dict(required=False), - appname=dict(required=False, removed_in_version='7.0.0', removed_from_collection='community.general'), - environment=dict(required=False, removed_in_version='7.0.0', removed_from_collection='community.general'), validate_certs=dict(default=True, type='bool'), + app_name_exact_match=dict(required=False, type='bool', default=False), ), required_one_of=[['app_name', 'application_id']], + required_if=[('app_name_exact_match', True, ['app_name'])], supports_check_mode=True ) @@ -125,7 +121,6 @@ def main(): params = {} if module.params["app_name"] and module.params["application_id"]: module.fail_json(msg="only one of 'app_name' or 'application_id' can be set") - app_id = None if module.params["app_name"]: app_id = get_application_id(module) @@ -164,6 +159,7 @@ def main(): def get_application_id(module): url = "https://api.newrelic.com/v2/applications.json" data = "filter[name]=%s" % module.params["app_name"] + application_id = None headers = { 'Api-Key': module.params["token"], } @@ -175,7 +171,17 @@ def get_application_id(module): if result is None or len(result.get("applications", "")) == 0: module.fail_json(msg='No application found with name "%s"' % module.params["app_name"]) - return result["applications"][0]["id"] + if module.params["app_name_exact_match"]: + for item in result["applications"]: + if item["name"] == module.params["app_name"]: + application_id = item["id"] + break + if application_id is None: + module.fail_json(msg='No application found with exact name "%s"' % module.params["app_name"]) + else: + application_id = result["applications"][0]["id"] + + return application_id if __name__ == '__main__': diff --git a/ansible_collections/community/general/plugins/modules/nexmo.py b/ansible_collections/community/general/plugins/modules/nexmo.py index 7461c1cb9..39f127f98 100644 --- a/ansible_collections/community/general/plugins/modules/nexmo.py +++ b/ansible_collections/community/general/plugins/modules/nexmo.py @@ -50,7 +50,7 @@ options: required: true validate_certs: description: - - If C(false), SSL certificates will not be validated. This should only be used + - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. type: bool default: true diff --git a/ansible_collections/community/general/plugins/modules/nictagadm.py b/ansible_collections/community/general/plugins/modules/nictagadm.py index 074e09b4a..5b81861e8 100644 --- a/ansible_collections/community/general/plugins/modules/nictagadm.py +++ b/ansible_collections/community/general/plugins/modules/nictagadm.py @@ -31,23 +31,23 @@ options: type: str mac: description: - - Specifies the I(mac) address to attach the nic tag to when not creating an I(etherstub). - - Parameters I(mac) and I(etherstub) are mutually exclusive. + - Specifies the O(mac) address to attach the nic tag to when not creating an O(etherstub). + - Parameters O(mac) and O(etherstub) are mutually exclusive. type: str etherstub: description: - - Specifies that the nic tag will be attached to a created I(etherstub). - - Parameter I(etherstub) is mutually exclusive with both I(mtu), and I(mac). + - Specifies that the nic tag will be attached to a created O(etherstub). + - Parameter O(etherstub) is mutually exclusive with both O(mtu), and O(mac). type: bool default: false mtu: description: - - Specifies the size of the I(mtu) of the desired nic tag. - - Parameters I(mtu) and I(etherstub) are mutually exclusive. + - Specifies the size of the O(mtu) of the desired nic tag. + - Parameters O(mtu) and O(etherstub) are mutually exclusive. type: int force: description: - - When I(state) is absent set this switch will use the C(-f) parameter and delete the nic tag regardless of existing VMs. + - When O(state=absent) this switch will use the C(-f) parameter and delete the nic tag regardless of existing VMs. type: bool default: false state: diff --git a/ansible_collections/community/general/plugins/modules/nmcli.py b/ansible_collections/community/general/plugins/modules/nmcli.py index 08680bf6e..9360ce37d 100644 --- a/ansible_collections/community/general/plugins/modules/nmcli.py +++ b/ansible_collections/community/general/plugins/modules/nmcli.py @@ -52,23 +52,25 @@ options: description: - The interface to bind the connection to. - The connection will only be applicable to this interface name. - - A special value of C('*') can be used for interface-independent connections. + - A special value of V('*') can be used for interface-independent connections. - The ifname argument is mandatory for all connection types except bond, team, bridge, vlan and vpn. - - This parameter defaults to C(conn_name) when left unset for all connection types except vpn that removes it. + - This parameter defaults to O(conn_name) when left unset for all connection types except vpn that removes it. type: str type: description: - This is the type of device or network connection that you wish to create or modify. - - Type C(dummy) is added in community.general 3.5.0. - - Type C(generic) is added in Ansible 2.5. - - Type C(infiniband) is added in community.general 2.0.0. - - Type C(gsm) is added in community.general 3.7.0. - - Type C(macvlan) is added in community.general 6.6.0. - - Type C(wireguard) is added in community.general 4.3.0. - - Type C(vpn) is added in community.general 5.1.0. + - Type V(dummy) is added in community.general 3.5.0. + - Type V(gsm) is added in community.general 3.7.0. + - Type V(infiniband) is added in community.general 2.0.0. + - Type V(loopback) is added in community.general 8.1.0. + - Type V(macvlan) is added in community.general 6.6.0. + - Type V(wireguard) is added in community.general 4.3.0. + - Type V(vpn) is added in community.general 5.1.0. + - Using V(bond-slave), V(bridge-slave), or V(team-slave) implies V(ethernet) connection type with corresponding O(slave_type) option. + - If you want to control non-ethernet connection attached to V(bond), V(bridge), or V(team) consider using O(slave_type) option. type: str choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, macvlan, sit, team, team-slave, vlan, vxlan, - wifi, gsm, wireguard, vpn ] + wifi, gsm, wireguard, vpn, loopback ] mode: description: - This is the type of device or network connection that you wish to create for a bond or bridge. @@ -81,21 +83,28 @@ options: type: str choices: [ datagram, connected ] version_added: 5.8.0 + slave_type: + description: + - Type of the device of this slave's master connection (for example V(bond)). + type: str + choices: [ 'bond', 'bridge', 'team' ] + version_added: 7.0.0 master: description: - Master