diff options
Diffstat (limited to 'ansible_collections/openstack/cloud')
165 files changed, 38671 insertions, 0 deletions
diff --git a/ansible_collections/openstack/cloud/CHANGELOG.rst b/ansible_collections/openstack/cloud/CHANGELOG.rst new file mode 100644 index 00000000..7feb2ad5 --- /dev/null +++ b/ansible_collections/openstack/cloud/CHANGELOG.rst @@ -0,0 +1,534 @@ +============================================= +Openstack Cloud Ansilbe modules Release Notes +============================================= + +.. contents:: Topics + + +v1.10.0 +======= + +Release Summary +--------------- + +Enable logging of openstacksdk activities and warn users about incompatible openstacksdk releases when using inventory plugin + +Bugfixes +-------- + +- Add SDK logging option for openstack ansible collections +- Don't use deprecated distutils from python 3.10 +- Ensure openstacksdk compatibility in inventory plugin +- Lowered maximum OpenStack SDK version to 0.98.999 in inventory plugin + +Known Issues +------------ + +- For compatibility with OpenStack SDK >= 0.99.0 use Ansible OpenStack collection 2.0.0 or later which is currently under development. +- Release series 1.x.x of this collection is compatible to OpenStack SDK prior to 0.99.0 only. + +v1.9.1 +====== + +Release Summary +--------------- + +Bugfix in keypair module + +Bugfixes +-------- + +- Do not remove trailing spaces when reading public key in keypair module + +Known Issues +------------ + +- For compatibility with OpenStack SDK >= 0.99.0 use Ansible OpenStack collection 2.0.0 or later which is currently under development. +- Release series 1.x.x of this collection is compatible to OpenStack SDK prior to 0.99.0 only. + +v1.9.0 +====== + +Release Summary +--------------- + +This release will enforce openstacksdk<0.99.0, has a dozen modules refactored and several bugs fixed. + +Bugfixes +-------- + +- Added support for specifying a maximum version of the OpenStack SDK +- Constrain filters in compute_service_info to SDK >= 0.53.0 +- Drop username from return values of identity_user_info +- Fix logic in routers_info +- Fixed return value disable{d,s}_reason in compute_service_info module +- Fixed return values in compute_service_info module again +- Follow up to bump of minimum required OpenStack SDK release to SDK 0.36.0 (Train) +- Lowered maximum OpenStack SDK version to 0.98.999 +- Move dns zone info to use proxy layer +- Refactored catalog_service module +- Refactored endpoint module +- Refactored host_aggregate module +- Refactored identity_domain_info module +- Refactored identity_group_info module +- Refactored identity_role module +- Refactored identity_role_info module +- Refactored identity_user module +- Refactored identity_user_info module +- Refactored image_info module +- Refactored keypair_info module +- Refactored recordset module +- Refactored role_assignment module +- Set owner in image module +- Support description in sg-rule creation +- Warn users about us breaking backward compatibility + +Known Issues +------------ + +- For compatibility with OpenStack SDK >= 0.99.0 use Ansible OpenStack collection 2.0.0 or later which is currently under development. +- Release series 1.x.x of this collection is compatible to OpenStack SDK prior to 0.99.0 only. + +v1.8.0 +====== + +Release Summary +--------------- + +Subnet pool module and bugfixes + +Bugfixes +-------- + +- Add 'all_projects' to server_action module +- Add subnet pool module +- Bumped minimum required OpenStack SDK release to SDK 0.36.0 (Train) +- Changed compute_flavor_info module to use OpenStack SDK's proxy layer +- Dropped deprecated return values in floating_ip_info and assert remaining fields +- Fix ansible-lint issues for the newest version +- Fix assertion after stack deletion +- Handle aggregate host list set to None +- Reenabled check-import.sh which tests imports to Ansible Galaxy +- Remove old, unsupported parameters from documentation in image_info module +- Router - Remove unneeded 'filter' parameter +- Updated return value docs of compute_service_info module + +New Modules +----------- + +- openstack.cloud.subnet_pool - Create or Delete subnet pools from OpenStack. + +v1.7.2 +====== + +Release Summary +--------------- + +Bugfixes + +Bugfixes +-------- + +- Fix collection guidelines + +v1.7.1 +====== + +Release Summary +--------------- + +Bugfixes + +Minor Changes +------------- + +- lb_member - Add monitor_[address,port] parameter + +Bugfixes +-------- + +- openstack_inventory - Fix documentation +- quota - Fix description of volumes_types parameter + +v1.7.0 +====== + +Release Summary +--------------- + +New modules for Ironic and bugfixes + +Minor Changes +------------- + +- openstack_inventory - Adds use_name variable +- port - Add dns_[name,domain] to the port module +- project - Remove project properties tests and support + +Bugfixes +-------- + +- identity_user_info - Fix identity user lookup with a domain +- keystone_domain - Move identity domain to use proxy layer + +New Modules +----------- + +- openstack.cloud.baremetal_node_info - Retrieve information about Bare Metal nodes from OpenStack an object. +- openstack.cloud.baremetal_port - Create, Update, Remove ironic ports from OpenStack +- openstack.cloud.baremetal_port_info - Retrieve information about Bare Metal ports from OpenStack an object. + +v1.6.0 +====== + +Release Summary +--------------- + +New modules for RBAC and Nova services + +Minor Changes +------------- + +- quota - Adds metadata_items parameter + +New Modules +----------- + +- openstack.cloud.compute_service_info - Retrieve information about one or more OpenStack compute services +- openstack.cloud.neutron_rbac_policies_info - Fetch Neutron policies. +- openstack.cloud.neutron_rbac_policy - Create or delete a Neutron policy to apply a RBAC rule against an object. + +v1.5.3 +====== + +Release Summary +--------------- + +Bugfixes + +Bugfixes +-------- + +- Don't require allowed_address_pairs for port +- server_volume - check specified server is found + +v1.5.2 +====== + +Release Summary +--------------- + +Bugfixes + +Minor Changes +------------- + +- Add documentation links to README.md +- Don't run functional jobs on galaxy.yml change +- Move CI to use Ansible 2.12 version as main + +Bugfixes +-------- + +- Add client and member listener timeouts for persistence (Ex. SSH) +- Added missing warn() used in cloud.openstack.quota +- Fix issue with same host and group names +- Flavor properties are not deleted on changes and id will stay + +v1.5.1 +====== + +Release Summary +--------------- + +Bugfixes for networking modules + +Minor Changes +------------- + +- Changed minversion in tox to 3.18.0 +- Update IRC server in README + +Bugfixes +-------- + +- Add mandatory requires_ansible version to metadata +- Add protocol listener octavia +- Add support check mode for all info modules +- Allow to attach multiple floating ips to a server +- Only add or remove router interfaces when needed +- Wait for pool to be active and online + +v1.5.0 +====== + +Release Summary +--------------- + +New modules for DNS and FIPs and bugfixes. + +Minor Changes +------------- + +- Add bindep.txt for ansible-builder +- Add check_mode attribute to OpenstackModule +- Migrating image module from AnsibleModule to OpenStackModule +- Switch KeystoneFederationProtocolInfo module to OpenStackModule +- Switch ProjectAccess module to OpenStackModule +- Switch Quota module to OpenStackModule +- Switch Recordset module to OpenStackModule +- Switch ServerGroup module to OpenStackModule +- Switch ServerMetadata module to OpenStackModule +- Switch Snapshot module to OpenStackModule +- Switch Stack module to OpenStackModule +- Switch auth module to OpenStackModule +- Switch catalog_service module to OpenStackModule +- Switch coe_cluster module to OpenStackModule +- Switch coe_cluster_template module to OpenStackModule +- Switch endpoint module to OpenStackModule +- Switch federation_idp module to OpenStackModule +- Switch federation_idp_info module to OpenStackModule +- Switch federation_mapping module to OpenStackModule +- Switch federation_mapping_info module to OpenStackModule +- Switch federation_protocol module to OpenStackModule +- Switch flavor module to OpenStackModule +- Switch flavor_info module to OpenStackModule +- Switch floating_ip module to OpenStackModule +- Switch group_assignment module to OpenStackModule +- Switch hostaggregate module to OpenStackModule +- Switch identity_domain module to OpenStackModule +- Switch identity_domain_info module to OpenStackModule +- Switch identity_group module to OpenStackModule +- Switch identity_group_info module to OpenStackModule +- Switch identity_role module to OpenStackModule +- Switch identity_user module to OpenStackModule +- Switch lb_listener module to OpenStackModule +- Switch lb_member module to OpenStackModule +- Switch lb_pool module to OpenStackModule +- Switch object module to OpenStackModule +- Switch port module to OpenStackModule +- Switch port_info module to OpenStackModule +- Switch project and project_info module to OpenStackModule +- Switch role_assignment module to OpenStackModule +- Switch user_info module to OpenStackModule +- image - Add support to setting image tags + +Bugfixes +-------- + +- Update checks for validate_certs in openstack_cloud_from_module +- compute_flavor - Fix the idempotent of compute_flavor module +- host_aggregate - Fix host_aggregate to tolerate aggregate.hosts being None +- inventory/openstack - Fix inventory plugin on Ansible 2.11 +- port - fix update on empty list of allowed address pairs +- setup.cfg Replace dashes with underscores +- subnet - Only apply necessary changes to subnets +- volume - Fail if referenced source image for a new volume does not exist + +New Modules +----------- + +- openstack.cloud.address_scope - Create or delete address scopes from OpenStack +- openstack.cloud.dns_zone_info - Getting information about dns zones +- openstack.cloud.floating_ip_info - Get information about floating ips + +v1.4.0 +====== + +Release Summary +--------------- + +New object_container module and bugfixes. + +Bugfixes +-------- + +- Add Octavia job for testing Load Balancer +- Add binding profile to port module +- Add execution environment metadata +- Fix CI for latest ansible-test with no_log +- Fix issues with newest ansible-test 2.11 +- Prepare for Ansible 2.11 tests +- add option to exclude legacy groups +- security_group_rule add support ipv6-icmp + +New Modules +----------- + +- openstack.cloud.object_container - Manage Swift container + +v1.3.0 +====== + +Release Summary +--------------- + +New modules and bugfixes. + +Minor Changes +------------- + +- Fix some typos in readme +- Guidelines Fix links and formatting +- baremetal_node - Add support for new features +- baremetal_node - ironic deprecate sub-options of driver_info +- baremetal_node - ironic stop putting meaningless values to properties +- image_info - Migrating image_info module from AnsibleModule to OpenStackModule +- recordset - Update recordset docu +- server - Allow description field to be set with os_server +- server_action - Added shelve and unshelve as new server actions + +Bugfixes +-------- + +- port - Fixed check for None in os_port +- project - Fix setting custom property on os_project +- security_group_rule - Remove protocols choice in security rules +- volume_info - Fix volume_info result for SDK < 0.19 + +New Modules +----------- + +- openstack.cloud.identity_role_info - Retrieve information about Openstack Identity roles. +- openstack.cloud.keypair_info - Retrieve information about Openstack key pairs. +- openstack.cloud.security_group_info - Retrieve information about Openstack Security Groups. +- openstack.cloud.security_group_rule_info - Retrieve information about Openstack Security Group rules. +- openstack.cloud.stack_info - Retrieve information about Openstack Heat stacks. + +v1.2.1 +====== + +Release Summary +--------------- + +Porting modules to new OpenstackModule class and fixes. + +Minor Changes +------------- + +- dns_zone - Migrating dns_zone from AnsibleModule to OpenStackModule +- dns_zone, recordset - Enable update for recordset and add tests for dns and recordset module +- endpoint - Do not fail when endpoint state is absent +- ironic - Refactor ironic authentication into a new module_utils module +- loadbalancer - Refactor loadbalancer module +- network - Migrating network from AnsibleModule to OpenStackModule +- networks_info - Migrating networks_info from AnsibleModule to OpenStackModule +- openstack - Add galaxy.yml to support install from git +- openstack - Fix docs-args mismatch in modules +- openstack - OpenStackModule Support defining a minimum version of the SDK +- router - Migrating routers from AnsibleModule to OpenStackModule +- routers_info - Added deprecated_names for router_info module +- routers_info - Migrating routers_info from AnsibleModule to OpenStackModule +- security_group.py - Migrating security_group from AnsibleModule to OpenStackModule +- security_group_rule - Refactor TCP/UDP port check +- server.py - Improve "server" module with OpenstackModule class +- server_volume - Migrating server_volume from AnsibleModule to OpenStackModule +- subnet - Fix subnets update and idempotency +- subnet - Migrating subnet module from AnsibleModule to OpenStackModule +- subnets_info - Migrating subnets_info from AnsibleModule to OpenStackModule +- volume.py - Migrating volume from AnsibleModule to OpenStackModule +- volume_info - Fix volume_info arguments for SDK 0.19 + +v1.2.0 +====== + +Release Summary +--------------- + +New volume backup modules. + +Minor Changes +------------- + +- lb_health_monitor - Make it possible to create a health monitor to a pool + +New Modules +----------- + +- openstack.cloud.volume_backup module - Add/Delete Openstack volumes backup. +- openstack.cloud.volume_backup_info module - Retrieve information about Openstack volume backups. +- openstack.cloud.volume_snapshot_info module - Retrieve information about Openstack volume snapshots. + +v1.1.0 +====== + +Release Summary +--------------- + +Starting redesign modules and bugfixes. + +Minor Changes +------------- + +- A basic module subclass was introduced and a few modules moved to inherit from it. +- Add more useful information from exception +- Added pip installation option for collection. +- Added template for generation of artibtrary module. +- baremetal modules - Do not require ironic_url if cloud or auth.endpoint is provided +- inventory_openstack - Add openstack logger and Ansible display utility +- loadbalancer - Add support for setting the Flavor when creating a load balancer + +Bugfixes +-------- + +- Fix non existing attribuites in SDK exception +- security_group_rule - Don't pass tenant_id for remote group + +New Modules +----------- + +- openstack.cloud.volume_info - Retrieve information about Openstack volumes. + +v1.0.1 +====== + +Release Summary +--------------- + +Bugfix for server_info + +Bugfixes +-------- + +- server_info - Fix broken server_info module and add tests + +v1.0.0 +====== + +Release Summary +--------------- + +Initial release of collection. + +Minor Changes +------------- + +- Renaming all modules and removing "os" prefix from names. +- baremetal_node_action - Support json type for the ironic_node config_drive parameter +- config - Update os_client_config to use openstacksdk +- host_aggregate - Add support for not 'purging' missing hosts +- project - Add properties for os_project +- server_action - pass imageRef to rebuild +- subnet - Updated allocation pool checks + +Bugfixes +-------- + +- baremetal_node - Correct parameter name +- coe_cluster - Retrive id/uuid correctly +- federation_mapping - Fixup some minor nits found in followup reviews +- inventory_openstack - Fix constructed compose +- network - Bump minimum openstacksdk version when using os_network/dns_domain +- role_assignment - Fix os_user_role for groups in multidomain context +- role_assignment - Fix os_user_role issue to grant a role in a domain + +New Modules +----------- + +- openstack.cloud.federation_idp - Add support for Keystone Identity Providers +- openstack.cloud.federation_idp_info - Add support for fetching the information about federation IDPs +- openstack.cloud.federation_mapping - Add support for Keystone mappings +- openstack.cloud.federation_mapping_info - Add support for fetching the information about Keystone mappings +- openstack.cloud.keystone_federation_protocol - Add support for Keystone federation Protocols +- openstack.cloud.keystone_federation_protocol_info - Add support for getting information about Keystone federation Protocols +- openstack.cloud.routers_info - Retrieve information about one or more OpenStack routers. diff --git a/ansible_collections/openstack/cloud/CONTRIBUTING.rst b/ansible_collections/openstack/cloud/CONTRIBUTING.rst new file mode 100644 index 00000000..cf632ce3 --- /dev/null +++ b/ansible_collections/openstack/cloud/CONTRIBUTING.rst @@ -0,0 +1,40 @@ +.. _contributing: + +============================================= +Contributing to ansible-collections-openstack +============================================= + +If you're interested in contributing to the ansible-collections-openstack project, +the following will help get you started. + +Developer Workflow +------------------ + +OpenStack uses OpenDev for it's development, and patches are submitted to +`OpenDev Gerrit`_. Please read `DeveloperWorkflow`_ before sending your +first patch for review. + +Pull requests submitted through GitHub will be ignored. + +.. seealso:: + + * https://wiki.openstack.org/wiki/How_To_Contribute + * https://wiki.openstack.org/wiki/CLA + +.. _OpenDev Gerrit: https://review.opendev.org/ +.. _DeveloperWorkflow: https://docs.openstack.org/infra/manual/developers.html#development-workflow + +Project Hosting Details +----------------------- + +Bug tracker + https://storyboard.openstack.org/#!/project/openstack/ansible-collections-openstack + +Mailing list (prefix subjects with ``[ansible]`` for faster responses) + http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss + +Code Hosting + https://opendev.org/openstack/ansible-collections-openstack + +Code Review + https://review.opendev.org/#/q/status:open+project:openstack/ansible-collections-openstack,n,z diff --git a/ansible_collections/openstack/cloud/COPYING b/ansible_collections/openstack/cloud/COPYING new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/ansible_collections/openstack/cloud/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/ansible_collections/openstack/cloud/FILES.json b/ansible_collections/openstack/cloud/FILES.json new file mode 100644 index 00000000..15234f6a --- /dev/null +++ b/ansible_collections/openstack/cloud/FILES.json @@ -0,0 +1,1216 @@ +{ + "files": [ + { + "name": ".", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "scripts", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "scripts/inventory", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "scripts/inventory/openstack.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9d2659ec793a078ba38a521ea59eb2c281193c9bde5002da230c76381f71e95d", + "format": 1 + }, + { + "name": "scripts/inventory/openstack_inventory.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "99e4ff9d5016c4410b3a2f0558bfabdaa2ec4b1b01a354b59c6b0277e4deaceb", + "format": 1 + }, + { + "name": "requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "deeb6c325faa9932a41908dd06468762b1c2a4d24d471b24e91f02ee3eae9363", + "format": 1 + }, + { + "name": "docs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/openstack_guidelines.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3f010ee14ee1a28bf27c4f041054282052d5bd57e08faef0a63af7960030f714", + "format": 1 + }, + { + "name": "setup.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7478e980356fe29366647e7c7d6687b7da68d1ea763d4ca865a75ca9498db1c2", + "format": 1 + }, + { + "name": "COPYING", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903", + "format": 1 + }, + { + "name": "CONTRIBUTING.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c5017ba9aff6506564036e816e8bbb7d8d7c9e8acd4a94ffce3c269c51b96ee1", + "format": 1 + }, + { + "name": "CHANGELOG.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8de9b2e83ca22ed1b3b97b5103f3396d934f04e34511e436cf6ade954e444506", + "format": 1 + }, + { + "name": "meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "meta/runtime.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2aeded4e76257888e106bdd3fec30ce7eff64dd44b58eb576122f6045491e530", + "format": 1 + }, + { + "name": "README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f8c2f9a2fece180fd309842b8d8926b0e2e9369149711f0582e71606b7191db6", + "format": 1 + }, + { + "name": "plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/doc_fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/doc_fragments/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "plugins/doc_fragments/openstack.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4911368e21c53178b13c45256e5134188b6ed45a4e4620b711534f3bc6405d1d", + "format": 1 + }, + { + "name": "plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "plugins/module_utils/ironic.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c639b9bcd7ceb1f0b0fb8116cb7d7db33ceb2ee957f3b56d173d9146c845b38f", + "format": 1 + }, + { + "name": "plugins/module_utils/openstack.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f4ac8ccab9e5f6cffc972d895b380952e8dffb457f4fb84b175eba5226f65b63", + "format": 1 + }, + { + "name": "plugins/inventory", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/inventory/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "plugins/inventory/openstack.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "57e8deb888d12c4f4d5da23f6bfb9381373a7637fb67fc42a9be9e4c3b9f0903", + "format": 1 + }, + { + "name": "plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/modules/os_flavor_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bdc398e7e8a4eb653d4cd8dee462cfd3b9df04c5d81f25cb95a232ac1336470c", + "format": 1 + }, + { + "name": "plugins/modules/os_project_access.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4a2c872a3cdac209eed029402650a31da1ced47621f866c792b1af930774b160", + "format": 1 + }, + { + "name": "plugins/modules/os_security_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "754dbf89abfcad4a7a7b9487a19123910f8ea5fde2677d1c209f0a90b0a418b7", + "format": 1 + }, + { + "name": "plugins/modules/object_container.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9bca51b4da9ccefff04fa55f8999ca60f8fdf9aae565be0fa70bef9b08962032", + "format": 1 + }, + { + "name": "plugins/modules/os_keypair.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "83b0c1c2e8be79e9f4c665b1715619ee6fa09d032bbf227e5bae9f8492fc4eec", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_federation_protocol.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2de6941c90bae6b288ba1f11fe5cd85f34441b487d2256c6f7fd5325cc7e4f0b", + "format": 1 + }, + { + "name": "plugins/modules/os_routers_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4930f4982ddbe6f823d5266e443e000c36e16adb59d511b091182f0be58e8d7e", + "format": 1 + }, + { + "name": "plugins/modules/neutron_rbac_policy.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c9634c6e8336bc13766bcf54dd5b8aeb5bc6d3d4f1bb2653cf56c50ddbd318a1", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_identity_provider_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c44bc1d617c4780fb5d45ed43d937ddce5140b01b10c0c769c85a3bfa55e9b72", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_role.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "48db5ab304c1f74dd891c68ca4934788dfc84734135637dc93821bbee1f1f747", + "format": 1 + }, + { + "name": "plugins/modules/volume_snapshot.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e34df3ee0c37bb951eff1623a6d9f1faf974846ff7c4a82411907e2c57156727", + "format": 1 + }, + { + "name": "plugins/modules/quota.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "be2e920833e045fe816b233888b8d78f9612ddbf963e7366e8d58790dae87c96", + "format": 1 + }, + { + "name": "plugins/modules/os_nova_flavor.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7d1d887e09cae6cfa4b2dcc913d496fbacc0f01b3f1ef6f6d71319798032f3f0", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_service.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0a3c446e8f01dfb3b071aa3dbd27baf27faf882009f4c7594ae18cd43f30eb68", + "format": 1 + }, + { + "name": "plugins/modules/identity_domain.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c4b16be01667ff78a36a99bc41b97a769a4600e330c1deeb4bd139443033691", + "format": 1 + }, + { + "name": "plugins/modules/os_volume.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e9973db07f021b2af58f3911790697fd220955b3ca8e03492ffcda1b4f7b7f19", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_endpoint.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c550b7ed488bdbf198238436c2f8fbd365e729a94da3cb56d28dfc63fb9a9417", + "format": 1 + }, + { + "name": "plugins/modules/object.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c4c17b189f9f81865a9d95efe200756632f8c43a1cf66db39408e3bc5684f909", + "format": 1 + }, + { + "name": "plugins/modules/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "plugins/modules/security_group_rule.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3d9230992be555a34e15a589e63d4df7c1901dbf2a8926126965ca828189ba65", + "format": 1 + }, + { + "name": "plugins/modules/role_assignment.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "75f260a78e46b8e5635ebe2302862558beb6ffe98cf372041d2311efd11a61a3", + "format": 1 + }, + { + "name": "plugins/modules/stack_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "33847b5635e14e2234e526d1f3829a4161cc8756a53e1746662ebc77a3b57ea2", + "format": 1 + }, + { + "name": "plugins/modules/os_auth.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ab740bc0c9a9944e4a573e867b2dc10a8539e2b4bfe8b0a5d3e3ea47f4c74f18", + "format": 1 + }, + { + "name": "plugins/modules/identity_group_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fc02b94aa894a64913d29222f90a13d17ef46e5537e0bac70813133f0e111c55", + "format": 1 + }, + { + "name": "plugins/modules/os_image.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e2b874819fa5022396d165d07b96b2cf4d7ead88efec899f6d260936aae2bbf0", + "format": 1 + }, + { + "name": "plugins/modules/server_action.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f7195b495cb1d734003383d0ab355dd79ae1e7ac7a19719e0e44ca52577bec0d", + "format": 1 + }, + { + "name": "plugins/modules/identity_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a8c24a4db33fd00809fa6f53be4fc8b74822fcc5d83a09fa31973306f576fc2a", + "format": 1 + }, + { + "name": "plugins/modules/volume.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e9973db07f021b2af58f3911790697fd220955b3ca8e03492ffcda1b4f7b7f19", + "format": 1 + }, + { + "name": "plugins/modules/image.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e2b874819fa5022396d165d07b96b2cf4d7ead88efec899f6d260936aae2bbf0", + "format": 1 + }, + { + "name": "plugins/modules/os_nova_host_aggregate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "404e5db01a7c1ffc69bbb21d09a96da253559968af4ec552520bc3cb5f56c827", + "format": 1 + }, + { + "name": "plugins/modules/network.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "60e4f2066700eb7070c73a6d53cf0adf62d0f81d2214e319d1796f63d03cd576", + "format": 1 + }, + { + "name": "plugins/modules/os_project_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b77706b7f9243c3a8e22ffa091dc47857d91b3aec0ef81843f1c062a0225e3bf", + "format": 1 + }, + { + "name": "plugins/modules/os_recordset.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8f2142288c16a584ce2bd4b4e5d76c1a593331c3a505371d2e41f8e7c7722838", + "format": 1 + }, + { + "name": "plugins/modules/identity_role.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "48db5ab304c1f74dd891c68ca4934788dfc84734135637dc93821bbee1f1f747", + "format": 1 + }, + { + "name": "plugins/modules/os_volume_snapshot.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e34df3ee0c37bb951eff1623a6d9f1faf974846ff7c4a82411907e2c57156727", + "format": 1 + }, + { + "name": "plugins/modules/coe_cluster_template.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "58c1323cac4f27ba42e5e59af64e3d06f47737e47f5f2c1bbc1ef483aa93a1cb", + "format": 1 + }, + { + "name": "plugins/modules/endpoint.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c550b7ed488bdbf198238436c2f8fbd365e729a94da3cb56d28dfc63fb9a9417", + "format": 1 + }, + { + "name": "plugins/modules/keystone_federation_protocol.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2de6941c90bae6b288ba1f11fe5cd85f34441b487d2256c6f7fd5325cc7e4f0b", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_domain.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c4b16be01667ff78a36a99bc41b97a769a4600e330c1deeb4bd139443033691", + "format": 1 + }, + { + "name": "plugins/modules/os_ironic_node.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "af77101be82c61538b12f94e8ec21b55945fd0a68fa23db280a4bb5afba26c18", + "format": 1 + }, + { + "name": "plugins/modules/server_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5273ffa48ad2c9e6d9ac54b72d1379272b29339d336b2caf6cef27a8b416c870", + "format": 1 + }, + { + "name": "plugins/modules/os_stack.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3cf3d41172c98d6139cc28f6db50fb4026ca1ff47b144ee8f54ac3f3f13c24f0", + "format": 1 + }, + { + "name": "plugins/modules/loadbalancer.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "367e7e79be4296d54095d5c9f0b6814b3f2ddacd529aed86b572350cb7000fa3", + "format": 1 + }, + { + "name": "plugins/modules/os_ironic.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6b061ee236ec1cfcde3d261df46e20d5d27853f7a89b8386ff93b5a1c743f7b6", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_federation_protocol_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e069c7cfacae0add6d6045141729925555babf7cf6adbc0ec68b9ee5ab240773", + "format": 1 + }, + { + "name": "plugins/modules/dns_zone_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bcc162b7985aad2aa2c3dfe9fa0eff2494f7c1b66770e3a05da31443fe50944e", + "format": 1 + }, + { + "name": "plugins/modules/networks_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "09bd90ad0409668097c1389e259fd1e0b2c5f06d9ed36d7381d5edb1c6aaaf3b", + "format": 1 + }, + { + "name": "plugins/modules/project_access.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4a2c872a3cdac209eed029402650a31da1ced47621f866c792b1af930774b160", + "format": 1 + }, + { + "name": "plugins/modules/os_user_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "da27f9292c32782a778eb0da6881a34e02622bcd6ecafcde4337bab4f053b49c", + "format": 1 + }, + { + "name": "plugins/modules/baremetal_port_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e2829ed9ea2a9a4f05b333d2a97485fc9fe496736624093ecf3d25d6b2a30668", + "format": 1 + }, + { + "name": "plugins/modules/os_group_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fc02b94aa894a64913d29222f90a13d17ef46e5537e0bac70813133f0e111c55", + "format": 1 + }, + { + "name": "plugins/modules/os_user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d0723c67688a88c69ce248f44ef3fc192e4eaeab81d09738fb82ec8e6dc5b147", + "format": 1 + }, + { + "name": "plugins/modules/identity_user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d0723c67688a88c69ce248f44ef3fc192e4eaeab81d09738fb82ec8e6dc5b147", + "format": 1 + }, + { + "name": "plugins/modules/os_member.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adad29807477d5cfe6dde616ec978f71bee30c1bdf3af52c5151a24aee4dc1b7", + "format": 1 + }, + { + "name": "plugins/modules/identity_role_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6668597267f5fa47317032812a0c42cafe8767e63f30d880bbebec1ad0574799", + "format": 1 + }, + { + "name": "plugins/modules/lb_health_monitor.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e9e1e13be7e52b6a00826ce22c9e1e631ad63aa4ba138b3155bb59064136fd03", + "format": 1 + }, + { + "name": "plugins/modules/os_port.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c6215bcaa7c1b61a5dbcd698acb32cd5ba0afd2ae3e3c9d1b6b9e651f51d23ad", + "format": 1 + }, + { + "name": "plugins/modules/identity_user_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "da27f9292c32782a778eb0da6881a34e02622bcd6ecafcde4337bab4f053b49c", + "format": 1 + }, + { + "name": "plugins/modules/address_scope.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "71312db50316e108b4dd2e35c8a91ba2ae638112e9c472fc659c3aef62e6c3c6", + "format": 1 + }, + { + "name": "plugins/modules/dns_zone.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9dc86ee13a42726ead899f2ca54decfc9323ae4f2507df2b78c175e3c166d68f", + "format": 1 + }, + { + "name": "plugins/modules/os_subnets_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6b078e592ad361328d9ddf84a98c4066f88e59d9ce4a7bda25ccadd5830466fa", + "format": 1 + }, + { + "name": "plugins/modules/lb_listener.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d3a00bfefc05b0b03b2a91ce04c03792ead2d823a5c83e487df3c8449ec48fa3", + "format": 1 + }, + { + "name": "plugins/modules/baremetal_node_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "58e6246299b21cd59482bbbae20c434ba348f6e6d0994a663b42966249268019", + "format": 1 + }, + { + "name": "plugins/modules/group_assignment.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e871d633590fbc8f686a084ce38a130c58cd1ac6dfd0cd151082655748de9f6", + "format": 1 + }, + { + "name": "plugins/modules/federation_mapping.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b3b701b5ad55032a6e104e445d8cfd781b7f809719aa2a142463c395bde9854f", + "format": 1 + }, + { + "name": "plugins/modules/keypair.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "83b0c1c2e8be79e9f4c665b1715619ee6fa09d032bbf227e5bae9f8492fc4eec", + "format": 1 + }, + { + "name": "plugins/modules/os_floating_ip.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a5f29ed923bb4aa438f6f2ed33d0668edca8b5c7de22388d34b2998bf8348bd9", + "format": 1 + }, + { + "name": "plugins/modules/security_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "754dbf89abfcad4a7a7b9487a19123910f8ea5fde2677d1c209f0a90b0a418b7", + "format": 1 + }, + { + "name": "plugins/modules/subnet.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a1fc690f2df7e249da41125c7d8194134f2e6f465b53d4974c5f479bcc6be848", + "format": 1 + }, + { + "name": "plugins/modules/os_server_action.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f7195b495cb1d734003383d0ab355dd79ae1e7ac7a19719e0e44ca52577bec0d", + "format": 1 + }, + { + "name": "plugins/modules/federation_idp_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c44bc1d617c4780fb5d45ed43d937ddce5140b01b10c0c769c85a3bfa55e9b72", + "format": 1 + }, + { + "name": "plugins/modules/coe_cluster.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ddfd58e836da83c52bfa09b5c8199b386bcdebe57d4a9108d094f278e9304073", + "format": 1 + }, + { + "name": "plugins/modules/os_client_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e0e3782be88d4734a2207cb3047a97ccbb9d1cd8e21d9eae845a667e7731ec8", + "format": 1 + }, + { + "name": "plugins/modules/os_image_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "db4db547e390ab5632e9741f88d1caec377122a318e006d4513a5c73b27a311b", + "format": 1 + }, + { + "name": "plugins/modules/os_server.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bb975f024413bc4de5d0c57f8455f5d6460b7131e850e4a4b5acd583135ca5ac", + "format": 1 + }, + { + "name": "plugins/modules/os_network.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "60e4f2066700eb7070c73a6d53cf0adf62d0f81d2214e319d1796f63d03cd576", + "format": 1 + }, + { + "name": "plugins/modules/os_server_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4d0f68578f8146796d6399b96788c230a4577dfeb247f4e84874abb5d7a7938", + "format": 1 + }, + { + "name": "plugins/modules/catalog_service.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0a3c446e8f01dfb3b071aa3dbd27baf27faf882009f4c7594ae18cd43f30eb68", + "format": 1 + }, + { + "name": "plugins/modules/os_zone.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9dc86ee13a42726ead899f2ca54decfc9323ae4f2507df2b78c175e3c166d68f", + "format": 1 + }, + { + "name": "plugins/modules/lb_pool.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c512b83408267d8411649149c79f106137f9297cb15dfe18e5964af2424385d", + "format": 1 + }, + { + "name": "plugins/modules/volume_snapshot_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "58ebc78344140e5336a41295fcd782a6f562894eee1d9d159a466737f1d6ddc1", + "format": 1 + }, + { + "name": "plugins/modules/lb_member.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adad29807477d5cfe6dde616ec978f71bee30c1bdf3af52c5151a24aee4dc1b7", + "format": 1 + }, + { + "name": "plugins/modules/os_project.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "660fbcce9d3ee244be7cde0f5dac04448660525df2750b7a4b0a025924f6c859", + "format": 1 + }, + { + "name": "plugins/modules/compute_flavor_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bdc398e7e8a4eb653d4cd8dee462cfd3b9df04c5d81f25cb95a232ac1336470c", + "format": 1 + }, + { + "name": "plugins/modules/container.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9bca51b4da9ccefff04fa55f8999ca60f8fdf9aae565be0fa70bef9b08962032", + "format": 1 + }, + { + "name": "plugins/modules/os_listener.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d3a00bfefc05b0b03b2a91ce04c03792ead2d823a5c83e487df3c8449ec48fa3", + "format": 1 + }, + { + "name": "plugins/modules/volume_backup.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "01f6678f0c0abebf952b14891074b9f043970bd88e17d984847a0a19ae94d7b8", + "format": 1 + }, + { + "name": "plugins/modules/federation_mapping_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "700e84e9ef15cb3cd728e30e73a9cf996af0bf0053272591807459b283f750d1", + "format": 1 + }, + { + "name": "plugins/modules/volume_backup_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b05c61791bdbeeff7560eebd50babc4b20c0e6737acea040bc06c9aedbf7820d", + "format": 1 + }, + { + "name": "plugins/modules/os_coe_cluster_template.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "58c1323cac4f27ba42e5e59af64e3d06f47737e47f5f2c1bbc1ef483aa93a1cb", + "format": 1 + }, + { + "name": "plugins/modules/baremetal_port.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8456b8b728694c510138c48f618f487922b97a4dbe4f983c74675d9cfe47e516", + "format": 1 + }, + { + "name": "plugins/modules/neutron_rbac_policies_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "88a186ca5deb3454ebd414c7cb083446eef0191065ae8f7072990c467fc6f4a2", + "format": 1 + }, + { + "name": "plugins/modules/os_user_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e871d633590fbc8f686a084ce38a130c58cd1ac6dfd0cd151082655748de9f6", + "format": 1 + }, + { + "name": "plugins/modules/security_group_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e489f79e6951049d88e64960506f10eabe8da1179ba23227dfdffc2d11f34df9", + "format": 1 + }, + { + "name": "plugins/modules/server_metadata.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a1a2b905c8255917e024ece1e639f6c1218a73af5f523d2011e91f4a2bcb3bbe", + "format": 1 + }, + { + "name": "plugins/modules/server_volume.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bf7ec5168e5bcdbec0f738815b46da9c429d644fed768284312af7a6e5822d99", + "format": 1 + }, + { + "name": "plugins/modules/config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e0e3782be88d4734a2207cb3047a97ccbb9d1cd8e21d9eae845a667e7731ec8", + "format": 1 + }, + { + "name": "plugins/modules/federation_idp.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "68de32545531603a81fec1152aea68b39a481c9b65dd0f6a1b78b04b5a3dee49", + "format": 1 + }, + { + "name": "plugins/modules/compute_service_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b84427fb13bcce805d0841462699a9a8f6709b96a68da61ef2f309625102714d", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_mapping_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "700e84e9ef15cb3cd728e30e73a9cf996af0bf0053272591807459b283f750d1", + "format": 1 + }, + { + "name": "plugins/modules/identity_domain_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4f4ac14078e948505b4c14a1a17916264f80a68f0149bfd116c284db472caf7", + "format": 1 + }, + { + "name": "plugins/modules/port.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c6215bcaa7c1b61a5dbcd698acb32cd5ba0afd2ae3e3c9d1b6b9e651f51d23ad", + "format": 1 + }, + { + "name": "plugins/modules/project.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "660fbcce9d3ee244be7cde0f5dac04448660525df2750b7a4b0a025924f6c859", + "format": 1 + }, + { + "name": "plugins/modules/os_server_metadata.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a1a2b905c8255917e024ece1e639f6c1218a73af5f523d2011e91f4a2bcb3bbe", + "format": 1 + }, + { + "name": "plugins/modules/routers_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4930f4982ddbe6f823d5266e443e000c36e16adb59d511b091182f0be58e8d7e", + "format": 1 + }, + { + "name": "plugins/modules/os_port_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d2c81ab4cc3ca798c3fcf039dff0c5dbf54c9b5db2da43af839e0a7dbc9f48c1", + "format": 1 + }, + { + "name": "plugins/modules/baremetal_node_action.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "af77101be82c61538b12f94e8ec21b55945fd0a68fa23db280a4bb5afba26c18", + "format": 1 + }, + { + "name": "plugins/modules/subnets_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6b078e592ad361328d9ddf84a98c4066f88e59d9ce4a7bda25ccadd5830466fa", + "format": 1 + }, + { + "name": "plugins/modules/server.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bb975f024413bc4de5d0c57f8455f5d6460b7131e850e4a4b5acd583135ca5ac", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_domain_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4f4ac14078e948505b4c14a1a17916264f80a68f0149bfd116c284db472caf7", + "format": 1 + }, + { + "name": "plugins/modules/security_group_rule_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "28903c09f07bb03948bc07c8f419d94adb73adfad780ae96a1b45d7ef4cc3732", + "format": 1 + }, + { + "name": "plugins/modules/project_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b77706b7f9243c3a8e22ffa091dc47857d91b3aec0ef81843f1c062a0225e3bf", + "format": 1 + }, + { + "name": "plugins/modules/volume_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6166466c8cd8989261f40c6928c9e4c1dd62a3749ca10e5bc16f8c633b009db6", + "format": 1 + }, + { + "name": "plugins/modules/image_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "db4db547e390ab5632e9741f88d1caec377122a318e006d4513a5c73b27a311b", + "format": 1 + }, + { + "name": "plugins/modules/os_server_volume.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bf7ec5168e5bcdbec0f738815b46da9c429d644fed768284312af7a6e5822d99", + "format": 1 + }, + { + "name": "plugins/modules/os_pool.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c512b83408267d8411649149c79f106137f9297cb15dfe18e5964af2424385d", + "format": 1 + }, + { + "name": "plugins/modules/os_ironic_inspect.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ff83c3b479131331f3af9366d00ef47328faf1050fb7be44ccb18f9dd02e9f1b", + "format": 1 + }, + { + "name": "plugins/modules/floating_ip.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a5f29ed923bb4aa438f6f2ed33d0668edca8b5c7de22388d34b2998bf8348bd9", + "format": 1 + }, + { + "name": "plugins/modules/subnet_pool.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f58ea2c0d44c77b0b5f6c540cd0294a1b32d3c354d4ab859dbf8c778039735b", + "format": 1 + }, + { + "name": "plugins/modules/os_router.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "caccea04d162cdedfc635ac1e702529af4fbaca498dc31f6da14005e1c773584", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_mapping.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b3b701b5ad55032a6e104e445d8cfd781b7f809719aa2a142463c395bde9854f", + "format": 1 + }, + { + "name": "plugins/modules/recordset.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8f2142288c16a584ce2bd4b4e5d76c1a593331c3a505371d2e41f8e7c7722838", + "format": 1 + }, + { + "name": "plugins/modules/server_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4d0f68578f8146796d6399b96788c230a4577dfeb247f4e84874abb5d7a7938", + "format": 1 + }, + { + "name": "plugins/modules/compute_flavor.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7d1d887e09cae6cfa4b2dcc913d496fbacc0f01b3f1ef6f6d71319798032f3f0", + "format": 1 + }, + { + "name": "plugins/modules/baremetal_node.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6b061ee236ec1cfcde3d261df46e20d5d27853f7a89b8386ff93b5a1c743f7b6", + "format": 1 + }, + { + "name": "plugins/modules/os_subnet.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a1fc690f2df7e249da41125c7d8194134f2e6f465b53d4974c5f479bcc6be848", + "format": 1 + }, + { + "name": "plugins/modules/os_networks_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "09bd90ad0409668097c1389e259fd1e0b2c5f06d9ed36d7381d5edb1c6aaaf3b", + "format": 1 + }, + { + "name": "plugins/modules/router.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "caccea04d162cdedfc635ac1e702529af4fbaca498dc31f6da14005e1c773584", + "format": 1 + }, + { + "name": "plugins/modules/keystone_federation_protocol_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e069c7cfacae0add6d6045141729925555babf7cf6adbc0ec68b9ee5ab240773", + "format": 1 + }, + { + "name": "plugins/modules/os_user_role.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "75f260a78e46b8e5635ebe2302862558beb6ffe98cf372041d2311efd11a61a3", + "format": 1 + }, + { + "name": "plugins/modules/keypair_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a0cc8acbe1f58ee675d72fdd92332a881233ae71d320468dd409080889a56223", + "format": 1 + }, + { + "name": "plugins/modules/auth.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ab740bc0c9a9944e4a573e867b2dc10a8539e2b4bfe8b0a5d3e3ea47f4c74f18", + "format": 1 + }, + { + "name": "plugins/modules/os_server_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5273ffa48ad2c9e6d9ac54b72d1379272b29339d336b2caf6cef27a8b416c870", + "format": 1 + }, + { + "name": "plugins/modules/os_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a8c24a4db33fd00809fa6f53be4fc8b74822fcc5d83a09fa31973306f576fc2a", + "format": 1 + }, + { + "name": "plugins/modules/os_loadbalancer.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "367e7e79be4296d54095d5c9f0b6814b3f2ddacd529aed86b572350cb7000fa3", + "format": 1 + }, + { + "name": "plugins/modules/os_coe_cluster.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ddfd58e836da83c52bfa09b5c8199b386bcdebe57d4a9108d094f278e9304073", + "format": 1 + }, + { + "name": "plugins/modules/os_object.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c4c17b189f9f81865a9d95efe200756632f8c43a1cf66db39408e3bc5684f909", + "format": 1 + }, + { + "name": "plugins/modules/floating_ip_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b867eca5a2ed722c00be2d086d3575604c293b549aa2b1ed9e83c6e45c15d9d4", + "format": 1 + }, + { + "name": "plugins/modules/os_security_group_rule.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3d9230992be555a34e15a589e63d4df7c1901dbf2a8926126965ca828189ba65", + "format": 1 + }, + { + "name": "plugins/modules/stack.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3cf3d41172c98d6139cc28f6db50fb4026ca1ff47b144ee8f54ac3f3f13c24f0", + "format": 1 + }, + { + "name": "plugins/modules/port_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d2c81ab4cc3ca798c3fcf039dff0c5dbf54c9b5db2da43af839e0a7dbc9f48c1", + "format": 1 + }, + { + "name": "plugins/modules/host_aggregate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "404e5db01a7c1ffc69bbb21d09a96da253559968af4ec552520bc3cb5f56c827", + "format": 1 + }, + { + "name": "plugins/modules/baremetal_inspect.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ff83c3b479131331f3af9366d00ef47328faf1050fb7be44ccb18f9dd02e9f1b", + "format": 1 + }, + { + "name": "plugins/modules/os_keystone_identity_provider.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "68de32545531603a81fec1152aea68b39a481c9b65dd0f6a1b78b04b5a3dee49", + "format": 1 + }, + { + "name": "plugins/modules/os_quota.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "be2e920833e045fe816b233888b8d78f9612ddbf963e7366e8d58790dae87c96", + "format": 1 + }, + { + "name": "bindep.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "955ed5d9eb93b169f416a9532f5b39a7551d2ec74f6841b8b819c50f135f39e8", + "format": 1 + } + ], + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/openstack/cloud/MANIFEST.json b/ansible_collections/openstack/cloud/MANIFEST.json new file mode 100644 index 00000000..4f726d8c --- /dev/null +++ b/ansible_collections/openstack/cloud/MANIFEST.json @@ -0,0 +1,33 @@ +{ + "collection_info": { + "namespace": "openstack", + "name": "cloud", + "version": "1.10.0", + "authors": [ + "Openstack" + ], + "readme": "README.md", + "tags": [ + "cloud", + "openstack" + ], + "description": "Openstack Ansible modules", + "license": [ + "GPL-3.0-or-later" + ], + "license_file": null, + "dependencies": {}, + "repository": "https://opendev.org/openstack/ansible-collections-openstack", + "documentation": "https://docs.ansible.com/ansible/latest/collections/openstack/cloud/index.html", + "homepage": "https://opendev.org/openstack/ansible-collections-openstack", + "issues": "https://storyboard.openstack.org/#!/project/openstack/ansible-collections-openstack" + }, + "file_manifest_file": { + "name": "FILES.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "35409e9382d24dca7a0a4982b613e6526f68e2dd2036c9a9dbb74dece1f7ab7a", + "format": 1 + }, + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/openstack/cloud/README.md b/ansible_collections/openstack/cloud/README.md new file mode 100644 index 00000000..5dd8c9d0 --- /dev/null +++ b/ansible_collections/openstack/cloud/README.md @@ -0,0 +1,164 @@ +[![OpenDev Zuul Builds - Ansible Collection OpenStack](https://zuul-ci.org/gated.svg)](http://zuul.opendev.org/t/openstack/builds?project=openstack%2Fansible-collections-openstack#) + +# Ansible Collection: openstack.cloud + +This repo hosts the `openstack.cloud` Ansible Collection. + +The collection includes the Openstack modules and plugins supported by Openstack community to help the management of Openstack infrastructure. + +## Breaking backward compatibility :warning: + +Dear contributors and users of the Ansible OpenStack collection! +Our codebase has been split into two separate release series: + +* `2.x.x` releases of Ansible OpenStack collection are compatible with OpenStack SDK `1.x.x` and its release candidates + `0.99.x` *only* (OpenStack Zed and later). Our `master` branch tracks our `2.x.x` releases. +* `1.x.x` releases of Ansible OpenStack collection are compatible with OpenStack SDK `0.x.x` prior to `0.99.0` *only* + (OpenStack Yoga and earlier). Our `stable/1.0.0` branch tracks our `1.x.x` releases. + +Both branches will be developed in parallel for the time being. Patches from `master` will be backported to +`stable/1.0.0` on a best effort basis but expect new features to be introduced in our `master` branch only. +Contributions are welcome for both branches! +Differences between both branches are mainly renamed and sometimes dropped module return values. We try to keep our +module parameters backward compatible by offering aliases but e.g. the semantics of `filters` parameters in `*_info` +modules have changed due to updates in the OpenStack SDK. + +Our decision to break backward compatibility was not taken lightly. OpenStack SDK's first major release (`1.0.0` and its +release candidates `0.99.x`) has streamlined and improved large parts of its codebase. For example, its Connection +interface now consistently uses the Resource interfaces under the hood. This required breaking changes from older SDK +releases though. The Ansible OpenStack collection is heavily based on OpenStack SDK. With OpenStack SDK becoming +backward incompatible, so does our Ansible OpenStack collection. We simply lack the devpower to maintain a backward +compatible interface in Ansible OpenStack collection across several SDK releases. + +Our first `2.0.0` release is currently under development and we still have a long way to go. If you use modules of the +Ansible OpenStack collection and want to join us in porting them to the upcoming OpenStack SDK, please contact us! +Ping Jakob Meng <mail@jakobmeng.de> (jm1) or Rafael Castillo <rcastill@redhat.com> (rcastillo) and we will give you a +quick introduction. We are also hanging around on `irc.oftc.net/#openstack-ansible-sig` and `irc.oftc.net/#oooq` 😎 + +We have extensive documentation on [why, what and how we are adopting and reviewing the new modules]( +https://hackmd.io/szgyWa5qSUOWw3JJBXLmOQ?view), [how to set up a working DevStack environment for hacking on the +collection](https://hackmd.io/PI10x-iCTBuO09duvpeWgQ?view) and, most importantly, [a list of modules where we are +coordinating our porting efforts](https://hackmd.io/7NtovjRkRn-tKraBXfz9jw?view). + +## Installation and Usage + +### Installing dependencies + +For using the Openstack Cloud collection firstly you need to install `ansible` and `openstacksdk` Python modules on your Ansible controller. +For example with pip: + +```bash +pip install "ansible>=2.9" "openstacksdk>=0.36,<0.99.0" +``` + +OpenStackSDK has to be available to Ansible and to the Python interpreter on the host, where Ansible executes the module (target host). +Please note, that under some circumstances Ansible might invoke a non-standard Python interpreter on the target host. +Using Python version 3 is highly recommended for OpenstackSDK and strongly required from OpenstackSDK version 0.39.0. + +--- + +#### NOTE + +OpenstackSDK is better to be the last stable version. It should NOT be installed on Openstack nodes, +but rather on operators host (aka "Ansible controller"). OpenstackSDK from last version supports +operations on all Openstack cloud versions. Therefore OpenstackSDK module version doesn't have to match +Openstack cloud version usually. + +--- + +### Installing the Collection from Ansible Galaxy + +Before using the Openstack Cloud collection, you need to install the collection with the `ansible-galaxy` CLI: + +`ansible-galaxy collection install openstack.cloud` + +You can also include it in a `requirements.yml` file and install it through `ansible-galaxy collection install -r requirements.yml` using the format: + +```yaml +collections: +- name: openstack.cloud +``` + +### Playbooks + +To use a module from the Openstack Cloud collection, please reference the full namespace, collection name, and module name that you want to use: + +```yaml +--- +- name: Using Openstack Cloud collection + hosts: localhost + tasks: + - openstack.cloud.server: + name: vm + state: present + cloud: openstack + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + boot_from_volume: True + volume_size: 75 +``` + +Or you can add the full namespace and collection name in the `collections` element: + +```yaml +--- +- name: Using Openstack Cloud collection + hosts: localhost + collections: + - openstack.cloud + tasks: + - server_volume: + state: present + cloud: openstack + server: Mysql-server + volume: mysql-data + device: /dev/vdb +``` + +### Usage + +See the collection docs at Ansible site: + +* [openstack.cloud collection docs (version released in Ansible package)](https://docs.ansible.com/ansible/latest/collections/openstack/cloud/index.html) + +* [openstack.cloud collection docs (devel version)](https://docs.ansible.com/ansible/devel/collections/openstack/cloud/index.html) + +## Contributing + +For information on contributing, please see [CONTRIBUTING](https://opendev.org/openstack/ansible-collections-openstack/src/branch/master/CONTRIBUTING.rst) + +There are many ways in which you can participate in the project, for example: + +- Submit [bugs and feature requests](https://storyboard.openstack.org/#!/project/openstack/ansible-collections-openstack), and help us verify them +- Submit and review source code changes in [Openstack Gerrit](https://review.opendev.org/#/q/project:openstack/ansible-collections-openstack) +- Add new modules for Openstack Cloud + +We work with [OpenDev Gerrit](https://review.opendev.org/), pull requests submitted through GitHub will be ignored. + +## Testing and Development + +If you want to develop new content for this collection or improve what is already here, the easiest way to work on the collection is to clone it into one of the configured [`COLLECTIONS_PATHS`](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths), and work on it there. + +### Testing with `ansible-test` + +We use `ansible-test` for sanity: + +```bash +tox -e linters +``` + +## More Information + +TBD + +## Communication + +We have a dedicated Interest Group for Openstack Ansible modules. +You can find other people interested in this in `#openstack-ansible-sig` on [OFTC IRC](https://www.oftc.net/). + +## License + +GNU General Public License v3.0 or later + +See [LICENCE](https://opendev.org/openstack/ansible-collections-openstack/src/branch/master/COPYING) to see the full text. diff --git a/ansible_collections/openstack/cloud/bindep.txt b/ansible_collections/openstack/cloud/bindep.txt new file mode 100644 index 00000000..46039574 --- /dev/null +++ b/ansible_collections/openstack/cloud/bindep.txt @@ -0,0 +1,7 @@ +# This is a cross-platform list tracking distribution packages needed by tests; +# see https://docs.openstack.org/infra/bindep/ for additional information. + +gcc [compile platform:centos-8 platform:rhel-8] +python38-cryptography [platform:centos-8 platform:rhel-8] +python38-devel [compile platform:centos-8 platform:rhel-8] +python38-requests [platform:centos-8 platform:rhel-8] diff --git a/ansible_collections/openstack/cloud/docs/openstack_guidelines.rst b/ansible_collections/openstack/cloud/docs/openstack_guidelines.rst new file mode 100644 index 00000000..8da91a4c --- /dev/null +++ b/ansible_collections/openstack/cloud/docs/openstack_guidelines.rst @@ -0,0 +1,68 @@ +.. _OpenStack_module_development: + +OpenStack Ansible Modules +========================= + +These are a set of modules for interacting with the OpenStack API as either an admin +or an end user. + +.. contents:: + :local: + +Naming +------ + +* This is a collection named ``openstack.cloud``. There is no need for further namespace prefixing. +* Name any module that a cloud consumer would expect to use after the logical resource it manages: + ``server`` not ``nova``. This naming convention acknowledges that the end user does not care + which service manages the resource - that is a deployment detail. For example cloud consumers may + not know whether their floating IPs are managed by Nova or Neutron. + +Interface +--------- + +* If the resource being managed has an id, it should be returned. +* If the resource being managed has an associated object more complex than + an id, it should also be returned. +* Return format shall be a dictionary or list + +Interoperability +---------------- + +* It should be assumed that the cloud consumer does not know + details about the deployment choices their cloud provider made. A best + effort should be made to present one sane interface to the Ansible user + regardless of deployer choices. +* It should be assumed that a user may have more than one cloud account that + they wish to combine as part of a single Ansible-managed infrastructure. +* All modules should work appropriately against all existing versions of + OpenStack regardless of upstream EOL status. The reason for this is that + the Ansible modules are for consumers of cloud APIs who are not in a + position to impact what version of OpenStack their cloud provider is + running. It is known that there are OpenStack Public Clouds running rather + old versions of OpenStack, but from a user point of view the Ansible + modules can still support these users without impacting use of more + modern versions. + +Libraries +--------- + +* All modules should use ``OpenStackModule`` from + ``ansible_collections.openstack.cloud.plugins.module_utils.openstack`` + as their base class. +* All modules should include ``extends_documentation_fragment: openstack``. +* All complex cloud interaction or interoperability code should be housed in + the `openstacksdk <https://opendev.org/openstack/openstacksdk>`_ + library. +* All OpenStack API interactions should happen via the openstackSDK and not via + OpenStack Client libraries. The OpenStack Client libraries do no have end + users as a primary audience, they are for intra-server communication. +* All modules should be registered in ``meta/action_groups.yml`` for enabling the + variables to be set in `group level + <https://docs.ansible.com/ansible/latest/user_guide/playbooks_module_defaults.html>`_. + +Testing +------- + +* Integration testing is currently done in `OpenStack's CI system + <https://opendev.org/openstack/ansible-collections-openstack/src/branch/master/.zuul.yaml>`_ diff --git a/ansible_collections/openstack/cloud/meta/runtime.yml b/ansible_collections/openstack/cloud/meta/runtime.yml new file mode 100644 index 00000000..29e358ed --- /dev/null +++ b/ansible_collections/openstack/cloud/meta/runtime.yml @@ -0,0 +1,613 @@ +requires_ansible: ">=2.8" +action_groups: + openstack: + - address_scope + - auth + - baremetal_inspect + - baremetal_inspect + - baremetal_node + - baremetal_node + - baremetal_node_action + - baremetal_node_action + - baremetal_node_info + - baremetal_port_info + - baremetal_port + - catalog_endpoint + - catalog_service + - catalog_service + - coe_cluster + - coe_cluster_template + - compute_flavor + - compute_flavor + - compute_flavor + - compute_flavor_info + - compute_flavor_info + - compute_service_info + - compute_service_info + - config + - config + - dns_zone + - dns_zone_info + - endpoint + - endpoint + - federation_idp + - federation_idp + - federation_idp_info + - federation_idp_info + - federation_mapping + - federation_mapping + - federation_mapping_info + - federation_mapping_info + - floating_ip + - floating_ip_info + - group_assignment + - group_assignment + - host_aggregate + - host_aggregate + - identity_domain + - identity_domain + - identity_domain_info + - identity_domain_info + - identity_group + - identity_group + - identity_group_info + - identity_group_info + - identity_role + - identity_role + - identity_user + - identity_user + - identity_user_info + - identity_user_info + - image + - image_info + - keypair + - keypair_info + - keystone_federation_protocol + - keystone_federation_protocol_info + - lb_listener + - lb_listener + - lb_member + - lb_member + - lb_pool + - lb_pool + - loadbalancer + - network + - networks_info + - object + - object_container + - port + - port_info + - project + - project_access + - project_info + - quota + - recordset + - role_assignment + - role_assignment + - router + - routers_info + - security_group + - security_group_info + - security_group_rule + - security_group_rule_info + - server + - server_action + - server_group + - server_info + - server_metadata + - server_volume + - stack + - subnet + - subnets_info + - subnet_pool + - volume + - volume_backup + - volume_backup_info + - volume_info + - volume_snapshot + - volume_snapshot_info + os: + - auth + - baremetal_inspect + - baremetal_inspect + - baremetal_node + - baremetal_node + - baremetal_node_action + - baremetal_node_action + - catalog_endpoint + - catalog_service + - catalog_service + - coe_cluster + - coe_cluster_template + - compute_flavor + - compute_flavor + - compute_flavor + - compute_flavor_info + - compute_flavor_info + - config + - config + - dns_zone + - dns_zone + - endpoint + - endpoint + - federation_idp + - federation_idp + - federation_idp_info + - federation_idp_info + - federation_mapping + - federation_mapping + - federation_mapping_info + - federation_mapping_info + - floating_ip + - group_assignment + - group_assignment + - host_aggregate + - host_aggregate + - identity_domain + - identity_domain + - identity_domain_info + - identity_domain_info + - identity_group + - identity_group + - identity_group_info + - identity_group_info + - identity_role + - identity_role + - identity_user + - identity_user + - identity_user_info + - identity_user_info + - image + - image_info + - keypair + - keypair_info + - keystone_federation_protocol + - keystone_federation_protocol_info + - lb_listener + - lb_listener + - lb_member + - lb_member + - lb_pool + - lb_pool + - loadbalancer + - network + - networks_info + - object + - object_container + - port + - port_info + - project + - project_access + - project_info + - quota + - recordset + - role_assignment + - role_assignment + - router + - routers_info + - security_group + - security_group_info + - security_group_rule + - security_group_rule_info + - server + - server_action + - server_group + - server_info + - server_metadata + - server_volume + - stack + - subnet + - subnets_info + - volume + - volume_backup + - volume_backup_info + - volume_info + - volume_snapshot + - volume_snapshot_info + - os_auth + - os_client_config + - os_client_config + - os_coe_cluster + - os_coe_cluster_template + - os_endpoint + - os_flavor + - os_flavor_info + - os_flavor_info + - os_floating_ip + - os_group + - os_group + - os_group_info + - os_group_info + - os_image + - os_image_info + - os_ironic + - os_ironic + - os_ironic_inspect + - os_ironic_inspect + - os_ironic_node + - os_ironic_node + - os_keypair + - os_keystone_domain + - os_keystone_domain + - os_keystone_domain_info + - os_keystone_domain_info + - os_keystone_endpoint + - os_keystone_endpoint + - os_keystone_federation_protocol + - os_keystone_federation_protocol_info + - os_keystone_identity_provider + - os_keystone_identity_provider + - os_keystone_identity_provider_info + - os_keystone_identity_provider_info + - os_keystone_mapping + - os_keystone_mapping + - os_keystone_mapping_info + - os_keystone_mapping_info + - os_keystone_role + - os_keystone_role + - os_keystone_service + - os_keystone_service + - os_listener + - os_listener + - os_loadbalancer + - os_member + - os_member + - os_network + - os_networks_info + - os_nova_flavor + - os_nova_flavor + - os_nova_host_aggregate + - os_nova_host_aggregate + - os_object + - os_pool + - os_pool + - os_port + - os_port_info + - os_project + - os_project_access + - os_project_info + - os_quota + - os_recordset + - os_router + - os_routers_info + - os_security_group + - os_security_group_rule + - os_server + - os_server_action + - os_server_group + - os_server_info + - os_server_metadata + - os_server_volume + - os_stack + - os_subnet + - os_subnets_info + - os_user + - os_user + - os_user_group + - os_user_group + - os_user_info + - os_user_info + - os_user_role + - os_user_role + - os_volume + - os_volume_snapshot + - os_zone + - os_zone + +plugin_routing: + modules: + os_auth: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.auth + redirect: openstack.cloud.auth + os_client_config: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.config + redirect: openstack.cloud.config + os_coe_cluster: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.coe_cluster + redirect: openstack.cloud.coe_cluster + os_coe_cluster_template: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.coe_cluster_template + redirect: openstack.cloud.coe_cluster_template + os_endpoint: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.catalog_endpoint + redirect: openstack.cloud.catalog_endpoint + os_flavor: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.compute_flavor + redirect: openstack.cloud.compute_flavor + os_flavor_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.compute_flavor_info + redirect: openstack.cloud.compute_flavor_info + os_floating_ip: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.floating_ip + redirect: openstack.cloud.floating_ip + os_group: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.identity_group + redirect: openstack.cloud.identity_group + os_group_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.identity_group_info + redirect: openstack.cloud.identity_group_info + os_image: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.image + redirect: openstack.cloud.image + os_image_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.image_info + redirect: openstack.cloud.image_info + os_ironic: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.baremetal_node + redirect: openstack.cloud.baremetal_node + os_ironic_inspect: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.baremetal_inspect + redirect: openstack.cloud.baremetal_inspect + os_ironic_node: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.baremetal_node_action + redirect: openstack.cloud.baremetal_node_action + os_keypair: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.keypair + redirect: openstack.cloud.keypair + os_keystone_domain: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.identity_domain + redirect: openstack.cloud.identity_domain + os_keystone_domain_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.identity_domain_info + redirect: openstack.cloud.identity_domain_info + os_keystone_endpoint: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.endpoint + redirect: openstack.cloud.endpoint + os_keystone_federation_protocol: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.keystone_federation_protocol + redirect: openstack.cloud.keystone_federation_protocol + os_keystone_federation_protocol_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.keystone_federation_protocol_info + redirect: openstack.cloud.keystone_federation_protocol_info + os_keystone_identity_provider: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.federation_idp + redirect: openstack.cloud.federation_idp + os_keystone_identity_provider_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.federation_idp_info + redirect: openstack.cloud.federation_idp_info + os_keystone_mapping: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.federation_mapping + redirect: openstack.cloud.federation_mapping + os_keystone_mapping_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.federation_mapping_info + redirect: openstack.cloud.federation_mapping_info + os_keystone_role: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.identity_role + redirect: openstack.cloud.identity_role + os_keystone_service: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.catalog_service + redirect: openstack.cloud.catalog_service + os_listener: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.lb_listener + redirect: openstack.cloud.lb_listener + os_loadbalancer: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.loadbalancer + redirect: openstack.cloud.loadbalancer + os_member: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.lb_member + redirect: openstack.cloud.lb_member + os_network: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.network + redirect: openstack.cloud.network + os_networks_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.networks_info + redirect: openstack.cloud.networks_info + os_nova_flavor: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.compute_flavor + redirect: openstack.cloud.compute_flavor + os_nova_host_aggregate: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.host_aggregate + redirect: openstack.cloud.host_aggregate + os_object: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.object + redirect: openstack.cloud.object + os_pool: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.lb_pool + redirect: openstack.cloud.lb_pool + os_port: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.port + redirect: openstack.cloud.port + os_port_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.port_info + redirect: openstack.cloud.port_info + os_project: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.project + redirect: openstack.cloud.project + os_project_access: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.project_access + redirect: openstack.cloud.project_access + os_project_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.project_info + redirect: openstack.cloud.project_info + os_quota: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.quota + redirect: openstack.cloud.quota + os_recordset: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.recordset + redirect: openstack.cloud.recordset + os_router: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.router + redirect: openstack.cloud.router + os_routers_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.routers_info + redirect: openstack.cloud.routers_info + os_security_group: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.security_group + redirect: openstack.cloud.security_group + os_security_group_rule: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.security_group_rule + redirect: openstack.cloud.security_group_rule + os_server: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.server + redirect: openstack.cloud.server + os_server_action: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.server_action + redirect: openstack.cloud.server_action + os_server_group: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.server_group + redirect: openstack.cloud.server_group + os_server_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.server_info + redirect: openstack.cloud.server_info + os_server_metadata: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.server_metadata + redirect: openstack.cloud.server_metadata + os_server_volume: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.server_volume + redirect: openstack.cloud.server_volume + os_stack: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.stack + redirect: openstack.cloud.stack + os_subnet: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.subnet + redirect: openstack.cloud.subnet + os_subnets_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.subnets_info + redirect: openstack.cloud.subnets_info + os_user: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.identity_user + redirect: openstack.cloud.identity_user + os_user_group: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.group_assignment + redirect: openstack.cloud.group_assignment + os_user_info: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.identity_user_info + redirect: openstack.cloud.identity_user_info + os_user_role: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.role_assignment + redirect: openstack.cloud.role_assignment + os_volume: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.volume + redirect: openstack.cloud.volume + os_volume_snapshot: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.volume_snapshot + redirect: openstack.cloud.volume_snapshot + os_zone: + deprecation: + removal_date: 2021-12-12 + warning_text: os_ prefixed module names are deprecated, use openstack.cloud.dns_zone + redirect: openstack.cloud.dns_zone diff --git a/ansible_collections/openstack/cloud/plugins/doc_fragments/__init__.py b/ansible_collections/openstack/cloud/plugins/doc_fragments/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/doc_fragments/__init__.py diff --git a/ansible_collections/openstack/cloud/plugins/doc_fragments/openstack.py b/ansible_collections/openstack/cloud/plugins/doc_fragments/openstack.py new file mode 100644 index 00000000..37d51bb2 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/doc_fragments/openstack.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2014, Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + # Standard openstack documentation fragment + DOCUMENTATION = r''' +options: + cloud: + description: + - Named cloud or cloud config to operate against. + If I(cloud) is a string, it references a named cloud config as defined + in an OpenStack clouds.yaml file. Provides default values for I(auth) + and I(auth_type). This parameter is not needed if I(auth) is provided + or if OpenStack OS_* environment variables are present. + If I(cloud) is a dict, it contains a complete cloud configuration like + would be in a section of clouds.yaml. + type: raw + auth: + description: + - Dictionary containing auth information as needed by the cloud's auth + plugin strategy. For the default I(password) plugin, this would contain + I(auth_url), I(username), I(password), I(project_name) and any + information about domains (for example, I(user_domain_name) or + I(project_domain_name)) if the cloud supports them. + For other plugins, + this param will need to contain whatever parameters that auth plugin + requires. This parameter is not needed if a named cloud is provided or + OpenStack OS_* environment variables are present. + type: dict + auth_type: + description: + - Name of the auth plugin to use. If the cloud uses something other than + password authentication, the name of the plugin should be indicated here + and the contents of the I(auth) parameter should be updated accordingly. + type: str + region_name: + description: + - Name of the region. + type: str + wait: + description: + - Should ansible wait until the requested resource is complete. + type: bool + default: yes + timeout: + description: + - How long should ansible wait for the requested resource. + type: int + default: 180 + api_timeout: + description: + - How long should the socket layer wait before timing out for API calls. + If this is omitted, nothing will be passed to the requests library. + type: int + validate_certs: + description: + - Whether or not SSL API requests should be verified. + - Before Ansible 2.3 this defaulted to C(yes). + type: bool + aliases: [ verify ] + ca_cert: + description: + - A path to a CA Cert bundle that can be used as part of verifying + SSL API requests. + type: str + aliases: [ cacert ] + client_cert: + description: + - A path to a client certificate to use as part of the SSL transaction. + type: str + aliases: [ cert ] + client_key: + description: + - A path to a client key to use as part of the SSL transaction. + type: str + aliases: [ key ] + interface: + description: + - Endpoint URL type to fetch from the service catalog. + type: str + choices: [ admin, internal, public ] + default: public + aliases: [ endpoint_type ] + availability_zone: + description: + - Ignored. Present for backwards compatibility + type: str + sdk_log_path: + description: + - Path to the logfile of the OpenStackSDK. If empty no log is written + type: str + sdk_log_level: + description: Log level of the OpenStackSDK + type: str + default: INFO + choices: [INFO, DEBUG] +requirements: + - python >= 3.6 + - openstacksdk >= 0.36, < 0.99.0 +notes: + - The standard OpenStack environment variables, such as C(OS_USERNAME) + may be used instead of providing explicit values. + - Auth information is driven by openstacksdk, which means that values + can come from a yaml config file in /etc/ansible/openstack.yaml, + /etc/openstack/clouds.yaml or ~/.config/openstack/clouds.yaml, then from + standard environment variables, then finally by explicit parameters in + plays. More information can be found at + U(https://docs.openstack.org/openstacksdk/) +''' diff --git a/ansible_collections/openstack/cloud/plugins/inventory/__init__.py b/ansible_collections/openstack/cloud/plugins/inventory/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/inventory/__init__.py diff --git a/ansible_collections/openstack/cloud/plugins/inventory/openstack.py b/ansible_collections/openstack/cloud/plugins/inventory/openstack.py new file mode 100644 index 00000000..def6c04b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/inventory/openstack.py @@ -0,0 +1,415 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2012, Marco Vito Moscaritolo <marco@agavee.com> +# Copyright (c) 2013, Jesse Keating <jesse.keating@rackspace.com> +# Copyright (c) 2015, Hewlett-Packard Development Company, L.P. +# Copyright (c) 2016, Rackspace Australia +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = ''' +--- +name: openstack +author: OpenStack Ansible SIG +short_description: OpenStack inventory source +description: + - Get inventory hosts from OpenStack clouds + - Uses openstack.(yml|yaml) YAML configuration file to configure the inventory plugin + - Uses standard clouds.yaml YAML configuration file to configure cloud credentials +options: + plugin: + description: token that ensures this is a source file for the 'openstack' plugin. + required: True + choices: ['openstack', 'openstack.cloud.openstack'] + show_all: + description: toggles showing all vms vs only those with a working IP + type: bool + default: false + inventory_hostname: + description: | + What to register as the inventory hostname. + If set to 'uuid' the uuid of the server will be used and a + group will be created for the server name. + If set to 'name' the name of the server will be used unless + there are more than one server with the same name in which + case the 'uuid' logic will be used. + Default is to do 'name', which is the opposite of the old + openstack.py inventory script's option use_hostnames) + type: string + choices: + - name + - uuid + default: "name" + use_names: + description: | + Use the host's 'name' instead of 'interface_ip' for the 'ansible_host' and + 'ansible_ssh_host' facts. This might be desired when using jump or + bastion hosts and the name is the FQDN of the host. + type: bool + default: false + expand_hostvars: + description: | + Run extra commands on each host to fill in additional + information about the host. May interrogate cinder and + neutron and can be expensive for people with many hosts. + (Note, the default value of this is opposite from the default + old openstack.py inventory script's option expand_hostvars) + type: bool + default: false + private: + description: | + Use the private interface of each server, if it has one, as + the host's IP in the inventory. This can be useful if you are + running ansible inside a server in the cloud and would rather + communicate to your servers over the private network. + type: bool + default: false + only_clouds: + description: | + List of clouds from clouds.yaml to use, instead of using + the whole list. + type: list + elements: str + default: [] + fail_on_errors: + description: | + Causes the inventory to fail and return no hosts if one cloud + has failed (for example, bad credentials or being offline). + When set to False, the inventory will return as many hosts as + it can from as many clouds as it can contact. (Note, the + default value of this is opposite from the old openstack.py + inventory script's option fail_on_errors) + type: bool + default: false + all_projects: + description: | + Lists servers from all projects + type: bool + default: false + clouds_yaml_path: + description: | + Override path to clouds.yaml file. If this value is given it + will be searched first. The default path for the + ansible inventory adds /etc/ansible/openstack.yaml and + /etc/ansible/openstack.yml to the regular locations documented + at https://docs.openstack.org/os-client-config/latest/user/configuration.html#config-files + type: list + elements: str + env: + - name: OS_CLIENT_CONFIG_FILE + compose: + description: Create vars from jinja2 expressions. + type: dictionary + default: {} + groups: + description: Add hosts to group based on Jinja2 conditionals. + type: dictionary + default: {} + legacy_groups: + description: Automatically create groups from host variables. + type: bool + default: true +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.28, < 0.99.0" +extends_documentation_fragment: +- inventory_cache +- constructed + +''' + +EXAMPLES = ''' +# file must be named openstack.yaml or openstack.yml +# Make the plugin behave like the default behavior of the old script +plugin: openstack.cloud.openstack +expand_hostvars: yes +fail_on_errors: yes +all_projects: yes +''' + +import collections +import sys +import logging + +from ansible.errors import AnsibleParserError +from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable +from ansible.utils.display import Display +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + ensure_compatibility +) + +display = Display() +os_logger = logging.getLogger("openstack") + +try: + # Due to the name shadowing we should import other way + import importlib + sdk = importlib.import_module('openstack') + sdk_inventory = importlib.import_module('openstack.cloud.inventory') + client_config = importlib.import_module('openstack.config.loader') + sdk_exceptions = importlib.import_module("openstack.exceptions") + HAS_SDK = True +except ImportError: + display.vvvv("Couldn't import Openstack SDK modules") + HAS_SDK = False + + +class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): + ''' Host inventory provider for ansible using OpenStack clouds. ''' + + NAME = 'openstack.cloud.openstack' + + def parse(self, inventory, loader, path, cache=True): + + super(InventoryModule, self).parse(inventory, loader, path) + + cache_key = self._get_cache_prefix(path) + + # file is config file + self._config_data = self._read_config_data(path) + + msg = '' + if not self._config_data: + msg = 'File empty. this is not my config file' + elif 'plugin' in self._config_data and self._config_data['plugin'] not in (self.NAME, 'openstack'): + msg = 'plugin config file, but not for us: %s' % self._config_data['plugin'] + elif 'plugin' not in self._config_data and 'clouds' not in self._config_data: + msg = "it's not a plugin configuration nor a clouds.yaml file" + elif not HAS_SDK: + msg = "openstacksdk is required for the OpenStack inventory plugin. OpenStack inventory sources will be skipped." + + if not msg: + try: + ensure_compatibility(sdk.version.__version__) + except ImportError as e: + msg = ("Incompatible openstacksdk library found: {error}." + .format(error=str(e))) + + if msg: + display.vvvv(msg) + raise AnsibleParserError(msg) + + if 'clouds' in self._config_data: + self.display.vvvv( + "Found clouds config file instead of plugin config. " + "Using default configuration." + ) + self._config_data = {} + + # update cache if the user has caching enabled and the cache is being refreshed + # will update variable below in the case of an expired cache + cache_needs_update = not cache and self.get_option('cache') + + if cache: + cache = self.get_option('cache') + source_data = None + if cache: + self.display.vvvv("Reading inventory data from cache: %s" % cache_key) + try: + source_data = self._cache[cache_key] + except KeyError: + # cache expired or doesn't exist yet + display.vvvv("Inventory data cache not found") + cache_needs_update = True + + if not source_data: + self.display.vvvv("Getting hosts from Openstack clouds") + clouds_yaml_path = self._config_data.get('clouds_yaml_path') + if clouds_yaml_path: + config_files = ( + clouds_yaml_path + + client_config.CONFIG_FILES + ) + else: + config_files = None + + # Redict logging to stderr so it does not mix with output + # particular ansible-inventory JSON output + # TODO(mordred) Integrate openstack's logging with ansible's logging + if self.display.verbosity > 3: + sdk.enable_logging(debug=True, stream=sys.stderr) + else: + sdk.enable_logging(stream=sys.stderr) + + cloud_inventory = sdk_inventory.OpenStackInventory( + config_files=config_files, + private=self._config_data.get('private', False)) + self.display.vvvv("Found %d cloud(s) in Openstack" % + len(cloud_inventory.clouds)) + only_clouds = self._config_data.get('only_clouds', []) + if only_clouds and not isinstance(only_clouds, list): + raise ValueError( + 'OpenStack Inventory Config Error: only_clouds must be' + ' a list') + if only_clouds: + new_clouds = [] + for cloud in cloud_inventory.clouds: + self.display.vvvv("Looking at cloud : %s" % cloud.name) + if cloud.name in only_clouds: + self.display.vvvv("Selecting cloud : %s" % cloud.name) + new_clouds.append(cloud) + cloud_inventory.clouds = new_clouds + + self.display.vvvv("Selected %d cloud(s)" % + len(cloud_inventory.clouds)) + + expand_hostvars = self._config_data.get('expand_hostvars', False) + fail_on_errors = self._config_data.get('fail_on_errors', False) + all_projects = self._config_data.get('all_projects', False) + self.use_names = self._config_data.get('use_names', False) + + source_data = [] + try: + source_data = cloud_inventory.list_hosts( + expand=expand_hostvars, fail_on_cloud_config=fail_on_errors, + all_projects=all_projects) + except Exception as e: + self.display.warning("Couldn't list Openstack hosts. " + "See logs for details") + os_logger.error(e.message) + finally: + if cache_needs_update: + self._cache[cache_key] = source_data + + self._populate_from_source(source_data) + + def _populate_from_source(self, source_data): + groups = collections.defaultdict(list) + firstpass = collections.defaultdict(list) + hostvars = {} + + use_server_id = ( + self._config_data.get('inventory_hostname', 'name') != 'name') + show_all = self._config_data.get('show_all', False) + + for server in source_data: + if 'interface_ip' not in server and not show_all: + continue + firstpass[server['name']].append(server) + + for name, servers in firstpass.items(): + if len(servers) == 1 and not use_server_id: + self._append_hostvars(hostvars, groups, name, servers[0]) + else: + server_ids = set() + # Trap for duplicate results + for server in servers: + server_ids.add(server['id']) + if len(server_ids) == 1 and not use_server_id: + self._append_hostvars(hostvars, groups, name, servers[0]) + else: + for server in servers: + self._append_hostvars( + hostvars, groups, server['id'], server, + namegroup=True) + + self._set_variables(hostvars, groups) + + def _set_variables(self, hostvars, groups): + + strict = self.get_option('strict') + + # set vars in inventory from hostvars + for host in hostvars: + + # actually update inventory + for key in hostvars[host]: + self.inventory.set_variable(host, key, hostvars[host][key]) + + # create composite vars + self._set_composite_vars( + self._config_data.get('compose'), self.inventory.get_host(host).get_vars(), host, strict) + + # constructed groups based on conditionals + self._add_host_to_composed_groups( + self._config_data.get('groups'), hostvars[host], host, strict) + + # constructed groups based on jinja expressions + self._add_host_to_keyed_groups( + self._config_data.get('keyed_groups'), hostvars[host], host, strict) + + for group_name, group_hosts in groups.items(): + gname = self.inventory.add_group(group_name) + for host in group_hosts: + if gname == host: + display.vvvv("Same name for host %s and group %s" % (host, gname)) + self.inventory.add_host(host, gname) + else: + self.inventory.add_child(gname, host) + + def _get_groups_from_server(self, server_vars, namegroup=True): + groups = [] + + region = server_vars['region'] + cloud = server_vars['cloud'] + metadata = server_vars.get('metadata', {}) + + # Create a group for the cloud + groups.append(cloud) + + # Create a group on region + if region: + groups.append(region) + + # And one by cloud_region + groups.append("%s_%s" % (cloud, region)) + + # Check if group metadata key in servers' metadata + if 'group' in metadata: + groups.append(metadata['group']) + + for extra_group in metadata.get('groups', '').split(','): + if extra_group: + groups.append(extra_group.strip()) + + groups.append('instance-%s' % server_vars['id']) + if namegroup: + groups.append(server_vars['name']) + + for key in ('flavor', 'image'): + if 'name' in server_vars[key]: + groups.append('%s-%s' % (key, server_vars[key]['name'])) + + for key, value in iter(metadata.items()): + groups.append('meta-%s_%s' % (key, value)) + + az = server_vars.get('az', None) + if az: + # Make groups for az, region_az and cloud_region_az + groups.append(az) + groups.append('%s_%s' % (region, az)) + groups.append('%s_%s_%s' % (cloud, region, az)) + return groups + + def _append_hostvars(self, hostvars, groups, current_host, + server, namegroup=False): + if not self.use_names: + hostvars[current_host] = dict( + ansible_ssh_host=server['interface_ip'], + ansible_host=server['interface_ip'], + openstack=server, + ) + + if self.use_names: + hostvars[current_host] = dict( + ansible_ssh_host=server['name'], + ansible_host=server['name'], + openstack=server, + ) + + self.inventory.add_host(current_host) + + if self.get_option('legacy_groups'): + for group in self._get_groups_from_server(server, namegroup=namegroup): + groups[group].append(current_host) + + def verify_file(self, path): + + if super(InventoryModule, self).verify_file(path): + for fn in ('openstack', 'clouds'): + for suffix in ('yaml', 'yml'): + maybe = '{fn}.{suffix}'.format(fn=fn, suffix=suffix) + if path.endswith(maybe): + self.display.vvvv("Valid plugin config file found") + return True + return False diff --git a/ansible_collections/openstack/cloud/plugins/module_utils/__init__.py b/ansible_collections/openstack/cloud/plugins/module_utils/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/module_utils/__init__.py diff --git a/ansible_collections/openstack/cloud/plugins/module_utils/ironic.py b/ansible_collections/openstack/cloud/plugins/module_utils/ironic.py new file mode 100644 index 00000000..a7ab19ef --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/module_utils/ironic.py @@ -0,0 +1,68 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import openstack_full_argument_spec + + +def ironic_argument_spec(**kwargs): + spec = dict( + auth_type=dict(required=False), + ironic_url=dict(required=False), + ) + spec.update(kwargs) + return openstack_full_argument_spec(**spec) + + +# TODO(dtantsur): inherit the collection's base module +class IronicModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._update_ironic_auth() + + def _update_ironic_auth(self): + """Validate and update authentication parameters for ironic.""" + if ( + self.params['auth_type'] in [None, 'None', 'none'] + and self.params['ironic_url'] is None + and not self.params['cloud'] + and not (self.params['auth'] + and self.params['auth'].get('endpoint')) + ): + self.fail_json(msg=("Authentication appears to be disabled, " + "Please define either ironic_url, or cloud, " + "or auth.endpoint")) + + if ( + self.params['ironic_url'] + and self.params['auth_type'] in [None, 'None', 'none'] + and not (self.params['auth'] + and self.params['auth'].get('endpoint')) + ): + self.params['auth'] = dict( + endpoint=self.params['ironic_url'] + ) diff --git a/ansible_collections/openstack/cloud/plugins/module_utils/openstack.py b/ansible_collections/openstack/cloud/plugins/module_utils/openstack.py new file mode 100644 index 00000000..8663d2fc --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/module_utils/openstack.py @@ -0,0 +1,470 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright 2019 Red Hat, Inc. +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import abc +import copy +from ansible.module_utils.six import raise_from +try: + from ansible.module_utils.compat.version import StrictVersion +except ImportError: + try: + from distutils.version import StrictVersion + except ImportError as exc: + raise_from(ImportError('To use this plugin or module with ansible-core' + ' < 2.11, you need to use Python < 3.12 with ' + 'distutils.version present'), exc) +import importlib +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems + +OVERRIDES = {'os_client_config': 'config', + 'os_endpoint': 'catalog_endpoint', + 'os_flavor': 'compute_flavor', + 'os_flavor_info': 'compute_flavor_info', + 'os_group': 'identity_group', + 'os_group_info': 'identity_group_info', + 'os_ironic': 'baremetal_node', + 'os_ironic_inspect': 'baremetal_inspect', + 'os_ironic_node': 'baremetal_node_action', + 'os_keystone_domain': 'identity_domain', + 'os_keystone_domain_info': 'identity_domain_info', + 'os_keystone_endpoint': 'endpoint', + 'os_keystone_identity_provider': 'federation_idp', + 'os_keystone_identity_provider_info': 'federation_idp_info', + 'os_keystone_mapping': 'federation_mapping', + 'os_keystone_mapping_info': 'federation_mapping_info', + 'os_keystone_role': 'identity_role', + 'os_keystone_service': 'catalog_service', + 'os_listener': 'lb_listener', + 'os_member': 'lb_member', + 'os_nova_flavor': 'compute_flavor', + 'os_nova_host_aggregate': 'host_aggregate', + 'os_pool': 'lb_pool', + 'os_user': 'identity_user', + 'os_user_group': 'group_assignment', + 'os_user_info': 'identity_user_info', + 'os_user_role': 'role_assignment', + 'os_zone': 'dns_zone'} + +CUSTOM_VAR_PARAMS = ['min_ver', 'max_ver'] + +MINIMUM_SDK_VERSION = '0.36.0' +MAXIMUM_SDK_VERSION = '0.98.999' + + +def ensure_compatibility(version, min_version=None, max_version=None): + """ Raises ImportError if the specified version does not + meet the minimum and maximum version requirements""" + + if min_version and MINIMUM_SDK_VERSION: + min_version = max(StrictVersion(MINIMUM_SDK_VERSION), + StrictVersion(min_version)) + elif MINIMUM_SDK_VERSION: + min_version = StrictVersion(MINIMUM_SDK_VERSION) + + if max_version and MAXIMUM_SDK_VERSION: + max_version = min(StrictVersion(MAXIMUM_SDK_VERSION), + StrictVersion(max_version)) + elif MAXIMUM_SDK_VERSION: + max_version = StrictVersion(MAXIMUM_SDK_VERSION) + + if min_version and StrictVersion(version) < min_version: + raise ImportError( + "Version MUST be >={min_version} and <={max_version}, but" + " {version} is smaller than minimum version {min_version}" + .format(version=version, + min_version=min_version, + max_version=max_version)) + + if max_version and StrictVersion(version) > max_version: + raise ImportError( + "Version MUST be >={min_version} and <={max_version}, but" + " {version} is larger than maximum version {max_version}" + .format(version=version, + min_version=min_version, + max_version=max_version)) + + +def openstack_argument_spec(): + # DEPRECATED: This argument spec is only used for the deprecated old + # OpenStack modules. It turns out that modern OpenStack auth is WAY + # more complex than this. + # Consume standard OpenStack environment variables. + # This is mainly only useful for ad-hoc command line operation as + # in playbooks one would assume variables would be used appropriately + OS_AUTH_URL = os.environ.get('OS_AUTH_URL', 'http://127.0.0.1:35357/v2.0/') + OS_PASSWORD = os.environ.get('OS_PASSWORD', None) + OS_REGION_NAME = os.environ.get('OS_REGION_NAME', None) + OS_USERNAME = os.environ.get('OS_USERNAME', 'admin') + OS_TENANT_NAME = os.environ.get('OS_TENANT_NAME', OS_USERNAME) + + spec = dict( + login_username=dict(default=OS_USERNAME), + auth_url=dict(default=OS_AUTH_URL), + region_name=dict(default=OS_REGION_NAME), + availability_zone=dict(), + ) + if OS_PASSWORD: + spec['login_password'] = dict(default=OS_PASSWORD) + else: + spec['login_password'] = dict(required=True) + if OS_TENANT_NAME: + spec['login_tenant_name'] = dict(default=OS_TENANT_NAME) + else: + spec['login_tenant_name'] = dict(required=True) + return spec + + +def openstack_find_nova_addresses(addresses, ext_tag, key_name=None): + + ret = [] + for (k, v) in iteritems(addresses): + if key_name and k == key_name: + ret.extend([addrs['addr'] for addrs in v]) + else: + for interface_spec in v: + if 'OS-EXT-IPS:type' in interface_spec and interface_spec['OS-EXT-IPS:type'] == ext_tag: + ret.append(interface_spec['addr']) + return ret + + +def openstack_full_argument_spec(**kwargs): + spec = dict( + cloud=dict(default=None, type='raw'), + auth_type=dict(default=None), + auth=dict(default=None, type='dict', no_log=True), + region_name=dict(default=None), + availability_zone=dict(default=None), + validate_certs=dict(default=None, type='bool', aliases=['verify']), + ca_cert=dict(default=None, aliases=['cacert']), + client_cert=dict(default=None, aliases=['cert']), + client_key=dict(default=None, no_log=True, aliases=['key']), + wait=dict(default=True, type='bool'), + timeout=dict(default=180, type='int'), + api_timeout=dict(default=None, type='int'), + interface=dict( + default='public', choices=['public', 'internal', 'admin'], + aliases=['endpoint_type']), + sdk_log_path=dict(default=None, type='str'), + sdk_log_level=dict( + default='INFO', type='str', choices=['INFO', 'DEBUG']), + ) + # Filter out all our custom parameters before passing to AnsibleModule + kwargs_copy = copy.deepcopy(kwargs) + for v in kwargs_copy.values(): + for c in CUSTOM_VAR_PARAMS: + v.pop(c, None) + spec.update(kwargs_copy) + return spec + + +def openstack_module_kwargs(**kwargs): + ret = {} + for key in ('mutually_exclusive', 'required_together', 'required_one_of'): + if key in kwargs: + if key in ret: + ret[key].extend(kwargs[key]) + else: + ret[key] = kwargs[key] + return ret + + +# for compatibility with old versions +def openstack_cloud_from_module(module, min_version=None, max_version=None): + try: + # Due to the name shadowing we should import other way + sdk = importlib.import_module('openstack') + except ImportError: + module.fail_json(msg='openstacksdk is required for this module') + + try: + ensure_compatibility(sdk.version.__version__, + min_version, max_version) + except ImportError as e: + module.fail_json( + msg="Incompatible openstacksdk library found: {error}." + .format(error=str(e))) + + cloud_config = module.params.pop('cloud', None) + try: + if isinstance(cloud_config, dict): + fail_message = ( + "A cloud config dict was provided to the cloud parameter" + " but also a value was provided for {param}. If a cloud" + " config dict is provided, {param} should be" + " excluded.") + for param in ( + 'auth', 'region_name', 'validate_certs', + 'ca_cert', 'client_key', 'api_timeout', 'auth_type'): + if module.params[param] is not None: + module.fail_json(msg=fail_message.format(param=param)) + # For 'interface' parameter, fail if we receive a non-default value + if module.params['interface'] != 'public': + module.fail_json(msg=fail_message.format(param='interface')) + return sdk, sdk.connect(**cloud_config) + else: + return sdk, sdk.connect( + cloud=cloud_config, + auth_type=module.params['auth_type'], + auth=module.params['auth'], + region_name=module.params['region_name'], + verify=module.params['validate_certs'], + cacert=module.params['ca_cert'], + key=module.params['client_key'], + api_timeout=module.params['api_timeout'], + interface=module.params['interface'], + ) + except sdk.exceptions.SDKException as e: + # Probably a cloud configuration/login error + module.fail_json(msg=str(e)) + + +class OpenStackModule: + """Openstack Module is a base class for all Openstack Module classes. + + The class has `run` function that should be overriden in child classes, + the provided methods include: + + Methods: + params: Dictionary of Ansible module parameters. + module_name: Module name (i.e. server_action) + sdk_version: Version of used OpenstackSDK. + results: Dictionary for return of Ansible module, + must include `changed` keyword. + exit, exit_json: Exit module and return data inside, must include + changed` keyword in a data. + fail, fail_json: Exit module with failure, has `msg` keyword to + specify a reason of failure. + conn: Connection to SDK object. + log: Print message to system log. + debug: Print debug message to system log, prints if Ansible Debug is + enabled or verbosity is more than 2. + check_deprecated_names: Function that checks if module was called with + a deprecated name and prints the correct name + with deprecation warning. + check_versioned: helper function to check that all arguments are known + in the current SDK version. + run: method that executes and shall be overriden in inherited classes. + + Args: + deprecated_names: Should specify deprecated modules names for current + module. + argument_spec: Used for construction of Openstack common arguments. + module_kwargs: Additional arguments for Ansible Module. + """ + + deprecated_names = () + argument_spec = {} + module_kwargs = {} + module_min_sdk_version = None + module_max_sdk_version = None + + def __init__(self): + """Initialize Openstack base class. + + Set up variables, connection to SDK and check if there are + deprecated names. + """ + self.ansible = AnsibleModule( + openstack_full_argument_spec(**self.argument_spec), + **self.module_kwargs) + self.params = self.ansible.params + self.module_name = self.ansible._name + self.check_mode = self.ansible.check_mode + self.sdk_version = None + self.results = {'changed': False} + self.exit = self.exit_json = self.ansible.exit_json + self.fail = self.fail_json = self.ansible.fail_json + self.warn = self.ansible.warn + self.sdk, self.conn = self.openstack_cloud_from_module() + self.check_deprecated_names() + self.setup_sdk_logging() + + def log(self, msg): + """Prints log message to system log. + + Arguments: + msg {str} -- Log message + """ + self.ansible.log(msg) + + def debug(self, msg): + """Prints debug message to system log + + Arguments: + msg {str} -- Debug message. + """ + if self.ansible._debug or self.ansible._verbosity > 2: + self.ansible.log( + " ".join(['[DEBUG]', msg])) + + def setup_sdk_logging(self): + log_path = self.params.get('sdk_log_path') + if log_path is not None: + log_level = self.params.get('sdk_log_level') + self.sdk.enable_logging( + debug=True if log_level == 'DEBUG' else False, + http_debug=True if log_level == 'DEBUG' else False, + path=log_path + ) + + def check_deprecated_names(self): + """Check deprecated module names if `deprecated_names` variable is set. + """ + new_module_name = OVERRIDES.get(self.module_name) + if self.module_name in self.deprecated_names and new_module_name: + self.ansible.deprecate( + "The '%s' module has been renamed to '%s' in openstack " + "collection: openstack.cloud.%s" % ( + self.module_name, new_module_name, new_module_name), + version='2.0.0', collection_name='openstack.cloud') + + def openstack_cloud_from_module(self): + """Sets up connection to cloud using provided options. Checks if all + provided variables are supported for the used SDK version. + """ + try: + # Due to the name shadowing we should import other way + sdk = importlib.import_module('openstack') + self.sdk_version = sdk.version.__version__ + except ImportError: + self.fail_json(msg='openstacksdk is required for this module') + + try: + ensure_compatibility(self.sdk_version, + self.module_min_sdk_version, + self.module_max_sdk_version) + except ImportError as e: + self.fail_json( + msg="Incompatible openstacksdk library found: {error}." + .format(error=str(e))) + + # Fail if there are set unsupported for this version parameters + # New parameters should NOT use 'default' but rely on SDK defaults + for param in self.argument_spec: + if (self.params[param] is not None + and 'min_ver' in self.argument_spec[param] + and StrictVersion(self.sdk_version) < self.argument_spec[param]['min_ver']): + self.fail_json( + msg="To use parameter '{param}' with module '{module}', the installed version of " + "the openstacksdk library MUST be >={min_version}.".format( + min_version=self.argument_spec[param]['min_ver'], + param=param, + module=self.module_name)) + if (self.params[param] is not None + and 'max_ver' in self.argument_spec[param] + and StrictVersion(self.sdk_version) > self.argument_spec[param]['max_ver']): + self.fail_json( + msg="To use parameter '{param}' with module '{module}', the installed version of " + "the openstacksdk library MUST be <={max_version}.".format( + max_version=self.argument_spec[param]['max_ver'], + param=param, + module=self.module_name)) + + cloud_config = self.params.pop('cloud', None) + if isinstance(cloud_config, dict): + fail_message = ( + "A cloud config dict was provided to the cloud parameter" + " but also a value was provided for {param}. If a cloud" + " config dict is provided, {param} should be" + " excluded.") + for param in ( + 'auth', 'region_name', 'validate_certs', + 'ca_cert', 'client_key', 'api_timeout', 'auth_type'): + if self.params[param] is not None: + self.fail_json(msg=fail_message.format(param=param)) + # For 'interface' parameter, fail if we receive a non-default value + if self.params['interface'] != 'public': + self.fail_json(msg=fail_message.format(param='interface')) + else: + cloud_config = dict( + cloud=cloud_config, + auth_type=self.params['auth_type'], + auth=self.params['auth'], + region_name=self.params['region_name'], + verify=self.params['validate_certs'], + cacert=self.params['ca_cert'], + key=self.params['client_key'], + api_timeout=self.params['api_timeout'], + interface=self.params['interface'], + ) + try: + return sdk, sdk.connect(**cloud_config) + except sdk.exceptions.SDKException as e: + # Probably a cloud configuration/login error + self.fail_json(msg=str(e)) + + # Filter out all arguments that are not from current SDK version + def check_versioned(self, **kwargs): + """Check that provided arguments are supported by current SDK version + + Returns: + versioned_result {dict} dictionary of only arguments that are + supported by current SDK version. All others + are dropped. + """ + versioned_result = {} + for var_name in kwargs: + if ('min_ver' in self.argument_spec[var_name] + and StrictVersion(self.sdk_version) < self.argument_spec[var_name]['min_ver']): + continue + if ('max_ver' in self.argument_spec[var_name] + and StrictVersion(self.sdk_version) > self.argument_spec[var_name]['max_ver']): + continue + versioned_result.update({var_name: kwargs[var_name]}) + return versioned_result + + @abc.abstractmethod + def run(self): + """Function for overriding in inhetired classes, it's executed by default. + """ + pass + + def __call__(self): + """Execute `run` function when calling the class. + """ + try: + results = self.run() + if results and isinstance(results, dict): + self.ansible.exit_json(**results) + except self.sdk.exceptions.OpenStackCloudException as e: + params = { + 'msg': str(e), + 'extra_data': { + 'data': getattr(e, 'extra_data', 'None'), + 'details': getattr(e, 'details', 'None'), + 'response': getattr(getattr(e, 'response', ''), + 'text', 'None') + } + } + self.ansible.fail_json(**params) + # if we got to this place, modules didn't exit + self.ansible.exit_json(**self.results) diff --git a/ansible_collections/openstack/cloud/plugins/modules/__init__.py b/ansible_collections/openstack/cloud/plugins/modules/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/__init__.py diff --git a/ansible_collections/openstack/cloud/plugins/modules/address_scope.py b/ansible_collections/openstack/cloud/plugins/modules/address_scope.py new file mode 100644 index 00000000..eb5b187a --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/address_scope.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2021 by Uemit Seren <uemit.seren@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: address_scope +short_description: Create or delete address scopes from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Delete address scopes from OpenStack. +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Name to be give to the address scope + required: true + type: str + project: + description: + - Unique name or ID of the project. + type: str + ip_version: + description: + - The IP version of the subnet 4 or 6 + default: '4' + type: str + choices: ['4', '6'] + shared: + description: + - Whether this address scope is shared or not. + type: bool + default: 'no' + extra_specs: + description: + - Dictionary with extra key/value pairs passed to the API + required: false + default: {} + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create an IPv4 address scope. +- openstack.cloud.address_scope: + cloud: mycloud + state: present + name: my_adress_scope + +# Create a shared IPv6 address scope for a given project. +- openstack.cloud.address_scope: + cloud: mycloud + state: present + ip_version: 6 + name: ipv6_address_scope + project: myproj + +# Delete address scope. +- openstack.cloud.address_scope: + cloud: mycloud + state: absent + name: my_adress_scope +''' + +RETURN = ''' +address_scope: + description: Dictionary describing the address scope. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Address Scope ID. + type: str + sample: "474acfe5-be34-494c-b339-50f06aa143e4" + name: + description: Address Scope name. + type: str + sample: "my_address_scope" + tenant_id: + description: The tenant ID. + type: str + sample: "861174b82b43463c9edc5202aadc60ef" + ip_version: + description: The IP version of the subnet 4 or 6. + type: str + sample: "4" + is_shared: + description: Indicates whether this address scope is shared across all tenants. + type: bool + sample: false + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class AddressScopeModule(OpenStackModule): + argument_spec = dict( + state=dict(default='present', choices=['absent', 'present']), + name=dict(required=True), + shared=dict(default=False, type='bool'), + ip_version=dict(type='str', default='4', choices=['4', '6']), + project=dict(default=None), + extra_specs=dict(type='dict', default=dict()) + ) + + def _needs_update(self, address_scope, filters=None): + """Decide if the given address_scope needs an update. + """ + ip_version = int(self.params['ip_version']) + if address_scope['is_shared'] != self.params['shared']: + return True + if ip_version and address_scope['ip_version'] != ip_version: + self.fail_json(msg='Cannot update ip_version in existing address scope') + return False + + def _system_state_change(self, address_scope, filters=None): + """Check if the system state would be changed.""" + state = self.params['state'] + if state == 'absent' and address_scope: + return True + if state == 'present': + if not address_scope: + return True + return self._needs_update(address_scope, filters) + return False + + def run(self): + + state = self.params['state'] + name = self.params['name'] + shared = self.params['shared'] + ip_version = self.params['ip_version'] + project = self.params['project'] + extra_specs = self.params['extra_specs'] + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail(msg='Project %s could not be found' % project) + project_id = proj['id'] + else: + project_id = self.conn.current_project_id + + address_scope = self.conn.network.find_address_scope(name_or_id=name) + if self.ansible.check_mode: + self.exit_json( + changed=self._system_state_change(address_scope) + ) + + if state == 'present': + changed = False + + if not address_scope: + kwargs = dict( + name=name, + ip_version=ip_version, + is_shared=shared, + tenant_id=project_id) + dup_args = set(kwargs.keys()) & set(extra_specs.keys()) + if dup_args: + raise ValueError('Duplicate key(s) {0} in extra_specs' + .format(list(dup_args))) + kwargs = dict(kwargs, **extra_specs) + address_scope = self.conn.network.create_address_scope(**kwargs) + changed = True + else: + if self._needs_update(address_scope): + address_scope = self.conn.network.update_address_scope(address_scope['id'], is_shared=shared) + changed = True + else: + changed = False + self.exit_json(changed=changed, address_scope=address_scope, id=address_scope['id']) + + elif state == 'absent': + if not address_scope: + self.exit(changed=False) + else: + self.conn.network.delete_address_scope(address_scope['id']) + self.exit_json(changed=True) + + +def main(): + module = AddressScopeModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/auth.py b/ansible_collections/openstack/cloud/plugins/modules/auth.py new file mode 100644 index 00000000..1f2c516e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/auth.py @@ -0,0 +1,62 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: auth +short_description: Retrieve an auth token +author: OpenStack Ansible SIG +description: + - Retrieve an auth token from an OpenStack Cloud +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Authenticate to the cloud and retrieve the service catalog + openstack.cloud.auth: + cloud: rax-dfw + +- name: Show service catalog + debug: + var: service_catalog +''' + +RETURN = ''' +auth_token: + description: Openstack API Auth Token + returned: success + type: str +service_catalog: + description: A dictionary of available API endpoints + returned: success + type: dict +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class AuthModule(OpenStackModule): + argument_spec = dict() + module_kwargs = dict() + + def run(self): + self.exit_json( + changed=False, + ansible_facts=dict( + auth_token=self.conn.auth_token, + service_catalog=self.conn.service_catalog)) + + +def main(): + module = AuthModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/baremetal_inspect.py b/ansible_collections/openstack/cloud/plugins/modules/baremetal_inspect.py new file mode 100644 index 00000000..f7d90d1c --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/baremetal_inspect.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2015-2016, Hewlett Packard Enterprise Development Company LP +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: baremetal_inspect +short_description: Explicitly triggers baremetal node introspection in ironic. +author: OpenStack Ansible SIG +description: + - Requests Ironic to set a node into inspect state in order to collect metadata regarding the node. + This command may be out of band or in-band depending on the ironic driver configuration. + This is only possible on nodes in 'manageable' and 'available' state. +options: + mac: + description: + - unique mac address that is used to attempt to identify the host. + type: str + uuid: + description: + - globally unique identifier (UUID) to identify the host. + type: str + name: + description: + - unique name identifier to identify the host in Ironic. + type: str + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the endpoint URL for the Ironic API. + Use with "auth" and "auth_type" settings set to None. + type: str + timeout: + description: + - A timeout in seconds to tell the role to wait for the node to complete introspection if wait is set to True. + default: 1200 + type: int + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +ansible_facts: + description: Dictionary of new facts representing discovered properties of the node.. + returned: changed + type: complex + contains: + memory_mb: + description: Amount of node memory as updated in the node properties + type: str + sample: "1024" + cpu_arch: + description: Detected CPU architecture type + type: str + sample: "x86_64" + local_gb: + description: Total size of local disk storage as updated in node properties. + type: str + sample: "10" + cpus: + description: Count of cpu cores defined in the updated node properties. + type: str + sample: "1" +''' + +EXAMPLES = ''' +# Invoke node inspection +- openstack.cloud.baremetal_inspect: + name: "testnode1" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( + IronicModule, + ironic_argument_spec, +) +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_module_kwargs, + openstack_cloud_from_module +) + + +def _choose_id_value(module): + if module.params['uuid']: + return module.params['uuid'] + if module.params['name']: + return module.params['name'] + return None + + +def main(): + argument_spec = ironic_argument_spec( + uuid=dict(required=False), + name=dict(required=False), + mac=dict(required=False), + timeout=dict(default=1200, type='int', required=False), + ) + module_kwargs = openstack_module_kwargs() + module = IronicModule(argument_spec, **module_kwargs) + + sdk, cloud = openstack_cloud_from_module(module) + try: + if module.params['name'] or module.params['uuid']: + server = cloud.get_machine(_choose_id_value(module)) + elif module.params['mac']: + server = cloud.get_machine_by_mac(module.params['mac']) + else: + module.fail_json(msg="The worlds did not align, " + "the host was not found as " + "no name, uuid, or mac was " + "defined.") + if server: + cloud.inspect_machine(server['uuid'], module.params['wait']) + # TODO(TheJulia): diff properties, ?and ports? and determine + # if a change occurred. In theory, the node is always changed + # if introspection is able to update the record. + module.exit_json(changed=True, + ansible_facts=server['properties']) + + else: + module.fail_json(msg="node not found.") + + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/baremetal_node.py b/ansible_collections/openstack/cloud/plugins/modules/baremetal_node.py new file mode 100644 index 00000000..1adb560d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/baremetal_node.py @@ -0,0 +1,441 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2014, Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: baremetal_node +short_description: Create/Delete Bare Metal Resources from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Remove Ironic nodes from OpenStack. +options: + state: + description: + - Indicates desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + uuid: + description: + - globally unique identifier (UUID) to be given to the resource. Will + be auto-generated if not specified, and name is specified. + - Definition of a UUID will always take precedence to a name value. + type: str + name: + description: + - unique name identifier to be given to the resource. + type: str + driver: + description: + - The name of the Ironic Driver to use with this node. + - Required when I(state=present) + type: str + chassis_uuid: + description: + - Associate the node with a pre-defined chassis. + type: str + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the + endpoint URL for the Ironic API. Use with "auth" and "auth_type" + settings set to None. + type: str + resource_class: + description: + - The specific resource type to which this node belongs. + type: str + bios_interface: + description: + - The bios interface for this node, e.g. "no-bios". + type: str + boot_interface: + description: + - The boot interface for this node, e.g. "pxe". + type: str + console_interface: + description: + - The console interface for this node, e.g. "no-console". + type: str + deploy_interface: + description: + - The deploy interface for this node, e.g. "iscsi". + type: str + inspect_interface: + description: + - The interface used for node inspection, e.g. "no-inspect". + type: str + management_interface: + description: + - The interface for out-of-band management of this node, e.g. + "ipmitool". + type: str + network_interface: + description: + - The network interface provider to use when describing + connections for this node. + type: str + power_interface: + description: + - The interface used to manage power actions on this node, e.g. + "ipmitool". + type: str + raid_interface: + description: + - Interface used for configuring raid on this node. + type: str + rescue_interface: + description: + - Interface used for node rescue, e.g. "no-rescue". + type: str + storage_interface: + description: + - Interface used for attaching and detaching volumes on this node, e.g. + "cinder". + type: str + vendor_interface: + description: + - Interface for all vendor-specific actions on this node, e.g. + "no-vendor". + type: str + driver_info: + description: + - Information for this server's driver. Will vary based on which + driver is in use. Any sub-field which is populated will be validated + during creation. For compatibility reasons sub-fields `power`, + `deploy`, `management` and `console` are flattened. + required: true + type: dict + nics: + description: + - 'A list of network interface cards, eg, " - mac: aa:bb:cc:aa:bb:cc"' + required: true + type: list + elements: dict + suboptions: + mac: + description: The MAC address of the network interface card. + type: str + required: true + properties: + description: + - Definition of the physical characteristics of this server, used for scheduling purposes + type: dict + suboptions: + cpu_arch: + description: + - CPU architecture (x86_64, i686, ...) + default: x86_64 + cpus: + description: + - Number of CPU cores this machine has + default: 1 + ram: + description: + - amount of RAM this machine has, in MB + default: 1 + disk_size: + description: + - size of first storage device in this machine (typically /dev/sda), in GB + default: 1 + capabilities: + description: + - special capabilities for the node, such as boot_option, node_role etc + (see U(https://docs.openstack.org/ironic/latest/install/advanced.html) + for more information) + default: "" + root_device: + description: + - Root disk device hints for deployment. + - See U(https://docs.openstack.org/ironic/latest/install/advanced.html#specifying-the-disk-for-deployment-root-device-hints) + for allowed hints. + default: "" + skip_update_of_masked_password: + description: + - Allows the code that would assert changes to nodes to skip the + update if the change is a single line consisting of the password + field. + - As of Kilo, by default, passwords are always masked to API + requests, which means the logic as a result always attempts to + re-assert the password field. + - C(skip_update_of_driver_password) is deprecated alias and will be removed in openstack.cloud 2.0.0. + type: bool + aliases: + - skip_update_of_driver_password +requirements: + - "python >= 3.6" + - "openstacksdk" + - "jsonpatch" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Enroll a node with some basic properties and driver info +- openstack.cloud.baremetal_node: + cloud: "devstack" + driver: "pxe_ipmitool" + uuid: "00000000-0000-0000-0000-000000000002" + properties: + cpus: 2 + cpu_arch: "x86_64" + ram: 8192 + disk_size: 64 + capabilities: "boot_option:local" + root_device: + wwn: "0x4000cca77fc4dba1" + nics: + - mac: "aa:bb:cc:aa:bb:cc" + - mac: "dd:ee:ff:dd:ee:ff" + driver_info: + ipmi_address: "1.2.3.4" + ipmi_username: "admin" + ipmi_password: "adminpass" + chassis_uuid: "00000000-0000-0000-0000-000000000001" + +''' + +try: + import jsonpatch + HAS_JSONPATCH = True +except ImportError: + HAS_JSONPATCH = False + + +from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( + IronicModule, + ironic_argument_spec, +) +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_module_kwargs, + openstack_cloud_from_module +) + + +_PROPERTIES = { + 'cpu_arch': 'cpu_arch', + 'cpus': 'cpus', + 'ram': 'memory_mb', + 'disk_size': 'local_gb', + 'capabilities': 'capabilities', + 'root_device': 'root_device', +} + + +def _parse_properties(module): + """Convert ansible properties into native ironic values. + + Also filter out any properties that are not set. + """ + p = module.params['properties'] + return {to_key: p[from_key] for (from_key, to_key) in _PROPERTIES.items() + if p.get(from_key) is not None} + + +def _parse_driver_info(sdk, module): + info = module.params['driver_info'].copy() + for deprecated in ('power', 'console', 'management', 'deploy'): + if deprecated in info: + info.update(info.pop(deprecated)) + module.deprecate("Suboption %s of the driver_info parameter of " + "'openstack.cloud.baremetal_node' is deprecated" + % deprecated, version='2.0.0', + collection_name='openstack.cloud') + return info + + +def _choose_id_value(module): + if module.params['uuid']: + return module.params['uuid'] + if module.params['name']: + return module.params['name'] + return None + + +def _choose_if_password_only(module, patch): + if len(patch) == 1: + if 'password' in patch[0]['path'] and module.params['skip_update_of_masked_password']: + # Return false to abort update as the password appears + # to be the only element in the patch. + return False + return True + + +def _exit_node_not_updated(module, server): + module.exit_json( + changed=False, + result="Node not updated", + uuid=server['uuid'], + provision_state=server['provision_state'] + ) + + +def main(): + argument_spec = ironic_argument_spec( + uuid=dict(required=False), + name=dict(required=False), + driver=dict(required=False), + resource_class=dict(required=False), + bios_interface=dict(required=False), + boot_interface=dict(required=False), + console_interface=dict(required=False), + deploy_interface=dict(required=False), + inspect_interface=dict(required=False), + management_interface=dict(required=False), + network_interface=dict(required=False), + power_interface=dict(required=False), + raid_interface=dict(required=False), + rescue_interface=dict(required=False), + storage_interface=dict(required=False), + vendor_interface=dict(required=False), + driver_info=dict(type='dict', required=True), + nics=dict(type='list', required=True, elements="dict"), + properties=dict(type='dict', default={}), + chassis_uuid=dict(required=False), + skip_update_of_masked_password=dict( + required=False, + type='bool', + aliases=['skip_update_of_driver_password'], + deprecated_aliases=[dict( + name='skip_update_of_driver_password', + version='2.0.0', + collection_name='openstack.cloud')] + ), + state=dict(required=False, default='present', choices=['present', 'absent']) + ) + module_kwargs = openstack_module_kwargs() + module = IronicModule(argument_spec, **module_kwargs) + + if not HAS_JSONPATCH: + module.fail_json(msg='jsonpatch is required for this module') + + node_id = _choose_id_value(module) + + sdk, cloud = openstack_cloud_from_module(module) + try: + server = cloud.get_machine(node_id) + if module.params['state'] == 'present': + if module.params['driver'] is None: + module.fail_json(msg="A driver must be defined in order " + "to set a node to present.") + + properties = _parse_properties(module) + driver_info = _parse_driver_info(sdk, module) + kwargs = dict( + driver=module.params['driver'], + properties=properties, + driver_info=driver_info, + name=module.params['name'], + ) + optional_field_names = ('resource_class', + 'bios_interface', + 'boot_interface', + 'console_interface', + 'deploy_interface', + 'inspect_interface', + 'management_interface', + 'network_interface', + 'power_interface', + 'raid_interface', + 'rescue_interface', + 'storage_interface', + 'vendor_interface') + for i in optional_field_names: + if module.params[i]: + kwargs[i] = module.params[i] + + if module.params['chassis_uuid']: + kwargs['chassis_uuid'] = module.params['chassis_uuid'] + + if server is None: + # Note(TheJulia): Add a specific UUID to the request if + # present in order to be able to re-use kwargs for if + # the node already exists logic, since uuid cannot be + # updated. + if module.params['uuid']: + kwargs['uuid'] = module.params['uuid'] + + server = cloud.register_machine(module.params['nics'], + **kwargs) + module.exit_json(changed=True, uuid=server['uuid'], + provision_state=server['provision_state']) + else: + # TODO(TheJulia): Presently this does not support updating + # nics. Support needs to be added. + # + # Note(TheJulia): This message should never get logged + # however we cannot realistically proceed if neither a + # name or uuid was supplied to begin with. + if not node_id: + module.fail_json(msg="A uuid or name value " + "must be defined") + + # Note(TheJulia): Constructing the configuration to compare + # against. The items listed in the server_config block can + # be updated via the API. + + server_config = dict( + driver=server['driver'], + properties=server['properties'], + driver_info=server['driver_info'], + name=server['name'], + ) + + # Add the pre-existing chassis_uuid only if + # it is present in the server configuration. + if hasattr(server, 'chassis_uuid'): + server_config['chassis_uuid'] = server['chassis_uuid'] + + # Note(TheJulia): If a password is defined and concealed, a + # patch will always be generated and re-asserted. + patch = jsonpatch.JsonPatch.from_diff(server_config, kwargs) + + if not patch: + _exit_node_not_updated(module, server) + elif _choose_if_password_only(module, list(patch)): + # Note(TheJulia): Normally we would allow the general + # exception catch below, however this allows a specific + # message. + try: + server = cloud.patch_machine( + server['uuid'], + list(patch)) + except Exception as e: + module.fail_json(msg="Failed to update node, " + "Error: %s" % e.message) + + # Enumerate out a list of changed paths. + change_list = [] + for change in list(patch): + change_list.append(change['path']) + module.exit_json(changed=True, + result="Node Updated", + changes=change_list, + uuid=server['uuid'], + provision_state=server['provision_state']) + + # Return not updated by default as the conditions were not met + # to update. + _exit_node_not_updated(module, server) + + if module.params['state'] == 'absent': + if not node_id: + module.fail_json(msg="A uuid or name value must be defined " + "in order to remove a node.") + + if server is not None: + cloud.unregister_machine(module.params['nics'], + server['uuid']) + module.exit_json(changed=True, result="deleted") + else: + module.exit_json(changed=False, result="Server not found") + + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/baremetal_node_action.py b/ansible_collections/openstack/cloud/plugins/modules/baremetal_node_action.py new file mode 100644 index 00000000..267e4308 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/baremetal_node_action.py @@ -0,0 +1,362 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2015, Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: baremetal_node_action +short_description: Activate/Deactivate Bare Metal Resources from OpenStack +author: OpenStack Ansible SIG +description: + - Deploy to nodes controlled by Ironic. +options: + name: + description: + - Name of the node to create. + type: str + state: + description: + - Indicates desired state of the resource. + - I(state) can be C('present'), C('absent'), C('maintenance') or C('off'). + default: present + type: str + deploy: + description: + - Indicates if the resource should be deployed. Allows for deployment + logic to be disengaged and control of the node power or maintenance + state to be changed. + type: str + default: 'yes' + uuid: + description: + - globally unique identifier (UUID) to be given to the resource. + type: str + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the + endpoint URL for the Ironic API. Use with "auth" and "auth_type" + settings set to None. + type: str + config_drive: + description: + - A configdrive file or HTTP(S) URL that will be passed along to the + node. + type: raw + instance_info: + description: + - Definition of the instance information which is used to deploy + the node. This information is only required when an instance is + set to present. + type: dict + suboptions: + image_source: + description: + - An HTTP(S) URL where the image can be retrieved from. + image_checksum: + description: + - The checksum of image_source. + image_disk_format: + description: + - The type of image that has been requested to be deployed. + power: + description: + - A setting to allow power state to be asserted allowing nodes + that are not yet deployed to be powered on, and nodes that + are deployed to be powered off. + - I(power) can be C('present'), C('absent'), C('maintenance') or C('off'). + default: present + type: str + maintenance: + description: + - A setting to allow the direct control if a node is in + maintenance mode. + - I(maintenance) can be C('yes'), C('no'), C('True'), or C('False'). + type: str + maintenance_reason: + description: + - A string expression regarding the reason a node is in a + maintenance mode. + type: str + wait: + description: + - A boolean value instructing the module to wait for node + activation or deactivation to complete before returning. + type: bool + default: 'no' + timeout: + description: + - An integer value representing the number of seconds to + wait for the node activation or deactivation to complete. + default: 1800 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Activate a node by booting an image with a configdrive attached +- openstack.cloud.baremetal_node_action: + cloud: "openstack" + uuid: "d44666e1-35b3-4f6b-acb0-88ab7052da69" + state: present + power: present + deploy: True + maintenance: False + config_drive: "http://192.168.1.1/host-configdrive.iso" + instance_info: + image_source: "http://192.168.1.1/deploy_image.img" + image_checksum: "356a6b55ecc511a20c33c946c4e678af" + image_disk_format: "qcow" + delegate_to: localhost + +# Activate a node by booting an image with a configdrive json object +- openstack.cloud.baremetal_node_action: + uuid: "d44666e1-35b3-4f6b-acb0-88ab7052da69" + auth_type: None + ironic_url: "http://192.168.1.1:6385/" + config_drive: + meta_data: + hostname: node1 + public_keys: + default: ssh-rsa AAA...BBB== + instance_info: + image_source: "http://192.168.1.1/deploy_image.img" + image_checksum: "356a6b55ecc511a20c33c946c4e678af" + image_disk_format: "qcow" + delegate_to: localhost +''' + + +from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( + IronicModule, + ironic_argument_spec, +) +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_module_kwargs, + openstack_cloud_from_module +) + + +def _choose_id_value(module): + if module.params['uuid']: + return module.params['uuid'] + if module.params['name']: + return module.params['name'] + return None + + +def _is_true(value): + true_values = [True, 'yes', 'Yes', 'True', 'true', 'present', 'on'] + if value in true_values: + return True + return False + + +def _is_false(value): + false_values = [False, None, 'no', 'No', 'False', 'false', 'absent', 'off'] + if value in false_values: + return True + return False + + +def _check_set_maintenance(module, cloud, node): + if _is_true(module.params['maintenance']): + if _is_false(node['maintenance']): + cloud.set_machine_maintenance_state( + node['uuid'], + True, + reason=module.params['maintenance_reason']) + module.exit_json(changed=True, msg="Node has been set into " + "maintenance mode") + else: + # User has requested maintenance state, node is already in the + # desired state, checking to see if the reason has changed. + if (str(node['maintenance_reason']) not in + str(module.params['maintenance_reason'])): + cloud.set_machine_maintenance_state( + node['uuid'], + True, + reason=module.params['maintenance_reason']) + module.exit_json(changed=True, msg="Node maintenance reason " + "updated, cannot take any " + "additional action.") + elif _is_false(module.params['maintenance']): + if node['maintenance'] is True: + cloud.remove_machine_from_maintenance(node['uuid']) + return True + else: + module.fail_json(msg="maintenance parameter was set but a valid " + "the value was not recognized.") + return False + + +def _check_set_power_state(module, cloud, node): + if 'power on' in str(node['power_state']): + if _is_false(module.params['power']): + # User has requested the node be powered off. + cloud.set_machine_power_off(node['uuid']) + module.exit_json(changed=True, msg="Power requested off") + if 'power off' in str(node['power_state']): + if ( + _is_false(module.params['power']) + and _is_false(module.params['state']) + ): + return False + if ( + _is_false(module.params['power']) + and _is_false(module.params['state']) + ): + module.exit_json( + changed=False, + msg="Power for node is %s, node must be reactivated " + "OR set to state absent" + ) + # In the event the power has been toggled on and + # deployment has been requested, we need to skip this + # step. + if ( + _is_true(module.params['power']) + and _is_false(module.params['deploy']) + ): + # Node is powered down when it is not awaiting to be provisioned + cloud.set_machine_power_on(node['uuid']) + return True + # Default False if no action has been taken. + return False + + +def main(): + argument_spec = ironic_argument_spec( + uuid=dict(required=False), + name=dict(required=False), + instance_info=dict(type='dict', required=False), + config_drive=dict(type='raw', required=False), + state=dict(required=False, default='present'), + maintenance=dict(required=False), + maintenance_reason=dict(required=False), + power=dict(required=False, default='present'), + deploy=dict(required=False, default='yes'), + wait=dict(type='bool', required=False, default=False), + timeout=dict(required=False, type='int', default=1800), + ) + module_kwargs = openstack_module_kwargs() + module = IronicModule(argument_spec, **module_kwargs) + + if ( + module.params['config_drive'] + and not isinstance(module.params['config_drive'], (str, dict)) + ): + config_drive_type = type(module.params['config_drive']) + msg = ('argument config_drive is of type %s and we expected' + ' str or dict') % config_drive_type + module.fail_json(msg=msg) + + node_id = _choose_id_value(module) + + if not node_id: + module.fail_json(msg="A uuid or name value must be defined " + "to use this module.") + sdk, cloud = openstack_cloud_from_module(module) + try: + node = cloud.get_machine(node_id) + + if node is None: + module.fail_json(msg="node not found") + + uuid = node['uuid'] + instance_info = module.params['instance_info'] + changed = False + wait = module.params['wait'] + timeout = module.params['timeout'] + + # User has requested desired state to be in maintenance state. + if module.params['state'] == 'maintenance': + module.params['maintenance'] = True + + if node['provision_state'] in [ + 'cleaning', + 'deleting', + 'wait call-back']: + module.fail_json(msg="Node is in %s state, cannot act upon the " + "request as the node is in a transition " + "state" % node['provision_state']) + # TODO(TheJulia) This is in-development code, that requires + # code in the shade library that is still in development. + if _check_set_maintenance(module, cloud, node): + if node['provision_state'] in 'active': + module.exit_json(changed=True, + result="Maintenance state changed") + changed = True + node = cloud.get_machine(node_id) + + if _check_set_power_state(module, cloud, node): + changed = True + node = cloud.get_machine(node_id) + + if _is_true(module.params['state']): + if _is_false(module.params['deploy']): + module.exit_json( + changed=changed, + result="User request has explicitly disabled " + "deployment logic" + ) + + if 'active' in node['provision_state']: + module.exit_json( + changed=changed, + result="Node already in an active state." + ) + + if instance_info is None: + module.fail_json( + changed=changed, + msg="When setting an instance to present, " + "instance_info is a required variable.") + + # TODO(TheJulia): Update instance info, however info is + # deployment specific. Perhaps consider adding rebuild + # support, although there is a known desire to remove + # rebuild support from Ironic at some point in the future. + cloud.update_machine(uuid, instance_info=instance_info) + cloud.validate_node(uuid) + if not wait: + cloud.activate_node(uuid, module.params['config_drive']) + else: + cloud.activate_node( + uuid, + configdrive=module.params['config_drive'], + wait=wait, + timeout=timeout) + # TODO(TheJulia): Add more error checking.. + module.exit_json(changed=changed, result="node activated") + + elif _is_false(module.params['state']): + if node['provision_state'] not in "deleted": + cloud.update_machine(uuid, instance_info={}) + if not wait: + cloud.deactivate_node(uuid) + else: + cloud.deactivate_node( + uuid, + wait=wait, + timeout=timeout) + + module.exit_json(changed=True, result="deleted") + else: + module.exit_json(changed=False, result="node not found") + else: + module.fail_json(msg="State must be present, absent, " + "maintenance, off") + + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/baremetal_node_info.py b/ansible_collections/openstack/cloud/plugins/modules/baremetal_node_info.py new file mode 100644 index 00000000..8141fcdf --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/baremetal_node_info.py @@ -0,0 +1,555 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2021 by Red Hat, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = ''' +module: baremetal_node_info +short_description: Retrieve information about Bare Metal nodes from OpenStack +author: OpenStack Ansible SIG +description: + - Retrieve information about Bare Metal nodes from OpenStack. +options: + node: + description: + - Name or globally unique identifier (UUID) to identify the host. + type: str + mac: + description: + - Unique mac address that is used to attempt to identify the host. + type: str + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the + endpoint URL for the Ironic API. Use with "auth" and "auth_type" + settings set to None. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about all baremeal nodes +- openstack.cloud.baremetal_node_info: + cloud: "devstack" + register: result +- debug: + msg: "{{ result.baremetal_nodes }}" +# Gather information about a baremeal node +- openstack.cloud.baremetal_node_info: + cloud: "devstack" + node: "00000000-0000-0000-0000-000000000002" + register: result +- debug: + msg: "{{ result.baremetal_nodes }}" +''' + +RETURN = ''' +baremetal_nodes: + description: Bare Metal node list. A subset of the dictionary keys + listed below may be returned, depending on your cloud + provider. + returned: always, but can be null + type: complex + contains: + allocation_uuid: + description: The UUID of the allocation associated with the node. + If not null, will be the same as instance_uuid (the + opposite is not always true). Unlike instance_uuid, + this field is read-only. Please use the Allocation API + to remove allocations. + returned: success + type: str + automated_clean: + description: Indicates whether the node will perform automated + clean or not. + returned: success + type: bool + bios_interface: + description: The bios interface to be used for this node. + returned: success + type: str + boot_interface: + description: The boot interface for a Node, e.g. "pxe". + returned: success + type: str + boot_mode: + description: The boot mode for a node, either "uefi" or "bios" + returned: success + type: str + chassis_uuid: + description: UUID of the chassis associated with this Node. May be + empty or None. + returned: success + type: str + clean_step: + description: The current clean step. + returned: success + type: str + conductor: + description: The conductor currently servicing a node. This field + is read-only. + returned: success + type: str + conductor_group: + description: The conductor group for a node. Case-insensitive + string up to 255 characters, containing a-z, 0-9, _, + -, and .. + returned: success + type: str + console_enabled: + description: Indicates whether console access is enabled or + disabled on this node. + returned: success + type: bool + console_interface: + description: The console interface for a node, e.g. "no-console". + returned: success + type: str + created_at: + description: Bare Metal node created at timestamp. + returned: success + type: str + deploy_interface: + description: The deploy interface for a node, e.g. "direct". + returned: success + type: str + deploy_step: + description: The current deploy step. + returned: success + type: str + driver: + description: The name of the driver. + returned: success + type: str + driver_info: + description: All the metadata required by the driver to manage this + Node. List of fields varies between drivers, and can + be retrieved from the + /v1/drivers/<DRIVER_NAME>/properties resource. + returned: success + type: dict + driver_internal_info: + description: Internal metadata set and stored by the Node's driver. + returned: success + type: dict + extra: + description: A set of one or more arbitrary metadata key and value + pairs. + returned: success + type: dict + fault: + description: The fault indicates the active fault detected by + ironic, typically the Node is in "maintenance mode". + None means no fault has been detected by ironic. + "power failure" indicates ironic failed to retrieve + power state from this node. There are other possible + types, e.g., "clean failure" and "rescue abort + failure". + returned: success + type: str + id: + description: The UUID for the resource. + returned: success + type: str + inspect_interface: + description: The interface used for node inspection. + returned: success + type: str + instance_info: + description: Information used to customize the deployed image. May + include root partition size, a base 64 encoded config + drive, and other metadata. Note that this field is + erased automatically when the instance is deleted + (this is done by requesting the Node provision state + be changed to DELETED). + returned: success + type: dict + instance_uuid: + description: UUID of the Nova instance associated with this Node. + returned: success + type: str + last_error: + description: Any error from the most recent (last) transaction that + started but failed to finish. + returned: success + type: str + maintenance: + description: Whether or not this Node is currently in "maintenance + mode". Setting a Node into maintenance mode removes it + from the available resource pool and halts some + internal automation. This can happen manually (eg, via + an API request) or automatically when Ironic detects a + hardware fault that prevents communication with the + machine. + returned: success + type: bool + maintenance_reason: + description: User-settable description of the reason why this Node + was placed into maintenance mode + returned: success + type: str + management_interface: + description: Interface for out-of-band node management. + returned: success + type: str + name: + description: Human-readable identifier for the Node resource. May + be undefined. Certain words are reserved. + returned: success + type: str + network_interface: + description: Which Network Interface provider to use when plumbing + the network connections for this Node. + returned: success + type: str + owner: + description: A string or UUID of the tenant who owns the object. + returned: success + type: str + portgroups: + description: List of ironic portgroups on this node. + returned: success + type: list + elements: dict + contains: + address: + description: Physical hardware address of this Portgroup, + typically the hardware MAC address. + returned: success + type: str + created_at: + description: The UTC date and time when the resource was + created, ISO 8601 format. + returned: success + type: str + extra: + description: A set of one or more arbitrary metadata key and + value pairs. + returned: success + type: dict + id: + description: The UUID for the resource. + returned: success + type: str + internal_info: + description: Internal metadata set and stored by the Portgroup. + This field is read-only. + returned: success + type: dict + is_standalone_ports_supported: + description: Indicates whether ports that are members of this + portgroup can be used as stand-alone ports. + returned: success + type: bool + mode: + description: Mode of the port group. For possible values, refer + to https://www.kernel.org/doc/Documentation/networking/bonding.txt. + If not specified in a request to create a port + group, it will be set to the value of the + [DEFAULT]default_portgroup_mode configuration + option. When set, can not be removed from the port + group. + returned: success + type: str + name: + description: Human-readable identifier for the Portgroup + resource. May be undefined. + returned: success + type: str + node_id: + description: UUID of the Node this resource belongs to. + returned: success + type: str + ports: + description: List of port UUID's of ports belonging to this + portgroup. + returned: success + type: list + properties: + description: Key/value properties related to the port group's + configuration. + returned: success + type: dict + updated_at: + description: The UTC date and time when the resource was + updated, ISO 8601 format. May be "null". + returned: success + type: str + ports: + description: List of ironic ports on this node. + returned: success + type: list + elements: dict + contains: + address: + description: Physical hardware address of this network Port, + typically the hardware MAC address. + returned: success + type: str + created_at: + description: The UTC date and time when the resource was + created, ISO 8601 format. + returned: success + type: str + extra: + description: A set of one or more arbitrary metadata key and + value pairs. + returned: success + type: dict + id: + description: The UUID for the resource. + returned: success + type: str + internal_info: + description: Internal metadata set and stored by the Port. This + field is read-only. + returned: success + type: dict + local_link_connection: + description: The Port binding profile. If specified, must + contain switch_id (only a MAC address or an + OpenFlow based datapath_id of the switch are + accepted in this field) and port_id (identifier of + the physical port on the switch to which node's + port is connected to) fields. switch_info is an + optional string field to be used to store any + vendor-specific information. + returned: success + type: dict + name: + description: The name of the resource. + returned: success + type: str + node_uuid: + description: UUID of the Node this resource belongs to. + returned: success + type: str + physical_network: + description: The name of the physical network to which a port + is connected. May be empty. + returned: success + type: str + portgroup_uuid: + description: UUID of the Portgroup this resource belongs to. + returned: success + type: str + pxe_enabled: + description: Indicates whether PXE is enabled or disabled on + the Port. + returned: success + type: str + updated_at: + description: The UTC date and time when the resource was + updated, ISO 8601 format. May be "null". + returned: success + type: str + uuid: + description: The UUID for the resource. + returned: success + type: str + power_interface: + description: Interface used for performing power actions on the + node, e.g. "ipmitool". + returned: success + type: str + power_state: + description: The current power state of this Node. Usually, "power + on" or "power off", but may be "None" if Ironic is + unable to determine the power state (eg, due to + hardware failure). + returned: success + type: str + properties: + description: Physical characteristics of this Node. Populated by + ironic-inspector during inspection. May be edited via + the REST API at any time. + returned: success + type: dict + protected: + description: Whether the node is protected from undeploying, + rebuilding and deletion. + returned: success + type: bool + protected_reason: + description: The reason the node is marked as protected. + returned: success + type: str + provision_state: + description: The current provisioning state of this Node. + returned: success + type: str + raid_config: + description: Represents the current RAID configuration of the node. + Introduced with the cleaning feature. + returned: success + type: dict + raid_interface: + description: Interface used for configuring RAID on this node. + returned: success + type: str + rescue_interface: + description: The interface used for node rescue, e.g. "no-rescue". + returned: success + type: str + reservation: + description: The name of an Ironic Conductor host which is holding + a lock on this node, if a lock is held. Usually + "null", but this field can be useful for debugging. + returned: success + type: str + resource_class: + description: A string which can be used by external schedulers to + identify this Node as a unit of a specific type of + resource. For more details, see + https://docs.openstack.org/ironic/latest/install/configure-nova-flavors.html + returned: success + type: str + retired: + description: Whether the node is retired and can hence no longer be + provided, i.e. move from manageable to available, and + will end up in manageable after cleaning (rather than + available). + returned: success + type: bool + retired_reason: + description: The reason the node is marked as retired. + returned: success + type: str + secure_boot: + description: Indicates whether node is currently booted with + secure_boot turned on. + returned: success + type: bool + storage_interface: + description: Interface used for attaching and detaching volumes on + this node, e.g. "cinder". + returned: success + type: str + target_power_state: + description: If a power state transition has been requested, this + field represents the requested (ie, "target") state, + either "power on" or "power off". + returned: success + type: str + target_provision_state: + description: If a provisioning action has been requested, this + field represents the requested (ie, "target") state. + Note that a Node may go through several states during + its transition to this target state. For instance, + when requesting an instance be deployed to an + AVAILABLE Node, the Node may go through the following + state change progression, AVAILABLE -> DEPLOYING -> + DEPLOYWAIT -> DEPLOYING -> ACTIVE + returned: success + type: str + target_raid_config: + description: Represents the requested RAID configuration of the + node, which will be applied when the Node next + transitions through the CLEANING state. Introduced + with the cleaning feature. + returned: success + type: dict + traits: + description: List of traits for this node. + returned: success + type: list + updated_at: + description: Bare Metal node updated at timestamp. + returned: success + type: str + uuid: + description: The UUID for the resource. + returned: success + type: str + vendor_interface: + description: Interface for vendor-specific functionality on this + node, e.g. "no-vendor". + returned: success + type: str +''' + + +from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( + IronicModule, + ironic_argument_spec, +) +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_module_kwargs, + openstack_cloud_from_module +) + + +def cleanup_node_properties(machine, cloud): + # states are links, not useful + machine.pop('states', None) + + for port in machine.ports: + # links are not useful + port.pop('links', None) + # redundant, location is in on machine as well + port.pop('location', None) + + for portgroup in machine.portgroups: + # links are not useful + portgroup.pop('links', None) + # redundant, location is in on machine as well + portgroup.pop('location', None) + # links to ports are not useful, replace with list of port uuid's + portgroup['ports'] = [x.id for x in list( + cloud.baremetal.ports(portgroup=portgroup['id']))] + + +def get_ports_and_portgroups(cloud, machine): + machine.ports = cloud.list_nics_for_machine(machine.uuid) + machine.portgroups = [dict(x) for x in + list(cloud.baremetal.port_groups(node=machine.uuid, + details=True))] + + +def main(): + argument_spec = ironic_argument_spec( + node=dict(required=False), + mac=dict(required=False), + ) + module_kwargs = openstack_module_kwargs() + module_kwargs['supports_check_mode'] = True + + module = IronicModule(argument_spec, **module_kwargs) + + machine = None + machines = list() + + sdk, cloud = openstack_cloud_from_module(module) + try: + if module.params['node']: + machine = cloud.get_machine(module.params['node']) + elif module.params['mac']: + machine = cloud.get_machine_by_mac(module.params['mac']) + + # Fail if node not found + if (module.params['node'] or module.params['mac']) and not machine: + module.fail_json(msg='The baremetal node was not found') + + if machine: + machines.append(machine) + else: + machines = cloud.list_machines() + + for machine in machines: + get_ports_and_portgroups(cloud, machine) + cleanup_node_properties(machine, cloud) + + module.exit_json(changed=False, baremetal_nodes=machines) + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/baremetal_port.py b/ansible_collections/openstack/cloud/plugins/modules/baremetal_port.py new file mode 100644 index 00000000..a72c1da6 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/baremetal_port.py @@ -0,0 +1,373 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# Copyright (c) 2021 by Red Hat, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +module: baremetal_port +short_description: Create/Delete Bare Metal port Resources from OpenStack +author: OpenStack Ansible SIG +description: + - Create, Update and Remove ironic ports from OpenStack. +options: + state: + description: + - Indicates desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + uuid: + description: + - globally unique identifier (UUID) to be given to the resource. Will + be auto-generated if not specified. + type: str + node: + description: + - UUID or Name of the Node this resource belongs to. + type: str + address: + description: + - Physical hardware address of this network Port, typically the + hardware MAC address. + type: str + portgroup: + description: + - UUID or Name of the Portgroup this resource belongs to. + type: str + local_link_connection: + description: + - The Port binding profile. + type: dict + suboptions: + switch_id: + description: + - A MAC address or an OpenFlow based datapath_id of the switch. + type: str + port_id: + description: + - Identifier of the physical port on the switch to which node's + port is connected to. + type: str + switch_info: + description: + - An optional string field to be used to store any vendor-specific + information. + type: str + is_pxe_enabled: + description: + - Whether PXE should be enabled or disabled on the Port. + type: bool + physical_network: + description: + - The name of the physical network to which a port is connected. + type: str + extra: + description: + - A set of one or more arbitrary metadata key and value pairs. + type: dict + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the + endpoint URL for the Ironic API. Use with "auth" and "auth_type" + settings set to None. + type: str + +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create Bare Metal port +- name: Create Bare Metal port + openstack.cloud.baremetal_port: + cloud: devstack + state: present + node: bm-0 + address: fa:16:3e:aa:aa:aa + pxe_enabled: True + local_link_connection: + switch_id: 0a:1b:2c:3d:4e:5f + port_id: Ethernet3/1 + switch_info: switch1 + extra: + something: extra + physical_network: datacenter + register: result +# Delete Bare Metal port +- name: Delete Bare Metal port + openstack.cloud.baremetal_port: + cloud: devstack + state: absent + address: fa:16:3e:aa:aa:aa + register: result +# Update Bare Metal port +- name: Update Bare Metal port + openstack.cloud.baremetal_port: + cloud: devstack + state: present + uuid: 1a85ebca-22bf-42eb-ad9e-f640789b8098 + pxe_enabled: False + local_link_connection: + switch_id: a0:b1:c2:d3:e4:f5 + port_id: Ethernet4/12 + switch_info: switch2 +''' + +RETURN = ''' +id: + description: Unique UUID of the port. + returned: always, but can be null + type: str +result: + description: A short text describing the result. + returned: success + type: str +changes: + description: Map showing from -> to values for properties that was changed + after port update. + returned: success + type: dict +port: + description: A port dictionary, subset of the dictionary keys listed below + may be returned, depending on your cloud provider. + returned: success + type: complex + contains: + address: + description: Physical hardware address of this network Port, + typically the hardware MAC address. + returned: success + type: str + created_at: + description: Bare Metal port created at timestamp. + returned: success + type: str + extra: + description: A set of one or more arbitrary metadata key and value + pairs. + returned: success + type: dict + id: + description: The UUID for the Baremetal Port resource. + returned: success + type: str + internal_info: + description: Internal metadata set and stored by the Port. This + field is read-only. + returned: success + type: dict + is_pxe_enabled: + description: Whether PXE is enabled or disabled on the Port. + returned: success + type: bool + local_link_connection: + description: The Port binding profile. If specified, must contain + switch_id (only a MAC address or an OpenFlow based + datapath_id of the switch are accepted in this field + and port_id (identifier of the physical port on the + switch to which node's port is connected to) fields. + switch_info is an optional string field to be used to + store any vendor-specific information. + returned: success + type: dict + location: + description: Cloud location of this resource (cloud, project, + region, zone) + returned: success + type: dict + name: + description: Bare Metal port name. + returned: success + type: str + node_id: + description: UUID of the Bare Metal Node this resource belongs to. + returned: success + type: str + physical_network: + description: The name of the physical network to which a port is + connected. + returned: success + type: str + port_group_id: + description: UUID of the Portgroup this resource belongs to. + returned: success + type: str + updated_at: + description: Bare Metal port updated at timestamp. + returned: success + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( + IronicModule, + ironic_argument_spec, +) +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_module_kwargs, + openstack_cloud_from_module +) + +_PROP_TO_ATTR_MAP = { + 'pxe_enabled': 'is_pxe_enabled', + 'address': 'address', + 'extra': 'extra', + 'local_link_connection': 'local_link_connection', + 'physical_network': 'physical_network', + 'node_uuid': 'node_id', + 'portgroup_uuid': 'port_group_id', + 'uuid': 'id', +} + + +def find_port(module, cloud): + port = None + if module.params['uuid']: + port = cloud.baremetal.find_port(module.params['uuid']) + elif module.params['address']: + ports = list(cloud.baremetal.ports(address=module.params['address'], + details=True)) + if ports and len(ports) == 1: + port = ports[0] + elif len(ports) > 1: + module.fail_json( + msg="Multiple ports with address {address} found. A uuid must " + "be defined in order to identify the correct port" + .format(address=module.params['address'])) + + return port + + +def add_port(module, cloud): + port = find_port(module, cloud) + if port: + update_port(module, cloud, port=port) + + if not module.params['node'] or not module.params['address']: + module.fail_json( + msg="A Bare Metal node (name or uuid) and an address is required " + "to create a port") + + machine = cloud.get_machine(module.params['node']) + if not machine: + module.fail_json( + msg="Bare Metal node {node} could not be found".format( + node=module.params['node'])) + + module.params['node_uuid'] = machine.id + props = {k: module.params[k] for k in _PROP_TO_ATTR_MAP.keys() + if k in module.params} + port = cloud.baremetal.create_port(**props) + port_dict = port.to_dict() + port_dict.pop('links', None) + module.exit_json( + changed=True, + result="Port successfully created", + changes=None, + port=port_dict, + id=port_dict['id']) + + +def update_port(module, cloud, port=None): + if not port: + port = find_port(module, cloud) + + if module.params['node']: + machine = cloud.get_machine(module.params['node']) + if machine: + module.params['node_uuid'] = machine.id + + old_props = {k: port[v] for k, v in _PROP_TO_ATTR_MAP.items()} + new_props = {k: module.params[k] for k in _PROP_TO_ATTR_MAP.keys() + if k in module.params and module.params[k] is not None} + prop_diff = {k: new_props[k] for k in _PROP_TO_ATTR_MAP.keys() + if k in new_props and old_props[k] != new_props[k]} + + if not prop_diff: + port_dict = port.to_dict() + port_dict.pop('links', None) + module.exit_json( + changed=False, + result="No port update required", + changes=None, + port=port_dict, + id=port_dict['id']) + + port = cloud.baremetal.update_port(port.id, **prop_diff) + port_dict = port.to_dict() + port_dict.pop('links', None) + module.exit_json( + changed=True, + result="Port successfully updated", + changes={k: {'to': new_props[k], 'from': old_props[k]} + for k in prop_diff}, + port=port_dict, + id=port_dict['id']) + + +def remove_port(module, cloud): + if not module.params['uuid'] and not module.params['address']: + module.fail_json( + msg="A uuid or an address value must be defined in order to " + "remove a port.") + if module.params['uuid']: + port = cloud.baremetal.delete_port(module.params['uuid']) + if not port: + module.exit_json( + changed=False, + result="Port not found", + changes=None, + id=module.params['uuid']) + else: + port = find_port(module, cloud) + if not port: + module.exit_json( + changed=False, + result="Port not found", + changes=None, + id=None) + port = cloud.baremetal.delete_port(port.id) + + module.exit_json( + changed=True, + result="Port successfully removed", + changes=None, + id=port.id) + + +def main(): + argument_spec = ironic_argument_spec( + uuid=dict(required=False), + node=dict(required=False), + address=dict(required=False), + portgroup=dict(required=False), + local_link_connection=dict(required=False, type='dict'), + is_pxe_enabled=dict(required=False, type='bool'), + physical_network=dict(required=False), + extra=dict(required=False, type='dict'), + state=dict(required=False, + default='present', + choices=['present', 'absent']) + ) + + module_kwargs = openstack_module_kwargs() + module = IronicModule(argument_spec, **module_kwargs) + + module.params['pxe_enabled'] = module.params.pop('is_pxe_enabled', None) + + sdk, cloud = openstack_cloud_from_module(module) + try: + if module.params['state'] == 'present': + add_port(module, cloud) + + if module.params['state'] == 'absent': + remove_port(module, cloud) + + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/baremetal_port_info.py b/ansible_collections/openstack/cloud/plugins/modules/baremetal_port_info.py new file mode 100644 index 00000000..d70c284d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/baremetal_port_info.py @@ -0,0 +1,208 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# Copyright (c) 2021 by Red Hat, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +module: baremetal_port_info +short_description: Retrieve information about Bare Metal ports from OpenStack +author: OpenStack Ansible SIG +description: + - Retrieve information about Bare Metal ports from OpenStack. +options: + uuid: + description: + - Name or globally unique identifier (UUID) to identify the port. + type: str + address: + description: + - Physical hardware address of this network Port, typically the + hardware MAC address. + type: str + node: + description: + - Name or globally unique identifier (UUID) to identify a Baremetal + Node. + type: str + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the + endpoint URL for the Ironic API. Use with "auth" and "auth_type" + settings set to None. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about all baremetal ports +- openstack.cloud.baremetal_port_info: + cloud: devstack + register: result +# Gather information about a baremetal port by address +- openstack.cloud.baremetal_port_info: + cloud: devstack + address: fa:16:3e:aa:aa:aa + register: result +# Gather information about a baremetal port by address +- openstack.cloud.baremetal_port_info: + cloud: devstack + uuid: a2b6bd99-77b9-43f0-9ddc-826568e68dec + register: result +# Gather information about a baremetal ports associated with a baremetal node +- openstack.cloud.baremetal_port_info: + cloud: devstack + node: bm-0 + register: result +''' + +RETURN = ''' +baremetal_ports: + description: Bare Metal port list. A subset of the dictionary keys + listed below may be returned, depending on your cloud + provider. + returned: always, but can be null + type: list + elements: dict + contains: + address: + description: Physical hardware address of this network Port, + typically the hardware MAC address. + returned: success + type: str + created_at: + description: Bare Metal port created at timestamp. + returned: success + type: str + extra: + description: A set of one or more arbitrary metadata key and + value pairs. + returned: success + type: dict + id: + description: The UUID for the Baremetal Port resource. + returned: success + type: str + internal_info: + description: Internal metadata set and stored by the Port. This + field is read-only. + returned: success + type: dict + is_pxe_enabled: + description: Whether PXE is enabled or disabled on the Port. + returned: success + type: bool + local_link_connection: + description: The Port binding profile. + returned: success + type: dict + contains: + switch_id: + description: A MAC address or an OpenFlow based datapath_id of + the switch. + type: str + port_id: + description: Identifier of the physical port on the switch to + which node's port is connected to. + type: str + switch_info: + description: An optional string field to be used to store any + vendor-specific information. + type: str + location: + description: Cloud location of this resource (cloud, project, + region, zone) + returned: success + type: dict + name: + description: Bare Metal port name. + returned: success + type: str + node_id: + description: UUID of the Bare Metal Node this resource belongs to. + returned: success + type: str + physical_network: + description: The name of the physical network to which a port is + connected. + returned: success + type: str + port_group_id: + description: UUID of the Portgroup this resource belongs to. + returned: success + type: str + updated_at: + description: Bare Metal port updated at timestamp. + returned: success + type: str +''' + + +from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( + IronicModule, + ironic_argument_spec, +) +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_module_kwargs, + openstack_cloud_from_module +) + + +def main(): + argument_spec = ironic_argument_spec( + uuid=dict(required=False), + address=dict(required=False), + node=dict(required=False), + ) + module_kwargs = openstack_module_kwargs() + module_kwargs['supports_check_mode'] = True + module = IronicModule(argument_spec, **module_kwargs) + + ports = list() + sdk, cloud = openstack_cloud_from_module(module) + try: + if module.params['uuid']: + port = cloud.baremetal.find_port(module.params['uuid']) + if not port: + module.fail_json( + msg='Baremetal port with uuid {uuid} was not found' + .format(uuid=module.params['uuid'])) + ports.append(port) + + elif module.params['address']: + ports = list( + cloud.baremetal.ports(address=module.params['address'], + details=True)) + if not ports: + module.fail_json( + msg='Baremetal port with address {address} was not found' + .format(address=module.params['address'])) + + elif module.params['node']: + machine = cloud.get_machine(module.params['node']) + if not machine: + module.fail_json( + msg='Baremetal node {node} was not found' + .format(node=module.params['node'])) + ports = list( + cloud.baremetal.ports(node_uuid=machine.uuid, details=True)) + + else: + ports = list(cloud.baremetal.ports(details=True)) + + # Convert ports to dictionaries and cleanup properties + ports = [port.to_dict() for port in ports] + for port in ports: + # links are not useful + port.pop('links', None) + + module.exit_json(changed=False, baremetal_ports=ports) + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/catalog_service.py b/ansible_collections/openstack/cloud/plugins/modules/catalog_service.py new file mode 100644 index 00000000..6d1962f3 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/catalog_service.py @@ -0,0 +1,190 @@ +#!/usr/bin/python +# Copyright 2016 Sam Yaple +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: catalog_service +short_description: Manage OpenStack Identity services +author: OpenStack Ansible SIG +description: + - Create, update, or delete OpenStack Identity service. If a service + with the supplied name already exists, it will be updated with the + new description and enabled attributes. +options: + name: + description: + - Name of the service + required: true + type: str + description: + description: + - Description of the service + type: str + enabled: + description: + - Is the service enabled + type: bool + default: 'yes' + aliases: ['is_enabled'] + type: + description: + - The type of service + required: true + type: str + aliases: ['service_type'] + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a service for glance +- openstack.cloud.catalog_service: + cloud: mycloud + state: present + name: glance + type: image + description: OpenStack Image Service +# Delete a service +- openstack.cloud.catalog_service: + cloud: mycloud + state: absent + name: glance + type: image +''' + +RETURN = ''' +service: + description: Dictionary describing the service. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Service ID. + type: str + sample: "3292f020780b4d5baf27ff7e1d224c44" + name: + description: Service name. + type: str + sample: "glance" + type: + description: Service type. + type: str + sample: "image" + service_type: + description: Service type. + type: str + sample: "image" + description: + description: Service description. + type: str + sample: "OpenStack Image Service" + enabled: + description: Service status. + type: bool + sample: True +id: + description: The service ID. + returned: On success when I(state) is 'present' + type: str + sample: "3292f020780b4d5baf27ff7e1d224c44" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityCatalogServiceModule(OpenStackModule): + argument_spec = dict( + description=dict(default=None), + enabled=dict(default=True, aliases=['is_enabled'], type='bool'), + name=dict(required=True), + type=dict(required=True, aliases=['service_type']), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, service): + for parameter in ('enabled', 'description', 'type'): + if service[parameter] != self.params[parameter]: + return True + return False + + def _system_state_change(self, service): + state = self.params['state'] + if state == 'absent' and service: + return True + + if state == 'present': + if service is None: + return True + return self._needs_update(service) + + return False + + def run(self): + description = self.params['description'] + enabled = self.params['enabled'] + name = self.params['name'] + state = self.params['state'] + type = self.params['type'] + + services = self.conn.search_services( + name_or_id=name, filters=(dict(type=type) if type else None)) + + service = None + if len(services) > 1: + self.fail_json( + msg='Service name %s and type %s are not unique' + % (name, type)) + elif len(services) == 1: + service = services[0] + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(service)) + + args = {'name': name, 'enabled': enabled, 'type': type} + if description: + args['description'] = description + + if state == 'present': + if service is None: + service = self.conn.create_service(**args) + changed = True + else: + if self._needs_update(service): + service = self.conn.update_service(service, + **args) + changed = True + else: + changed = False + self.exit_json(changed=changed, service=service, id=service.id) + + elif state == 'absent': + if service is None: + changed = False + else: + self.conn.identity.delete_service(service.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityCatalogServiceModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/coe_cluster.py b/ansible_collections/openstack/cloud/plugins/modules/coe_cluster.py new file mode 100644 index 00000000..feb202a3 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/coe_cluster.py @@ -0,0 +1,292 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst IT Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: coe_cluster +short_description: Add/Remove COE cluster from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove COE cluster from the OpenStack Container Infra service. +options: + cluster_template_id: + description: + - The template ID of cluster template. + required: true + type: str + discovery_url: + description: + - Url used for cluster node discovery + type: str + docker_volume_size: + description: + - The size in GB of the docker volume + type: int + flavor_id: + description: + - The flavor of the minion node for this ClusterTemplate + type: str + keypair: + description: + - Name of the keypair to use. + type: str + labels: + description: + - One or more key/value pairs + type: raw + master_flavor_id: + description: + - The flavor of the master node for this ClusterTemplate + type: str + master_count: + description: + - The number of master nodes for this cluster + default: 1 + type: int + name: + description: + - Name that has to be given to the cluster template + required: true + type: str + node_count: + description: + - The number of nodes for this cluster + default: 1 + type: int + state: + description: + - Indicate desired state of the resource. + choices: [present, absent] + default: present + type: str + timeout: + description: + - Timeout for creating the cluster in minutes. Default to 60 mins + if not set + default: 60 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The cluster UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +cluster: + description: Dictionary describing the cluster. + returned: On success when I(state) is 'present' + type: complex + contains: + api_address: + description: + - Api address of cluster master node + type: str + sample: https://172.24.4.30:6443 + cluster_template_id: + description: The cluster_template UUID + type: str + sample: '7b1418c8-cea8-48fc-995d-52b66af9a9aa' + coe_version: + description: + - Version of the COE software currently running in this cluster + type: str + sample: v1.11.1 + container_version: + description: + - "Version of the container software. Example: docker version." + type: str + sample: 1.12.6 + created_at: + description: + - The time in UTC at which the cluster is created + type: str + sample: "2018-08-16T10:29:45+00:00" + create_timeout: + description: + - Timeout for creating the cluster in minutes. Default to 60 if + not set. + type: int + sample: 60 + discovery_url: + description: + - Url used for cluster node discovery + type: str + sample: https://discovery.etcd.io/a42ee38e7113f31f4d6324f24367aae5 + faults: + description: + - Fault info collected from the Heat resources of this cluster + type: dict + sample: {'0': 'ResourceInError: resources[0].resources...'} + flavor_id: + description: + - The flavor of the minion node for this cluster + type: str + sample: c1.c1r1 + keypair: + description: + - Name of the keypair to use. + type: str + sample: mykey + labels: + description: One or more key/value pairs + type: dict + sample: {'key1': 'value1', 'key2': 'value2'} + master_addresses: + description: + - IP addresses of cluster master nodes + type: list + sample: ['172.24.4.5'] + master_count: + description: + - The number of master nodes for this cluster. + type: int + sample: 1 + master_flavor_id: + description: + - The flavor of the master node for this cluster + type: str + sample: c1.c1r1 + name: + description: + - Name that has to be given to the cluster + type: str + sample: k8scluster + node_addresses: + description: + - IP addresses of cluster slave nodes + type: list + sample: ['172.24.4.8'] + node_count: + description: + - The number of master nodes for this cluster. + type: int + sample: 1 + stack_id: + description: + - Stack id of the Heat stack + type: str + sample: '07767ec6-85f5-44cb-bd63-242a8e7f0d9d' + status: + description: Status of the cluster from the heat stack + type: str + sample: 'CREATE_COMLETE' + status_reason: + description: + - Status reason of the cluster from the heat stack + type: str + sample: 'Stack CREATE completed successfully' + updated_at: + description: + - The time in UTC at which the cluster is updated + type: str + sample: '2018-08-16T10:39:25+00:00' + id: + description: + - Unique UUID for this cluster + type: str + sample: '86246a4d-a16c-4a58-9e96ad7719fe0f9d' +''' + +EXAMPLES = ''' +# Create a new Kubernetes cluster +- openstack.cloud.coe_cluster: + name: k8s + cluster_template_id: k8s-ha + keypair: mykey + master_count: 3 + node_count: 5 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class CoeClusterModule(OpenStackModule): + argument_spec = dict( + cluster_template_id=dict(required=True), + discovery_url=dict(default=None), + docker_volume_size=dict(type='int'), + flavor_id=dict(default=None), + keypair=dict(default=None, no_log=False), + labels=dict(default=None, type='raw'), + master_count=dict(type='int', default=1), + master_flavor_id=dict(default=None), + name=dict(required=True), + node_count=dict(type='int', default=1), + state=dict(default='present', choices=['absent', 'present']), + timeout=dict(type='int', default=60), + ) + module_kwargs = dict() + + def _parse_labels(self, labels): + if isinstance(labels, str): + labels_dict = {} + for kv_str in labels.split(","): + k, v = kv_str.split("=") + labels_dict[k] = v + return labels_dict + if not labels: + return {} + return labels + + def run(self): + params = self.params.copy() + + state = self.params['state'] + name = self.params['name'] + cluster_template_id = self.params['cluster_template_id'] + + kwargs = dict( + discovery_url=self.params['discovery_url'], + docker_volume_size=self.params['docker_volume_size'], + flavor_id=self.params['flavor_id'], + keypair=self.params['keypair'], + labels=self._parse_labels(params['labels']), + master_count=self.params['master_count'], + master_flavor_id=self.params['master_flavor_id'], + node_count=self.params['node_count'], + create_timeout=self.params['timeout'], + ) + + changed = False + cluster = self.conn.get_coe_cluster( + name_or_id=name, filters={'cluster_template_id': cluster_template_id}) + + if state == 'present': + if not cluster: + cluster = self.conn.create_coe_cluster( + name, cluster_template_id=cluster_template_id, **kwargs) + changed = True + else: + changed = False + + # NOTE (brtknr): At present, create_coe_cluster request returns + # cluster_id as `uuid` whereas get_coe_cluster request returns the + # same field as `id`. This behaviour may change in the future + # therefore try `id` first then `uuid`. + cluster_id = cluster.get('id', cluster.get('uuid')) + cluster['id'] = cluster['uuid'] = cluster_id + self.exit_json(changed=changed, cluster=cluster, id=cluster_id) + elif state == 'absent': + if not cluster: + self.exit_json(changed=False) + else: + self.conn.delete_coe_cluster(name) + self.exit_json(changed=True) + + +def main(): + module = CoeClusterModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/coe_cluster_template.py b/ansible_collections/openstack/cloud/plugins/modules/coe_cluster_template.py new file mode 100644 index 00000000..0596f39b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/coe_cluster_template.py @@ -0,0 +1,388 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst IT Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: coe_cluster_template +short_description: Add/Remove COE cluster template from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove COE cluster template from the OpenStack Container Infra + service. +options: + coe: + description: + - The Container Orchestration Engine for this clustertemplate + choices: [kubernetes, swarm, mesos] + type: str + required: true + dns_nameserver: + description: + - The DNS nameserver address + default: '8.8.8.8' + type: str + docker_storage_driver: + description: + - Docker storage driver + choices: [devicemapper, overlay, overlay2] + type: str + docker_volume_size: + description: + - The size in GB of the docker volume + type: int + external_network_id: + description: + - The external network to attach to the Cluster + type: str + fixed_network: + description: + - The fixed network name to attach to the Cluster + type: str + fixed_subnet: + description: + - The fixed subnet name to attach to the Cluster + type: str + flavor_id: + description: + - The flavor of the minion node for this ClusterTemplate + type: str + floating_ip_enabled: + description: + - Indicates whether created clusters should have a floating ip or not + type: bool + default: true + keypair_id: + description: + - Name or ID of the keypair to use. + type: str + image_id: + description: + - Image id the cluster will be based on + type: str + required: true + labels: + description: + - One or more key/value pairs + type: raw + http_proxy: + description: + - Address of a proxy that will receive all HTTP requests and relay them + The format is a URL including a port number + type: str + https_proxy: + description: + - Address of a proxy that will receive all HTTPS requests and relay + them. The format is a URL including a port number + type: str + master_flavor_id: + description: + - The flavor of the master node for this ClusterTemplate + type: str + master_lb_enabled: + description: + - Indicates whether created clusters should have a load balancer + for master nodes or not + type: bool + default: 'no' + name: + description: + - Name that has to be given to the cluster template + required: true + type: str + network_driver: + description: + - The name of the driver used for instantiating container networks + choices: [flannel, calico, docker] + type: str + no_proxy: + description: + - A comma separated list of IPs for which proxies should not be + used in the cluster + type: str + public: + description: + - Indicates whether the ClusterTemplate is public or not + type: bool + default: 'no' + registry_enabled: + description: + - Indicates whether the docker registry is enabled + type: bool + default: 'no' + server_type: + description: + - Server type for this ClusterTemplate + choices: [vm, bm] + default: vm + type: str + state: + description: + - Indicate desired state of the resource. + choices: [present, absent] + default: present + type: str + tls_disabled: + description: + - Indicates whether the TLS should be disabled + type: bool + default: 'no' + volume_driver: + description: + - The name of the driver used for instantiating container volumes + choices: [cinder, rexray] + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The cluster UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +cluster_template: + description: Dictionary describing the template. + returned: On success when I(state) is 'present' + type: complex + contains: + coe: + description: The Container Orchestration Engine for this clustertemplate + type: str + sample: kubernetes + dns_nameserver: + description: The DNS nameserver address + type: str + sample: '8.8.8.8' + docker_storage_driver: + description: Docker storage driver + type: str + sample: devicemapper + docker_volume_size: + description: The size in GB of the docker volume + type: int + sample: 5 + external_network_id: + description: The external network to attach to the Cluster + type: str + sample: public + fixed_network: + description: The fixed network name to attach to the Cluster + type: str + sample: 07767ec6-85f5-44cb-bd63-242a8e7f0d9d + fixed_subnet: + description: + - The fixed subnet name to attach to the Cluster + type: str + sample: 05567ec6-85f5-44cb-bd63-242a8e7f0d9d + flavor_id: + description: + - The flavor of the minion node for this ClusterTemplate + type: str + sample: c1.c1r1 + floating_ip_enabled: + description: + - Indicates whether created clusters should have a floating ip or not + type: bool + sample: true + keypair_id: + description: + - Name or ID of the keypair to use. + type: str + sample: mykey + image_id: + description: + - Image id the cluster will be based on + type: str + sample: 05567ec6-85f5-44cb-bd63-242a8e7f0e9d + labels: + description: One or more key/value pairs + type: dict + sample: {'key1': 'value1', 'key2': 'value2'} + http_proxy: + description: + - Address of a proxy that will receive all HTTP requests and relay them + The format is a URL including a port number + type: str + sample: http://10.0.0.11:9090 + https_proxy: + description: + - Address of a proxy that will receive all HTTPS requests and relay + them. The format is a URL including a port number + type: str + sample: https://10.0.0.10:8443 + master_flavor_id: + description: + - The flavor of the master node for this ClusterTemplate + type: str + sample: c1.c1r1 + master_lb_enabled: + description: + - Indicates whether created clusters should have a load balancer + for master nodes or not + type: bool + sample: true + name: + description: + - Name that has to be given to the cluster template + type: str + sample: k8scluster + network_driver: + description: + - The name of the driver used for instantiating container networks + type: str + sample: calico + no_proxy: + description: + - A comma separated list of IPs for which proxies should not be + used in the cluster + type: str + sample: 10.0.0.4,10.0.0.5 + public: + description: + - Indicates whether the ClusterTemplate is public or not + type: bool + sample: false + registry_enabled: + description: + - Indicates whether the docker registry is enabled + type: bool + sample: false + server_type: + description: + - Server type for this ClusterTemplate + type: str + sample: vm + tls_disabled: + description: + - Indicates whether the TLS should be disabled + type: bool + sample: false + volume_driver: + description: + - The name of the driver used for instantiating container volumes + type: str + sample: cinder +''' + +EXAMPLES = ''' +# Create a new Kubernetes cluster template +- openstack.cloud.coe_cluster_template: + name: k8s + coe: kubernetes + keypair_id: mykey + image_id: 2a8c9888-9054-4b06-a1ca-2bb61f9adb72 + public: no +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class CoeClusterTemplateModule(OpenStackModule): + argument_spec = dict( + coe=dict(required=True, choices=['kubernetes', 'swarm', 'mesos']), + dns_nameserver=dict(default='8.8.8.8'), + docker_storage_driver=dict(choices=['devicemapper', 'overlay', 'overlay2']), + docker_volume_size=dict(type='int'), + external_network_id=dict(default=None), + fixed_network=dict(default=None), + fixed_subnet=dict(default=None), + flavor_id=dict(default=None), + floating_ip_enabled=dict(type='bool', default=True), + keypair_id=dict(default=None), + image_id=dict(required=True), + labels=dict(default=None, type='raw'), + http_proxy=dict(default=None), + https_proxy=dict(default=None), + master_lb_enabled=dict(type='bool', default=False), + master_flavor_id=dict(default=None), + name=dict(required=True), + network_driver=dict(choices=['flannel', 'calico', 'docker']), + no_proxy=dict(default=None), + public=dict(type='bool', default=False), + registry_enabled=dict(type='bool', default=False), + server_type=dict(default="vm", choices=['vm', 'bm']), + state=dict(default='present', choices=['absent', 'present']), + tls_disabled=dict(type='bool', default=False), + volume_driver=dict(choices=['cinder', 'rexray']), + ) + module_kwargs = dict() + + def _parse_labels(self, labels): + if isinstance(labels, str): + labels_dict = {} + for kv_str in labels.split(","): + k, v = kv_str.split("=") + labels_dict[k] = v + return labels_dict + if not labels: + return {} + return labels + + def run(self): + params = self.params.copy() + + state = self.params['state'] + name = self.params['name'] + coe = self.params['coe'] + image_id = self.params['image_id'] + + kwargs = dict( + dns_nameserver=self.params['dns_nameserver'], + docker_storage_driver=self.params['docker_storage_driver'], + docker_volume_size=self.params['docker_volume_size'], + external_network_id=self.params['external_network_id'], + fixed_network=self.params['fixed_network'], + fixed_subnet=self.params['fixed_subnet'], + flavor_id=self.params['flavor_id'], + floating_ip_enabled=self.params['floating_ip_enabled'], + keypair_id=self.params['keypair_id'], + labels=self._parse_labels(params['labels']), + http_proxy=self.params['http_proxy'], + https_proxy=self.params['https_proxy'], + master_lb_enabled=self.params['master_lb_enabled'], + master_flavor_id=self.params['master_flavor_id'], + network_driver=self.params['network_driver'], + no_proxy=self.params['no_proxy'], + public=self.params['public'], + registry_enabled=self.params['registry_enabled'], + server_type=self.params['server_type'], + tls_disabled=self.params['tls_disabled'], + volume_driver=self.params['volume_driver'], + ) + + changed = False + template = self.conn.get_coe_cluster_template( + name_or_id=name, filters={'coe': coe, 'image_id': image_id}) + + if state == 'present': + if not template: + template = self.conn.create_coe_cluster_template( + name, coe=coe, image_id=image_id, **kwargs) + changed = True + else: + changed = False + + self.exit_json( + changed=changed, cluster_template=template, id=template['uuid']) + elif state == 'absent': + if not template: + self.exit_json(changed=False) + else: + self.conn.delete_coe_cluster_template(name) + self.exit_json(changed=True) + + +def main(): + module = CoeClusterTemplateModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/compute_flavor.py b/ansible_collections/openstack/cloud/plugins/modules/compute_flavor.py new file mode 100644 index 00000000..8a993ca5 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/compute_flavor.py @@ -0,0 +1,274 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: compute_flavor +short_description: Manage OpenStack compute flavors +author: OpenStack Ansible SIG +description: + - Add or remove flavors from OpenStack. +options: + state: + description: + - Indicate desired state of the resource. When I(state) is 'present', + then I(ram), I(vcpus), and I(disk) are all required. There are no + default values for those parameters. + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Flavor name. + required: true + type: str + ram: + description: + - Amount of memory, in MB. + type: int + vcpus: + description: + - Number of virtual CPUs. + type: int + disk: + description: + - Size of local disk, in GB. + default: 0 + type: int + ephemeral: + description: + - Ephemeral space size, in GB. + default: 0 + type: int + swap: + description: + - Swap space size, in MB. + default: 0 + type: int + rxtx_factor: + description: + - RX/TX factor. + default: 1.0 + type: float + is_public: + description: + - Make flavor accessible to the public. + type: bool + default: 'yes' + flavorid: + description: + - ID for the flavor. This is optional as a unique UUID will be + assigned if a value is not specified. + default: "auto" + type: str + extra_specs: + description: + - Metadata dictionary + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: "Create 'tiny' flavor with 1024MB of RAM, 1 virtual CPU, and 10GB of local disk, and 10GB of ephemeral." + openstack.cloud.compute_flavor: + cloud: mycloud + state: present + name: tiny + ram: 1024 + vcpus: 1 + disk: 10 + ephemeral: 10 + +- name: "Delete 'tiny' flavor" + openstack.cloud.compute_flavor: + cloud: mycloud + state: absent + name: tiny + +- name: Create flavor with metadata + openstack.cloud.compute_flavor: + cloud: mycloud + state: present + name: tiny + ram: 1024 + vcpus: 1 + disk: 10 + extra_specs: + "quota:disk_read_iops_sec": 5000 + "aggregate_instance_extra_specs:pinned": false +''' + +RETURN = ''' +flavor: + description: Dictionary describing the flavor. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Flavor ID. + returned: success + type: str + sample: "515256b8-7027-4d73-aa54-4e30a4a4a339" + name: + description: Flavor name. + returned: success + type: str + sample: "tiny" + disk: + description: Size of local disk, in GB. + returned: success + type: int + sample: 10 + ephemeral: + description: Ephemeral space size, in GB. + returned: success + type: int + sample: 10 + ram: + description: Amount of memory, in MB. + returned: success + type: int + sample: 1024 + swap: + description: Swap space size, in MB. + returned: success + type: int + sample: 100 + vcpus: + description: Number of virtual CPUs. + returned: success + type: int + sample: 2 + is_public: + description: Make flavor accessible to the public. + returned: success + type: bool + sample: true + extra_specs: + description: Flavor metadata + returned: success + type: dict + sample: + "quota:disk_read_iops_sec": 5000 + "aggregate_instance_extra_specs:pinned": false +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ComputeFlavorModule(OpenStackModule): + argument_spec = dict( + state=dict(required=False, default='present', + choices=['absent', 'present']), + name=dict(required=True), + + # required when state is 'present' + ram=dict(required=False, type='int'), + vcpus=dict(required=False, type='int'), + + disk=dict(required=False, default=0, type='int'), + ephemeral=dict(required=False, default=0, type='int'), + swap=dict(required=False, default=0, type='int'), + rxtx_factor=dict(required=False, default=1.0, type='float'), + is_public=dict(required=False, default=True, type='bool'), + flavorid=dict(required=False, default="auto"), + extra_specs=dict(required=False, default=None, type='dict'), + ) + + module_kwargs = dict( + required_if=[ + ('state', 'present', ['ram', 'vcpus', 'disk']) + ], + supports_check_mode=True + ) + + def _system_state_change(self, flavor): + state = self.params['state'] + if state == 'present' and not flavor: + return True + if state == 'absent' and flavor: + return True + return False + + def run(self): + state = self.params['state'] + name = self.params['name'] + extra_specs = self.params['extra_specs'] or {} + + flavor = self.conn.get_flavor(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(flavor)) + + if state == 'present': + old_extra_specs = {} + require_update = False + + if flavor: + old_extra_specs = flavor['extra_specs'] + if flavor['swap'] == "": + flavor['swap'] = 0 + for param_key in ['ram', 'vcpus', 'disk', 'ephemeral', + 'swap', 'rxtx_factor', 'is_public']: + if self.params[param_key] != flavor[param_key]: + require_update = True + break + flavorid = self.params['flavorid'] + if flavor and require_update: + self.conn.delete_flavor(name) + old_extra_specs = {} + if flavorid == 'auto': + flavorid = flavor['id'] + flavor = None + + if not flavor: + flavor = self.conn.create_flavor( + name=name, + ram=self.params['ram'], + vcpus=self.params['vcpus'], + disk=self.params['disk'], + flavorid=flavorid, + ephemeral=self.params['ephemeral'], + swap=self.params['swap'], + rxtx_factor=self.params['rxtx_factor'], + is_public=self.params['is_public'] + ) + changed = True + else: + changed = False + + new_extra_specs = dict([(k, str(v)) for k, v in extra_specs.items()]) + unset_keys = set(old_extra_specs.keys()) - set(extra_specs.keys()) + + if unset_keys and not require_update: + self.conn.unset_flavor_specs(flavor['id'], unset_keys) + + if old_extra_specs != new_extra_specs: + self.conn.set_flavor_specs(flavor['id'], extra_specs) + + changed = (changed or old_extra_specs != new_extra_specs) + + self.exit_json( + changed=changed, flavor=flavor, id=flavor['id']) + + elif state == 'absent': + if flavor: + self.conn.delete_flavor(name) + self.exit_json(changed=True) + self.exit_json(changed=False) + + +def main(): + module = ComputeFlavorModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/compute_flavor_info.py b/ansible_collections/openstack/cloud/plugins/modules/compute_flavor_info.py new file mode 100644 index 00000000..61ee7a5b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/compute_flavor_info.py @@ -0,0 +1,247 @@ +#!/usr/bin/python + +# Copyright (c) 2015 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: compute_flavor_info +short_description: Retrieve information about one or more flavors +author: OpenStack Ansible SIG +description: + - Retrieve information about available OpenStack instance flavors. By default, + information about ALL flavors are retrieved. Filters can be applied to get + information for only matching flavors. For example, you can filter on the + amount of RAM available to the flavor, or the number of virtual CPUs + available to the flavor, or both. When specifying multiple filters, + *ALL* filters must match on a flavor before that flavor is returned as + a fact. + - This module was called C(openstack.cloud.compute_flavor_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.compute_flavor_info) module no longer returns C(ansible_facts)! +notes: + - The result contains a list of unsorted flavors. +options: + name: + description: + - A flavor name. Cannot be used with I(ram) or I(vcpus) or I(ephemeral). + type: str + ram: + description: + - "A string used for filtering flavors based on the amount of RAM + (in MB) desired. This string accepts the following special values: + 'MIN' (return flavors with the minimum amount of RAM), and 'MAX' + (return flavors with the maximum amount of RAM)." + + - "A specific amount of RAM may also be specified. Any flavors with this + exact amount of RAM will be returned." + + - "A range of acceptable RAM may be given using a special syntax. Simply + prefix the amount of RAM with one of these acceptable range values: + '<', '>', '<=', '>='. These values represent less than, greater than, + less than or equal to, and greater than or equal to, respectively." + type: str + vcpus: + description: + - A string used for filtering flavors based on the number of virtual + CPUs desired. Format is the same as the I(ram) parameter. + type: str + limit: + description: + - Limits the number of flavors returned. All matching flavors are + returned by default. + type: int + ephemeral: + description: + - A string used for filtering flavors based on the amount of ephemeral + storage. Format is the same as the I(ram) parameter + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about all available flavors +- openstack.cloud.compute_flavor_info: + cloud: mycloud + register: result + +- debug: + msg: "{{ result.openstack_flavors }}" + +# Gather information for the flavor named "xlarge-flavor" +- openstack.cloud.compute_flavor_info: + cloud: mycloud + name: "xlarge-flavor" + +# Get all flavors that have exactly 512 MB of RAM. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: "512" + +# Get all flavors that have 1024 MB or more of RAM. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: ">=1024" + +# Get a single flavor that has the minimum amount of RAM. Using the 'limit' +# option will guarantee only a single flavor is returned. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: "MIN" + limit: 1 + +# Get all flavors with 1024 MB of RAM or more, AND exactly 2 virtual CPUs. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: ">=1024" + vcpus: "2" + +# Get all flavors with 1024 MB of RAM or more, exactly 2 virtual CPUs, and +# less than 30gb of ephemeral storage. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: ">=1024" + vcpus: "2" + ephemeral: "<30" +''' + + +RETURN = ''' +openstack_flavors: + description: Dictionary describing the flavors. + returned: On success. + type: complex + contains: + id: + description: Flavor ID. + returned: success + type: str + sample: "515256b8-7027-4d73-aa54-4e30a4a4a339" + name: + description: Flavor name. + returned: success + type: str + sample: "tiny" + description: + description: Description of the flavor + returned: success + type: str + sample: "Small flavor" + is_disabled: + description: Wether the flavor is enabled or not + returned: success + type: bool + sample: False + rxtx_factor: + description: Factor to be multiplied by the rxtx_base property of + the network it is attached to in order to have a + different bandwidth cap. + returned: success + type: float + sample: 1.0 + extra_specs: + description: Optional parameters to configure different flavors + options. + returned: success + type: dict + sample: "{'hw_rng:allowed': True}" + disk: + description: Size of local disk, in GB. + returned: success + type: int + sample: 10 + ephemeral: + description: Ephemeral space size, in GB. + returned: success + type: int + sample: 10 + ram: + description: Amount of memory, in MB. + returned: success + type: int + sample: 1024 + swap: + description: Swap space size, in MB. + returned: success + type: int + sample: 100 + vcpus: + description: Number of virtual CPUs. + returned: success + type: int + sample: 2 + is_public: + description: Make flavor accessible to the public. + returned: success + type: bool + sample: true +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ComputeFlavorInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + ram=dict(required=False, default=None), + vcpus=dict(required=False, default=None), + limit=dict(required=False, default=None, type='int'), + ephemeral=dict(required=False, default=None), + ) + module_kwargs = dict( + mutually_exclusive=[ + ['name', 'ram'], + ['name', 'vcpus'], + ['name', 'ephemeral'] + ], + supports_check_mode=True + ) + + deprecated_names = ('openstack.cloud.compute_flavor_facts') + + def run(self): + name = self.params['name'] + vcpus = self.params['vcpus'] + ram = self.params['ram'] + ephemeral = self.params['ephemeral'] + limit = self.params['limit'] + + filters = {} + if vcpus: + filters['vcpus'] = vcpus + if ram: + filters['ram'] = ram + if ephemeral: + filters['ephemeral'] = ephemeral + + if name: + # extra_specs are exposed in the flavor representation since Rocky, so we do not + # need get_extra_specs=True which is not available in OpenStack SDK 0.36 (Train) + # Ref.: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html + flavor = self.conn.compute.find_flavor(name) + flavors = [flavor] if flavor else [] + + else: + flavors = list(self.conn.compute.flavors()) + if filters: + flavors = self.conn.range_search(flavors, filters) + + if limit is not None: + flavors = flavors[:limit] + + # Transform entries to dict + flavors = [flavor.to_dict(computed=True) for flavor in flavors] + self.exit_json(changed=False, openstack_flavors=flavors) + + +def main(): + module = ComputeFlavorInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/compute_service_info.py b/ansible_collections/openstack/cloud/plugins/modules/compute_service_info.py new file mode 100644 index 00000000..6665dd63 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/compute_service_info.py @@ -0,0 +1,122 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: compute_service_info +short_description: Retrieve information about one or more OpenStack compute services +author: OpenStack Ansible SIG +description: + - Retrieve information about nova compute services +options: + binary: + description: + - Filter by service binary type. Requires openstacksdk>=0.53. + type: str + host: + description: + - Filter by service host. Requires openstacksdk>=0.53. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about compute services +- openstack.cloud.compute_service_info: + cloud: awesomecloud + binary: "nova-compute" + host: "localhost" + register: result +- openstack.cloud.compute_service_info: + cloud: awesomecloud + register: result +- debug: + msg: "{{ result.openstack_compute_services }}" +''' + + +RETURN = ''' +openstack_compute_services: + description: has all the OpenStack information about compute services + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + binary: + description: The binary name of the service. + returned: success + type: str + host: + description: The name of the host. + returned: success + type: str + disabled_reason: + description: The reason why the service is disabled + returned: success and OpenStack SDK >= 0.53 + type: str + disables_reason: + description: The reason why the service is disabled + returned: success and OpenStack SDK < 0.53 + type: str + availability_zone: + description: The availability zone name. + returned: success + type: str + is_forced_down: + description: If the service has been forced down or nova-compute + returned: success + type: bool + name: + description: Service name + returned: success + type: str + status: + description: The status of the service. One of enabled or disabled. + returned: success + type: str + state: + description: The state of the service. One of up or down. + returned: success + type: str + update_at: + description: The date and time when the resource was updated + returned: success + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ComputeServiceInfoModule(OpenStackModule): + argument_spec = dict( + binary=dict(required=False, default=None, min_ver='0.53.0'), + host=dict(required=False, default=None, min_ver='0.53.0'), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + filters = self.check_versioned(binary=self.params['binary'], host=self.params['host']) + filters = {k: v for k, v in filters.items() if v is not None} + services = self.conn.compute.services(**filters) + services = [service.to_dict(computed=True) for service in services] + self.exit_json(changed=False, openstack_compute_services=services) + + +def main(): + module = ComputeServiceInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/config.py b/ansible_collections/openstack/cloud/plugins/modules/config.py new file mode 100644 index 00000000..94036e49 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/config.py @@ -0,0 +1,76 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: config +short_description: Get OpenStack Client config +description: + - Get I(openstack) client config data from clouds.yaml or environment +notes: + - Facts are placed in the C(openstack.clouds) variable. +options: + clouds: + description: + - List of clouds to limit the return list to. No value means return + information on all configured clouds + required: false + default: [] + type: list + elements: str +requirements: + - "python >= 3.6" + - "openstacksdk" +author: OpenStack Ansible SIG +''' + +EXAMPLES = ''' +- name: Get list of clouds that do not support security groups + openstack.cloud.config: + +- debug: + var: "{{ item }}" + with_items: "{{ openstack.clouds | rejectattr('secgroup_source', 'none') | list }}" + +- name: Get the information back just about the mordred cloud + openstack.cloud.config: + clouds: + - mordred +''' + +try: + import openstack.config + from openstack import exceptions + HAS_OPENSTACKSDK = True +except ImportError: + HAS_OPENSTACKSDK = False + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule(argument_spec=dict( + clouds=dict(required=False, type='list', default=[], elements='str'), + )) + + if not HAS_OPENSTACKSDK: + module.fail_json(msg='openstacksdk is required for this module') + + p = module.params + + try: + config = openstack.config.OpenStackConfig() + clouds = [] + for cloud in config.get_all_clouds(): + if not p['clouds'] or cloud.name in p['clouds']: + cloud.config['name'] = cloud.name + clouds.append(cloud.config) + module.exit_json(ansible_facts=dict(openstack=dict(clouds=clouds))) + except exceptions.ConfigException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/container.py b/ansible_collections/openstack/cloud/plugins/modules/container.py new file mode 100644 index 00000000..23ed38e5 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/container.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2021 by Open Telekom Cloud, operated by T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: object_container +short_description: Manage Swift container. +author: OpenStack Ansible SIG +description: + - Manage Swift container. +options: + container: + description: Name of a container in Swift. + type: str + required: true + metadata: + description: + - Key/value pairs to be set as metadata on the container. + - If a container doesn't exist, it will be created. + - Both custom and system metadata can be set. + - Custom metadata are keys and values defined by the user. + - The system metadata keys are content_type, content_encoding, content_disposition, delete_after,\ + delete_at, is_content_type_detected + type: dict + required: false + keys: + description: Keys from 'metadata' to be deleted. + type: list + elements: str + required: false + delete_with_all_objects: + description: + - Whether the container should be deleted with all objects or not. + - Without this parameter set to "true", an attempt to delete a container that contains objects will fail. + type: bool + default: False + required: false + state: + description: Whether resource should be present or absent. + default: 'present' + choices: ['present', 'absent'] + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +container: + description: Specifies the container. + returned: On success when C(state=present) + type: dict + sample: + { + "bytes": 5449, + "bytes_used": 5449, + "content_type": null, + "count": 1, + "id": "otc", + "if_none_match": null, + "is_content_type_detected": null, + "is_newest": null, + "meta_temp_url_key": null, + "meta_temp_url_key_2": null, + "name": "otc", + "object_count": 1, + "read_ACL": null, + "sync_key": null, + "sync_to": null, + "timestamp": null, + "versions_location": null, + "write_ACL": null + } +''' + +EXAMPLES = ''' +# Create empty container + - openstack.cloud.object_container: + container: "new-container" + state: present + +# Set metadata for container + - openstack.cloud.object_container: + container: "new-container" + metadata: "Cache-Control='no-cache'" + +# Delete some keys from metadata of a container + - openstack.cloud.object_container: + container: "new-container" + keys: + - content_type + +# Delete container + - openstack.cloud.object_container: + container: "new-container" + state: absent + +# Delete container and its objects + - openstack.cloud.object_container: + container: "new-container" + delete_with_all_objects: true + state: absent +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ContainerModule(OpenStackModule): + + argument_spec = dict( + container=dict(type='str', required=True), + metadata=dict(type='dict', required=False), + keys=dict(type='list', required=False, elements='str', no_log=False), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), + delete_with_all_objects=dict(type='bool', default=False, required=False) + ) + + def create(self, container): + + data = {} + if self._container_exist(container): + self.exit_json(changed=False) + + container_data = self.conn.object_store.create_container(name=container).to_dict() + container_data.pop('location') + data['container'] = container_data + self.exit_json(changed=True, **data) + + def delete(self, container): + + delete_with_all_objects = self.params['delete_with_all_objects'] + + changed = False + if self._container_exist(container): + objects = [] + for raw in self.conn.object_store.objects(container): + dt = raw.to_dict() + dt.pop('location') + objects.append(dt) + if len(objects) > 0: + if delete_with_all_objects: + for obj in objects: + self.conn.object_store.delete_object(container=container, obj=obj['id']) + else: + self.fail_json(msg="Container has objects") + self.conn.object_store.delete_container(container=container) + changed = True + + self.exit(changed=changed) + + def set_metadata(self, container, metadata): + + data = {} + + if not self._container_exist(container): + new_container = self.conn.object_store.create_container(name=container).to_dict() + + new_container = self.conn.object_store.set_container_metadata(container, **metadata).to_dict() + new_container.pop('location') + data['container'] = new_container + self.exit(changed=True, **data) + + def delete_metadata(self, container, keys): + + if not self._container_exist(container): + self.fail_json(msg="Container doesn't exist") + + self.conn.object_store.delete_container_metadata(container=container, keys=keys) + self.exit(changed=True) + + def _container_exist(self, container): + try: + self.conn.object_store.get_container_metadata(container) + return True + except self.sdk.exceptions.ResourceNotFound: + return False + + def run(self): + + container = self.params['container'] + state = self.params['state'] + metadata = self.params['metadata'] + keys = self.params['keys'] + + if state == 'absent': + self.delete(container) + if metadata: + self.set_metadata(container, metadata) + if keys: + self.delete_metadata(container, keys) + + self.create(container) + + +def main(): + module = ContainerModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/dns_zone.py b/ansible_collections/openstack/cloud/plugins/modules/dns_zone.py new file mode 100644 index 00000000..98cf655e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/dns_zone.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: dns_zone +short_description: Manage OpenStack DNS zones +author: OpenStack Ansible SIG +description: + - Manage OpenStack DNS zones. Zones can be created, deleted or + updated. Only the I(email), I(description), I(ttl) and I(masters) values + can be updated. +options: + name: + description: + - Zone name + required: true + type: str + zone_type: + description: + - Zone type + choices: [primary, secondary] + type: str + email: + description: + - Email of the zone owner (only applies if zone_type is primary) + type: str + description: + description: + - Zone description + type: str + ttl: + description: + - TTL (Time To Live) value in seconds + type: int + masters: + description: + - Master nameservers (only applies if zone_type is secondary) + type: list + elements: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a zone named "example.net" +- openstack.cloud.dns_zone: + cloud: mycloud + state: present + name: example.net. + zone_type: primary + email: test@example.net + description: Test zone + ttl: 3600 + +# Update the TTL on existing "example.net." zone +- openstack.cloud.dns_zone: + cloud: mycloud + state: present + name: example.net. + ttl: 7200 + +# Delete zone named "example.net." +- openstack.cloud.dns_zone: + cloud: mycloud + state: absent + name: example.net. +''' + +RETURN = ''' +zone: + description: Dictionary describing the zone. + returned: On success when I(state) is 'present'. + type: complex + contains: + id: + description: Unique zone ID + type: str + sample: "c1c530a3-3619-46f3-b0f6-236927b2618c" + name: + description: Zone name + type: str + sample: "example.net." + type: + description: Zone type + type: str + sample: "PRIMARY" + email: + description: Zone owner email + type: str + sample: "test@example.net" + description: + description: Zone description + type: str + sample: "Test description" + ttl: + description: Zone TTL value + type: int + sample: 3600 + masters: + description: Zone master nameservers + type: list + sample: [] +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class DnsZoneModule(OpenStackModule): + + argument_spec = dict( + name=dict(required=True, type='str'), + zone_type=dict(required=False, choices=['primary', 'secondary'], type='str'), + email=dict(required=False, type='str'), + description=dict(required=False, type='str'), + ttl=dict(required=False, type='int'), + masters=dict(required=False, type='list', elements='str'), + state=dict(default='present', choices=['absent', 'present'], type='str'), + ) + + def _system_state_change(self, state, email, description, ttl, masters, zone): + if state == 'present': + if not zone: + return True + if email is not None and zone.email != email: + return True + if description is not None and zone.description != description: + return True + if ttl is not None and zone.ttl != ttl: + return True + if masters is not None and zone.masters != masters: + return True + if state == 'absent' and zone: + return True + return False + + def _wait(self, timeout, zone, state): + """Wait for a zone to reach the desired state for the given state.""" + + for count in self.sdk.utils.iterate_timeout( + timeout, + "Timeout waiting for zone to be %s" % state): + + if (state == 'absent' and zone is None) or (state == 'present' and zone and zone.status == 'ACTIVE'): + return + + try: + zone = self.conn.get_zone(zone.id) + except Exception: + continue + + if zone and zone.status == 'ERROR': + self.fail_json(msg="Zone reached ERROR state while waiting for it to be %s" % state) + + def run(self): + + name = self.params['name'] + state = self.params['state'] + wait = self.params['wait'] + timeout = self.params['timeout'] + + zone = self.conn.get_zone(name) + + if state == 'present': + + zone_type = self.params['zone_type'] + email = self.params['email'] + description = self.params['description'] + ttl = self.params['ttl'] + masters = self.params['masters'] + + kwargs = {} + + if email: + kwargs['email'] = email + if description: + kwargs['description'] = description + if ttl: + kwargs['ttl'] = ttl + if masters: + kwargs['masters'] = masters + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, email, + description, ttl, + masters, zone)) + + if zone is None: + zone = self.conn.create_zone( + name=name, zone_type=zone_type, **kwargs) + changed = True + else: + if masters is None: + masters = [] + + pre_update_zone = zone + changed = self._system_state_change(state, email, + description, ttl, + masters, pre_update_zone) + if changed: + zone = self.conn.update_zone( + name, **kwargs) + + if wait: + self._wait(timeout, zone, state) + + self.exit_json(changed=changed, zone=zone) + + elif state == 'absent': + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, None, + None, None, + None, zone)) + + if zone is None: + changed = False + else: + self.conn.delete_zone(name) + changed = True + + if wait: + self._wait(timeout, zone, state) + + self.exit_json(changed=changed) + + +def main(): + module = DnsZoneModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/dns_zone_info.py b/ansible_collections/openstack/cloud/plugins/modules/dns_zone_info.py new file mode 100644 index 00000000..22a3da5c --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/dns_zone_info.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2021 by Open Telekom Cloud, operated by T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = ''' +--- +module: dns_zone_info +short_description: Getting information about dns zones +author: OpenStack Ansible SIG +description: + - Getting information about dns zones. Output can be filtered. +options: + name: + description: + - Zone name. + type: str + type: + description: + - Zone type. + choices: [primary, secondary] + type: str + email: + description: + - Email of the zone owner (only applies if zone_type is primary). + type: str + description: + description: + - Zone description. + type: str + ttl: + description: + - TTL (Time To Live) value in seconds. + type: int + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a zone named "example.net" +- openstack.cloud.dns_zones: + +''' + +RETURN = ''' +zone: + description: Dictionary describing the zone. + returned: On success when I(state) is 'present'. + type: complex + contains: + action: + description: Current action in progress on the resource. + type: str + sample: "CREATE" + attributes: + description: Key:Value pairs of information about this zone, and the pool the user would like to place \ + the zone in. This information can be used by the scheduler to place zones on the correct pool. + type: dict + sample: {"tier": "gold", "ha": "true"} + created_at: + description: Date / Time when resource was created. + type: str + sample: "2014-07-07T18:25:31.275934" + description: + description: Description for this zone. + type: str + sample: "This is an example zone." + email: + description: E-mail for the zone. Used in SOA records for the zone. + type: str + sample: "test@example.org" + id: + description: ID for the resource. + type: int + sample: "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3" + links: + description: Links to the resource, and other related resources. When a response has been broken into\ + pages, we will include a next link that should be followed to retrieve all results. + type: dict + sample: {"self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3"} + masters: + description: Mandatory for secondary zones. The servers to slave from to get DNS information. + type: list + sample: "[]" + name: + description: DNS Name for the zone. + type: str + sample: "test.test." + pool_id: + description: ID for the pool hosting this zone. + type: str + sample: "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3" + project_id: + description: ID for the project that owns the resource. + type: str + sample: "4335d1f0-f793-11e2-b778-0800200c9a66" + serial: + description: Current serial number for the zone. + type: int + sample: 1404757531 + status: + description: Status of the resource. + type: str + sample: "ACTIVE" + ttl: + description: TTL (Time to Live) for the zone. + type: int + sample: 7200 + type: + description: Type of zone. PRIMARY is controlled by Designate, SECONDARY zones are slaved from another\ + DNS Server. Defaults to PRIMARY + type: str + sample: "PRIMARY" + updated_at: + description: Date / Time when resource last updated. + type: str + sample: "2014-07-07T18:25:31.275934" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class DnsZoneInfoModule(OpenStackModule): + + argument_spec = dict( + name=dict(required=False, type='str'), + type=dict(required=False, choices=['primary', 'secondary'], type='str'), + email=dict(required=False, type='str'), + description=dict(required=False, type='str'), + ttl=dict(required=False, type='int') + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + + name = self.params['name'] + type = self.params['type'] + email = self.params['email'] + description = self.params['description'] + ttl = self.params['ttl'] + + kwargs = {} + + if name: + kwargs['name'] = name + if type: + kwargs['type'] = type + if email: + kwargs['email'] = email + if description: + kwargs['description'] = description + if ttl: + kwargs['ttl'] = ttl + + data = [zone.to_dict(computed=False) for zone in + self.conn.dns.zones(**kwargs)] + + self.exit_json(zones=data, changed=False) + + +def main(): + module = DnsZoneInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/endpoint.py b/ansible_collections/openstack/cloud/plugins/modules/endpoint.py new file mode 100644 index 00000000..e7864ecf --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/endpoint.py @@ -0,0 +1,218 @@ +#!/usr/bin/python + +# Copyright: (c) 2017, VEXXHOST, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: endpoint +short_description: Manage OpenStack Identity service endpoints +author: OpenStack Ansible SIG +description: + - Create, update, or delete OpenStack Identity service endpoints. If a + service with the same combination of I(service), I(interface) and I(region) + exist, the I(url) and I(state) (C(present) or C(absent)) will be updated. +options: + service: + description: + - Name or id of the service. + required: true + type: str + endpoint_interface: + description: + - Interface of the service. + choices: [admin, public, internal] + required: true + type: str + url: + description: + - URL of the service. + required: true + type: str + region: + description: + - Region that the service belongs to. Note that I(region_name) is used for authentication. + type: str + enabled: + description: + - Is the service enabled. + default: True + type: bool + state: + description: + - Should the resource be C(present) or C(absent). + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.13.0" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a service for glance + openstack.cloud.endpoint: + cloud: mycloud + service: glance + endpoint_interface: public + url: http://controller:9292 + region: RegionOne + state: present + +- name: Delete a service for nova + openstack.cloud.endpoint: + cloud: mycloud + service: nova + endpoint_interface: public + region: RegionOne + state: absent +''' + +RETURN = ''' +endpoint: + description: Dictionary describing the endpoint. + returned: On success when I(state) is C(present) + type: complex + contains: + id: + description: Endpoint ID. + type: str + sample: 3292f020780b4d5baf27ff7e1d224c44 + interface: + description: Endpoint Interface. + type: str + sample: public + enabled: + description: Service status. + type: bool + sample: True + links: + description: Links for the endpoint + type: str + sample: http://controller/identity/v3/endpoints/123 + region: + description: Same as C(region_id). Deprecated. + type: str + sample: RegionOne + region_id: + description: Region ID. + type: str + sample: RegionOne + service_id: + description: Service ID. + type: str + sample: b91f1318f735494a825a55388ee118f3 + url: + description: Service URL. + type: str + sample: http://controller:9292 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityEndpointModule(OpenStackModule): + argument_spec = dict( + service=dict(type='str', required=True), + endpoint_interface=dict(type='str', required=True, choices=['admin', 'public', 'internal']), + url=dict(type='str', required=True), + region=dict(type='str'), + enabled=dict(type='bool', default=True), + state=dict(type='str', default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, endpoint): + if endpoint.enabled != self.params['enabled']: + return True + if endpoint.url != self.params['url']: + return True + return False + + def _system_state_change(self, endpoint): + state = self.params['state'] + if state == 'absent' and endpoint: + return True + + if state == 'present': + if endpoint is None: + return True + return self._needs_update(endpoint) + + return False + + def run(self): + service_name_or_id = self.params['service'] + interface = self.params['endpoint_interface'] + url = self.params['url'] + region = self.params['region'] + enabled = self.params['enabled'] + state = self.params['state'] + + service = self.conn.get_service(service_name_or_id) + + if service is None and state == 'absent': + self.exit_json(changed=False) + + if service is None and state == 'present': + self.fail_json(msg='Service %s does not exist' % service_name_or_id) + + filters = dict(service_id=service.id, interface=interface) + if region is not None: + filters['region'] = region + endpoints = self.conn.search_endpoints(filters=filters) + + endpoint = None + if len(endpoints) > 1: + self.fail_json(msg='Service %s, interface %s and region %s are ' + 'not unique' % + (service_name_or_id, interface, region)) + elif len(endpoints) == 1: + endpoint = endpoints[0] + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(endpoint)) + + if state == 'present': + if endpoint is None: + args = {'url': url, 'interface': interface, + 'service_name_or_id': service.id, 'enabled': enabled, + 'region': region} + endpoints = self.conn.create_endpoint(**args) + # safe because endpoints contains a single item when url is + # given to self.conn.create_endpoint() + endpoint = endpoints[0] + + changed = True + else: + if self._needs_update(endpoint): + endpoint = self.conn.update_endpoint( + endpoint.id, url=url, enabled=enabled) + changed = True + else: + changed = False + self.exit_json(changed=changed, + endpoint=endpoint) + + elif state == 'absent': + if endpoint is None: + changed = False + else: + self.conn.delete_endpoint(endpoint.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityEndpointModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/federation_idp.py b/ansible_collections/openstack/cloud/plugins/modules/federation_idp.py new file mode 100644 index 00000000..35606cca --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/federation_idp.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: federation_idp +short_description: manage a federation Identity Provider +author: OpenStack Ansible SIG +description: + - Manage a federation Identity Provider. +options: + name: + description: + - The name of the Identity Provider. + type: str + required: true + aliases: ['id'] + state: + description: + - Whether the Identity Provider should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str + description: + description: + - The description of the Identity Provider. + type: str + domain_id: + description: + - The ID of a domain that is associated with the Identity Provider. + Federated users that authenticate with the Identity Provider will be + created under the domain specified. + - Required when creating a new Identity Provider. + type: str + enabled: + description: + - Whether the Identity Provider is enabled or not. + - Will default to C(true) when creating a new Identity Provider. + type: bool + aliases: ['is_enabled'] + remote_ids: + description: + - "List of the unique Identity Provider's remote IDs." + - Will default to an empty list when creating a new Identity Provider. + type: list + elements: str +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create an identity provider + openstack.cloud.federation_idp: + cloud: example_cloud + name: example_provider + domain_id: 0123456789abcdef0123456789abcdef + description: 'My example IDP' + remote_ids: + - 'https://auth.example.com/auth/realms/ExampleRealm' + +- name: Delete an identity provider + openstack.cloud.federation_idp: + cloud: example_cloud + name: example_provider + state: absent +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationIdpModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True, aliases=['id']), + state=dict(default='present', choices=['absent', 'present']), + description=dict(), + domain_id=dict(), + enabled=dict(type='bool', aliases=['is_enabled']), + remote_ids=dict(type='list', elements='str'), + ) + module_kwargs = dict( + supports_check_mode=True, + ) + + def normalize_idp(self, idp): + """ + Normalizes the IDP definitions so that the outputs are consistent with the + parameters + + - "enabled" (parameter) == "is_enabled" (SDK) + - "name" (parameter) == "id" (SDK) + """ + if idp is None: + return None + + _idp = idp.to_dict() + _idp['enabled'] = idp['is_enabled'] + _idp['name'] = idp['id'] + return _idp + + def delete_identity_provider(self, idp): + """ + Delete an existing Identity Provider + + returns: the "Changed" state + """ + if idp is None: + return False + + if self.ansible.check_mode: + return True + + self.conn.identity.delete_identity_provider(idp) + return True + + def create_identity_provider(self, name): + """ + Create a new Identity Provider + + returns: the "Changed" state and the new identity provider + """ + + if self.ansible.check_mode: + return True, None + + description = self.params.get('description') + enabled = self.params.get('enabled') + domain_id = self.params.get('domain_id') + remote_ids = self.params.get('remote_ids') + + if enabled is None: + enabled = True + if remote_ids is None: + remote_ids = [] + + attributes = { + 'domain_id': domain_id, + 'enabled': enabled, + 'remote_ids': remote_ids, + } + if description is not None: + attributes['description'] = description + + idp = self.conn.identity.create_identity_provider(id=name, **attributes) + return (True, idp) + + def update_identity_provider(self, idp): + """ + Update an existing Identity Provider + + returns: the "Changed" state and the new identity provider + """ + + description = self.params.get('description') + enabled = self.params.get('enabled') + domain_id = self.params.get('domain_id') + remote_ids = self.params.get('remote_ids') + + attributes = {} + + if (description is not None) and (description != idp.description): + attributes['description'] = description + if (enabled is not None) and (enabled != idp.is_enabled): + attributes['enabled'] = enabled + if (domain_id is not None) and (domain_id != idp.domain_id): + attributes['domain_id'] = domain_id + if (remote_ids is not None) and (remote_ids != idp.remote_ids): + attributes['remote_ids'] = remote_ids + + if not attributes: + return False, idp + + if self.ansible.check_mode: + return True, None + + new_idp = self.conn.identity.update_identity_provider(idp, **attributes) + return (True, new_idp) + + def run(self): + """ Module entry point """ + + name = self.params.get('name') + state = self.params.get('state') + changed = False + + idp = self.conn.identity.find_identity_provider(name) + + if state == 'absent': + if idp is not None: + changed = self.delete_identity_provider(idp) + self.exit_json(changed=changed) + + # state == 'present' + else: + if idp is None: + if self.params.get('domain_id') is None: + self.fail_json(msg='A domain_id must be passed when creating' + ' an identity provider') + (changed, idp) = self.create_identity_provider(name) + idp = self.normalize_idp(idp) + self.exit_json(changed=changed, identity_provider=idp) + + (changed, new_idp) = self.update_identity_provider(idp) + new_idp = self.normalize_idp(new_idp) + self.exit_json(changed=changed, identity_provider=new_idp) + + +def main(): + module = IdentityFederationIdpModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/federation_idp_info.py b/ansible_collections/openstack/cloud/plugins/modules/federation_idp_info.py new file mode 100644 index 00000000..4fe71949 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/federation_idp_info.py @@ -0,0 +1,89 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: federation_idp_info +short_description: Get the information about the available federation identity + providers +author: OpenStack Ansible SIG +description: + - Fetch a federation identity provider. +options: + name: + description: + - The name of the identity provider to fetch. + - If I(name) is specified, the module will return failed if the identity + provider doesn't exist. + type: str + aliases: ['id'] +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Fetch a specific identity provider + openstack.cloud.federation_idp_info: + cloud: example_cloud + name: example_provider + +- name: Fetch all providers + openstack.cloud.federation_idp_info: + cloud: example_cloud +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationIdpInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(aliases=['id']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def normalize_idp(self, idp): + """ + Normalizes the IDP definitions so that the outputs are consistent with the + parameters + + - "enabled" (parameter) == "is_enabled" (SDK) + - "name" (parameter) == "id" (SDK) + """ + if idp is None: + return + + _idp = idp.to_dict() + _idp['enabled'] = idp['is_enabled'] + _idp['name'] = idp['id'] + return _idp + + def run(self): + """ Module entry point """ + + name = self.params.get('name') + + if name: + idp = self.normalize_idp(self.conn.identity.get_identity_provider(name)) + self.exit_json(changed=False, identity_providers=[idp]) + + else: + providers = list(map(self.normalize_idp, self.conn.identity.identity_providers())) + self.exit_json(changed=False, identity_providers=providers) + + +def main(): + module = IdentityFederationIdpInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/federation_mapping.py b/ansible_collections/openstack/cloud/plugins/modules/federation_mapping.py new file mode 100644 index 00000000..6c07a41d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/federation_mapping.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: federation_mapping +short_description: Manage a federation mapping +author: OpenStack Ansible SIG +description: + - Manage a federation mapping. +options: + name: + description: + - The name of the mapping to manage. + required: true + type: str + aliases: ['id'] + state: + description: + - Whether the mapping should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str + rules: + description: + - The rules that comprise the mapping. These are pairs of I(local) and + I(remote) definitions. For more details on how these work please see + the OpenStack documentation + U(https://docs.openstack.org/keystone/latest/admin/federation/mapping_combinations.html). + - Required if I(state=present) + type: list + elements: dict + suboptions: + local: + description: + - Information on what local attributes will be mapped. + required: true + type: list + elements: dict + remote: + description: + - Information on what remote attributes will be mapped. + required: true + type: list + elements: dict +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a new mapping + openstack.cloud.federation_mapping: + cloud: example_cloud + name: example_mapping + rules: + - local: + - user: + name: '{0}' + - group: + id: '0cd5e9' + remote: + - type: UserName + - type: orgPersonType + any_one_of: + - Contractor + - SubContractor + +- name: Delete a mapping + openstack.cloud.federation_mapping: + name: example_mapping + state: absent +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationMappingModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True, aliases=['id']), + state=dict(default='present', choices=['absent', 'present']), + rules=dict(type='list', elements='dict', options=dict( + local=dict(required=True, type='list', elements='dict'), + remote=dict(required=True, type='list', elements='dict') + )), + ) + module_kwargs = dict( + required_if=[('state', 'present', ['rules'])], + supports_check_mode=True + ) + + def normalize_mapping(self, mapping): + """ + Normalizes the mapping definitions so that the outputs are consistent with + the parameters + + - "name" (parameter) == "id" (SDK) + """ + if mapping is None: + return None + + _mapping = mapping.to_dict() + _mapping['name'] = mapping['id'] + return _mapping + + def create_mapping(self, name): + """ + Attempt to create a Mapping + + returns: A tuple containing the "Changed" state and the created mapping + """ + + if self.ansible.check_mode: + return (True, None) + + rules = self.params.get('rules') + + mapping = self.conn.identity.create_mapping(id=name, rules=rules) + return (True, mapping) + + def delete_mapping(self, mapping): + """ + Attempt to delete a Mapping + + returns: the "Changed" state + """ + if mapping is None: + return False + + if self.ansible.check_mode: + return True + + self.conn.identity.delete_mapping(mapping) + return True + + def update_mapping(self, mapping): + """ + Attempt to delete a Mapping + + returns: The "Changed" state and the the new mapping + """ + + current_rules = mapping.rules + new_rules = self.params.get('rules') + + # Nothing to do + if current_rules == new_rules: + return (False, mapping) + + if self.ansible.check_mode: + return (True, None) + + new_mapping = self.conn.identity.update_mapping(mapping, rules=new_rules) + return (True, new_mapping) + + def run(self): + """ Module entry point """ + + name = self.params.get('name') + state = self.params.get('state') + changed = False + + mapping = self.conn.identity.find_mapping(name) + + if state == 'absent': + if mapping is not None: + changed = self.delete_mapping(mapping) + self.exit_json(changed=changed) + + # state == 'present' + else: + if len(self.params.get('rules')) < 1: + self.fail_json(msg='At least one rule must be passed') + + if mapping is None: + (changed, mapping) = self.create_mapping(name) + mapping = self.normalize_mapping(mapping) + self.exit_json(changed=changed, mapping=mapping) + else: + (changed, new_mapping) = self.update_mapping(mapping) + new_mapping = self.normalize_mapping(new_mapping) + self.exit_json(mapping=new_mapping, changed=changed) + + +def main(): + module = IdentityFederationMappingModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/federation_mapping_info.py b/ansible_collections/openstack/cloud/plugins/modules/federation_mapping_info.py new file mode 100644 index 00000000..2ba317c9 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/federation_mapping_info.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: federation_mapping_info +short_description: Get the information about the available federation mappings +author: OpenStack Ansible SIG +description: + - Fetch a federation mapping. +options: + name: + description: + - The name of the mapping to fetch. + - If I(name) is specified, the module will return failed if the mapping + doesn't exist. + type: str + aliases: ['id'] +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Fetch a specific mapping + openstack.cloud.federation_mapping_info: + cloud: example_cloud + name: example_mapping + +- name: Fetch all mappings + openstack.cloud.federation_mapping_info: + cloud: example_cloud +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationMappingInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(aliases=['id']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + module_min_sdk_version = "0.44" + + def normalize_mapping(self, mapping): + """ + Normalizes the mapping definitions so that the outputs are consistent with the + parameters + + - "name" (parameter) == "id" (SDK) + """ + if mapping is None: + return None + + _mapping = mapping.to_dict() + _mapping['name'] = mapping['id'] + return _mapping + + def run(self): + """ Module entry point """ + name = self.params.get('name') + + if name: + mapping = self.normalize_mapping( + self.conn.identity.get_mapping(name)) + self.exit_json(changed=False, mappings=[mapping]) + else: + mappings = list(map( + self.normalize_mapping, self.conn.identity.mappings())) + self.exit_json(changed=False, mappings=mappings) + + +def main(): + module = IdentityFederationMappingInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/floating_ip.py b/ansible_collections/openstack/cloud/plugins/modules/floating_ip.py new file mode 100644 index 00000000..6b5fb0d6 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/floating_ip.py @@ -0,0 +1,307 @@ +#!/usr/bin/python + +# Copyright: (c) 2015, Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: floating_ip +author: OpenStack Ansible SIG +short_description: Add/Remove floating IP from an instance +description: + - Add or Remove a floating IP to an instance. + - Returns the floating IP when attaching only if I(wait=true). + - When detaching a floating IP there might be a delay until an instance does not list the floating IP any more. +options: + server: + description: + - The name or ID of the instance to which the IP address + should be assigned. + required: true + type: str + network: + description: + - The name or ID of a neutron external network or a nova pool name. + type: str + floating_ip_address: + description: + - A floating IP address to attach or to detach. When I(state) is present + can be used to specify a IP address to attach. I(floating_ip_address) + requires I(network) to be set. + type: str + reuse: + description: + - When I(state) is present, and I(floating_ip_address) is not present, + this parameter can be used to specify whether we should try to reuse + a floating IP address already allocated to the project. + type: bool + default: 'no' + fixed_address: + description: + - To which fixed IP of server the floating IP address should be + attached to. + type: str + nat_destination: + description: + - The name or id of a neutron private network that the fixed IP to + attach floating IP is on + aliases: ["fixed_network", "internal_network"] + type: str + wait: + description: + - When attaching a floating IP address, specify whether to wait for it to appear as attached. + - Must be set to C(yes) for the module to return the value of the floating IP when attaching. + type: bool + default: 'no' + timeout: + description: + - Time to wait for an IP address to appear as attached. See wait. + required: false + default: 60 + type: int + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + purge: + description: + - When I(state) is absent, indicates whether or not to delete the floating + IP completely, or only detach it from the server. Default is to detach only. + type: bool + default: 'no' +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Assign a floating IP to the first interface of `cattle001` from an existing +# external network or nova pool. A new floating IP from the first available +# external network is allocated to the project. +- openstack.cloud.floating_ip: + cloud: dguerri + server: cattle001 + +# Assign a new floating IP to the instance fixed ip `192.0.2.3` of +# `cattle001`. If a free floating IP is already allocated to the project, it is +# reused; if not, a new one is created. +- openstack.cloud.floating_ip: + cloud: dguerri + state: present + reuse: yes + server: cattle001 + network: ext_net + fixed_address: 192.0.2.3 + wait: true + timeout: 180 + +# Assign a new floating IP from the network `ext_net` to the instance fixed +# ip in network `private_net` of `cattle001`. +- openstack.cloud.floating_ip: + cloud: dguerri + state: present + server: cattle001 + network: ext_net + nat_destination: private_net + wait: true + timeout: 180 + +# Detach a floating IP address from a server +- openstack.cloud.floating_ip: + cloud: dguerri + state: absent + floating_ip_address: 203.0.113.2 + server: cattle001 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule +import itertools + + +class NetworkingFloatingIPModule(OpenStackModule): + argument_spec = dict( + server=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + network=dict(required=False, default=None), + floating_ip_address=dict(required=False, default=None), + reuse=dict(required=False, type='bool', default=False), + fixed_address=dict(required=False, default=None), + nat_destination=dict(required=False, default=None, + aliases=['fixed_network', 'internal_network']), + wait=dict(required=False, type='bool', default=False), + timeout=dict(required=False, type='int', default=60), + purge=dict(required=False, type='bool', default=False), + ) + + module_kwargs = dict( + required_if=[ + ['state', 'absent', ['floating_ip_address']] + ], + required_by=dict( + floating_ip_address=('network',) + ) + ) + + def _get_floating_ip(self, floating_ip_address): + f_ips = self.conn.search_floating_ips( + filters={'floating_ip_address': floating_ip_address}) + + if not f_ips: + return None + + return f_ips[0] + + def _list_floating_ips(self, server): + return itertools.chain.from_iterable([ + (addr['addr'] for addr in server.addresses[net] if addr['OS-EXT-IPS:type'] == 'floating') + for net in server.addresses + ]) + + def _match_floating_ip(self, server, + floating_ip_address, + network_id, + fixed_address, + nat_destination): + + if floating_ip_address: + return self._get_floating_ip(floating_ip_address) + elif not fixed_address and nat_destination: + nat_destination_name = self.conn.get_network(nat_destination)['name'] + return next( + (self._get_floating_ip(addr['addr']) + for addr in server.addresses.get(nat_destination_name, []) + if addr['OS-EXT-IPS:type'] == 'floating'), + None) + else: + # not floating_ip_address and (fixed_address or not nat_destination) + + # get any of the floating ips that matches fixed_address and/or network + f_ip_addrs = self._list_floating_ips(server) + f_ips = [f_ip for f_ip in self.conn.list_floating_ips() if f_ip['floating_ip_address'] in f_ip_addrs] + return next( + (f_ip for f_ip in f_ips + if ((fixed_address and f_ip.fixed_ip_address == fixed_address) or not fixed_address) + and ((network_id and f_ip.network == network_id) or not network_id)), + None) + + def run(self): + server_name_or_id = self.params['server'] + state = self.params['state'] + network = self.params['network'] + floating_ip_address = self.params['floating_ip_address'] + reuse = self.params['reuse'] + fixed_address = self.params['fixed_address'] + nat_destination = self.params['nat_destination'] + wait = self.params['wait'] + timeout = self.params['timeout'] + purge = self.params['purge'] + + server = self.conn.get_server(server_name_or_id) + if not server: + self.fail_json( + msg="server {0} not found".format(server_name_or_id)) + + # Extract floating ips from server + f_ip_addrs = self._list_floating_ips(server) + + # Get details about requested floating ip + f_ip = self._get_floating_ip(floating_ip_address) if floating_ip_address else None + + if network: + network_id = self.conn.get_network(name_or_id=network)["id"] + else: + network_id = None + + if state == 'present': + if floating_ip_address and f_ip and floating_ip_address in f_ip_addrs: + # Floating ip address has been assigned to server + self.exit_json(changed=False, floating_ip=f_ip) + + if f_ip and f_ip['attached'] and floating_ip_address not in f_ip_addrs: + # Requested floating ip has been attached to different server + self.fail_json(msg="floating-ip {floating_ip_address} already has been attached to different server" + .format(floating_ip_address=floating_ip_address)) + + if not floating_ip_address: + # No specific floating ip requested, i.e. if any floating ip is already assigned to server, + # check that it matches requirements. + + if not fixed_address and nat_destination: + # Check if we have any floating ip on the given nat_destination network + nat_destination_name = self.conn.get_network(nat_destination)['name'] + for addr in server.addresses.get(nat_destination_name, []): + if addr['OS-EXT-IPS:type'] == 'floating': + # A floating ip address has been assigned to the requested nat_destination + f_ip = self._get_floating_ip(addr['addr']) + self.exit_json(changed=False, floating_ip=f_ip) + # else fixed_address or not nat_destination, hence an + # analysis of all floating ips of server is required + f_ips = [f_ip for f_ip in self.conn.list_floating_ips() if f_ip['floating_ip_address'] in f_ip_addrs] + for f_ip in f_ips: + if network_id and f_ip.network != network_id: + # requested network does not match network of floating ip + continue + + if not fixed_address and not nat_destination: + # any floating ip will fullfil these requirements + self.exit_json(changed=False, floating_ip=f_ip) + + if fixed_address and f_ip.fixed_ip_address == fixed_address: + # a floating ip address has been assigned that points to the requested fixed_address + self.exit_json(changed=False, floating_ip=f_ip) + + if floating_ip_address and not f_ip: + # openstacksdk's create_ip requires floating_ip_address and floating_network_id to be set + self.conn.network.create_ip(floating_ip_address=floating_ip_address, floating_network_id=network_id) + # Else floating ip either does not exist or has not been attached yet + + # Both floating_ip_address and network are mutually exclusive in add_ips_to_server, i.e. + # add_ips_to_server will ignore floating_ip_address if network is set + # Ref.: https://github.com/openstack/openstacksdk/blob/a6b0ece2821ea79330c4067100295f6bdcbe456e/openstack/cloud/_floating_ip.py#L987 + server = self.conn.add_ips_to_server( + server=server, + ips=floating_ip_address, + ip_pool=network if not floating_ip_address else None, + reuse=reuse, + fixed_address=fixed_address, + wait=wait, + timeout=timeout, nat_destination=nat_destination) + + # Update the floating ip status + f_ip = self._match_floating_ip(server, floating_ip_address, network_id, fixed_address, nat_destination) + self.exit_json(changed=True, floating_ip=f_ip) + + elif state == 'absent': + f_ip = self._match_floating_ip(server, floating_ip_address, network_id, fixed_address, nat_destination) + if not f_ip: + # Nothing to detach + self.exit_json(changed=False) + changed = False + + if f_ip["fixed_ip_address"]: + self.conn.detach_ip_from_server(server_id=server['id'], floating_ip_id=f_ip['id']) + # OpenStackSDK sets {"port_id": None} to detach a floating ip from an instance, + # but there might be a delay until a server does not list it in addresses any more. + + # Update the floating IP status + f_ip = self.conn.get_floating_ip(id=f_ip['id']) + changed = True + + if purge: + self.conn.delete_floating_ip(f_ip['id']) + self.exit_json(changed=True) + self.exit_json(changed=changed, floating_ip=f_ip) + + +def main(): + module = NetworkingFloatingIPModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/floating_ip_info.py b/ansible_collections/openstack/cloud/plugins/modules/floating_ip_info.py new file mode 100644 index 00000000..50e7c879 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/floating_ip_info.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2021 by Open Telekom Cloud, operated by T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: floating_ip_info +short_description: Get information about floating ips +author: OpenStack Ansible SIG +description: + - Get a generator of floating ips. +options: + description: + description: + - The description of a floating IP. + type: str + fixed_ip_address: + description: + - The fixed IP address associated with a floating IP address. + type: str + floating_ip_address: + description: + - The IP address of a floating IP. + type: str + floating_network: + description: + - The name or id of the network associated with a floating IP. + type: str + port: + description: + - The name or id of the port to which a floating IP is associated. + type: str + project_id: + description: + - The ID of the project a floating IP is associated with. + type: str + router: + description: + - The name or id of an associated router. + type: str + status: + description: + - The status of a floating IP, which can be ``ACTIVE``or ``DOWN``. + choices: ['active', 'down'] + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +floating_ips: + description: The floating ip objects list. + type: complex + returned: On Success. + contains: + created_at: + description: Timestamp at which the floating IP was assigned. + type: str + description: + description: The description of a floating IP. + type: str + dns_domain: + description: The DNS domain. + type: str + dns_name: + description: The DNS name. + type: str + fixed_ip_address: + description: The fixed IP address associated with a floating IP address. + type: str + floating_ip_address: + description: The IP address of a floating IP. + type: str + floating_network_id: + description: The id of the network associated with a floating IP. + type: str + id: + description: Id of the floating ip. + type: str + name: + description: Name of the floating ip. + type: str + port_details: + description: The details of the port that this floating IP associates \ + with. Present if ``fip-port-details`` extension is loaded. + type: str + port_id: + description: The port ID floating ip associated with. + type: str + project_id: + description: The ID of the project this floating IP is associated with. + type: str + qos_policy_id: + description: The ID of the QoS policy attached to the floating IP. + type: str + revision_number: + description: Revision number. + type: str + router_id: + description: The id of the router floating ip associated with. + type: str + status: + description: The status of a floating IP, which can be ``ACTIVE``or ``DOWN``.\ + Can be 'ACTIVE' and 'DOWN'. + type: str + subnet_id: + description: The id of the subnet the floating ip associated with. + type: str + tags: + description: List of tags. + type: str + updated_at: + description: Timestamp at which the floating IP was last updated. + type: str +''' + +EXAMPLES = ''' +# Getting all floating ips +- openstack.cloud.floating_ip_info: + register: fips + +# Getting fip by associated fixed IP address. +- openstack.cloud.floating_ip_info: + fixed_ip_address: 192.168.10.8 + register: fip + +# Getting fip by associated router. +- openstack.cloud.floating_ip_info: + router: my-router + register: fip +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class FloatingIPInfoModule(OpenStackModule): + argument_spec = dict( + description=dict(required=False), + fixed_ip_address=dict(required=False), + floating_ip_address=dict(required=False), + floating_network=dict(required=False), + port=dict(required=False), + project_id=dict(required=False), + router=dict(required=False), + status=dict(required=False, choices=['active', 'down']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + + description = self.params['description'] + fixed_ip_address = self.params['fixed_ip_address'] + floating_ip_address = self.params['floating_ip_address'] + floating_network = self.params['floating_network'] + port = self.params['port'] + project_id = self.params['project_id'] + router = self.params['router'] + status = self.params['status'] + + query = {} + if description: + query['description'] = description + if fixed_ip_address: + query['fixed_ip_address'] = fixed_ip_address + if floating_ip_address: + query['floating_ip_address'] = floating_ip_address + if floating_network: + try: + query['floating_network_id'] = self.conn.network.find_network(name_or_id=floating_network, + ignore_missing=False).id + except self.sdk.exceptions.ResourceNotFound: + self.fail_json(msg="floating_network not found") + if port: + try: + query['port_id'] = self.conn.network.find_port(name_or_id=port, ignore_missing=False).id + except self.sdk.exceptions.ResourceNotFound: + self.fail_json(msg="port not found") + if project_id: + query['project_id'] = project_id + if router: + try: + query['router_id'] = self.conn.network.find_router(name_or_id=router, ignore_missing=False).id + except self.sdk.exceptions.ResourceNotFound: + self.fail_json(msg="router not found") + if status: + query['status'] = status.upper() + + ips = [ip.to_dict(computed=False) for ip in self.conn.network.ips(**query)] + self.exit_json(changed=False, floating_ips=ips) + + +def main(): + module = FloatingIPInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/group_assignment.py b/ansible_collections/openstack/cloud/plugins/modules/group_assignment.py new file mode 100644 index 00000000..ce8f28e1 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/group_assignment.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: group_assignment +short_description: Associate OpenStack Identity users and groups +author: OpenStack Ansible SIG +description: + - Add and remove users from groups +options: + user: + description: + - Name or id for the user + required: true + type: str + group: + description: + - Name or id for the group. + required: true + type: str + state: + description: + - Should the user be present or absent in the group + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Add the demo user to the demo group +- openstack.cloud.group_assignment: + cloud: mycloud + user: demo + group: demo +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityGroupAssignment(OpenStackModule): + argument_spec = dict( + user=dict(required=True), + group=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _system_state_change(self, state, in_group): + if state == 'present' and not in_group: + return True + if state == 'absent' and in_group: + return True + return False + + def run(self): + user = self.params['user'] + group = self.params['group'] + state = self.params['state'] + + in_group = self.conn.is_user_in_group(user, group) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, in_group)) + + changed = False + if state == 'present': + if not in_group: + self.conn.add_user_to_group(user, group) + changed = True + + elif state == 'absent': + if in_group: + self.conn.remove_user_from_group(user, group) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = IdentityGroupAssignment() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/host_aggregate.py b/ansible_collections/openstack/cloud/plugins/modules/host_aggregate.py new file mode 100644 index 00000000..4c95fd29 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/host_aggregate.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# Copyright 2016 Jakub Jursa <jakub.jursa1@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: host_aggregate +short_description: Manage OpenStack host aggregates +author: OpenStack Ansible SIG +description: + - Create, update, or delete OpenStack host aggregates. If a aggregate + with the supplied name already exists, it will be updated with the + new name, new availability zone, new metadata and new list of hosts. +options: + name: + description: Name of the aggregate. + required: true + type: str + metadata: + description: Metadata dict. + type: dict + availability_zone: + description: Availability zone to create aggregate into. + type: str + hosts: + description: List of hosts to set for an aggregate. + type: list + elements: str + purge_hosts: + description: Whether hosts not in I(hosts) should be removed from the aggregate + type: bool + default: true + state: + description: Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a host aggregate +- openstack.cloud.host_aggregate: + cloud: mycloud + state: present + name: db_aggregate + hosts: + - host1 + - host2 + metadata: + type: dbcluster + +# Add an additional host to the aggregate +- openstack.cloud.host_aggregate: + cloud: mycloud + state: present + name: db_aggregate + hosts: + - host3 + purge_hosts: false + metadata: + type: dbcluster + +# Delete an aggregate +- openstack.cloud.host_aggregate: + cloud: mycloud + state: absent + name: db_aggregate +''' + +RETURN = r''' +aggregate: + description: A host aggregate resource. + type: complex + returned: On success, when I(state) is present + contains: + availability_zone: + description: Availability zone of the aggregate + type: str + returned: always + deleted: + description: Whether or not the resource is deleted + type: bool + returned: always + hosts: + description: Hosts belonging to the aggregate + type: str + returned: always + id: + description: The UUID of the aggregate. + type: str + returned: always + metadata: + description: Metadata attached to the aggregate + type: str + returned: always + name: + description: Name of the aggregate + type: str + returned: always +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ComputeHostAggregateModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + metadata=dict(required=False, default=None, type='dict'), + availability_zone=dict(required=False, default=None), + hosts=dict(required=False, default=None, type='list', elements='str'), + purge_hosts=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _find_aggregate(self, name_or_id): + aggregates = self.conn.search_aggregates(name_or_id=name_or_id) + if len(aggregates) == 1: + return aggregates[0] + elif len(aggregates) == 0: + return None + raise Exception("Aggregate is not unique, this should be impossible") + + def _needs_update(self, aggregate): + new_metadata = self.params['metadata'] or {} + + if self.params['availability_zone'] is not None: + new_metadata['availability_zone'] = self.params['availability_zone'] + + if self.params['hosts'] is not None: + if self.params['purge_hosts']: + if set(self.params['hosts']) != set(aggregate.hosts): + return True + else: + intersection = set(self.params['hosts']).intersection(set(aggregate.hosts)) + if set(self.params['hosts']) != intersection: + return True + + for param in ('availability_zone', 'metadata'): + if self.params[param] is not None and \ + self.params[param] != aggregate[param]: + return True + + return False + + def _system_state_change(self, aggregate): + state = self.params['state'] + if state == 'absent' and aggregate: + return True + + if state == 'present': + if aggregate is None: + return True + return self._needs_update(aggregate) + + return False + + def _update_hosts(self, aggregate, hosts, purge_hosts): + if hosts is None: + return + + hosts_to_add = set(hosts) - set(aggregate['hosts'] or []) + for host in hosts_to_add: + self.conn.add_host_to_aggregate(aggregate.id, host) + + if not purge_hosts: + return + + hosts_to_remove = set(aggregate["hosts"] or []) - set(hosts) + for host in hosts_to_remove: + self.conn.remove_host_from_aggregate(aggregate.id, host) + + def run(self): + name = self.params['name'] + metadata = self.params['metadata'] + availability_zone = self.params['availability_zone'] + hosts = self.params['hosts'] + purge_hosts = self.params['purge_hosts'] + state = self.params['state'] + + if metadata is not None: + metadata.pop('availability_zone', None) + + aggregate = self._find_aggregate(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(aggregate)) + + changed = False + if state == 'present': + if aggregate is None: + aggregate = self.conn.create_aggregate( + name=name, availability_zone=availability_zone) + self._update_hosts(aggregate, hosts, False) + if metadata: + self.conn.set_aggregate_metadata(aggregate.id, metadata) + changed = True + elif self._needs_update(aggregate): + if availability_zone is not None: + aggregate = self.conn.update_aggregate( + aggregate.id, name=name, + availability_zone=availability_zone) + if metadata is not None: + metas = metadata + for i in set(aggregate.metadata.keys() - set(metadata.keys())): + if i != 'availability_zone': + metas[i] = None + self.conn.set_aggregate_metadata(aggregate.id, metas) + self._update_hosts(aggregate, hosts, purge_hosts) + changed = True + aggregate = self._find_aggregate(name) + self.exit_json(changed=changed, aggregate=aggregate) + + elif state == 'absent' and aggregate is not None: + self._update_hosts(aggregate, [], True) + self.conn.delete_aggregate(aggregate.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = ComputeHostAggregateModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/identity_domain.py b/ansible_collections/openstack/cloud/plugins/modules/identity_domain.py new file mode 100644 index 00000000..660748c4 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/identity_domain.py @@ -0,0 +1,175 @@ +#!/usr/bin/python +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_domain +short_description: Manage OpenStack Identity Domains +author: OpenStack Ansible SIG +description: + - Create, update, or delete OpenStack Identity domains. If a domain + with the supplied name already exists, it will be updated with the + new description and enabled attributes. +options: + name: + description: + - Name that has to be given to the instance + required: true + type: str + description: + description: + - Description of the domain + type: str + enabled: + description: + - Is the domain enabled + type: bool + default: 'yes' + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a domain +- openstack.cloud.identity_domain: + cloud: mycloud + state: present + name: demo + description: Demo Domain + +# Delete a domain +- openstack.cloud.identity_domain: + cloud: mycloud + state: absent + name: demo +''' + +RETURN = ''' +domain: + description: Dictionary describing the domain. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Domain ID. + type: str + sample: "474acfe5-be34-494c-b339-50f06aa143e4" + name: + description: Domain name. + type: str + sample: "demo" + description: + description: Domain description. + type: str + sample: "Demo Domain" + enabled: + description: Domain description. + type: bool + sample: True + +id: + description: The domain ID. + returned: On success when I(state) is 'present' + type: str + sample: "474acfe5-be34-494c-b339-50f06aa143e4" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityDomainModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + description=dict(default=None), + enabled=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, domain): + if self.params['description'] is not None and \ + domain.description != self.params['description']: + return True + if domain.get( + "is_enabled", domain.get("enabled")) != self.params['enabled']: + return True + return False + + def _system_state_change(self, domain): + state = self.params['state'] + if state == 'absent' and domain: + return True + + if state == 'present': + if domain is None: + return True + return self._needs_update(domain) + + return False + + def run(self): + name = self.params['name'] + description = self.params['description'] + enabled = self.params['enabled'] + state = self.params['state'] + + domains = list(self.conn.identity.domains(name=name)) + + if len(domains) > 1: + self.fail_json(msg='Domain name %s is not unique' % name) + elif len(domains) == 1: + domain = domains[0] + else: + domain = None + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(domain)) + + if state == 'present': + if domain is None: + domain = self.conn.create_domain( + name=name, description=description, enabled=enabled) + changed = True + else: + if self._needs_update(domain): + domain = self.conn.update_domain( + domain.id, name=name, description=description, + enabled=enabled) + changed = True + else: + changed = False + if hasattr(domain, "to_dict"): + domain = domain.to_dict() + domain.pop("location") + self.exit_json(changed=changed, domain=domain, id=domain['id']) + + elif state == 'absent': + if domain is None: + changed = False + else: + self.conn.delete_domain(domain.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityDomainModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/identity_domain_info.py b/ansible_collections/openstack/cloud/plugins/modules/identity_domain_info.py new file mode 100644 index 00000000..e0e33cde --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/identity_domain_info.py @@ -0,0 +1,119 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_domain_info +short_description: Retrieve information about one or more OpenStack domains +author: OpenStack Ansible SIG +description: + - Retrieve information about a one or more OpenStack domains + - This module was called C(openstack.cloud.identity_domain_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.identity_domain_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the domain + type: str + filters: + description: + - A dictionary of meta data to use for filtering. Elements of + this dictionary may be additional dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about previously created domain +- openstack.cloud.identity_domain_info: + cloud: awesomecloud + register: result +- debug: + msg: "{{ result.openstack_domains }}" + +# Gather information about a previously created domain by name +- openstack.cloud.identity_domain_info: + cloud: awesomecloud + name: demodomain + register: result +- debug: + msg: "{{ result.openstack_domains }}" + +# Gather information about a previously created domain with filter +- openstack.cloud.identity_domain_info: + cloud: awesomecloud + name: demodomain + filters: + enabled: false + register: result +- debug: + msg: "{{ result.openstack_domains }}" +''' + + +RETURN = ''' +openstack_domains: + description: has all the OpenStack information about domains + returned: always, but can be null + type: list + elements: dict + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the domain. + returned: success + type: str + description: + description: Description of the domain. + returned: success + type: str + enabled: + description: Flag to indicate if the domain is enabled. + returned: success + type: bool +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityDomainInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + deprecated_names = ('openstack.cloud.identity_domain_facts') + + def run(self): + name = self.params['name'] + filters = self.params['filters'] or {} + + args = {} + if name: + args['name_or_id'] = name + args['filters'] = filters + + domains = self.conn.search_domains(**args) + self.exit_json(changed=False, openstack_domains=domains) + + +def main(): + module = IdentityDomainInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/identity_group.py b/ansible_collections/openstack/cloud/plugins/modules/identity_group.py new file mode 100644 index 00000000..5b45efa4 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/identity_group.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# Copyright (c) 2016 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_group +short_description: Manage OpenStack Identity Groups +author: OpenStack Ansible SIG +description: + - Manage OpenStack Identity Groups. Groups can be created, deleted or + updated. Only the I(description) value can be updated. +options: + name: + description: + - Group name + required: true + type: str + description: + description: + - Group description + type: str + domain_id: + description: + - Domain id to create the group in if the cloud supports domains. + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a group named "demo" +- openstack.cloud.identity_group: + cloud: mycloud + state: present + name: demo + description: "Demo Group" + domain_id: demoid + +# Update the description on existing "demo" group +- openstack.cloud.identity_group: + cloud: mycloud + state: present + name: demo + description: "Something else" + domain_id: demoid + +# Delete group named "demo" +- openstack.cloud.identity_group: + cloud: mycloud + state: absent + name: demo +''' + +RETURN = ''' +group: + description: Dictionary describing the group. + returned: On success when I(state) is 'present'. + type: complex + contains: + id: + description: Unique group ID + type: str + sample: "ee6156ff04c645f481a6738311aea0b0" + name: + description: Group name + type: str + sample: "demo" + description: + description: Group description + type: str + sample: "Demo Group" + domain_id: + description: Domain for the group + type: str + sample: "default" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityGroupModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + description=dict(required=False, default=None), + domain_id=dict(required=False, default=None), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _system_state_change(self, state, description, group): + if state == 'present' and not group: + return True + if state == 'present' and description is not None and group.description != description: + return True + if state == 'absent' and group: + return True + return False + + def run(self): + name = self.params.get('name') + description = self.params.get('description') + state = self.params.get('state') + + domain_id = self.params.pop('domain_id') + + if domain_id: + group = self.conn.get_group(name, filters={'domain_id': domain_id}) + else: + group = self.conn.get_group(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, description, group)) + + if state == 'present': + if group is None: + group = self.conn.create_group( + name=name, description=description, domain=domain_id) + changed = True + else: + if description is not None and group.description != description: + group = self.conn.update_group( + group.id, description=description) + changed = True + else: + changed = False + self.exit_json(changed=changed, group=group) + + elif state == 'absent': + if group is None: + changed = False + else: + self.conn.delete_group(group.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityGroupModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/identity_group_info.py b/ansible_collections/openstack/cloud/plugins/modules/identity_group_info.py new file mode 100644 index 00000000..68f00d73 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/identity_group_info.py @@ -0,0 +1,150 @@ +#!/usr/bin/python + +# Copyright (c) 2019, Phillipe Smith <phillipelnx@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_group_info +short_description: Retrieve info about one or more OpenStack groups +author: OpenStack Ansible SIG +description: + - Retrieve info about a one or more OpenStack groups. +options: + name: + description: + - Name or ID of the group. + type: str + domain: + description: + - Name or ID of the domain containing the group if the cloud supports domains + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather info about previously created groups +- name: gather info + hosts: localhost + tasks: + - name: Gather info about previously created groups + openstack.cloud.identity_group_info: + cloud: awesomecloud + register: openstack_groups + - debug: + var: openstack_groups + +# Gather info about a previously created group by name +- name: gather info + hosts: localhost + tasks: + - name: Gather info about a previously created group by name + openstack.cloud.identity_group_info: + cloud: awesomecloud + name: demogroup + register: openstack_groups + - debug: + var: openstack_groups + +# Gather info about a previously created group in a specific domain +- name: gather info + hosts: localhost + tasks: + - name: Gather info about a previously created group in a specific domain + openstack.cloud.identity_group_info: + cloud: awesomecloud + name: demogroup + domain: admindomain + register: openstack_groups + - debug: + var: openstack_groups + +# Gather info about a previously created group in a specific domain with filter +- name: gather info + hosts: localhost + tasks: + - name: Gather info about a previously created group in a specific domain with filter + openstack.cloud.identity_group_info: + cloud: awesomecloud + name: demogroup + domain: admindomain + filters: + enabled: False + register: openstack_groups + - debug: + var: openstack_groups +''' + + +RETURN = ''' +openstack_groups: + description: Dictionary describing all the matching groups. + returned: always, but can be an empty list + type: complex + contains: + name: + description: Name given to the group. + returned: success + type: str + description: + description: Description of the group. + returned: success + type: str + id: + description: Unique UUID. + returned: success + type: str + domain_id: + description: Domain ID containing the group (keystone v3 clouds only) + returned: success + type: bool +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityGroupInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + domain=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + name = self.params['name'] + domain = self.params['domain'] + filters = self.params['filters'] or {} + + args = {} + if domain: + dom = self.conn.identity.find_domain(domain) + if dom: + args['domain_id'] = dom['id'] + else: + self.fail_json(msg='Domain name or ID does not exist') + + groups = self.conn.search_groups(name, filters, **args) + # groups is for backward (and forward) compatibility + self.exit_json(changed=False, groups=groups, openstack_groups=groups) + + +def main(): + module = IdentityGroupInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/identity_role.py b/ansible_collections/openstack/cloud/plugins/modules/identity_role.py new file mode 100644 index 00000000..272d9821 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/identity_role.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# Copyright (c) 2016 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_role +short_description: Manage OpenStack Identity Roles +author: OpenStack Ansible SIG +description: + - Manage OpenStack Identity Roles. +options: + name: + description: + - Role Name + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a role named "demo" +- openstack.cloud.identity_role: + cloud: mycloud + state: present + name: demo + +# Delete the role named "demo" +- openstack.cloud.identity_role: + cloud: mycloud + state: absent + name: demo +''' + +RETURN = ''' +role: + description: Dictionary describing the role. + returned: On success when I(state) is 'present'. + type: complex + contains: + domain_id: + description: Domain to which the role belongs + type: str + sample: default + id: + description: Unique role ID. + type: str + sample: "677bfab34c844a01b88a217aa12ec4c2" + name: + description: Role name. + type: str + sample: "demo" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityRoleModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _system_state_change(self, state, role): + if state == 'present' and not role: + return True + if state == 'absent' and role: + return True + return False + + def run(self): + name = self.params.get('name') + state = self.params.get('state') + + role = self.conn.get_role(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, role)) + + changed = False + if state == 'present': + if role is None: + role = self.conn.create_role(name=name) + changed = True + self.exit_json(changed=changed, role=role) + elif state == 'absent' and role is not None: + self.conn.identity.delete_role(role['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityRoleModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/identity_role_info.py b/ansible_collections/openstack/cloud/plugins/modules/identity_role_info.py new file mode 100644 index 00000000..42de17bd --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/identity_role_info.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2020, Sagi Shnaidman <sshnaidm@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_role_info +short_description: Retrieve information about roles +author: OpenStack Ansible SIG +description: + - Get information about identity roles in Openstack +options: + domain_id: + description: + - Domain ID which owns the role + type: str + required: false + name: + description: + - Name or ID of the role + type: str + required: false + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +RETURN = ''' +openstack_roles: + description: List of identity roles + returned: always + type: list + elements: dict + contains: + id: + description: Unique ID for the role + returned: success + type: str + name: + description: Unique role name, within the owning domain. + returned: success + type: str + domain_id: + description: References the domain ID which owns the role. + returned: success + type: str +''' + +EXAMPLES = ''' +# Retrieve info about all roles +- openstack.cloud.identity_role_info: + cloud: mycloud + +# Retrieve info about all roles in specific domain +- openstack.cloud.identity_role_info: + cloud: mycloud + domain_id: some_domain_id + +# Retrieve info about role 'admin' +- openstack.cloud.identity_role_info: + cloud: mycloud + name: admin + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityRoleInfoModule(OpenStackModule): + argument_spec = dict( + domain_id=dict(type='str', required=False), + name=dict(type='str', required=False), + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + def run(self): + params = { + 'domain_id': self.params['domain_id'], + 'name_or_id': self.params['name'], + } + params = {k: v for k, v in params.items() if v is not None} + + roles = self.conn.search_roles(**params) + self.exit_json(changed=False, openstack_roles=roles) + + +def main(): + module = IdentityRoleInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/identity_user.py b/ansible_collections/openstack/cloud/plugins/modules/identity_user.py new file mode 100644 index 00000000..047b3ed8 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/identity_user.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_user +short_description: Manage OpenStack Identity Users +author: OpenStack Ansible SIG +description: + - Manage OpenStack Identity users. Users can be created, + updated or deleted using this module. A user will be updated + if I(name) matches an existing user and I(state) is present. + The value for I(name) cannot be updated without deleting and + re-creating the user. +options: + name: + description: + - Username for the user + required: true + type: str + password: + description: + - Password for the user + type: str + update_password: + required: false + choices: ['always', 'on_create'] + default: on_create + description: + - C(always) will attempt to update password. C(on_create) will only + set the password for newly created users. + type: str + email: + description: + - Email address for the user + type: str + description: + description: + - Description about the user + type: str + default_project: + description: + - Project name or ID that the user should be associated with by default + type: str + domain: + description: + - Domain to create the user in if the cloud supports domains + type: str + enabled: + description: + - Is the user enabled + type: bool + default: 'yes' + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a user +- openstack.cloud.identity_user: + cloud: mycloud + state: present + name: demouser + password: secret + email: demo@example.com + domain: default + default_project: demo + +# Delete a user +- openstack.cloud.identity_user: + cloud: mycloud + state: absent + name: demouser + +# Create a user but don't update password if user exists +- openstack.cloud.identity_user: + cloud: mycloud + state: present + name: demouser + password: secret + update_password: on_create + email: demo@example.com + domain: default + default_project: demo + +# Create a user without password +- openstack.cloud.identity_user: + cloud: mycloud + state: present + name: demouser + email: demo@example.com + domain: default + default_project: demo +''' + + +RETURN = ''' +user: + description: Dictionary describing the user. + returned: On success when I(state) is 'present' + type: dict + contains: + default_project_id: + description: User default project ID. Only present with Keystone >= v3. + returned: success + type: str + sample: "4427115787be45f08f0ec22a03bfc735" + description: + description: The description of this user + returned: success + type: str + sample: "a user" + domain_id: + description: User domain ID. Only present with Keystone >= v3. + returned: success + type: str + sample: "default" + email: + description: User email address + returned: success + type: str + sample: "demo@example.com" + id: + description: User ID + returned: success + type: str + sample: "f59382db809c43139982ca4189404650" + enabled: + description: Indicates whether the user is enabled + type: bool + name: + description: Unique user name, within the owning domain + returned: success + type: str + sample: "demouser" + username: + description: Username with Identity API v2 (OpenStack Pike or earlier) else Null + returned: success + type: str + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityUserModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + password=dict(required=False, default=None, no_log=True), + email=dict(required=False, default=None), + default_project=dict(required=False, default=None), + description=dict(type='str'), + domain=dict(required=False, default=None), + enabled=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + update_password=dict(default='on_create', choices=['always', 'on_create']), + ) + + module_kwargs = dict() + + def _needs_update(self, params_dict, user): + for k in params_dict: + # We don't get password back in the user object, so assume any supplied + # password is a change. + if k == 'password': + return True + if k == 'default_project': + if user['default_project_id'] != params_dict['default_project']: + return True + else: + continue + if user[k] != params_dict[k]: + return True + return False + + def _get_domain_id(self, domain): + dom_obj = self.conn.identity.find_domain(domain) + if dom_obj is None: + # Ok, let's hope the user is non-admin and passing a sane id + return domain + return dom_obj.id + + def _get_default_project_id(self, default_project, domain_id): + project = self.conn.identity.find_project(default_project, domain_id=domain_id) + if not project: + self.fail_json(msg='Default project %s is not valid' % default_project) + return project['id'] + + def run(self): + name = self.params['name'] + password = self.params.get('password') + email = self.params['email'] + default_project = self.params['default_project'] + domain = self.params['domain'] + enabled = self.params['enabled'] + state = self.params['state'] + update_password = self.params['update_password'] + description = self.params['description'] + + if domain: + domain_id = self._get_domain_id(domain) + user = self.conn.get_user(name, domain_id=domain_id) + else: + domain_id = None + user = self.conn.get_user(name) + + changed = False + if state == 'present': + user_args = { + 'name': name, + 'email': email, + 'domain_id': domain_id, + 'description': description, + 'enabled': enabled, + } + if default_project: + default_project_id = self._get_default_project_id( + default_project, domain_id) + user_args['default_project'] = default_project_id + user_args = {k: v for k, v in user_args.items() if v is not None} + + changed = False + if user is None: + if password: + user_args['password'] = password + + user = self.conn.create_user(**user_args) + changed = True + else: + if update_password == 'always': + if not password: + self.fail_json(msg="update_password is always but a password value is missing") + user_args['password'] = password + + if self._needs_update(user_args, user): + user = self.conn.update_user(user['id'], **user_args) + changed = True + + self.exit_json(changed=changed, user=user) + elif state == 'absent' and user is not None: + self.conn.identity.delete_user(user['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityUserModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/identity_user_info.py b/ansible_collections/openstack/cloud/plugins/modules/identity_user_info.py new file mode 100644 index 00000000..c0e0d949 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/identity_user_info.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_user_info +short_description: Retrieve information about one or more OpenStack users +author: OpenStack Ansible SIG +description: + - Retrieve information about a one or more OpenStack users + - This module was called C(openstack.cloud.identity_user_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.identity_user_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the user + type: str + domain: + description: + - Name or ID of the domain containing the user if the cloud supports domains + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about previously created users +- openstack.cloud.identity_user_info: + cloud: awesomecloud + register: result +- debug: + msg: "{{ result.openstack_users }}" + +# Gather information about a previously created user by name +- openstack.cloud.identity_user_info: + cloud: awesomecloud + name: demouser + register: result +- debug: + msg: "{{ result.openstack_users }}" + +# Gather information about a previously created user in a specific domain +- openstack.cloud.identity_user_info: + cloud: awesomecloud + name: demouser + domain: admindomain + register: result +- debug: + msg: "{{ result.openstack_users }}" + +# Gather information about a previously created user in a specific domain with filter +- openstack.cloud.identity_user_info: + cloud: awesomecloud + name: demouser + domain: admindomain + filters: + enabled: False + register: result +- debug: + msg: "{{ result.openstack_users }}" +''' + + +RETURN = ''' +openstack_users: + description: has all the OpenStack information about users + returned: always + type: list + elements: dict + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Username of the user. + returned: success + type: str + default_project_id: + description: Default project ID of the user + returned: success + type: str + description: + description: The description of this user + returned: success + type: str + domain_id: + description: Domain ID containing the user + returned: success + type: str + email: + description: Email of the user + returned: success + type: str + enabled: + description: Flag to indicate if the user is enabled + returned: success + type: bool + username: + description: Username with Identity API v2 (OpenStack Pike or earlier) else Null + returned: success + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityUserInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + domain=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + deprecated_names = ('openstack.cloud.identity_user_facts') + + def run(self): + name = self.params['name'] + domain = self.params['domain'] + filters = self.params['filters'] + + args = {} + if domain: + dom_obj = self.conn.identity.find_domain(domain) + if dom_obj is None: + self.fail_json( + msg="Domain name or ID '{0}' does not exist".format(domain)) + args['domain_id'] = dom_obj.id + + users = self.conn.search_users(name, filters, **args) + self.exit_json(changed=False, openstack_users=users) + + +def main(): + module = IdentityUserInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/image.py b/ansible_collections/openstack/cloud/plugins/modules/image.py new file mode 100644 index 00000000..fae13a2e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/image.py @@ -0,0 +1,270 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +# TODO(mordred): we need to support "location"(v1) and "locations"(v2) + +DOCUMENTATION = ''' +--- +module: image +short_description: Add/Delete images from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove images from the OpenStack Image Repository +options: + name: + description: + - The name of the image when uploading - or the name/ID of the image if deleting + required: true + type: str + id: + description: + - The ID of the image when uploading an image + type: str + checksum: + description: + - The checksum of the image + type: str + disk_format: + description: + - The format of the disk that is getting uploaded + default: qcow2 + choices: ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso', 'vhdx', 'ploop'] + type: str + container_format: + description: + - The format of the container + default: bare + choices: ['ami', 'aki', 'ari', 'bare', 'ovf', 'ova', 'docker'] + type: str + project: + description: + - The name or ID of the project owning the image + type: str + aliases: ['owner'] + project_domain: + description: + - The domain the project owning the image belongs to + - May be used to identify a unique project when providing a name to the project argument and multiple projects with such name exist + type: str + min_disk: + description: + - The minimum disk space (in GB) required to boot this image + type: int + min_ram: + description: + - The minimum ram (in MB) required to boot this image + type: int + is_public: + description: + - Whether the image can be accessed publicly. Note that publicizing an image requires admin role by default. + type: bool + default: false + protected: + description: + - Prevent image from being deleted + type: bool + default: false + filename: + description: + - The path to the file which has to be uploaded + type: str + ramdisk: + description: + - The name of an existing ramdisk image that will be associated with this image + type: str + kernel: + description: + - The name of an existing kernel image that will be associated with this image + type: str + properties: + description: + - Additional properties to be associated with this image + default: {} + type: dict + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + tags: + description: + - List of tags to be applied to the image + default: [] + type: list + elements: str + volume: + description: + - ID of a volume to create an image from. + - The volume must be in AVAILABLE state. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Upload an image from a local file named cirros-0.3.0-x86_64-disk.img +- openstack.cloud.image: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + openstack.cloud.identity_user_domain_name: Default + openstack.cloud.project_domain_name: Default + name: cirros + container_format: bare + disk_format: qcow2 + state: present + filename: cirros-0.3.0-x86_64-disk.img + kernel: cirros-vmlinuz + ramdisk: cirros-initrd + tags: + - custom + properties: + cpu_arch: x86_64 + distro: ubuntu + +# Create image from volume attached to an instance +- name: create volume snapshot + openstack.cloud.volume_snapshot: + auth: + "{{ auth }}" + display_name: myvol_snapshot + volume: myvol + force: yes + register: myvol_snapshot + +- name: create volume from snapshot + openstack.cloud.volume: + auth: + "{{ auth }}" + size: "{{ myvol_snapshot.snapshot.size }}" + snapshot_id: "{{ myvol_snapshot.snapshot.id }}" + display_name: myvol_snapshot_volume + wait: yes + register: myvol_snapshot_volume + +- name: create image from volume snapshot + openstack.cloud.image: + auth: + "{{ auth }}" + volume: "{{ myvol_snapshot_volume.volume.id }}" + name: myvol_image +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ImageModule(OpenStackModule): + + deprecated_names = ('os_image', 'openstack.cloud.os_image') + + argument_spec = dict( + name=dict(required=True, type='str'), + id=dict(type='str'), + checksum=dict(type='str'), + disk_format=dict(default='qcow2', + choices=['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso', 'vhdx', 'ploop']), + container_format=dict(default='bare', choices=['ami', 'aki', 'ari', 'bare', 'ovf', 'ova', 'docker']), + project=dict(type='str', aliases=['owner']), + project_domain=dict(type='str'), + min_disk=dict(type='int', default=0), + min_ram=dict(type='int', default=0), + is_public=dict(type='bool', default=False), + protected=dict(type='bool', default=False), + filename=dict(type='str'), + ramdisk=dict(type='str'), + kernel=dict(type='str'), + properties=dict(type='dict', default={}), + volume=dict(type='str'), + tags=dict(type='list', default=[], elements='str'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + mutually_exclusive=[['filename', 'volume']], + ) + + def run(self): + + changed = False + if self.params['id']: + image = self.conn.get_image(name_or_id=self.params['id']) + elif self.params['checksum']: + image = self.conn.get_image(name_or_id=self.params['name'], filters={'checksum': self.params['checksum']}) + else: + image = self.conn.get_image(name_or_id=self.params['name']) + + if self.params['state'] == 'present': + if not image: + kwargs = {} + if self.params['id'] is not None: + kwargs['id'] = self.params['id'] + if self.params['project']: + project_domain = {'id': None} + if self.params['project_domain']: + project_domain = self.conn.get_domain(name_or_id=self.params['project_domain']) + if not project_domain or project_domain['id'] is None: + self.fail(msg='Project domain %s could not be found' % self.params['project_domain']) + project = self.conn.get_project(name_or_id=self.params['project'], domain_id=project_domain['id']) + if not project: + self.fail(msg='Project %s could not be found' % self.params['project']) + kwargs['owner'] = project['id'] + image = self.conn.create_image( + name=self.params['name'], + filename=self.params['filename'], + disk_format=self.params['disk_format'], + container_format=self.params['container_format'], + wait=self.params['wait'], + timeout=self.params['timeout'], + is_public=self.params['is_public'], + protected=self.params['protected'], + min_disk=self.params['min_disk'], + min_ram=self.params['min_ram'], + volume=self.params['volume'], + tags=self.params['tags'], + **kwargs + ) + changed = True + if not self.params['wait']: + self.exit(changed=changed, image=image, id=image.id) + + self.conn.update_image_properties( + image=image, + kernel=self.params['kernel'], + ramdisk=self.params['ramdisk'], + protected=self.params['protected'], + **self.params['properties']) + if self.params['tags']: + self.conn.image.update_image(image.id, tags=self.params['tags']) + image = self.conn.get_image(name_or_id=image.id) + self.exit(changed=changed, image=image, id=image.id) + + elif self.params['state'] == 'absent': + if not image: + changed = False + else: + self.conn.delete_image( + name_or_id=self.params['name'], + wait=self.params['wait'], + timeout=self.params['timeout']) + changed = True + self.exit(changed=changed) + + +def main(): + module = ImageModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/image_info.py b/ansible_collections/openstack/cloud/plugins/modules/image_info.py new file mode 100644 index 00000000..f02079c0 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/image_info.py @@ -0,0 +1,204 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: image_info +short_description: Retrieve information about an image within OpenStack. +author: OpenStack Ansible SIG +description: + - Retrieve information about a image image from OpenStack. + - This module was called C(openstack.cloud.image_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.image_info) module no longer returns C(ansible_facts)! +options: + image: + description: + - Name or ID of the image + required: false + type: str + filters: + description: + - Dict of properties of the images used for query + type: dict + required: false + aliases: ['properties'] +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Gather information about a previously created image named image1 + openstack.cloud.image_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + image: image1 + register: result + +- name: Show openstack information + debug: + msg: "{{ result.image }}" + +# Show all available Openstack images +- name: Retrieve all available Openstack images + openstack.cloud.image_info: + register: result + +- name: Show images + debug: + msg: "{{ result.image }}" + +# Show images matching requested properties +- name: Retrieve images having properties with desired values + openstack.cloud.image_facts: + filters: + some_property: some_value + OtherProp: OtherVal + +- name: Show images + debug: + msg: "{{ result.image }}" +''' + +RETURN = ''' +openstack_images: + description: has all the openstack information about the image + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the image. + returned: success + type: str + status: + description: Image status. + returned: success + type: str + created_at: + description: Image created at timestamp. + returned: success + type: str + container_format: + description: Container format of the image. + returned: success + type: str + direct_url: + description: URL to access the image file kept in external store. + returned: success + type: str + min_ram: + description: Min amount of RAM required for this image. + returned: success + type: int + disk_format: + description: Disk format of the image. + returned: success + type: str + file: + description: The URL for the virtual machine image file. + returned: success + type: str + os_hidden: + description: Controls whether an image is displayed in the default image-list response + returned: success + type: bool + locations: + description: A list of URLs to access the image file in external store. + returned: success + type: str + metadata: + description: The location metadata. + returned: success + type: str + schema: + description: URL for the schema describing a virtual machine image. + returned: success + type: str + updated_at: + description: Image updated at timestamp. + returned: success + type: str + virtual_size: + description: The virtual size of the image. + returned: success + type: str + min_disk: + description: Min amount of disk space required for this image. + returned: success + type: int + is_protected: + description: Image protected flag. + returned: success + type: bool + checksum: + description: Checksum for the image. + returned: success + type: str + owner: + description: Owner for the image. + returned: success + type: str + visibility: + description: Indicates who has access to the image. + returned: success + type: str + size: + description: Size of the image. + returned: success + type: int + tags: + description: List of tags assigned to the image + returned: success + type: list +''' +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ImageInfoModule(OpenStackModule): + + deprecated_names = ('openstack.cloud.os_image_facts', 'openstack.cloud.os_image_info') + + argument_spec = dict( + image=dict(type='str', required=False), + filters=dict(type='dict', required=False, aliases=['properties']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + args = { + 'name_or_id': self.params['image'], + 'filters': self.params['filters'], + } + args = {k: v for k, v in args.items() if v is not None} + images = self.conn.search_images(**args) + + # for backward compatibility + if 'name_or_id' in args: + image = images[0] if images else None + else: + image = images + + self.exit(changed=False, openstack_images=images, image=image) + + +def main(): + module = ImageInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/keypair.py b/ansible_collections/openstack/cloud/plugins/modules/keypair.py new file mode 100644 index 00000000..232d4985 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/keypair.py @@ -0,0 +1,156 @@ +#!/usr/bin/python + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# Copyright (c) 2013, John Dewey <john@dewey.ws> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: keypair +short_description: Add/Delete a keypair from OpenStack +author: OpenStack Ansible SIG +description: + - Add or Remove key pair from OpenStack +options: + name: + description: + - Name that has to be given to the key pair + required: true + type: str + public_key: + description: + - The public key that would be uploaded to nova and injected into VMs + upon creation. + type: str + public_key_file: + description: + - Path to local file containing ssh public key. Mutually exclusive + with public_key. + type: str + state: + description: + - Should the resource be present or absent. If state is replace and + the key exists but has different content, delete it and recreate it + with the new content. + choices: [present, absent, replace] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Creates a key pair with the running users public key +- openstack.cloud.keypair: + cloud: mordred + state: present + name: ansible_key + public_key_file: /home/me/.ssh/id_rsa.pub + +# Creates a new key pair and the private key returned after the run. +- openstack.cloud.keypair: + cloud: rax-dfw + state: present + name: ansible_key +''' + +RETURN = ''' +id: + description: Unique UUID. + returned: success + type: str +name: + description: Name given to the keypair. + returned: success + type: str +public_key: + description: The public key value for the keypair. + returned: success + type: str +private_key: + description: The private key value for the keypair. + returned: Only when a keypair is generated for the user (e.g., when creating one + and a public key is not specified). + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule) + + +class KeyPairModule(OpenStackModule): + deprecated_names = ('os_keypair', 'openstack.cloud.os_keypair') + + argument_spec = dict( + name=dict(required=True), + public_key=dict(default=None), + public_key_file=dict(default=None), + state=dict(default='present', + choices=['absent', 'present', 'replace']), + ) + + module_kwargs = dict( + mutually_exclusive=[['public_key', 'public_key_file']]) + + def _system_state_change(self, keypair): + state = self.params['state'] + if state == 'present' and not keypair: + return True + if state == 'absent' and keypair: + return True + return False + + def run(self): + + state = self.params['state'] + name = self.params['name'] + public_key = self.params['public_key'] + + if self.params['public_key_file']: + with open(self.params['public_key_file']) as public_key_fh: + public_key = public_key_fh.read() + + keypair = self.conn.get_keypair(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(keypair)) + + if state in ('present', 'replace'): + if keypair and keypair['name'] == name: + if public_key and (public_key != keypair['public_key']): + if state == 'present': + self.fail_json( + msg="Key name %s present but key hash not the same" + " as offered. Delete key first." % name + ) + else: + self.conn.delete_keypair(name) + keypair = self.conn.create_keypair(name, public_key) + changed = True + else: + changed = False + else: + keypair = self.conn.create_keypair(name, public_key) + changed = True + + self.exit_json(changed=changed, key=keypair, id=keypair['id']) + + elif state == 'absent': + if keypair: + self.conn.delete_keypair(name) + self.exit_json(changed=True) + self.exit_json(changed=False) + + +def main(): + module = KeyPairModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/keypair_info.py b/ansible_collections/openstack/cloud/plugins/modules/keypair_info.py new file mode 100644 index 00000000..1fffe2c8 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/keypair_info.py @@ -0,0 +1,141 @@ +#!/usr/bin/python + +# Copyright (c) 2021 T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: keypair_info +short_description: Get information about keypairs from OpenStack +author: OpenStack Ansible SIG +description: + - Get information about keypairs that are associated with the account +options: + name: + description: + - Name or ID of the keypair + type: str + user_id: + description: + - It allows admin users to operate key-pairs of specified user ID. + type: str + limit: + description: + - Requests a page size of items. + - Returns a number of items up to a limit value. + type: int + marker: + description: + - The last-seen item. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Get information about keypairs + openstack.cloud.keypair_info: + register: result + +- name: Get information about keypairs using optional parameters + openstack.cloud.keypair_info: + name: "test" + user_id: "fed75b36fd7a4078a769178d2b1bd844" + limit: 10 + marker: "jdksl" + register: result +''' + +RETURN = ''' +openstack_keypairs: + description: + - Lists keypairs that are associated with the account. + type: list + elements: dict + returned: always + contains: + created_at: + description: + - The date and time when the resource was created. + type: str + sample: "2021-01-19T14:52:07.261634" + id: + description: + - The id identifying the keypair + type: str + sample: "keypair-5d935425-31d5-48a7-a0f1-e76e9813f2c3" + is_deleted: + description: + - A boolean indicates whether this keypair is deleted or not. + type: bool + fingerprint: + description: + - The fingerprint for the keypair. + type: str + sample: "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd" + name: + description: + - A keypair name which will be used to reference it later. + type: str + sample: "keypair-5d935425-31d5-48a7-a0f1-e76e9813f2c3" + private_key: + description: + - The private key for the keypair. + type: str + sample: "MIICXAIBAAKBgQCqGKukO ... hZj6+H0qtjTkVxwTCpvKe4eCZ0FPq" + public_key: + description: + - The keypair public key. + type: str + sample: "ssh-rsa AAAAB3NzaC1yc ... 8rPsBUHNLQp Generated-by-Nova" + type: + description: + - The type of the keypair. + - Allowed values are ssh or x509. + type: str + sample: "ssh" + user_id: + description: + - It allows admin users to operate key-pairs of specified user ID. + type: str + sample: "59b10f2a2138428ea9358e10c7e44444" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule) + + +class KeyPairInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(type='str', required=False), + user_id=dict(type='str', required=False), + limit=dict(type='int', required=False), + marker=dict(type='str', required=False) + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + filters = {k: self.params[k] for k in + ['user_id', 'name', 'limit', 'marker'] + if self.params[k] is not None} + keypairs = self.conn.search_keypairs(name_or_id=self.params['name'], + filters=filters) + # self.conn.search_keypairs() returned munch.Munch objects before Train + result = [raw if isinstance(raw, dict) else raw.to_dict(computed=False) + for raw in keypairs] + self.exit(changed=False, openstack_keypairs=result) + + +def main(): + module = KeyPairInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/keystone_federation_protocol.py b/ansible_collections/openstack/cloud/plugins/modules/keystone_federation_protocol.py new file mode 100644 index 00000000..5a33d8a3 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/keystone_federation_protocol.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: keystone_federation_protocol +short_description: manage a federation Protocol +author: OpenStack Ansible SIG +description: + - Manage a federation Protocol. +options: + name: + description: + - The name of the Protocol. + type: str + required: true + aliases: ['id'] + state: + description: + - Whether the protocol should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str + idp_id: + description: + - The name of the Identity Provider this Protocol is associated with. + aliases: ['idp_name'] + required: true + type: str + mapping_id: + description: + - The name of the Mapping to use for this Protocol.' + - Required when creating a new Protocol. + type: str + aliases: ['mapping_name'] +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a protocol + openstack.cloud.keystone_federation_protocol: + cloud: example_cloud + name: example_protocol + idp_id: example_idp + mapping_id: example_mapping + +- name: Delete a protocol + openstack.cloud.keystone_federation_protocol: + cloud: example_cloud + name: example_protocol + idp_id: example_idp + state: absent +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationProtocolModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True, aliases=['id']), + state=dict(default='present', choices=['absent', 'present']), + idp_id=dict(required=True, aliases=['idp_name']), + mapping_id=dict(aliases=['mapping_name']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def normalize_protocol(self, protocol): + """ + Normalizes the protocol definitions so that the outputs are consistent with the + parameters + + - "name" (parameter) == "id" (SDK) + """ + if protocol is None: + return None + + _protocol = protocol.to_dict() + _protocol['name'] = protocol['id'] + # As of 0.44 SDK doesn't copy the URI parameters over, so let's add them + _protocol['idp_id'] = protocol['idp_id'] + return _protocol + + def delete_protocol(self, protocol): + """ + Delete an existing Protocol + + returns: the "Changed" state + """ + if protocol is None: + return False + + if self.ansible.check_mode: + return True + + self.conn.identity.delete_federation_protocol(None, protocol) + return True + + def create_protocol(self, name): + """ + Create a new Protocol + + returns: the "Changed" state and the new protocol + """ + if self.ansible.check_mode: + return True, None + + idp_name = self.params.get('idp_id') + mapping_id = self.params.get('mapping_id') + + attributes = { + 'idp_id': idp_name, + 'mapping_id': mapping_id, + } + + protocol = self.conn.identity.create_federation_protocol(id=name, **attributes) + return (True, protocol) + + def update_protocol(self, protocol): + """ + Update an existing Protocol + + returns: the "Changed" state and the new protocol + """ + mapping_id = self.params.get('mapping_id') + + attributes = {} + + if (mapping_id is not None) and (mapping_id != protocol.mapping_id): + attributes['mapping_id'] = mapping_id + + if not attributes: + return False, protocol + + if self.ansible.check_mode: + return True, None + + new_protocol = self.conn.identity.update_federation_protocol(None, protocol, **attributes) + return (True, new_protocol) + + def run(self): + """ Module entry point """ + name = self.params.get('name') + state = self.params.get('state') + idp = self.params.get('idp_id') + changed = False + + protocol = self.conn.identity.find_federation_protocol(idp, name) + + if state == 'absent': + if protocol is not None: + changed = self.delete_protocol(protocol) + self.exit_json(changed=changed) + + # state == 'present' + else: + if protocol is None: + if self.params.get('mapping_id') is None: + self.fail_json( + msg='A mapping_id must be passed when creating' + ' a protocol') + (changed, protocol) = self.create_protocol(name) + protocol = self.normalize_protocol(protocol) + self.exit_json(changed=changed, protocol=protocol) + + else: + (changed, new_protocol) = self.update_protocol(protocol) + new_protocol = self.normalize_protocol(new_protocol) + self.exit_json(changed=changed, protocol=new_protocol) + + +def main(): + module = IdentityFederationProtocolModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/keystone_federation_protocol_info.py b/ansible_collections/openstack/cloud/plugins/modules/keystone_federation_protocol_info.py new file mode 100644 index 00000000..b281b13e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/keystone_federation_protocol_info.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: keystone_federation_protocol_info +short_description: get information about federation Protocols +author: OpenStack Ansible SIG +description: + - Get information about federation Protocols. +options: + name: + description: + - The name of the Protocol. + type: str + aliases: ['id'] + idp_id: + description: + - The name of the Identity Provider this Protocol is associated with. + aliases: ['idp_name'] + required: true + type: str +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Describe a protocol + openstack.cloud.keystone_federation_protocol_info: + cloud: example_cloud + name: example_protocol + idp_id: example_idp + mapping_name: example_mapping + +- name: Describe all protocols attached to an IDP + openstack.cloud.keystone_federation_protocol_info: + cloud: example_cloud + idp_id: example_idp +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationProtocolInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(aliases=['id']), + idp_id=dict(required=True, aliases=['idp_name']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def normalize_protocol(self, protocol): + """ + Normalizes the protocol definitions so that the outputs are consistent with the + parameters + + - "name" (parameter) == "id" (SDK) + """ + if protocol is None: + return None + + _protocol = protocol.to_dict() + _protocol['name'] = protocol['id'] + # As of 0.44 SDK doesn't copy the URI parameters over, so let's add them + _protocol['idp_id'] = protocol['idp_id'] + return _protocol + + def run(self): + """ Module entry point """ + + name = self.params.get('name') + idp = self.params.get('idp_id') + + if name: + protocol = self.conn.identity.get_federation_protocol(idp, name) + protocol = self.normalize_protocol(protocol) + self.exit_json(changed=False, protocols=[protocol]) + + else: + protocols = list(map(self.normalize_protocol, self.conn.identity.federation_protocols(idp))) + self.exit_json(changed=False, protocols=protocols) + + +def main(): + module = IdentityFederationProtocolInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/lb_health_monitor.py b/ansible_collections/openstack/cloud/plugins/modules/lb_health_monitor.py new file mode 100644 index 00000000..94de4485 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/lb_health_monitor.py @@ -0,0 +1,291 @@ +#!/usr/bin/python + +# Copyright (c) 2020 Jesper Schmitz Mouridsen. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: lb_health_monitor +author: OpenStack Ansible SIG +short_description: Add/Delete a health m nonitor to a pool in the load balancing service from OpenStack Cloud +description: + - Add or Remove a health monitor to/from a pool in the OpenStack load-balancer service. +options: + name: + type: 'str' + description: + - Name that has to be given to the health monitor + required: true + state: + type: 'str' + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + pool: + required: true + type: 'str' + description: + - The pool name or id to monitor by the health monitor. + type: + type: 'str' + default: HTTP + description: + - One of HTTP, HTTPS, PING, SCTP, TCP, TLS-HELLO, or UDP-CONNECT. + choices: [HTTP, HTTPS, PING, SCTP, TCP, TLS-HELLO, UDP-CONNECT] + delay: + type: 'str' + required: true + description: + - the interval, in seconds, between health checks. + max_retries: + required: true + type: 'str' + description: + - The number of successful checks before changing the operating status of the member to ONLINE. + max_retries_down: + type: 'str' + default: '3' + description: + - The number of allowed check failures before changing the operating status of the member to ERROR. A valid value is from 1 to 10. The default is 3. + resp_timeout: + required: true + description: + - The time, in seconds, after which a health check times out. Must be less than delay + type: int + admin_state_up: + default: True + description: + - The admin state of the helath monitor true for up or false for down + type: bool + expected_codes: + type: 'str' + default: '200' + description: + - The list of HTTP status codes expected in response from the member to declare it healthy. Specify one of the following values + A single value, such as 200 + A list, such as 200, 202 + A range, such as 200-204 + http_method: + type: 'str' + default: GET + choices: ['GET', 'CONNECT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'] + description: + - The HTTP method that the health monitor uses for requests. One of CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, or TRACE. The default is GET. + url_path: + type: 'str' + default: '/' + description: + - The HTTP URL path of the request sent by the monitor to test the health of a backend member. + Must be a string that begins with a forward slash (/). The default URL path is /. +requirements: ["openstacksdk"] +extends_documentation_fragment: +- openstack.cloud.openstack +''' +EXAMPLES = ''' +#Create a healtmonitor named healthmonitor01 with method HEAD url_path /status and expect code 200 +- openstack.cloud.lb_health_monitor: + auth: + auth_url: "{{keystone_url}}" + username: "{{username}}" + password: "{{password}}" + project_domain_name: "{{domain_name}}" + user_domain_name: "{{domain_name}}" + project_name: "{{project_name}}" + wait: true + admin_state_up: True + expected_codes: '200' + max_retries_down: '4' + http_method: GET + url_path: "/status" + pool: '{{pool_id}}' + name: 'healthmonitor01' + delay: '10' + max_retries: '3' + resp_timeout: '5' + state: present +''' +RETURN = ''' +health_monitor: + description: Dictionary describing the health monitor. + returned: On success when C(state=present) + type: complex + contains: + id: + description: The health monitor UUID. + returned: On success when C(state=present) + type: str + admin_state_up: + returned: On success when C(state=present) + description: The administrative state of the resource. + type: bool + created_at: + returned: On success when C(state=present) + description: The UTC date and timestamp when the resource was created. + type: str + delay: + returned: On success when C(state=present) + description: The time, in seconds, between sending probes to members. + type: int + expected_codes: + returned: On success when C(state=present) + description: The list of HTTP status codes expected in response from the member to declare it healthy. + type: str + http_method: + returned: On success when C(state=present) + description: The HTTP method that the health monitor uses for requests. + type: str + max_retries: + returned: On success when C(state=present) + description: The number of successful checks before changing the operating status of the member to ONLINE. + type: str + max_retries_down: + returned: On success when C(state=present) + description: The number of allowed check failures before changing the operating status of the member to ERROR. + type: str + name: + returned: On success when C(state=present) + description: Human-readable name of the resource. + type: str + operating_status: + returned: On success when C(state=present) + description: The operating status of the resource. + type: str + pool_id: + returned: On success when C(state=present) + description: The id of the pool. + type: str + project_id: + returned: On success when C(state=present) + description: The ID of the project owning this resource. + type: str + provisioning_status: + returned: On success when C(state=present) + description: The provisioning status of the resource. + type: str + timeout: + returned: On success when C(state=present) + description: The maximum time, in seconds, that a monitor waits to connect before it times out. + type: int + type: + returned: On success when C(state=present) + description: The type of health monitor. + type: str + updated_at: + returned: On success when C(state=present) + description: The UTC date and timestamp when the resource was last updated. + type: str + url_path: + returned: On success when C(state=present) + description: The HTTP URL path of the request sent by the monitor to test the health of a backend member. + type: str +''' +import time + + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class HealthMonitorModule(OpenStackModule): + + def _wait_for_health_monitor_status(self, health_monitor_id, status, failures, interval=5): + timeout = self.params['timeout'] + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + health_monitor = self.conn.load_balancer.get_health_monitor(health_monitor_id) + provisioning_status = health_monitor.provisioning_status + if provisioning_status == status: + return health_monitor + if provisioning_status in failures: + self._fail_json( + msg="health monitor %s transitioned to failure state %s" % + (health_monitor, provisioning_status) + ) + + time.sleep(interval) + total_sleep += interval + + self._fail_json(msg="timeout waiting for health monitor %s to transition to %s" % + (health_monitor_id, status) + ) + + argument_spec = dict( + name=dict(required=True), + delay=dict(required=True), + max_retries=dict(required=True), + max_retries_down=dict(required=False, default="3"), + resp_timeout=dict(required=True, type='int'), + pool=dict(required=True), + expected_codes=dict(required=False, default="200"), + admin_state_up=dict(required=False, default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + http_method=dict(default="GET", required=False, choices=["GET", "CONNECT", "DELETE", + "HEAD", "OPTIONS", "PATCH", + "POST", "PUT", "TRACE"]), + url_path=dict(default="/", required=False), + type=dict(default='HTTP', + choices=['HTTP', 'HTTPS', 'PING', 'SCTP', 'TCP', 'TLS-HELLO', 'UDP-CONNECT'])) + + module_kwargs = dict(supports_check_mode=True) + + def run(self): + + try: + changed = False + health_monitor = self.conn.load_balancer.find_health_monitor(name_or_id=self.params['name']) + pool = self.conn.load_balancer.find_pool(name_or_id=self.params['pool']) + if self.params['state'] == 'present': + if not health_monitor: + changed = True + health_attrs = {"pool_id": pool.id, + "type": self.params["type"], + "delay": self.params['delay'], + "max_retries": self.params['max_retries'], + "max_retries_down": self.params['max_retries_down'], + "timeout": self.params['resp_timeout'], + "name": self.params['name'], + "admin_state_up": self.params["admin_state_up"], + } + if self.params["type"] in ["HTTP", "HTTPS"]: + health_attrs["expected_codes"] = self.params["expected_codes"] + health_attrs["http_method"] = self.params["http_method"] + health_attrs["url_path"] = self.params["url_path"] + + if self.ansible.check_mode: + self.exit_json(changed=True) + + health_monitor = self.conn.load_balancer.create_health_monitor(**health_attrs) + if not self.params['wait']: + self.exit_json(changed=changed, id=health_monitor.id, + health_monitor=health_monitor.to_dict()) + else: + health_monitor = self._wait_for_health_monitor_status(health_monitor.id, "ACTIVE", ["ERROR"]) + self.exit_json(changed=changed, id=health_monitor.id, + health_monitor=health_monitor.to_dict()) + else: + self.exit_json(changed=changed, id=health_monitor.id, + health_monitor=health_monitor.to_dict() + ) + elif self.params['state'] == 'absent': + if health_monitor: + if self.ansible.check_mode: + self.exit_json(changed=True) + self.conn.load_balancer.delete_health_monitor(health_monitor) + changed = True + + self.exit_json(changed=changed) + except Exception as e: + self.fail(msg=str(e)) + + +def main(): + module = HealthMonitorModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/lb_listener.py b/ansible_collections/openstack/cloud/plugins/modules/lb_listener.py new file mode 100644 index 00000000..f4cdad48 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/lb_listener.py @@ -0,0 +1,287 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst Cloud Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: lb_listener +short_description: Add/Delete a listener for a load balancer from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove a listener for a load balancer from the OpenStack load-balancer service. +options: + name: + description: + - Name that has to be given to the listener + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + loadbalancer: + description: + - The name or id of the load balancer that this listener belongs to. + required: true + type: str + protocol: + description: + - The protocol for the listener. + choices: [HTTP, HTTPS, TCP, TERMINATED_HTTPS, UDP, SCTP] + default: HTTP + type: str + protocol_port: + description: + - The protocol port number for the listener. + default: 80 + type: int + timeout_client_data: + description: + - Client inactivity timeout in milliseconds. + default: 50000 + type: int + timeout_member_data: + description: + - Member inactivity timeout in milliseconds. + default: 50000 + type: int + wait: + description: + - If the module should wait for the load balancer to be ACTIVE. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the load balancer to get + into ACTIVE state. + default: 180 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The listener UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +listener: + description: Dictionary describing the listener. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + name: + description: Name given to the listener. + type: str + sample: "test" + description: + description: The listener description. + type: str + sample: "description" + load_balancer_id: + description: The load balancer UUID this listener belongs to. + type: str + sample: "b32eef7e-d2a6-4ea4-a301-60a873f89b3b" + loadbalancers: + description: A list of load balancer IDs.. + type: list + sample: [{"id": "b32eef7e-d2a6-4ea4-a301-60a873f89b3b"}] + provisioning_status: + description: The provisioning status of the listener. + type: str + sample: "ACTIVE" + operating_status: + description: The operating status of the listener. + type: str + sample: "ONLINE" + is_admin_state_up: + description: The administrative state of the listener. + type: bool + sample: true + protocol: + description: The protocol for the listener. + type: str + sample: "HTTP" + protocol_port: + description: The protocol port number for the listener. + type: int + sample: 80 + timeout_client_data: + description: Client inactivity timeout in milliseconds. + type: int + sample: 50000 + timeout_member_data: + description: Member inactivity timeout in milliseconds. + type: int + sample: 50000 +''' + +EXAMPLES = ''' +# Create a listener, wait for the loadbalancer to be active. +- openstack.cloud.lb_listener: + cloud: mycloud + endpoint_type: admin + state: present + name: test-listener + loadbalancer: test-loadbalancer + protocol: HTTP + protocol_port: 8080 + +# Create a listener, do not wait for the loadbalancer to be active. +- openstack.cloud.lb_listener: + cloud: mycloud + endpoint_type: admin + state: present + name: test-listener + loadbalancer: test-loadbalancer + protocol: HTTP + protocol_port: 8080 + wait: no + +# Delete a listener +- openstack.cloud.lb_listener: + cloud: mycloud + endpoint_type: admin + state: absent + name: test-listener + loadbalancer: test-loadbalancer + +# Create a listener, increase timeouts for connection persistence (for SSH for example). +- openstack.cloud.lb_listener: + cloud: mycloud + endpoint_type: admin + state: present + name: test-listener + loadbalancer: test-loadbalancer + protocol: TCP + protocol_port: 22 + timeout_client_data: 1800000 + timeout_member_data: 1800000 +''' + +import time + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class LoadbalancerListenerModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + loadbalancer=dict(required=True), + protocol=dict(default='HTTP', + choices=['HTTP', 'HTTPS', 'TCP', 'TERMINATED_HTTPS', 'UDP', 'SCTP']), + protocol_port=dict(default=80, type='int', required=False), + timeout_client_data=dict(default=50000, type='int', required=False), + timeout_member_data=dict(default=50000, type='int', required=False), + ) + module_kwargs = dict() + + def _lb_wait_for_status(self, lb, status, failures, interval=5): + """Wait for load balancer to be in a particular provisioning status.""" + timeout = self.params['timeout'] + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + lb = self.conn.load_balancer.get_load_balancer(lb.id) + if lb.provisioning_status == status: + return None + if lb.provisioning_status in failures: + self.fail_json( + msg="Load Balancer %s transitioned to failure state %s" % + (lb.id, lb.provisioning_status) + ) + + time.sleep(interval) + total_sleep += interval + + self.fail_json( + msg="Timeout waiting for Load Balancer %s to transition to %s" % + (lb.id, status) + ) + + def run(self): + loadbalancer = self.params['loadbalancer'] + loadbalancer_id = None + + changed = False + listener = self.conn.load_balancer.find_listener( + name_or_id=self.params['name']) + + if self.params['state'] == 'present': + if not listener: + lb = self.conn.load_balancer.find_load_balancer(loadbalancer) + if not lb: + self.fail_json( + msg='load balancer %s is not found' % loadbalancer + ) + loadbalancer_id = lb.id + + listener = self.conn.load_balancer.create_listener( + name=self.params['name'], + loadbalancer_id=loadbalancer_id, + protocol=self.params['protocol'], + protocol_port=self.params['protocol_port'], + timeout_client_data=self.params['timeout_client_data'], + timeout_member_data=self.params['timeout_member_data'], + ) + changed = True + + if not self.params['wait']: + self.exit_json( + changed=changed, listener=listener.to_dict(), + id=listener.id) + + if self.params['wait']: + # Check in case the listener already exists. + lb = self.conn.load_balancer.find_load_balancer(loadbalancer) + if not lb: + self.fail_json( + msg='load balancer %s is not found' % loadbalancer + ) + self._lb_wait_for_status(lb, "ACTIVE", ["ERROR"]) + + self.exit_json( + changed=changed, listener=listener.to_dict(), id=listener.id) + elif self.params['state'] == 'absent': + if not listener: + changed = False + else: + self.conn.load_balancer.delete_listener(listener) + changed = True + + if self.params['wait']: + # Wait for the load balancer to be active after deleting + # the listener. + lb = self.conn.load_balancer.find_load_balancer(loadbalancer) + if not lb: + self.fail_json( + msg='load balancer %s is not found' % loadbalancer + ) + self._lb_wait_for_status(lb, "ACTIVE", ["ERROR"]) + + self.exit_json(changed=changed) + + +def main(): + module = LoadbalancerListenerModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/lb_member.py b/ansible_collections/openstack/cloud/plugins/modules/lb_member.py new file mode 100644 index 00000000..264f2b8e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/lb_member.py @@ -0,0 +1,235 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst Cloud Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: lb_member +short_description: Add/Delete a member for a pool in load balancer from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove a member for a pool from the OpenStack load-balancer service. +options: + name: + description: + - Name that has to be given to the member + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + pool: + description: + - The name or id of the pool that this member belongs to. + required: true + type: str + protocol_port: + description: + - The protocol port number for the member. + default: 80 + type: int + address: + description: + - The IP address of the member. + type: str + subnet_id: + description: + - The subnet ID the member service is accessible from. + type: str + wait: + description: + - If the module should wait for the load balancer to be ACTIVE. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the load balancer to get + into ACTIVE state. + default: 180 + type: int + monitor_address: + description: + - IP address used to monitor this member + type: str + monitor_port: + description: + - Port used to monitor this member + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The member UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +member: + description: Dictionary describing the member. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + name: + description: Name given to the member. + type: str + sample: "test" + description: + description: The member description. + type: str + sample: "description" + provisioning_status: + description: The provisioning status of the member. + type: str + sample: "ACTIVE" + operating_status: + description: The operating status of the member. + type: str + sample: "ONLINE" + is_admin_state_up: + description: The administrative state of the member. + type: bool + sample: true + protocol_port: + description: The protocol port number for the member. + type: int + sample: 80 + subnet_id: + description: The subnet ID the member service is accessible from. + type: str + sample: "489247fa-9c25-11e8-9679-00224d6b7bc1" + address: + description: The IP address of the backend member server. + type: str + sample: "192.168.2.10" +''' + +EXAMPLES = ''' +# Create a member, wait for the member to be created. +- openstack.cloud.lb_member: + cloud: mycloud + endpoint_type: admin + state: present + name: test-member + pool: test-pool + address: 192.168.10.3 + protocol_port: 8080 + +# Delete a listener +- openstack.cloud.lb_member: + cloud: mycloud + endpoint_type: admin + state: absent + name: test-member + pool: test-pool +''' + +import time + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class LoadbalancerMemberModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + pool=dict(required=True), + address=dict(default=None), + protocol_port=dict(default=80, type='int'), + subnet_id=dict(default=None), + monitor_address=dict(default=None), + monitor_port=dict(default=None, type='int') + ) + module_kwargs = dict() + + def _wait_for_member_status(self, pool_id, member_id, status, + failures, interval=5): + timeout = self.params['timeout'] + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + member = self.conn.load_balancer.get_member(member_id, pool_id) + provisioning_status = member.provisioning_status + if provisioning_status == status: + return member + if provisioning_status in failures: + self.fail_json( + msg="Member %s transitioned to failure state %s" % + (member_id, provisioning_status) + ) + + time.sleep(interval) + total_sleep += interval + + self.fail_json( + msg="Timeout waiting for member %s to transition to %s" % + (member_id, status) + ) + + def run(self): + name = self.params['name'] + pool = self.params['pool'] + + changed = False + + pool_ret = self.conn.load_balancer.find_pool(name_or_id=pool) + if not pool_ret: + self.fail_json(msg='pool %s is not found' % pool) + + pool_id = pool_ret.id + member = self.conn.load_balancer.find_member(name, pool_id) + + if self.params['state'] == 'present': + if not member: + member = self.conn.load_balancer.create_member( + pool_ret, + address=self.params['address'], + name=name, + protocol_port=self.params['protocol_port'], + subnet_id=self.params['subnet_id'], + monitor_address=self.params['monitor_address'], + monitor_port=self.params['monitor_port'] + ) + changed = True + + if not self.params['wait']: + self.exit_json( + changed=changed, member=member.to_dict(), id=member.id) + + if self.params['wait']: + member = self._wait_for_member_status( + pool_id, member.id, "ACTIVE", ["ERROR"]) + + self.exit_json( + changed=changed, member=member.to_dict(), id=member.id) + + elif self.params['state'] == 'absent': + if member: + self.conn.load_balancer.delete_member(member, pool_ret) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = LoadbalancerMemberModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/lb_pool.py b/ansible_collections/openstack/cloud/plugins/modules/lb_pool.py new file mode 100644 index 00000000..6f73ea1c --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/lb_pool.py @@ -0,0 +1,263 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst Cloud Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: lb_pool +short_description: Add/Delete a pool in the load balancing service from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove a pool from the OpenStack load-balancer service. +options: + name: + description: + - Name that has to be given to the pool + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + loadbalancer: + description: + - The name or id of the load balancer that this pool belongs to. + Either loadbalancer or listener must be specified for pool creation. + type: str + listener: + description: + - The name or id of the listener that this pool belongs to. + Either loadbalancer or listener must be specified for pool creation. + type: str + protocol: + description: + - The protocol for the pool. + choices: [HTTP, HTTPS, PROXY, TCP, UDP] + default: HTTP + type: str + lb_algorithm: + description: + - The load balancing algorithm for the pool. + choices: [LEAST_CONNECTIONS, ROUND_ROBIN, SOURCE_IP] + default: ROUND_ROBIN + type: str + wait: + description: + - If the module should wait for the pool to be ACTIVE. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the pool to get + into ACTIVE state. + default: 180 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The pool UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +listener: + description: Dictionary describing the pool. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + name: + description: Name given to the pool. + type: str + sample: "test" + description: + description: The pool description. + type: str + sample: "description" + loadbalancers: + description: A list of load balancer IDs. + type: list + sample: [{"id": "b32eef7e-d2a6-4ea4-a301-60a873f89b3b"}] + listeners: + description: A list of listener IDs. + type: list + sample: [{"id": "b32eef7e-d2a6-4ea4-a301-60a873f89b3b"}] + members: + description: A list of member IDs. + type: list + sample: [{"id": "b32eef7e-d2a6-4ea4-a301-60a873f89b3b"}] + loadbalancer_id: + description: The load balancer ID the pool belongs to. This field is set when the pool doesn't belong to any listener in the load balancer. + type: str + sample: "7c4be3f8-9c2f-11e8-83b3-44a8422643a4" + listener_id: + description: The listener ID the pool belongs to. + type: str + sample: "956aa716-9c2f-11e8-83b3-44a8422643a4" + provisioning_status: + description: The provisioning status of the pool. + type: str + sample: "ACTIVE" + operating_status: + description: The operating status of the pool. + type: str + sample: "ONLINE" + is_admin_state_up: + description: The administrative state of the pool. + type: bool + sample: true + protocol: + description: The protocol for the pool. + type: str + sample: "HTTP" + lb_algorithm: + description: The load balancing algorithm for the pool. + type: str + sample: "ROUND_ROBIN" +''' + +EXAMPLES = ''' +# Create a pool, wait for the pool to be active. +- openstack.cloud.lb_pool: + cloud: mycloud + endpoint_type: admin + state: present + name: test-pool + loadbalancer: test-loadbalancer + protocol: HTTP + lb_algorithm: ROUND_ROBIN + +# Delete a pool +- openstack.cloud.lb_pool: + cloud: mycloud + endpoint_type: admin + state: absent + name: test-pool +''' + +import time + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class LoadbalancerPoolModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + loadbalancer=dict(default=None), + listener=dict(default=None), + protocol=dict(default='HTTP', + choices=['HTTP', 'HTTPS', 'TCP', 'UDP', 'PROXY']), + lb_algorithm=dict( + default='ROUND_ROBIN', + choices=['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'] + ) + ) + module_kwargs = dict( + mutually_exclusive=[['loadbalancer', 'listener']] + ) + + def _wait_for_pool_status(self, pool_id, status, failures, + interval=5): + timeout = self.params['timeout'] + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + pool = self.conn.load_balancer.get_pool(pool_id) + provisioning_status = pool.provisioning_status + if provisioning_status == status: + return pool + if provisioning_status in failures: + self.fail_json( + msg="pool %s transitioned to failure state %s" % + (pool_id, provisioning_status) + ) + + time.sleep(interval) + total_sleep += interval + + self.fail_json( + msg="timeout waiting for pool %s to transition to %s" % + (pool_id, status) + ) + + def run(self): + loadbalancer = self.params['loadbalancer'] + listener = self.params['listener'] + + changed = False + pool = self.conn.load_balancer.find_pool(name_or_id=self.params['name']) + + if self.params['state'] == 'present': + if not pool: + loadbalancer_id = None + if not (loadbalancer or listener): + self.fail_json( + msg="either loadbalancer or listener must be provided" + ) + + if loadbalancer: + lb = self.conn.load_balancer.find_load_balancer(loadbalancer) + if not lb: + self.fail_json( + msg='load balancer %s is not found' % loadbalancer) + loadbalancer_id = lb.id + + listener_id = None + if listener: + listener_ret = self.conn.load_balancer.find_listener(listener) + if not listener_ret: + self.fail_json( + msg='listener %s is not found' % listener) + listener_id = listener_ret.id + + pool = self.conn.load_balancer.create_pool( + name=self.params['name'], + loadbalancer_id=loadbalancer_id, + listener_id=listener_id, + protocol=self.params['protocol'], + lb_algorithm=self.params['lb_algorithm'] + ) + changed = True + + if not self.params['wait']: + self.exit_json( + changed=changed, pool=pool.to_dict(), id=pool.id) + + if self.params['wait']: + pool = self._wait_for_pool_status( + pool.id, "ACTIVE", ["ERROR"]) + + self.exit_json( + changed=changed, pool=pool.to_dict(), id=pool.id) + + elif self.params['state'] == 'absent': + if pool: + self.conn.load_balancer.delete_pool(pool) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = LoadbalancerPoolModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/loadbalancer.py b/ansible_collections/openstack/cloud/plugins/modules/loadbalancer.py new file mode 100644 index 00000000..336da966 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/loadbalancer.py @@ -0,0 +1,691 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst Cloud Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: loadbalancer +short_description: Add/Delete load balancer from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove load balancer from the OpenStack load-balancer + service(Octavia). Load balancer update is not supported for now. +options: + name: + description: + - The name of the load balancer. + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + flavor: + description: + - The flavor of the load balancer. + type: str + vip_network: + description: + - The name or id of the network for the virtual IP of the load balancer. + One of I(vip_network), I(vip_subnet), or I(vip_port) must be specified + for creation. + type: str + vip_subnet: + description: + - The name or id of the subnet for the virtual IP of the load balancer. + One of I(vip_network), I(vip_subnet), or I(vip_port) must be specified + for creation. + type: str + vip_port: + description: + - The name or id of the load balancer virtual IP port. One of + I(vip_network), I(vip_subnet), or I(vip_port) must be specified for + creation. + type: str + vip_address: + description: + - IP address of the load balancer virtual IP. + type: str + public_ip_address: + description: + - Public IP address associated with the VIP. + type: str + auto_public_ip: + description: + - Allocate a public IP address and associate with the VIP automatically. + type: bool + default: 'no' + public_network: + description: + - The name or ID of a Neutron external network. + type: str + delete_public_ip: + description: + - When C(state=absent) and this option is true, any public IP address + associated with the VIP will be deleted along with the load balancer. + type: bool + default: 'no' + listeners: + description: + - A list of listeners that attached to the load balancer. + suboptions: + name: + description: + - The listener name or ID. + protocol: + description: + - The protocol for the listener. + default: HTTP + protocol_port: + description: + - The protocol port number for the listener. + default: 80 + allowed_cidrs: + description: + - A list of IPv4, IPv6 or mix of both CIDRs to be allowed access to the listener. The default is all allowed. + When a list of CIDRs is provided, the default switches to deny all. + Ignored on unsupported Octavia versions (less than 2.12) + default: [] + pool: + description: + - The pool attached to the listener. + suboptions: + name: + description: + - The pool name or ID. + protocol: + description: + - The protocol for the pool. + default: HTTP + lb_algorithm: + description: + - The load balancing algorithm for the pool. + default: ROUND_ROBIN + members: + description: + - A list of members that added to the pool. + suboptions: + name: + description: + - The member name or ID. + address: + description: + - The IP address of the member. + protocol_port: + description: + - The protocol port number for the member. + default: 80 + subnet: + description: + - The name or ID of the subnet the member service is + accessible from. + elements: dict + type: list + wait: + description: + - If the module should wait for the load balancer to be created or + deleted. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait. + default: 180 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The load balancer UUID. + returned: On success when C(state=present) + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +loadbalancer: + description: Dictionary describing the load balancer. + returned: On success when C(state=present) + type: complex + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + name: + description: Name given to the load balancer. + type: str + sample: "lingxian_test" + vip_network_id: + description: Network ID the load balancer virtual IP port belongs in. + type: str + sample: "f171db43-56fd-41cf-82d7-4e91d741762e" + vip_subnet_id: + description: Subnet ID the load balancer virtual IP port belongs in. + type: str + sample: "c53e3c70-9d62-409a-9f71-db148e7aa853" + vip_port_id: + description: The load balancer virtual IP port ID. + type: str + sample: "2061395c-1c01-47ab-b925-c91b93df9c1d" + vip_address: + description: The load balancer virtual IP address. + type: str + sample: "192.168.2.88" + public_vip_address: + description: The load balancer public VIP address. + type: str + sample: "10.17.8.254" + provisioning_status: + description: The provisioning status of the load balancer. + type: str + sample: "ACTIVE" + operating_status: + description: The operating status of the load balancer. + type: str + sample: "ONLINE" + is_admin_state_up: + description: The administrative state of the load balancer. + type: bool + sample: true + listeners: + description: The associated listener IDs, if any. + type: list + sample: [{"id": "7aa1b380-beec-459c-a8a7-3a4fb6d30645"}, {"id": "692d06b8-c4f8-4bdb-b2a3-5a263cc23ba6"}] + pools: + description: The associated pool IDs, if any. + type: list + sample: [{"id": "27b78d92-cee1-4646-b831-e3b90a7fa714"}, {"id": "befc1fb5-1992-4697-bdb9-eee330989344"}] +''' + +EXAMPLES = ''' +# Create a load balancer by specifying the VIP subnet. +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + state: present + name: my_lb + vip_subnet: my_subnet + timeout: 150 + +# Create a load balancer by specifying the VIP network and the IP address. +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + state: present + name: my_lb + vip_network: my_network + vip_address: 192.168.0.11 + +# Create a load balancer together with its sub-resources in the 'all in one' +# way. A public IP address is also allocated to the load balancer VIP. +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + name: lingxian_test + state: present + vip_subnet: kong_subnet + auto_public_ip: yes + public_network: public + listeners: + - name: lingxian_80 + protocol: TCP + protocol_port: 80 + pool: + name: lingxian_80_pool + protocol: TCP + members: + - name: mywebserver1 + address: 192.168.2.81 + protocol_port: 80 + subnet: webserver_subnet + - name: lingxian_8080 + protocol: TCP + protocol_port: 8080 + pool: + name: lingxian_8080-pool + protocol: TCP + members: + - name: mywebserver2 + address: 192.168.2.82 + protocol_port: 8080 + wait: yes + timeout: 600 + +# Delete a load balancer(and all its related resources) +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + state: absent + name: my_lb + +# Delete a load balancer(and all its related resources) together with the +# public IP address(if any) attached to it. +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + state: absent + name: my_lb + delete_public_ip: yes +''' + +import time +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class LoadBalancerModule(OpenStackModule): + + def _wait_for_pool(self, pool, provisioning_status, operating_status, failures, interval=5): + """Wait for pool to be in a particular provisioning and operating status.""" + timeout = self.params['timeout'] # reuse loadbalancer timeout + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + pool = self.conn.load_balancer.find_pool(name_or_id=pool.id) + if pool: + if pool.provisioning_status == provisioning_status and pool.operating_status == operating_status: + return None + if pool.provisioning_status in failures: + self.fail_json( + msg="Pool %s transitioned to failure state %s" % + (pool.id, pool.provisioning_status) + ) + else: + if provisioning_status == "DELETED": + return None + else: + self.fail_json( + msg="Pool %s transitioned to DELETED" % pool.id + ) + + time.sleep(interval) + total_sleep += interval + + def _wait_for_lb(self, lb, status, failures, interval=5): + """Wait for load balancer to be in a particular provisioning status.""" + timeout = self.params['timeout'] + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + lb = self.conn.load_balancer.find_load_balancer(lb.id) + + if lb: + if lb.provisioning_status == status: + return None + if lb.provisioning_status in failures: + self.fail_json( + msg="Load Balancer %s transitioned to failure state %s" % + (lb.id, lb.provisioning_status) + ) + else: + if status == "DELETED": + return None + else: + self.fail_json( + msg="Load Balancer %s transitioned to DELETED" % lb.id + ) + + time.sleep(interval) + total_sleep += interval + + self.fail_json( + msg="Timeout waiting for Load Balancer %s to transition to %s" % + (lb.id, status) + ) + + argument_spec = dict( + name=dict(required=True), + flavor=dict(required=False), + state=dict(default='present', choices=['absent', 'present']), + vip_network=dict(required=False), + vip_subnet=dict(required=False), + vip_port=dict(required=False), + vip_address=dict(required=False), + listeners=dict(type='list', default=[], elements='dict'), + public_ip_address=dict(required=False, default=None), + auto_public_ip=dict(required=False, default=False, type='bool'), + public_network=dict(required=False), + delete_public_ip=dict(required=False, default=False, type='bool'), + ) + module_kwargs = dict(supports_check_mode=True) + + def run(self): + flavor = self.params['flavor'] + vip_network = self.params['vip_network'] + vip_subnet = self.params['vip_subnet'] + vip_port = self.params['vip_port'] + listeners = self.params['listeners'] + public_vip_address = self.params['public_ip_address'] + allocate_fip = self.params['auto_public_ip'] + delete_fip = self.params['delete_public_ip'] + public_network = self.params['public_network'] + + vip_network_id = None + vip_subnet_id = None + vip_port_id = None + flavor_id = None + + try: + max_microversion = 1 + max_majorversion = 2 + changed = False + lb = self.conn.load_balancer.find_load_balancer( + name_or_id=self.params['name']) + + if self.params['state'] == 'present': + if lb and self.ansible.check_mode: + self.exit_json(changed=False) + if lb: + self.exit_json(changed=False) + ver_data = self.conn.load_balancer.get_all_version_data() + region = list(ver_data.keys())[0] + interface_type = list(ver_data[region].keys())[0] + versions = ver_data[region][interface_type]['load-balancer'] + for ver in versions: + if ver['status'] == 'CURRENT': + curversion = ver['version'].split(".") + max_majorversion = int(curversion[0]) + max_microversion = int(curversion[1]) + + if not lb: + if self.ansible.check_mode: + self.exit_json(changed=True) + + if not (vip_network or vip_subnet or vip_port): + self.fail_json( + msg="One of vip_network, vip_subnet, or vip_port must " + "be specified for load balancer creation" + ) + + if flavor: + _flavor = self.conn.load_balancer.find_flavor(flavor) + if not _flavor: + self.fail_json( + msg='flavor %s not found' % flavor + ) + flavor_id = _flavor.id + + if vip_network: + network = self.conn.get_network(vip_network) + if not network: + self.fail_json( + msg='network %s is not found' % vip_network + ) + vip_network_id = network.id + if vip_subnet: + subnet = self.conn.get_subnet(vip_subnet) + if not subnet: + self.fail_json( + msg='subnet %s is not found' % vip_subnet + ) + vip_subnet_id = subnet.id + if vip_port: + port = self.conn.get_port(vip_port) + + if not port: + self.fail_json( + msg='port %s is not found' % vip_port + ) + vip_port_id = port.id + lbargs = {"name": self.params['name'], + "vip_network_id": vip_network_id, + "vip_subnet_id": vip_subnet_id, + "vip_port_id": vip_port_id, + "vip_address": self.params['vip_address'] + } + if flavor_id is not None: + lbargs["flavor_id"] = flavor_id + + lb = self.conn.load_balancer.create_load_balancer(**lbargs) + + changed = True + + if not listeners and not self.params['wait']: + self.exit_json( + changed=changed, + loadbalancer=lb.to_dict(), + id=lb.id + ) + + self._wait_for_lb(lb, "ACTIVE", ["ERROR"]) + + for listener_def in listeners: + listener_name = listener_def.get("name") + pool_def = listener_def.get("pool") + + if not listener_name: + self.fail_json(msg='listener name is required') + + listener = self.conn.load_balancer.find_listener( + name_or_id=listener_name + ) + + if not listener: + self._wait_for_lb(lb, "ACTIVE", ["ERROR"]) + + protocol = listener_def.get("protocol", "HTTP") + protocol_port = listener_def.get("protocol_port", 80) + allowed_cidrs = listener_def.get("allowed_cidrs", []) + listenerargs = {"name": listener_name, + "loadbalancer_id": lb.id, + "protocol": protocol, + "protocol_port": protocol_port + } + if max_microversion >= 12 and max_majorversion >= 2: + listenerargs['allowed_cidrs'] = allowed_cidrs + listener = self.conn.load_balancer.create_listener(**listenerargs) + changed = True + + # Ensure pool in the listener. + if pool_def: + pool_name = pool_def.get("name") + members = pool_def.get('members', []) + + if not pool_name: + self.fail_json(msg='pool name is required') + + pool = self.conn.load_balancer.find_pool(name_or_id=pool_name) + + if not pool: + self._wait_for_lb(lb, "ACTIVE", ["ERROR"]) + + protocol = pool_def.get("protocol", "HTTP") + lb_algorithm = pool_def.get("lb_algorithm", + "ROUND_ROBIN") + + pool = self.conn.load_balancer.create_pool( + name=pool_name, + listener_id=listener.id, + protocol=protocol, + lb_algorithm=lb_algorithm + ) + self._wait_for_pool(pool, "ACTIVE", "ONLINE", ["ERROR"]) + changed = True + + # Ensure members in the pool + for member_def in members: + member_name = member_def.get("name") + if not member_name: + self.fail_json(msg='member name is required') + + member = self.conn.load_balancer.find_member(member_name, + pool.id + ) + + if not member: + self._wait_for_lb(lb, "ACTIVE", ["ERROR"]) + + address = member_def.get("address") + if not address: + self.fail_json( + msg='member address for member %s is ' + 'required' % member_name + ) + + subnet_id = member_def.get("subnet") + if subnet_id: + subnet = self.conn.get_subnet(subnet_id) + if not subnet: + self.fail_json( + msg='subnet %s for member %s is not ' + 'found' % (subnet_id, member_name) + ) + subnet_id = subnet.id + + protocol_port = member_def.get("protocol_port", 80) + + member = self.conn.load_balancer.create_member( + pool, + name=member_name, + address=address, + protocol_port=protocol_port, + subnet_id=subnet_id + ) + self._wait_for_pool(pool, "ACTIVE", "ONLINE", ["ERROR"]) + changed = True + + # Associate public ip to the load balancer VIP. If + # public_vip_address is provided, use that IP, otherwise, either + # find an available public ip or create a new one. + fip = None + orig_public_ip = None + new_public_ip = None + if public_vip_address or allocate_fip: + ips = self.conn.network.ips( + port_id=lb.vip_port_id, + fixed_ip_address=lb.vip_address + ) + ips = list(ips) + if ips: + orig_public_ip = ips[0] + new_public_ip = orig_public_ip.floating_ip_address + + if public_vip_address and public_vip_address != orig_public_ip: + fip = self.conn.network.find_ip(public_vip_address) + + if not fip: + self.fail_json( + msg='Public IP %s is unavailable' % public_vip_address + ) + + # Release origin public ip first + self.conn.network.update_ip( + orig_public_ip, + fixed_ip_address=None, + port_id=None + ) + + # Associate new public ip + self.conn.network.update_ip( + fip, + fixed_ip_address=lb.vip_address, + port_id=lb.vip_port_id + ) + + new_public_ip = public_vip_address + changed = True + elif allocate_fip and not orig_public_ip: + fip = self.conn.network.find_available_ip() + if not fip: + if not public_network: + self.fail_json(msg="Public network is not provided") + + pub_net = self.conn.network.find_network(public_network) + if not pub_net: + self.fail_json( + msg='Public network %s not found' % + public_network + ) + fip = self.conn.network.create_ip( + floating_network_id=pub_net.id + ) + + self.conn.network.update_ip( + fip, + fixed_ip_address=lb.vip_address, + port_id=lb.vip_port_id + ) + + new_public_ip = fip.floating_ip_address + changed = True + + # Include public_vip_address in the result. + lb = self.conn.load_balancer.find_load_balancer(name_or_id=lb.id) + lb_dict = lb.to_dict() + lb_dict.update({"public_vip_address": new_public_ip}) + + self.exit_json( + changed=changed, + loadbalancer=lb_dict, + id=lb.id + ) + elif self.params['state'] == 'absent': + changed = False + public_vip_address = None + + if lb: + if self.ansible.check_mode: + self.exit_json(changed=True) + if delete_fip: + ips = self.conn.network.ips( + port_id=lb.vip_port_id, + fixed_ip_address=lb.vip_address + ) + ips = list(ips) + if ips: + public_vip_address = ips[0] + + # Deleting load balancer with `cascade=False` does not make + # sense because the deletion will always fail if there are + # sub-resources. + self.conn.load_balancer.delete_load_balancer(lb, cascade=True) + changed = True + + if self.params['wait']: + self._wait_for_lb(lb, "DELETED", ["ERROR"]) + + if delete_fip and public_vip_address: + self.conn.network.delete_ip(public_vip_address) + changed = True + elif self.ansible.check_mode: + self.exit_json(changed=False) + + self.exit_json(changed=changed) + except Exception as e: + self.fail_json(msg=str(e)) + + +def main(): + module = LoadBalancerModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/network.py b/ansible_collections/openstack/cloud/plugins/modules/network.py new file mode 100644 index 00000000..780d49ba --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/network.py @@ -0,0 +1,245 @@ +#!/usr/bin/python + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: network +short_description: Creates/removes networks from OpenStack +author: OpenStack Ansible SIG +description: + - Add or remove network from OpenStack. +options: + name: + description: + - Name to be assigned to the network. + required: true + type: str + shared: + description: + - Whether this network is shared or not. + type: bool + default: 'no' + admin_state_up: + description: + - Whether the state should be marked as up or down. + type: bool + default: 'yes' + external: + description: + - Whether this network is externally accessible. + type: bool + default: 'no' + state: + description: + - Indicate desired state of the resource. + choices: ['present', 'absent'] + default: present + type: str + provider_physical_network: + description: + - The physical network where this network object is implemented. + type: str + provider_network_type: + description: + - The type of physical network that maps to this network resource. + type: str + provider_segmentation_id: + description: + - An isolated segment on the physical network. The I(network_type) + attribute defines the segmentation model. For example, if the + I(network_type) value is vlan, this ID is a vlan identifier. If + the I(network_type) value is gre, this ID is a gre key. + type: int + project: + description: + - Project name or ID containing the network (name admin-only) + type: str + port_security_enabled: + description: + - Whether port security is enabled on the network or not. + Network will use OpenStack defaults if this option is + not utilised. Requires openstacksdk>=0.18. + type: bool + mtu_size: + description: + - The maximum transmission unit (MTU) value to address fragmentation. + Network will use OpenStack defaults if this option is + not provided. Requires openstacksdk>=0.18. + type: int + aliases: ['mtu'] + dns_domain: + description: + - The DNS domain value to set. Requires openstacksdk>=0.29. + Network will use Openstack defaults if this option is + not provided. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create an externally accessible network named 'ext_network'. +- openstack.cloud.network: + cloud: mycloud + state: present + name: ext_network + external: true +''' + +RETURN = ''' +network: + description: Dictionary describing the network. + returned: On success when I(state) is 'present'. + type: complex + contains: + id: + description: Network ID. + type: str + sample: "4bb4f9a5-3bd2-4562-bf6a-d17a6341bb56" + name: + description: Network name. + type: str + sample: "ext_network" + shared: + description: Indicates whether this network is shared across all tenants. + type: bool + sample: false + status: + description: Network status. + type: str + sample: "ACTIVE" + mtu: + description: The MTU of a network resource. + type: int + sample: 0 + dns_domain: + description: The DNS domain of a network resource. + type: str + sample: "sample.openstack.org." + admin_state_up: + description: The administrative state of the network. + type: bool + sample: true + port_security_enabled: + description: The port security status + type: bool + sample: true + router:external: + description: Indicates whether this network is externally accessible. + type: bool + sample: true + tenant_id: + description: The tenant ID. + type: str + sample: "06820f94b9f54b119636be2728d216fc" + subnets: + description: The associated subnets. + type: list + sample: [] + "provider:physical_network": + description: The physical network where this network object is implemented. + type: str + sample: my_vlan_net + "provider:network_type": + description: The type of physical network that maps to this network resource. + type: str + sample: vlan + "provider:segmentation_id": + description: An isolated segment on the physical network. + type: str + sample: 101 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class NetworkModule(OpenStackModule): + + argument_spec = dict( + name=dict(required=True), + shared=dict(default=False, type='bool'), + admin_state_up=dict(default=True, type='bool'), + external=dict(default=False, type='bool'), + provider_physical_network=dict(required=False), + provider_network_type=dict(required=False), + provider_segmentation_id=dict(required=False, type='int'), + state=dict(default='present', choices=['absent', 'present']), + project=dict(default=None), + port_security_enabled=dict(type='bool', min_ver='0.18.0'), + mtu_size=dict(required=False, type='int', min_ver='0.18.0', aliases=['mtu']), + dns_domain=dict(required=False, min_ver='0.29.0') + ) + + def run(self): + + state = self.params['state'] + name = self.params['name'] + shared = self.params['shared'] + admin_state_up = self.params['admin_state_up'] + external = self.params['external'] + provider_physical_network = self.params['provider_physical_network'] + provider_network_type = self.params['provider_network_type'] + provider_segmentation_id = self.params['provider_segmentation_id'] + project = self.params['project'] + + kwargs = self.check_versioned( + mtu_size=self.params['mtu_size'], port_security_enabled=self.params['port_security_enabled'], + dns_domain=self.params['dns_domain'] + ) + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail_json(msg='Project %s could not be found' % project) + project_id = proj['id'] + filters = {'tenant_id': project_id} + else: + project_id = None + filters = None + net = self.conn.get_network(name, filters=filters) + + if state == 'present': + if not net: + provider = {} + if provider_physical_network: + provider['physical_network'] = provider_physical_network + if provider_network_type: + provider['network_type'] = provider_network_type + if provider_segmentation_id: + provider['segmentation_id'] = provider_segmentation_id + + if project_id is not None: + net = self.conn.create_network(name, shared, admin_state_up, + external, provider, project_id, + **kwargs) + else: + net = self.conn.create_network(name, shared, admin_state_up, + external, provider, + **kwargs) + changed = True + else: + changed = False + self.exit(changed=changed, network=net, id=net['id']) + + elif state == 'absent': + if not net: + self.exit(changed=False) + else: + self.conn.delete_network(name) + self.exit(changed=True) + + +def main(): + module = NetworkModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/networks_info.py b/ansible_collections/openstack/cloud/plugins/modules/networks_info.py new file mode 100644 index 00000000..251af3e7 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/networks_info.py @@ -0,0 +1,149 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: networks_info +short_description: Retrieve information about one or more OpenStack networks. +author: OpenStack Ansible SIG +description: + - Retrieve information about one or more networks from OpenStack. + - This module was called C(openstack.cloud.networks_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.networks_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the Network + required: false + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Gather information about previously created networks + openstack.cloud.networks_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + register: result + +- name: Show openstack networks + debug: + msg: "{{ result.openstack_networks }}" + +- name: Gather information about a previously created network by name + openstack.cloud.networks_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + name: network1 + register: result + +- name: Show openstack networks + debug: + msg: "{{ result.openstack_networks }}" + +- name: Gather information about a previously created network with filter + # Note: name and filters parameters are Not mutually exclusive + openstack.cloud.networks_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + filters: + tenant_id: 55e2ce24b2a245b09f181bf025724cbe + subnets: + - 057d4bdf-6d4d-4728-bb0f-5ac45a6f7400 + - 443d4dc0-91d4-4998-b21c-357d10433483 + register: result + +- name: Show openstack networks + debug: + msg: "{{ result.openstack_networks }}" +''' + +RETURN = ''' +openstack_networks: + description: has all the openstack information about the networks + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the network. + returned: success + type: str + status: + description: Network status. + returned: success + type: str + subnets: + description: Subnet(s) included in this network. + returned: success + type: list + elements: str + tenant_id: + description: Tenant id associated with this network. + returned: success + type: str + shared: + description: Network shared flag. + returned: success + type: bool +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class NetworkInfoModule(OpenStackModule): + + deprecated_names = ('networks_facts', 'openstack.cloud.networks_facts') + + argument_spec = dict( + name=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None) + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + + kwargs = self.check_versioned( + filters=self.params['filters'] + ) + if self.params['name']: + kwargs['name_or_id'] = self.params['name'] + networks = self.conn.search_networks(**kwargs) + + self.exit(changed=False, openstack_networks=networks) + + +def main(): + module = NetworkInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/neutron_rbac_policies_info.py b/ansible_collections/openstack/cloud/plugins/modules/neutron_rbac_policies_info.py new file mode 100644 index 00000000..b451bc26 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/neutron_rbac_policies_info.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: Ansible Project +# (c) 2021, Ashraf Hasson <ahasson@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: neutron_rbac_policies_info +short_description: Fetch Neutron policies. +author: OpenStack Ansible SIG +description: + - Get RBAC policies against a network, security group or a QoS Policy for one or more projects. + - If a C(policy_id) was not provided, this module will attempt to fetch all available policies. + - Accepts same arguments as OpenStackSDK network proxy C(find_rbac_policy) and C(rbac_policies) functions which are ultimately passed over to C(RBACPolicy) + - All parameters passed in to this module act as a filter for when no C(policy_id) was provided, otherwise they're ignored. + - Returns None if no matching policy was found as opposed to failing. + +options: + policy_id: + description: + - The RBAC policy ID + - If provided, all other filters are ignored + type: str + object_id: + description: + - The object ID (the subject of the policy) to which the RBAC rules applies + - This would be the ID of a network, security group or a qos policy + - Mutually exclusive with the C(object_type) + type: str + object_type: + description: + - Can be one of the following object types C(network), C(security_group) or C(qos_policy) + - Mutually exclusive with the C(object_id) + choices: ['network', 'security_group', 'qos_policy'] + type: str + target_project_id: + description: + - Filters the RBAC rules based on the target project id + - Logically AND'ed with other filters + - Mutually exclusive with C(project_id) + type: str + project_id: + description: + - Filters the RBAC rules based on the project id to which the object belongs to + - Logically AND'ed with other filters + - Mutually exclusive with C(target_project_id) + type: str + project: + description: + - Filters the RBAC rules based on the project name + - Logically AND'ed with other filters + type: str + action: + description: + - Can be either of the following options C(access_as_shared) | C(access_as_external) + - Logically AND'ed with other filters + choices: ['access_as_shared', 'access_as_external'] + type: str + +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = r''' +# Gather all rbac policies for a project +- name: Get all rbac policies for {{ project }} + openstack.cloud.neutron_rbac_policies_info: + project_id: "{{ project.id }}" +''' + +RETURN = r''' +# return value can either be plural or signular depending on what was passed in as parameters +policies: + description: + - List of rbac policies, this could also be returned as a singular element, i.e., 'policy' + type: complex + returned: always + contains: + object_id: + description: + - The UUID of the object to which the RBAC rules apply + type: str + sample: "7422172b-2961-475c-ac68-bd0f2a9960ad" + target_project_id: + description: + - The UUID of the target project + type: str + sample: "c201a689c016435c8037977166f77368" + project_id: + description: + - The UUID of the project to which access is granted + type: str + sample: "84b8774d595b41e89f3dfaa1fd76932c" + object_type: + description: + - The object type to which the RBACs apply + type: str + sample: "network" + action: + description: + - The access model specified by the RBAC rules + type: str + sample: "access_as_shared" + id: + description: + - The ID of the RBAC rule/policy + type: str + sample: "4154ce0c-71a7-4d87-a905-09762098ddb9" + name: + description: + - The name of the RBAC rule; usually null + type: str + sample: null + location: + description: + - A dictionary of the project details to which access is granted + type: dict + sample: >- + { + "cloud": "devstack", + "region_name": "", + "zone": null, + "project": { + "id": "84b8774d595b41e89f3dfaa1fd76932c", + "name": null, + "domain_id": null, + "domain_name": null + } + } +''' + +import re +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class NeutronRbacPoliciesInfo(OpenStackModule): + argument_spec = dict( + policy_id=dict(), + object_id=dict(), # ID of the object that this RBAC policy affects. + object_type=dict(choices=['security_group', 'qos_policy', 'network']), # Type of the object that this RBAC policy affects. + target_project_id=dict(), # The ID of the project this RBAC will be enforced. + project_id=dict(), # The owner project ID. + project=dict(), + action=dict(choices=['access_as_external', 'access_as_shared']), # Action for the RBAC policy. + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + def _filter_policies_by(self, policies, key, value): + filtered = [] + regexp = re.compile(r"location\.project\.([A-Za-z]+)") + if regexp.match(key): + attribute = key.split('.')[-1] + for p in policies: + if p['location']['project'][attribute] == value: + filtered.append(p) + else: + for p in policies: + if getattr(p, key) == value: + filtered.append(p) + + return filtered + + def _get_rbac_policies(self): + object_type = self.params.get('object_type') + project_id = self.params.get('project_id') + action = self.params.get('action') + + search_attributes = {} + if object_type is not None: + search_attributes['object_type'] = object_type + if project_id is not None: + search_attributes['project_id'] = project_id + if action is not None: + search_attributes['action'] = action + + try: + policies = [] + generator = self.conn.network.rbac_policies(**search_attributes) + for p in generator: + policies.append(p) + except self.sdk.exceptions.OpenStackCloudException as ex: + self.fail_json(msg='Failed to get RBAC policies: {0}'.format(str(ex))) + + return policies + + def run(self): + policy_id = self.params.get('policy_id') + object_id = self.params.get('object_id') + object_type = self.params.get('object_type') + project_id = self.params.get('project_id') + project = self.params.get('project') + target_project_id = self.params.get('target_project_id') + + if self.ansible.check_mode: + self.exit_json(changed=False) + + if policy_id is not None: + try: + policy = self.conn.network.get_rbac_policy(policy_id) + self.exit_json(changed=False, policy=policy) + except self.sdk.exceptions.ResourceNotFound: + self.exit_json(changed=False, policy=None) + except self.sdk.exceptions.OpenStackCloudException as ex: + self.fail_json(msg='Failed to get RBAC policy: {0}'.format(str(ex))) + else: + if object_id is not None and object_type is not None: + self.fail_json(msg='object_id and object_type are mutually exclusive, please specify one of the two.') + if project_id is not None and target_project_id is not None: + self.fail_json(msg='project_id and target_project_id are mutually exclusive, please specify one of the two.') + + filtered_policies = self._get_rbac_policies() + + if project is not None: + filtered_policies = self._filter_policies_by(filtered_policies, 'location.project.name', project) + if object_id is not None: + filtered_policies = self._filter_policies_by(filtered_policies, 'object_id', object_id) + if target_project_id is not None: + filtered_policies = self._filter_policies_by(filtered_policies, 'target_project_id', target_project_id) + + self.exit_json(policies=filtered_policies, changed=False) + + +def main(): + module = NeutronRbacPoliciesInfo() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/neutron_rbac_policy.py b/ansible_collections/openstack/cloud/plugins/modules/neutron_rbac_policy.py new file mode 100644 index 00000000..f5162e08 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/neutron_rbac_policy.py @@ -0,0 +1,308 @@ +#!/usr/bin/python + +# Copyright: Ansible Project +# (c) 2021, Ashraf Hasson <ahasson@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: neutron_rbac_policy +short_description: Create or delete a Neutron policy to apply a RBAC rule against an object. +author: OpenStack Ansible SIG +description: + - Create a policy to apply a RBAC rule against a network, security group or a QoS Policy or update/delete an existing policy. + - If a C(policy_id) was provided but not found, this module will attempt to create a new policy rather than error out when updating an existing rule. + - Accepts same arguments as OpenStackSDK network proxy C(find_rbac_policy) and C(rbac_policies) functions which are ultimately passed over to C(RBACPolicy) + +options: + policy_id: + description: + - The RBAC policy ID + - Required when deleting or updating an existing RBAC policy rule, ignored otherwise + type: str + object_id: + description: + - The object ID (the subject of the policy) to which the RBAC rule applies + - Cannot be changed when updating an existing policy + - Required when creating a RBAC policy rule, ignored when deleting a policy + type: str + object_type: + description: + - Can be one of the following object types C(network), C(security_group) or C(qos_policy) + - Cannot be changed when updating an existing policy + - Required when creating a RBAC policy rule, ignored when deleting a policy + choices: ['network', 'security_group', 'qos_policy'] + type: str + target_project_id: + description: + - The project to which access to be allowed or revoked/disallowed + - Can be specified/changed when updating an existing policy + - Required when creating or updating a RBAC policy rule, ignored when deleting a policy + type: str + project_id: + description: + - The project to which the object_id belongs + - Cannot be changed when updating an existing policy + - Required when creating a RBAC policy rule, ignored when deleting a policy + type: str + action: + description: + - Can be either of the following options C(access_as_shared) | C(access_as_external) + - Cannot be changed when updating an existing policy + - Required when creating a RBAC policy rule, ignored when deleting a policy + choices: ['access_as_shared', 'access_as_external'] + type: str + state: + description: + - Whether the RBAC rule should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str + +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = r''' +# Ensure network RBAC policy exists +- name: Create a new network RBAC policy + neutron_rbac_policy: + object_id: '7422172b-2961-475c-ac68-bd0f2a9960ad' + object_type: 'network' + target_project_id: 'a12f9ce1de0645e0a0b01c2e679f69ec' + project_id: '84b8774d595b41e89f3dfaa1fd76932d' + +# Update network RBAC policy +- name: Update an existing network RBAC policy + neutron_rbac_policy: + policy_id: 'f625242a-6a73-47ac-8d1f-91440b2c617f' + target_project_id: '163c89e065a94e069064e551e15daf0e' + +# Delete an existing RBAC policy +- name: Delete RBAC policy + openstack.cloud.openstack.neutron_rbac_policy: + policy_id: 'f625242a-6a73-47ac-8d1f-91440b2c617f' + state: absent +''' + +RETURN = r''' +policy: + description: + - A hash representing the policy + type: complex + returned: always + contains: + object_id: + description: + - The UUID of the object to which the RBAC rules apply + type: str + sample: "7422172b-2961-475c-ac68-bd0f2a9960ad" + target_project_id: + description: + - The UUID of the target project + type: str + sample: "c201a689c016435c8037977166f77368" + project_id: + description: + - The UUID of the project to which access is granted + type: str + sample: "84b8774d595b41e89f3dfaa1fd76932c" + object_type: + description: + - The object type to which the RBACs apply + type: str + sample: "network" + action: + description: + - The access model specified by the RBAC rules + type: str + sample: "access_as_shared" + id: + description: + - The ID of the RBAC rule/policy + type: str + sample: "4154ce0c-71a7-4d87-a905-09762098ddb9" + name: + description: + - The name of the RBAC rule; usually null + type: str + sample: null + location: + description: + - A dictionary of the project details to which access is granted + type: dict + sample: >- + { + "cloud": "devstack", + "region_name": "", + "zone": null, + "project": { + "id": "84b8774d595b41e89f3dfaa1fd76932c", + "name": null, + "domain_id": null, + "domain_name": null + } + } +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class NeutronRbacPolicy(OpenStackModule): + argument_spec = dict( + policy_id=dict(), + object_id=dict(), # ID of the object that this RBAC policy affects. + object_type=dict(choices=['security_group', 'qos_policy', 'network']), # Type of the object that this RBAC policy affects. + target_project_id=dict(), # The ID of the project this RBAC will be enforced. + project_id=dict(), # The owner project ID. + action=dict(choices=['access_as_external', 'access_as_shared']), # Action for the RBAC policy. + state=dict(default='present', choices=['absent', 'present']) + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + def _delete_rbac_policy(self, policy): + """ + Delete an existing RBAC policy + returns: the "Changed" state + """ + + if policy is None: + self.fail_json(msg='Must specify policy_id for delete') + + try: + self.conn.network.delete_rbac_policy(policy.id) + except self.sdk.exceptions.OpenStackCloudException as ex: + self.fail_json(msg='Failed to delete RBAC policy: {0}'.format(str(ex))) + + return True + + def _create_rbac_policy(self): + """ + Creates a new RBAC policy + returns: the "Changed" state of the RBAC policy + """ + + object_id = self.params.get('object_id') + object_type = self.params.get('object_type') + target_project_id = self.params.get('target_project_id') + project_id = self.params.get('project_id') + action = self.params.get('action') + + attributes = { + 'object_id': object_id, + 'object_type': object_type, + 'target_project_id': target_project_id, + 'project_id': project_id, + 'action': action + } + + if not all(attributes.values()): + self.fail_json(msg='Missing one or more required parameter for creating a RBAC policy') + + try: + search_attributes = dict(attributes) + del search_attributes['object_id'] + del search_attributes['target_project_id'] + policies = self.conn.network.rbac_policies(**search_attributes) + for p in policies: + if p.object_id == object_id and p.target_project_id == target_project_id: + return (False, p) + + # if no matching policy exists, attempt to create one + policy = self.conn.network.create_rbac_policy(**attributes) + except self.sdk.exceptions.OpenStackCloudException as ex: + self.fail_json(msg='Failed to create RBAC policy: {0}'.format(str(ex))) + + return (True, policy) + + def _update_rbac_policy(self, policy): + """ + Updates an existing RBAC policy + returns: the "Changed" state of the RBAC policy + """ + + object_id = self.params.get('object_id') + object_type = self.params.get('object_type') + target_project_id = self.params.get('target_project_id') + project_id = self.params.get('project_id') + action = self.params.get('action') + + allowed_attributes = { + 'rbac_policy': policy.id, + 'target_project_id': target_project_id + } + + disallowed_attributes = { + 'object_id': object_id, + 'object_type': object_type, + 'project_id': project_id, + 'action': action + } + + if not all(allowed_attributes.values()): + self.fail_json(msg='Missing one or more required parameter for updating a RBAC policy') + + if any(disallowed_attributes.values()): + self.fail_json(msg='Cannot change disallowed parameters while updating a RBAC policy: ["object_id", "object_type", "project_id", "action"]') + + try: + policy = self.conn.network.update_rbac_policy(**allowed_attributes) + except self.sdk.exceptions.OpenStackCloudException as ex: + self.fail_json(msg='Failed to update the RBAC policy: {0}'.format(str(ex))) + + return (True, policy) + + def _policy_state_change(self, policy): + state = self.params['state'] + if state == 'present': + if not policy: + return True + if state == 'absent' and policy: + return True + return False + + def run(self): + policy_id = self.params.get('policy_id') + state = self.params.get('state') + + if policy_id is not None: + try: + policy = self.conn.network.get_rbac_policy(policy_id) + except self.sdk.exceptions.ResourceNotFound: + policy = None + except self.sdk.exceptions.OpenStackCloudException as ex: + self.fail_json(msg='Failed to get RBAC policy: {0}'.format(str(ex))) + else: + policy = None + + if self.ansible.check_mode: + self.exit_json(changed=self._policy_state_change(policy), policy=policy) + + if state == 'absent': + if policy is None and policy_id: + self.exit_json(changed=False) + if policy_id is None: + self.fail_json(msg='Must specify policy_id when state is absent') + if policy is not None: + changed = self._delete_rbac_policy(policy) + self.exit_json(changed=changed) + # state == 'present' + else: + if policy is None: + (changed, new_policy) = self._create_rbac_policy() + else: + (changed, new_policy) = self._update_rbac_policy(policy) + + self.exit_json(changed=changed, policy=new_policy) + + +def main(): + module = NeutronRbacPolicy() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/object.py b/ansible_collections/openstack/cloud/plugins/modules/object.py new file mode 100644 index 00000000..4a22604e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/object.py @@ -0,0 +1,120 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: object +short_description: Create or Delete objects and containers from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Delete objects and containers from OpenStack +options: + container: + description: + - The name of the container in which to create the object + required: true + type: str + name: + description: + - Name to be give to the object. If omitted, operations will be on + the entire container + required: false + type: str + filename: + description: + - Path to local file to be uploaded. + required: false + type: str + container_access: + description: + - desired container access level. + required: false + choices: ['private', 'public'] + default: private + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: "Create a object named 'fstab' in the 'config' container" + openstack.cloud.object: + cloud: mordred + state: present + name: fstab + container: config + filename: /etc/fstab + +- name: Delete a container called config and all of its contents + openstack.cloud.object: + cloud: rax-iad + state: absent + container: config +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class SwiftObjectModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + container=dict(required=True), + filename=dict(required=False, default=None), + container_access=dict(default='private', choices=['private', 'public']), + state=dict(default='present', choices=['absent', 'present']), + ) + module_kwargs = dict() + + def process_object( + self, container, name, filename, container_access, **kwargs + ): + changed = False + container_obj = self.conn.get_container(container) + if kwargs['state'] == 'present': + if not container_obj: + container_obj = self.conn.create_container(container) + changed = True + if self.conn.get_container_access(container) != container_access: + self.conn.set_container_access(container, container_access) + changed = True + if name: + if self.conn.is_object_stale(container, name, filename): + self.conn.create_object(container, name, filename) + changed = True + else: + if container_obj: + if name: + if self.conn.get_object_metadata(container, name): + self.conn.delete_object(container, name) + changed = True + else: + self.conn.delete_container(container) + changed = True + return changed + + def run(self): + changed = self.process_object(**self.params) + + self.exit_json(changed=changed) + + +def main(): + module = SwiftObjectModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/object_container.py b/ansible_collections/openstack/cloud/plugins/modules/object_container.py new file mode 100644 index 00000000..23ed38e5 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/object_container.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2021 by Open Telekom Cloud, operated by T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: object_container +short_description: Manage Swift container. +author: OpenStack Ansible SIG +description: + - Manage Swift container. +options: + container: + description: Name of a container in Swift. + type: str + required: true + metadata: + description: + - Key/value pairs to be set as metadata on the container. + - If a container doesn't exist, it will be created. + - Both custom and system metadata can be set. + - Custom metadata are keys and values defined by the user. + - The system metadata keys are content_type, content_encoding, content_disposition, delete_after,\ + delete_at, is_content_type_detected + type: dict + required: false + keys: + description: Keys from 'metadata' to be deleted. + type: list + elements: str + required: false + delete_with_all_objects: + description: + - Whether the container should be deleted with all objects or not. + - Without this parameter set to "true", an attempt to delete a container that contains objects will fail. + type: bool + default: False + required: false + state: + description: Whether resource should be present or absent. + default: 'present' + choices: ['present', 'absent'] + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +container: + description: Specifies the container. + returned: On success when C(state=present) + type: dict + sample: + { + "bytes": 5449, + "bytes_used": 5449, + "content_type": null, + "count": 1, + "id": "otc", + "if_none_match": null, + "is_content_type_detected": null, + "is_newest": null, + "meta_temp_url_key": null, + "meta_temp_url_key_2": null, + "name": "otc", + "object_count": 1, + "read_ACL": null, + "sync_key": null, + "sync_to": null, + "timestamp": null, + "versions_location": null, + "write_ACL": null + } +''' + +EXAMPLES = ''' +# Create empty container + - openstack.cloud.object_container: + container: "new-container" + state: present + +# Set metadata for container + - openstack.cloud.object_container: + container: "new-container" + metadata: "Cache-Control='no-cache'" + +# Delete some keys from metadata of a container + - openstack.cloud.object_container: + container: "new-container" + keys: + - content_type + +# Delete container + - openstack.cloud.object_container: + container: "new-container" + state: absent + +# Delete container and its objects + - openstack.cloud.object_container: + container: "new-container" + delete_with_all_objects: true + state: absent +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ContainerModule(OpenStackModule): + + argument_spec = dict( + container=dict(type='str', required=True), + metadata=dict(type='dict', required=False), + keys=dict(type='list', required=False, elements='str', no_log=False), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), + delete_with_all_objects=dict(type='bool', default=False, required=False) + ) + + def create(self, container): + + data = {} + if self._container_exist(container): + self.exit_json(changed=False) + + container_data = self.conn.object_store.create_container(name=container).to_dict() + container_data.pop('location') + data['container'] = container_data + self.exit_json(changed=True, **data) + + def delete(self, container): + + delete_with_all_objects = self.params['delete_with_all_objects'] + + changed = False + if self._container_exist(container): + objects = [] + for raw in self.conn.object_store.objects(container): + dt = raw.to_dict() + dt.pop('location') + objects.append(dt) + if len(objects) > 0: + if delete_with_all_objects: + for obj in objects: + self.conn.object_store.delete_object(container=container, obj=obj['id']) + else: + self.fail_json(msg="Container has objects") + self.conn.object_store.delete_container(container=container) + changed = True + + self.exit(changed=changed) + + def set_metadata(self, container, metadata): + + data = {} + + if not self._container_exist(container): + new_container = self.conn.object_store.create_container(name=container).to_dict() + + new_container = self.conn.object_store.set_container_metadata(container, **metadata).to_dict() + new_container.pop('location') + data['container'] = new_container + self.exit(changed=True, **data) + + def delete_metadata(self, container, keys): + + if not self._container_exist(container): + self.fail_json(msg="Container doesn't exist") + + self.conn.object_store.delete_container_metadata(container=container, keys=keys) + self.exit(changed=True) + + def _container_exist(self, container): + try: + self.conn.object_store.get_container_metadata(container) + return True + except self.sdk.exceptions.ResourceNotFound: + return False + + def run(self): + + container = self.params['container'] + state = self.params['state'] + metadata = self.params['metadata'] + keys = self.params['keys'] + + if state == 'absent': + self.delete(container) + if metadata: + self.set_metadata(container, metadata) + if keys: + self.delete_metadata(container, keys) + + self.create(container) + + +def main(): + module = ContainerModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_auth.py b/ansible_collections/openstack/cloud/plugins/modules/os_auth.py new file mode 100644 index 00000000..1f2c516e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_auth.py @@ -0,0 +1,62 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: auth +short_description: Retrieve an auth token +author: OpenStack Ansible SIG +description: + - Retrieve an auth token from an OpenStack Cloud +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Authenticate to the cloud and retrieve the service catalog + openstack.cloud.auth: + cloud: rax-dfw + +- name: Show service catalog + debug: + var: service_catalog +''' + +RETURN = ''' +auth_token: + description: Openstack API Auth Token + returned: success + type: str +service_catalog: + description: A dictionary of available API endpoints + returned: success + type: dict +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class AuthModule(OpenStackModule): + argument_spec = dict() + module_kwargs = dict() + + def run(self): + self.exit_json( + changed=False, + ansible_facts=dict( + auth_token=self.conn.auth_token, + service_catalog=self.conn.service_catalog)) + + +def main(): + module = AuthModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_client_config.py b/ansible_collections/openstack/cloud/plugins/modules/os_client_config.py new file mode 100644 index 00000000..94036e49 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_client_config.py @@ -0,0 +1,76 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: config +short_description: Get OpenStack Client config +description: + - Get I(openstack) client config data from clouds.yaml or environment +notes: + - Facts are placed in the C(openstack.clouds) variable. +options: + clouds: + description: + - List of clouds to limit the return list to. No value means return + information on all configured clouds + required: false + default: [] + type: list + elements: str +requirements: + - "python >= 3.6" + - "openstacksdk" +author: OpenStack Ansible SIG +''' + +EXAMPLES = ''' +- name: Get list of clouds that do not support security groups + openstack.cloud.config: + +- debug: + var: "{{ item }}" + with_items: "{{ openstack.clouds | rejectattr('secgroup_source', 'none') | list }}" + +- name: Get the information back just about the mordred cloud + openstack.cloud.config: + clouds: + - mordred +''' + +try: + import openstack.config + from openstack import exceptions + HAS_OPENSTACKSDK = True +except ImportError: + HAS_OPENSTACKSDK = False + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule(argument_spec=dict( + clouds=dict(required=False, type='list', default=[], elements='str'), + )) + + if not HAS_OPENSTACKSDK: + module.fail_json(msg='openstacksdk is required for this module') + + p = module.params + + try: + config = openstack.config.OpenStackConfig() + clouds = [] + for cloud in config.get_all_clouds(): + if not p['clouds'] or cloud.name in p['clouds']: + cloud.config['name'] = cloud.name + clouds.append(cloud.config) + module.exit_json(ansible_facts=dict(openstack=dict(clouds=clouds))) + except exceptions.ConfigException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_coe_cluster.py b/ansible_collections/openstack/cloud/plugins/modules/os_coe_cluster.py new file mode 100644 index 00000000..feb202a3 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_coe_cluster.py @@ -0,0 +1,292 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst IT Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: coe_cluster +short_description: Add/Remove COE cluster from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove COE cluster from the OpenStack Container Infra service. +options: + cluster_template_id: + description: + - The template ID of cluster template. + required: true + type: str + discovery_url: + description: + - Url used for cluster node discovery + type: str + docker_volume_size: + description: + - The size in GB of the docker volume + type: int + flavor_id: + description: + - The flavor of the minion node for this ClusterTemplate + type: str + keypair: + description: + - Name of the keypair to use. + type: str + labels: + description: + - One or more key/value pairs + type: raw + master_flavor_id: + description: + - The flavor of the master node for this ClusterTemplate + type: str + master_count: + description: + - The number of master nodes for this cluster + default: 1 + type: int + name: + description: + - Name that has to be given to the cluster template + required: true + type: str + node_count: + description: + - The number of nodes for this cluster + default: 1 + type: int + state: + description: + - Indicate desired state of the resource. + choices: [present, absent] + default: present + type: str + timeout: + description: + - Timeout for creating the cluster in minutes. Default to 60 mins + if not set + default: 60 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The cluster UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +cluster: + description: Dictionary describing the cluster. + returned: On success when I(state) is 'present' + type: complex + contains: + api_address: + description: + - Api address of cluster master node + type: str + sample: https://172.24.4.30:6443 + cluster_template_id: + description: The cluster_template UUID + type: str + sample: '7b1418c8-cea8-48fc-995d-52b66af9a9aa' + coe_version: + description: + - Version of the COE software currently running in this cluster + type: str + sample: v1.11.1 + container_version: + description: + - "Version of the container software. Example: docker version." + type: str + sample: 1.12.6 + created_at: + description: + - The time in UTC at which the cluster is created + type: str + sample: "2018-08-16T10:29:45+00:00" + create_timeout: + description: + - Timeout for creating the cluster in minutes. Default to 60 if + not set. + type: int + sample: 60 + discovery_url: + description: + - Url used for cluster node discovery + type: str + sample: https://discovery.etcd.io/a42ee38e7113f31f4d6324f24367aae5 + faults: + description: + - Fault info collected from the Heat resources of this cluster + type: dict + sample: {'0': 'ResourceInError: resources[0].resources...'} + flavor_id: + description: + - The flavor of the minion node for this cluster + type: str + sample: c1.c1r1 + keypair: + description: + - Name of the keypair to use. + type: str + sample: mykey + labels: + description: One or more key/value pairs + type: dict + sample: {'key1': 'value1', 'key2': 'value2'} + master_addresses: + description: + - IP addresses of cluster master nodes + type: list + sample: ['172.24.4.5'] + master_count: + description: + - The number of master nodes for this cluster. + type: int + sample: 1 + master_flavor_id: + description: + - The flavor of the master node for this cluster + type: str + sample: c1.c1r1 + name: + description: + - Name that has to be given to the cluster + type: str + sample: k8scluster + node_addresses: + description: + - IP addresses of cluster slave nodes + type: list + sample: ['172.24.4.8'] + node_count: + description: + - The number of master nodes for this cluster. + type: int + sample: 1 + stack_id: + description: + - Stack id of the Heat stack + type: str + sample: '07767ec6-85f5-44cb-bd63-242a8e7f0d9d' + status: + description: Status of the cluster from the heat stack + type: str + sample: 'CREATE_COMLETE' + status_reason: + description: + - Status reason of the cluster from the heat stack + type: str + sample: 'Stack CREATE completed successfully' + updated_at: + description: + - The time in UTC at which the cluster is updated + type: str + sample: '2018-08-16T10:39:25+00:00' + id: + description: + - Unique UUID for this cluster + type: str + sample: '86246a4d-a16c-4a58-9e96ad7719fe0f9d' +''' + +EXAMPLES = ''' +# Create a new Kubernetes cluster +- openstack.cloud.coe_cluster: + name: k8s + cluster_template_id: k8s-ha + keypair: mykey + master_count: 3 + node_count: 5 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class CoeClusterModule(OpenStackModule): + argument_spec = dict( + cluster_template_id=dict(required=True), + discovery_url=dict(default=None), + docker_volume_size=dict(type='int'), + flavor_id=dict(default=None), + keypair=dict(default=None, no_log=False), + labels=dict(default=None, type='raw'), + master_count=dict(type='int', default=1), + master_flavor_id=dict(default=None), + name=dict(required=True), + node_count=dict(type='int', default=1), + state=dict(default='present', choices=['absent', 'present']), + timeout=dict(type='int', default=60), + ) + module_kwargs = dict() + + def _parse_labels(self, labels): + if isinstance(labels, str): + labels_dict = {} + for kv_str in labels.split(","): + k, v = kv_str.split("=") + labels_dict[k] = v + return labels_dict + if not labels: + return {} + return labels + + def run(self): + params = self.params.copy() + + state = self.params['state'] + name = self.params['name'] + cluster_template_id = self.params['cluster_template_id'] + + kwargs = dict( + discovery_url=self.params['discovery_url'], + docker_volume_size=self.params['docker_volume_size'], + flavor_id=self.params['flavor_id'], + keypair=self.params['keypair'], + labels=self._parse_labels(params['labels']), + master_count=self.params['master_count'], + master_flavor_id=self.params['master_flavor_id'], + node_count=self.params['node_count'], + create_timeout=self.params['timeout'], + ) + + changed = False + cluster = self.conn.get_coe_cluster( + name_or_id=name, filters={'cluster_template_id': cluster_template_id}) + + if state == 'present': + if not cluster: + cluster = self.conn.create_coe_cluster( + name, cluster_template_id=cluster_template_id, **kwargs) + changed = True + else: + changed = False + + # NOTE (brtknr): At present, create_coe_cluster request returns + # cluster_id as `uuid` whereas get_coe_cluster request returns the + # same field as `id`. This behaviour may change in the future + # therefore try `id` first then `uuid`. + cluster_id = cluster.get('id', cluster.get('uuid')) + cluster['id'] = cluster['uuid'] = cluster_id + self.exit_json(changed=changed, cluster=cluster, id=cluster_id) + elif state == 'absent': + if not cluster: + self.exit_json(changed=False) + else: + self.conn.delete_coe_cluster(name) + self.exit_json(changed=True) + + +def main(): + module = CoeClusterModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_coe_cluster_template.py b/ansible_collections/openstack/cloud/plugins/modules/os_coe_cluster_template.py new file mode 100644 index 00000000..0596f39b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_coe_cluster_template.py @@ -0,0 +1,388 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst IT Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: coe_cluster_template +short_description: Add/Remove COE cluster template from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove COE cluster template from the OpenStack Container Infra + service. +options: + coe: + description: + - The Container Orchestration Engine for this clustertemplate + choices: [kubernetes, swarm, mesos] + type: str + required: true + dns_nameserver: + description: + - The DNS nameserver address + default: '8.8.8.8' + type: str + docker_storage_driver: + description: + - Docker storage driver + choices: [devicemapper, overlay, overlay2] + type: str + docker_volume_size: + description: + - The size in GB of the docker volume + type: int + external_network_id: + description: + - The external network to attach to the Cluster + type: str + fixed_network: + description: + - The fixed network name to attach to the Cluster + type: str + fixed_subnet: + description: + - The fixed subnet name to attach to the Cluster + type: str + flavor_id: + description: + - The flavor of the minion node for this ClusterTemplate + type: str + floating_ip_enabled: + description: + - Indicates whether created clusters should have a floating ip or not + type: bool + default: true + keypair_id: + description: + - Name or ID of the keypair to use. + type: str + image_id: + description: + - Image id the cluster will be based on + type: str + required: true + labels: + description: + - One or more key/value pairs + type: raw + http_proxy: + description: + - Address of a proxy that will receive all HTTP requests and relay them + The format is a URL including a port number + type: str + https_proxy: + description: + - Address of a proxy that will receive all HTTPS requests and relay + them. The format is a URL including a port number + type: str + master_flavor_id: + description: + - The flavor of the master node for this ClusterTemplate + type: str + master_lb_enabled: + description: + - Indicates whether created clusters should have a load balancer + for master nodes or not + type: bool + default: 'no' + name: + description: + - Name that has to be given to the cluster template + required: true + type: str + network_driver: + description: + - The name of the driver used for instantiating container networks + choices: [flannel, calico, docker] + type: str + no_proxy: + description: + - A comma separated list of IPs for which proxies should not be + used in the cluster + type: str + public: + description: + - Indicates whether the ClusterTemplate is public or not + type: bool + default: 'no' + registry_enabled: + description: + - Indicates whether the docker registry is enabled + type: bool + default: 'no' + server_type: + description: + - Server type for this ClusterTemplate + choices: [vm, bm] + default: vm + type: str + state: + description: + - Indicate desired state of the resource. + choices: [present, absent] + default: present + type: str + tls_disabled: + description: + - Indicates whether the TLS should be disabled + type: bool + default: 'no' + volume_driver: + description: + - The name of the driver used for instantiating container volumes + choices: [cinder, rexray] + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The cluster UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +cluster_template: + description: Dictionary describing the template. + returned: On success when I(state) is 'present' + type: complex + contains: + coe: + description: The Container Orchestration Engine for this clustertemplate + type: str + sample: kubernetes + dns_nameserver: + description: The DNS nameserver address + type: str + sample: '8.8.8.8' + docker_storage_driver: + description: Docker storage driver + type: str + sample: devicemapper + docker_volume_size: + description: The size in GB of the docker volume + type: int + sample: 5 + external_network_id: + description: The external network to attach to the Cluster + type: str + sample: public + fixed_network: + description: The fixed network name to attach to the Cluster + type: str + sample: 07767ec6-85f5-44cb-bd63-242a8e7f0d9d + fixed_subnet: + description: + - The fixed subnet name to attach to the Cluster + type: str + sample: 05567ec6-85f5-44cb-bd63-242a8e7f0d9d + flavor_id: + description: + - The flavor of the minion node for this ClusterTemplate + type: str + sample: c1.c1r1 + floating_ip_enabled: + description: + - Indicates whether created clusters should have a floating ip or not + type: bool + sample: true + keypair_id: + description: + - Name or ID of the keypair to use. + type: str + sample: mykey + image_id: + description: + - Image id the cluster will be based on + type: str + sample: 05567ec6-85f5-44cb-bd63-242a8e7f0e9d + labels: + description: One or more key/value pairs + type: dict + sample: {'key1': 'value1', 'key2': 'value2'} + http_proxy: + description: + - Address of a proxy that will receive all HTTP requests and relay them + The format is a URL including a port number + type: str + sample: http://10.0.0.11:9090 + https_proxy: + description: + - Address of a proxy that will receive all HTTPS requests and relay + them. The format is a URL including a port number + type: str + sample: https://10.0.0.10:8443 + master_flavor_id: + description: + - The flavor of the master node for this ClusterTemplate + type: str + sample: c1.c1r1 + master_lb_enabled: + description: + - Indicates whether created clusters should have a load balancer + for master nodes or not + type: bool + sample: true + name: + description: + - Name that has to be given to the cluster template + type: str + sample: k8scluster + network_driver: + description: + - The name of the driver used for instantiating container networks + type: str + sample: calico + no_proxy: + description: + - A comma separated list of IPs for which proxies should not be + used in the cluster + type: str + sample: 10.0.0.4,10.0.0.5 + public: + description: + - Indicates whether the ClusterTemplate is public or not + type: bool + sample: false + registry_enabled: + description: + - Indicates whether the docker registry is enabled + type: bool + sample: false + server_type: + description: + - Server type for this ClusterTemplate + type: str + sample: vm + tls_disabled: + description: + - Indicates whether the TLS should be disabled + type: bool + sample: false + volume_driver: + description: + - The name of the driver used for instantiating container volumes + type: str + sample: cinder +''' + +EXAMPLES = ''' +# Create a new Kubernetes cluster template +- openstack.cloud.coe_cluster_template: + name: k8s + coe: kubernetes + keypair_id: mykey + image_id: 2a8c9888-9054-4b06-a1ca-2bb61f9adb72 + public: no +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class CoeClusterTemplateModule(OpenStackModule): + argument_spec = dict( + coe=dict(required=True, choices=['kubernetes', 'swarm', 'mesos']), + dns_nameserver=dict(default='8.8.8.8'), + docker_storage_driver=dict(choices=['devicemapper', 'overlay', 'overlay2']), + docker_volume_size=dict(type='int'), + external_network_id=dict(default=None), + fixed_network=dict(default=None), + fixed_subnet=dict(default=None), + flavor_id=dict(default=None), + floating_ip_enabled=dict(type='bool', default=True), + keypair_id=dict(default=None), + image_id=dict(required=True), + labels=dict(default=None, type='raw'), + http_proxy=dict(default=None), + https_proxy=dict(default=None), + master_lb_enabled=dict(type='bool', default=False), + master_flavor_id=dict(default=None), + name=dict(required=True), + network_driver=dict(choices=['flannel', 'calico', 'docker']), + no_proxy=dict(default=None), + public=dict(type='bool', default=False), + registry_enabled=dict(type='bool', default=False), + server_type=dict(default="vm", choices=['vm', 'bm']), + state=dict(default='present', choices=['absent', 'present']), + tls_disabled=dict(type='bool', default=False), + volume_driver=dict(choices=['cinder', 'rexray']), + ) + module_kwargs = dict() + + def _parse_labels(self, labels): + if isinstance(labels, str): + labels_dict = {} + for kv_str in labels.split(","): + k, v = kv_str.split("=") + labels_dict[k] = v + return labels_dict + if not labels: + return {} + return labels + + def run(self): + params = self.params.copy() + + state = self.params['state'] + name = self.params['name'] + coe = self.params['coe'] + image_id = self.params['image_id'] + + kwargs = dict( + dns_nameserver=self.params['dns_nameserver'], + docker_storage_driver=self.params['docker_storage_driver'], + docker_volume_size=self.params['docker_volume_size'], + external_network_id=self.params['external_network_id'], + fixed_network=self.params['fixed_network'], + fixed_subnet=self.params['fixed_subnet'], + flavor_id=self.params['flavor_id'], + floating_ip_enabled=self.params['floating_ip_enabled'], + keypair_id=self.params['keypair_id'], + labels=self._parse_labels(params['labels']), + http_proxy=self.params['http_proxy'], + https_proxy=self.params['https_proxy'], + master_lb_enabled=self.params['master_lb_enabled'], + master_flavor_id=self.params['master_flavor_id'], + network_driver=self.params['network_driver'], + no_proxy=self.params['no_proxy'], + public=self.params['public'], + registry_enabled=self.params['registry_enabled'], + server_type=self.params['server_type'], + tls_disabled=self.params['tls_disabled'], + volume_driver=self.params['volume_driver'], + ) + + changed = False + template = self.conn.get_coe_cluster_template( + name_or_id=name, filters={'coe': coe, 'image_id': image_id}) + + if state == 'present': + if not template: + template = self.conn.create_coe_cluster_template( + name, coe=coe, image_id=image_id, **kwargs) + changed = True + else: + changed = False + + self.exit_json( + changed=changed, cluster_template=template, id=template['uuid']) + elif state == 'absent': + if not template: + self.exit_json(changed=False) + else: + self.conn.delete_coe_cluster_template(name) + self.exit_json(changed=True) + + +def main(): + module = CoeClusterTemplateModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_flavor_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_flavor_info.py new file mode 100644 index 00000000..61ee7a5b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_flavor_info.py @@ -0,0 +1,247 @@ +#!/usr/bin/python + +# Copyright (c) 2015 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: compute_flavor_info +short_description: Retrieve information about one or more flavors +author: OpenStack Ansible SIG +description: + - Retrieve information about available OpenStack instance flavors. By default, + information about ALL flavors are retrieved. Filters can be applied to get + information for only matching flavors. For example, you can filter on the + amount of RAM available to the flavor, or the number of virtual CPUs + available to the flavor, or both. When specifying multiple filters, + *ALL* filters must match on a flavor before that flavor is returned as + a fact. + - This module was called C(openstack.cloud.compute_flavor_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.compute_flavor_info) module no longer returns C(ansible_facts)! +notes: + - The result contains a list of unsorted flavors. +options: + name: + description: + - A flavor name. Cannot be used with I(ram) or I(vcpus) or I(ephemeral). + type: str + ram: + description: + - "A string used for filtering flavors based on the amount of RAM + (in MB) desired. This string accepts the following special values: + 'MIN' (return flavors with the minimum amount of RAM), and 'MAX' + (return flavors with the maximum amount of RAM)." + + - "A specific amount of RAM may also be specified. Any flavors with this + exact amount of RAM will be returned." + + - "A range of acceptable RAM may be given using a special syntax. Simply + prefix the amount of RAM with one of these acceptable range values: + '<', '>', '<=', '>='. These values represent less than, greater than, + less than or equal to, and greater than or equal to, respectively." + type: str + vcpus: + description: + - A string used for filtering flavors based on the number of virtual + CPUs desired. Format is the same as the I(ram) parameter. + type: str + limit: + description: + - Limits the number of flavors returned. All matching flavors are + returned by default. + type: int + ephemeral: + description: + - A string used for filtering flavors based on the amount of ephemeral + storage. Format is the same as the I(ram) parameter + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about all available flavors +- openstack.cloud.compute_flavor_info: + cloud: mycloud + register: result + +- debug: + msg: "{{ result.openstack_flavors }}" + +# Gather information for the flavor named "xlarge-flavor" +- openstack.cloud.compute_flavor_info: + cloud: mycloud + name: "xlarge-flavor" + +# Get all flavors that have exactly 512 MB of RAM. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: "512" + +# Get all flavors that have 1024 MB or more of RAM. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: ">=1024" + +# Get a single flavor that has the minimum amount of RAM. Using the 'limit' +# option will guarantee only a single flavor is returned. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: "MIN" + limit: 1 + +# Get all flavors with 1024 MB of RAM or more, AND exactly 2 virtual CPUs. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: ">=1024" + vcpus: "2" + +# Get all flavors with 1024 MB of RAM or more, exactly 2 virtual CPUs, and +# less than 30gb of ephemeral storage. +- openstack.cloud.compute_flavor_info: + cloud: mycloud + ram: ">=1024" + vcpus: "2" + ephemeral: "<30" +''' + + +RETURN = ''' +openstack_flavors: + description: Dictionary describing the flavors. + returned: On success. + type: complex + contains: + id: + description: Flavor ID. + returned: success + type: str + sample: "515256b8-7027-4d73-aa54-4e30a4a4a339" + name: + description: Flavor name. + returned: success + type: str + sample: "tiny" + description: + description: Description of the flavor + returned: success + type: str + sample: "Small flavor" + is_disabled: + description: Wether the flavor is enabled or not + returned: success + type: bool + sample: False + rxtx_factor: + description: Factor to be multiplied by the rxtx_base property of + the network it is attached to in order to have a + different bandwidth cap. + returned: success + type: float + sample: 1.0 + extra_specs: + description: Optional parameters to configure different flavors + options. + returned: success + type: dict + sample: "{'hw_rng:allowed': True}" + disk: + description: Size of local disk, in GB. + returned: success + type: int + sample: 10 + ephemeral: + description: Ephemeral space size, in GB. + returned: success + type: int + sample: 10 + ram: + description: Amount of memory, in MB. + returned: success + type: int + sample: 1024 + swap: + description: Swap space size, in MB. + returned: success + type: int + sample: 100 + vcpus: + description: Number of virtual CPUs. + returned: success + type: int + sample: 2 + is_public: + description: Make flavor accessible to the public. + returned: success + type: bool + sample: true +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ComputeFlavorInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + ram=dict(required=False, default=None), + vcpus=dict(required=False, default=None), + limit=dict(required=False, default=None, type='int'), + ephemeral=dict(required=False, default=None), + ) + module_kwargs = dict( + mutually_exclusive=[ + ['name', 'ram'], + ['name', 'vcpus'], + ['name', 'ephemeral'] + ], + supports_check_mode=True + ) + + deprecated_names = ('openstack.cloud.compute_flavor_facts') + + def run(self): + name = self.params['name'] + vcpus = self.params['vcpus'] + ram = self.params['ram'] + ephemeral = self.params['ephemeral'] + limit = self.params['limit'] + + filters = {} + if vcpus: + filters['vcpus'] = vcpus + if ram: + filters['ram'] = ram + if ephemeral: + filters['ephemeral'] = ephemeral + + if name: + # extra_specs are exposed in the flavor representation since Rocky, so we do not + # need get_extra_specs=True which is not available in OpenStack SDK 0.36 (Train) + # Ref.: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html + flavor = self.conn.compute.find_flavor(name) + flavors = [flavor] if flavor else [] + + else: + flavors = list(self.conn.compute.flavors()) + if filters: + flavors = self.conn.range_search(flavors, filters) + + if limit is not None: + flavors = flavors[:limit] + + # Transform entries to dict + flavors = [flavor.to_dict(computed=True) for flavor in flavors] + self.exit_json(changed=False, openstack_flavors=flavors) + + +def main(): + module = ComputeFlavorInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_floating_ip.py b/ansible_collections/openstack/cloud/plugins/modules/os_floating_ip.py new file mode 100644 index 00000000..6b5fb0d6 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_floating_ip.py @@ -0,0 +1,307 @@ +#!/usr/bin/python + +# Copyright: (c) 2015, Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: floating_ip +author: OpenStack Ansible SIG +short_description: Add/Remove floating IP from an instance +description: + - Add or Remove a floating IP to an instance. + - Returns the floating IP when attaching only if I(wait=true). + - When detaching a floating IP there might be a delay until an instance does not list the floating IP any more. +options: + server: + description: + - The name or ID of the instance to which the IP address + should be assigned. + required: true + type: str + network: + description: + - The name or ID of a neutron external network or a nova pool name. + type: str + floating_ip_address: + description: + - A floating IP address to attach or to detach. When I(state) is present + can be used to specify a IP address to attach. I(floating_ip_address) + requires I(network) to be set. + type: str + reuse: + description: + - When I(state) is present, and I(floating_ip_address) is not present, + this parameter can be used to specify whether we should try to reuse + a floating IP address already allocated to the project. + type: bool + default: 'no' + fixed_address: + description: + - To which fixed IP of server the floating IP address should be + attached to. + type: str + nat_destination: + description: + - The name or id of a neutron private network that the fixed IP to + attach floating IP is on + aliases: ["fixed_network", "internal_network"] + type: str + wait: + description: + - When attaching a floating IP address, specify whether to wait for it to appear as attached. + - Must be set to C(yes) for the module to return the value of the floating IP when attaching. + type: bool + default: 'no' + timeout: + description: + - Time to wait for an IP address to appear as attached. See wait. + required: false + default: 60 + type: int + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + purge: + description: + - When I(state) is absent, indicates whether or not to delete the floating + IP completely, or only detach it from the server. Default is to detach only. + type: bool + default: 'no' +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Assign a floating IP to the first interface of `cattle001` from an existing +# external network or nova pool. A new floating IP from the first available +# external network is allocated to the project. +- openstack.cloud.floating_ip: + cloud: dguerri + server: cattle001 + +# Assign a new floating IP to the instance fixed ip `192.0.2.3` of +# `cattle001`. If a free floating IP is already allocated to the project, it is +# reused; if not, a new one is created. +- openstack.cloud.floating_ip: + cloud: dguerri + state: present + reuse: yes + server: cattle001 + network: ext_net + fixed_address: 192.0.2.3 + wait: true + timeout: 180 + +# Assign a new floating IP from the network `ext_net` to the instance fixed +# ip in network `private_net` of `cattle001`. +- openstack.cloud.floating_ip: + cloud: dguerri + state: present + server: cattle001 + network: ext_net + nat_destination: private_net + wait: true + timeout: 180 + +# Detach a floating IP address from a server +- openstack.cloud.floating_ip: + cloud: dguerri + state: absent + floating_ip_address: 203.0.113.2 + server: cattle001 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule +import itertools + + +class NetworkingFloatingIPModule(OpenStackModule): + argument_spec = dict( + server=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + network=dict(required=False, default=None), + floating_ip_address=dict(required=False, default=None), + reuse=dict(required=False, type='bool', default=False), + fixed_address=dict(required=False, default=None), + nat_destination=dict(required=False, default=None, + aliases=['fixed_network', 'internal_network']), + wait=dict(required=False, type='bool', default=False), + timeout=dict(required=False, type='int', default=60), + purge=dict(required=False, type='bool', default=False), + ) + + module_kwargs = dict( + required_if=[ + ['state', 'absent', ['floating_ip_address']] + ], + required_by=dict( + floating_ip_address=('network',) + ) + ) + + def _get_floating_ip(self, floating_ip_address): + f_ips = self.conn.search_floating_ips( + filters={'floating_ip_address': floating_ip_address}) + + if not f_ips: + return None + + return f_ips[0] + + def _list_floating_ips(self, server): + return itertools.chain.from_iterable([ + (addr['addr'] for addr in server.addresses[net] if addr['OS-EXT-IPS:type'] == 'floating') + for net in server.addresses + ]) + + def _match_floating_ip(self, server, + floating_ip_address, + network_id, + fixed_address, + nat_destination): + + if floating_ip_address: + return self._get_floating_ip(floating_ip_address) + elif not fixed_address and nat_destination: + nat_destination_name = self.conn.get_network(nat_destination)['name'] + return next( + (self._get_floating_ip(addr['addr']) + for addr in server.addresses.get(nat_destination_name, []) + if addr['OS-EXT-IPS:type'] == 'floating'), + None) + else: + # not floating_ip_address and (fixed_address or not nat_destination) + + # get any of the floating ips that matches fixed_address and/or network + f_ip_addrs = self._list_floating_ips(server) + f_ips = [f_ip for f_ip in self.conn.list_floating_ips() if f_ip['floating_ip_address'] in f_ip_addrs] + return next( + (f_ip for f_ip in f_ips + if ((fixed_address and f_ip.fixed_ip_address == fixed_address) or not fixed_address) + and ((network_id and f_ip.network == network_id) or not network_id)), + None) + + def run(self): + server_name_or_id = self.params['server'] + state = self.params['state'] + network = self.params['network'] + floating_ip_address = self.params['floating_ip_address'] + reuse = self.params['reuse'] + fixed_address = self.params['fixed_address'] + nat_destination = self.params['nat_destination'] + wait = self.params['wait'] + timeout = self.params['timeout'] + purge = self.params['purge'] + + server = self.conn.get_server(server_name_or_id) + if not server: + self.fail_json( + msg="server {0} not found".format(server_name_or_id)) + + # Extract floating ips from server + f_ip_addrs = self._list_floating_ips(server) + + # Get details about requested floating ip + f_ip = self._get_floating_ip(floating_ip_address) if floating_ip_address else None + + if network: + network_id = self.conn.get_network(name_or_id=network)["id"] + else: + network_id = None + + if state == 'present': + if floating_ip_address and f_ip and floating_ip_address in f_ip_addrs: + # Floating ip address has been assigned to server + self.exit_json(changed=False, floating_ip=f_ip) + + if f_ip and f_ip['attached'] and floating_ip_address not in f_ip_addrs: + # Requested floating ip has been attached to different server + self.fail_json(msg="floating-ip {floating_ip_address} already has been attached to different server" + .format(floating_ip_address=floating_ip_address)) + + if not floating_ip_address: + # No specific floating ip requested, i.e. if any floating ip is already assigned to server, + # check that it matches requirements. + + if not fixed_address and nat_destination: + # Check if we have any floating ip on the given nat_destination network + nat_destination_name = self.conn.get_network(nat_destination)['name'] + for addr in server.addresses.get(nat_destination_name, []): + if addr['OS-EXT-IPS:type'] == 'floating': + # A floating ip address has been assigned to the requested nat_destination + f_ip = self._get_floating_ip(addr['addr']) + self.exit_json(changed=False, floating_ip=f_ip) + # else fixed_address or not nat_destination, hence an + # analysis of all floating ips of server is required + f_ips = [f_ip for f_ip in self.conn.list_floating_ips() if f_ip['floating_ip_address'] in f_ip_addrs] + for f_ip in f_ips: + if network_id and f_ip.network != network_id: + # requested network does not match network of floating ip + continue + + if not fixed_address and not nat_destination: + # any floating ip will fullfil these requirements + self.exit_json(changed=False, floating_ip=f_ip) + + if fixed_address and f_ip.fixed_ip_address == fixed_address: + # a floating ip address has been assigned that points to the requested fixed_address + self.exit_json(changed=False, floating_ip=f_ip) + + if floating_ip_address and not f_ip: + # openstacksdk's create_ip requires floating_ip_address and floating_network_id to be set + self.conn.network.create_ip(floating_ip_address=floating_ip_address, floating_network_id=network_id) + # Else floating ip either does not exist or has not been attached yet + + # Both floating_ip_address and network are mutually exclusive in add_ips_to_server, i.e. + # add_ips_to_server will ignore floating_ip_address if network is set + # Ref.: https://github.com/openstack/openstacksdk/blob/a6b0ece2821ea79330c4067100295f6bdcbe456e/openstack/cloud/_floating_ip.py#L987 + server = self.conn.add_ips_to_server( + server=server, + ips=floating_ip_address, + ip_pool=network if not floating_ip_address else None, + reuse=reuse, + fixed_address=fixed_address, + wait=wait, + timeout=timeout, nat_destination=nat_destination) + + # Update the floating ip status + f_ip = self._match_floating_ip(server, floating_ip_address, network_id, fixed_address, nat_destination) + self.exit_json(changed=True, floating_ip=f_ip) + + elif state == 'absent': + f_ip = self._match_floating_ip(server, floating_ip_address, network_id, fixed_address, nat_destination) + if not f_ip: + # Nothing to detach + self.exit_json(changed=False) + changed = False + + if f_ip["fixed_ip_address"]: + self.conn.detach_ip_from_server(server_id=server['id'], floating_ip_id=f_ip['id']) + # OpenStackSDK sets {"port_id": None} to detach a floating ip from an instance, + # but there might be a delay until a server does not list it in addresses any more. + + # Update the floating IP status + f_ip = self.conn.get_floating_ip(id=f_ip['id']) + changed = True + + if purge: + self.conn.delete_floating_ip(f_ip['id']) + self.exit_json(changed=True) + self.exit_json(changed=changed, floating_ip=f_ip) + + +def main(): + module = NetworkingFloatingIPModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_group.py b/ansible_collections/openstack/cloud/plugins/modules/os_group.py new file mode 100644 index 00000000..5b45efa4 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_group.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# Copyright (c) 2016 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_group +short_description: Manage OpenStack Identity Groups +author: OpenStack Ansible SIG +description: + - Manage OpenStack Identity Groups. Groups can be created, deleted or + updated. Only the I(description) value can be updated. +options: + name: + description: + - Group name + required: true + type: str + description: + description: + - Group description + type: str + domain_id: + description: + - Domain id to create the group in if the cloud supports domains. + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a group named "demo" +- openstack.cloud.identity_group: + cloud: mycloud + state: present + name: demo + description: "Demo Group" + domain_id: demoid + +# Update the description on existing "demo" group +- openstack.cloud.identity_group: + cloud: mycloud + state: present + name: demo + description: "Something else" + domain_id: demoid + +# Delete group named "demo" +- openstack.cloud.identity_group: + cloud: mycloud + state: absent + name: demo +''' + +RETURN = ''' +group: + description: Dictionary describing the group. + returned: On success when I(state) is 'present'. + type: complex + contains: + id: + description: Unique group ID + type: str + sample: "ee6156ff04c645f481a6738311aea0b0" + name: + description: Group name + type: str + sample: "demo" + description: + description: Group description + type: str + sample: "Demo Group" + domain_id: + description: Domain for the group + type: str + sample: "default" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityGroupModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + description=dict(required=False, default=None), + domain_id=dict(required=False, default=None), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _system_state_change(self, state, description, group): + if state == 'present' and not group: + return True + if state == 'present' and description is not None and group.description != description: + return True + if state == 'absent' and group: + return True + return False + + def run(self): + name = self.params.get('name') + description = self.params.get('description') + state = self.params.get('state') + + domain_id = self.params.pop('domain_id') + + if domain_id: + group = self.conn.get_group(name, filters={'domain_id': domain_id}) + else: + group = self.conn.get_group(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, description, group)) + + if state == 'present': + if group is None: + group = self.conn.create_group( + name=name, description=description, domain=domain_id) + changed = True + else: + if description is not None and group.description != description: + group = self.conn.update_group( + group.id, description=description) + changed = True + else: + changed = False + self.exit_json(changed=changed, group=group) + + elif state == 'absent': + if group is None: + changed = False + else: + self.conn.delete_group(group.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityGroupModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_group_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_group_info.py new file mode 100644 index 00000000..68f00d73 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_group_info.py @@ -0,0 +1,150 @@ +#!/usr/bin/python + +# Copyright (c) 2019, Phillipe Smith <phillipelnx@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_group_info +short_description: Retrieve info about one or more OpenStack groups +author: OpenStack Ansible SIG +description: + - Retrieve info about a one or more OpenStack groups. +options: + name: + description: + - Name or ID of the group. + type: str + domain: + description: + - Name or ID of the domain containing the group if the cloud supports domains + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather info about previously created groups +- name: gather info + hosts: localhost + tasks: + - name: Gather info about previously created groups + openstack.cloud.identity_group_info: + cloud: awesomecloud + register: openstack_groups + - debug: + var: openstack_groups + +# Gather info about a previously created group by name +- name: gather info + hosts: localhost + tasks: + - name: Gather info about a previously created group by name + openstack.cloud.identity_group_info: + cloud: awesomecloud + name: demogroup + register: openstack_groups + - debug: + var: openstack_groups + +# Gather info about a previously created group in a specific domain +- name: gather info + hosts: localhost + tasks: + - name: Gather info about a previously created group in a specific domain + openstack.cloud.identity_group_info: + cloud: awesomecloud + name: demogroup + domain: admindomain + register: openstack_groups + - debug: + var: openstack_groups + +# Gather info about a previously created group in a specific domain with filter +- name: gather info + hosts: localhost + tasks: + - name: Gather info about a previously created group in a specific domain with filter + openstack.cloud.identity_group_info: + cloud: awesomecloud + name: demogroup + domain: admindomain + filters: + enabled: False + register: openstack_groups + - debug: + var: openstack_groups +''' + + +RETURN = ''' +openstack_groups: + description: Dictionary describing all the matching groups. + returned: always, but can be an empty list + type: complex + contains: + name: + description: Name given to the group. + returned: success + type: str + description: + description: Description of the group. + returned: success + type: str + id: + description: Unique UUID. + returned: success + type: str + domain_id: + description: Domain ID containing the group (keystone v3 clouds only) + returned: success + type: bool +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityGroupInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + domain=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + name = self.params['name'] + domain = self.params['domain'] + filters = self.params['filters'] or {} + + args = {} + if domain: + dom = self.conn.identity.find_domain(domain) + if dom: + args['domain_id'] = dom['id'] + else: + self.fail_json(msg='Domain name or ID does not exist') + + groups = self.conn.search_groups(name, filters, **args) + # groups is for backward (and forward) compatibility + self.exit_json(changed=False, groups=groups, openstack_groups=groups) + + +def main(): + module = IdentityGroupInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_image.py b/ansible_collections/openstack/cloud/plugins/modules/os_image.py new file mode 100644 index 00000000..fae13a2e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_image.py @@ -0,0 +1,270 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +# TODO(mordred): we need to support "location"(v1) and "locations"(v2) + +DOCUMENTATION = ''' +--- +module: image +short_description: Add/Delete images from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove images from the OpenStack Image Repository +options: + name: + description: + - The name of the image when uploading - or the name/ID of the image if deleting + required: true + type: str + id: + description: + - The ID of the image when uploading an image + type: str + checksum: + description: + - The checksum of the image + type: str + disk_format: + description: + - The format of the disk that is getting uploaded + default: qcow2 + choices: ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso', 'vhdx', 'ploop'] + type: str + container_format: + description: + - The format of the container + default: bare + choices: ['ami', 'aki', 'ari', 'bare', 'ovf', 'ova', 'docker'] + type: str + project: + description: + - The name or ID of the project owning the image + type: str + aliases: ['owner'] + project_domain: + description: + - The domain the project owning the image belongs to + - May be used to identify a unique project when providing a name to the project argument and multiple projects with such name exist + type: str + min_disk: + description: + - The minimum disk space (in GB) required to boot this image + type: int + min_ram: + description: + - The minimum ram (in MB) required to boot this image + type: int + is_public: + description: + - Whether the image can be accessed publicly. Note that publicizing an image requires admin role by default. + type: bool + default: false + protected: + description: + - Prevent image from being deleted + type: bool + default: false + filename: + description: + - The path to the file which has to be uploaded + type: str + ramdisk: + description: + - The name of an existing ramdisk image that will be associated with this image + type: str + kernel: + description: + - The name of an existing kernel image that will be associated with this image + type: str + properties: + description: + - Additional properties to be associated with this image + default: {} + type: dict + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + tags: + description: + - List of tags to be applied to the image + default: [] + type: list + elements: str + volume: + description: + - ID of a volume to create an image from. + - The volume must be in AVAILABLE state. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Upload an image from a local file named cirros-0.3.0-x86_64-disk.img +- openstack.cloud.image: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + openstack.cloud.identity_user_domain_name: Default + openstack.cloud.project_domain_name: Default + name: cirros + container_format: bare + disk_format: qcow2 + state: present + filename: cirros-0.3.0-x86_64-disk.img + kernel: cirros-vmlinuz + ramdisk: cirros-initrd + tags: + - custom + properties: + cpu_arch: x86_64 + distro: ubuntu + +# Create image from volume attached to an instance +- name: create volume snapshot + openstack.cloud.volume_snapshot: + auth: + "{{ auth }}" + display_name: myvol_snapshot + volume: myvol + force: yes + register: myvol_snapshot + +- name: create volume from snapshot + openstack.cloud.volume: + auth: + "{{ auth }}" + size: "{{ myvol_snapshot.snapshot.size }}" + snapshot_id: "{{ myvol_snapshot.snapshot.id }}" + display_name: myvol_snapshot_volume + wait: yes + register: myvol_snapshot_volume + +- name: create image from volume snapshot + openstack.cloud.image: + auth: + "{{ auth }}" + volume: "{{ myvol_snapshot_volume.volume.id }}" + name: myvol_image +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ImageModule(OpenStackModule): + + deprecated_names = ('os_image', 'openstack.cloud.os_image') + + argument_spec = dict( + name=dict(required=True, type='str'), + id=dict(type='str'), + checksum=dict(type='str'), + disk_format=dict(default='qcow2', + choices=['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso', 'vhdx', 'ploop']), + container_format=dict(default='bare', choices=['ami', 'aki', 'ari', 'bare', 'ovf', 'ova', 'docker']), + project=dict(type='str', aliases=['owner']), + project_domain=dict(type='str'), + min_disk=dict(type='int', default=0), + min_ram=dict(type='int', default=0), + is_public=dict(type='bool', default=False), + protected=dict(type='bool', default=False), + filename=dict(type='str'), + ramdisk=dict(type='str'), + kernel=dict(type='str'), + properties=dict(type='dict', default={}), + volume=dict(type='str'), + tags=dict(type='list', default=[], elements='str'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + mutually_exclusive=[['filename', 'volume']], + ) + + def run(self): + + changed = False + if self.params['id']: + image = self.conn.get_image(name_or_id=self.params['id']) + elif self.params['checksum']: + image = self.conn.get_image(name_or_id=self.params['name'], filters={'checksum': self.params['checksum']}) + else: + image = self.conn.get_image(name_or_id=self.params['name']) + + if self.params['state'] == 'present': + if not image: + kwargs = {} + if self.params['id'] is not None: + kwargs['id'] = self.params['id'] + if self.params['project']: + project_domain = {'id': None} + if self.params['project_domain']: + project_domain = self.conn.get_domain(name_or_id=self.params['project_domain']) + if not project_domain or project_domain['id'] is None: + self.fail(msg='Project domain %s could not be found' % self.params['project_domain']) + project = self.conn.get_project(name_or_id=self.params['project'], domain_id=project_domain['id']) + if not project: + self.fail(msg='Project %s could not be found' % self.params['project']) + kwargs['owner'] = project['id'] + image = self.conn.create_image( + name=self.params['name'], + filename=self.params['filename'], + disk_format=self.params['disk_format'], + container_format=self.params['container_format'], + wait=self.params['wait'], + timeout=self.params['timeout'], + is_public=self.params['is_public'], + protected=self.params['protected'], + min_disk=self.params['min_disk'], + min_ram=self.params['min_ram'], + volume=self.params['volume'], + tags=self.params['tags'], + **kwargs + ) + changed = True + if not self.params['wait']: + self.exit(changed=changed, image=image, id=image.id) + + self.conn.update_image_properties( + image=image, + kernel=self.params['kernel'], + ramdisk=self.params['ramdisk'], + protected=self.params['protected'], + **self.params['properties']) + if self.params['tags']: + self.conn.image.update_image(image.id, tags=self.params['tags']) + image = self.conn.get_image(name_or_id=image.id) + self.exit(changed=changed, image=image, id=image.id) + + elif self.params['state'] == 'absent': + if not image: + changed = False + else: + self.conn.delete_image( + name_or_id=self.params['name'], + wait=self.params['wait'], + timeout=self.params['timeout']) + changed = True + self.exit(changed=changed) + + +def main(): + module = ImageModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_image_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_image_info.py new file mode 100644 index 00000000..f02079c0 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_image_info.py @@ -0,0 +1,204 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: image_info +short_description: Retrieve information about an image within OpenStack. +author: OpenStack Ansible SIG +description: + - Retrieve information about a image image from OpenStack. + - This module was called C(openstack.cloud.image_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.image_info) module no longer returns C(ansible_facts)! +options: + image: + description: + - Name or ID of the image + required: false + type: str + filters: + description: + - Dict of properties of the images used for query + type: dict + required: false + aliases: ['properties'] +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Gather information about a previously created image named image1 + openstack.cloud.image_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + image: image1 + register: result + +- name: Show openstack information + debug: + msg: "{{ result.image }}" + +# Show all available Openstack images +- name: Retrieve all available Openstack images + openstack.cloud.image_info: + register: result + +- name: Show images + debug: + msg: "{{ result.image }}" + +# Show images matching requested properties +- name: Retrieve images having properties with desired values + openstack.cloud.image_facts: + filters: + some_property: some_value + OtherProp: OtherVal + +- name: Show images + debug: + msg: "{{ result.image }}" +''' + +RETURN = ''' +openstack_images: + description: has all the openstack information about the image + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the image. + returned: success + type: str + status: + description: Image status. + returned: success + type: str + created_at: + description: Image created at timestamp. + returned: success + type: str + container_format: + description: Container format of the image. + returned: success + type: str + direct_url: + description: URL to access the image file kept in external store. + returned: success + type: str + min_ram: + description: Min amount of RAM required for this image. + returned: success + type: int + disk_format: + description: Disk format of the image. + returned: success + type: str + file: + description: The URL for the virtual machine image file. + returned: success + type: str + os_hidden: + description: Controls whether an image is displayed in the default image-list response + returned: success + type: bool + locations: + description: A list of URLs to access the image file in external store. + returned: success + type: str + metadata: + description: The location metadata. + returned: success + type: str + schema: + description: URL for the schema describing a virtual machine image. + returned: success + type: str + updated_at: + description: Image updated at timestamp. + returned: success + type: str + virtual_size: + description: The virtual size of the image. + returned: success + type: str + min_disk: + description: Min amount of disk space required for this image. + returned: success + type: int + is_protected: + description: Image protected flag. + returned: success + type: bool + checksum: + description: Checksum for the image. + returned: success + type: str + owner: + description: Owner for the image. + returned: success + type: str + visibility: + description: Indicates who has access to the image. + returned: success + type: str + size: + description: Size of the image. + returned: success + type: int + tags: + description: List of tags assigned to the image + returned: success + type: list +''' +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ImageInfoModule(OpenStackModule): + + deprecated_names = ('openstack.cloud.os_image_facts', 'openstack.cloud.os_image_info') + + argument_spec = dict( + image=dict(type='str', required=False), + filters=dict(type='dict', required=False, aliases=['properties']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + args = { + 'name_or_id': self.params['image'], + 'filters': self.params['filters'], + } + args = {k: v for k, v in args.items() if v is not None} + images = self.conn.search_images(**args) + + # for backward compatibility + if 'name_or_id' in args: + image = images[0] if images else None + else: + image = images + + self.exit(changed=False, openstack_images=images, image=image) + + +def main(): + module = ImageInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_ironic.py b/ansible_collections/openstack/cloud/plugins/modules/os_ironic.py new file mode 100644 index 00000000..1adb560d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_ironic.py @@ -0,0 +1,441 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2014, Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: baremetal_node +short_description: Create/Delete Bare Metal Resources from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Remove Ironic nodes from OpenStack. +options: + state: + description: + - Indicates desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + uuid: + description: + - globally unique identifier (UUID) to be given to the resource. Will + be auto-generated if not specified, and name is specified. + - Definition of a UUID will always take precedence to a name value. + type: str + name: + description: + - unique name identifier to be given to the resource. + type: str + driver: + description: + - The name of the Ironic Driver to use with this node. + - Required when I(state=present) + type: str + chassis_uuid: + description: + - Associate the node with a pre-defined chassis. + type: str + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the + endpoint URL for the Ironic API. Use with "auth" and "auth_type" + settings set to None. + type: str + resource_class: + description: + - The specific resource type to which this node belongs. + type: str + bios_interface: + description: + - The bios interface for this node, e.g. "no-bios". + type: str + boot_interface: + description: + - The boot interface for this node, e.g. "pxe". + type: str + console_interface: + description: + - The console interface for this node, e.g. "no-console". + type: str + deploy_interface: + description: + - The deploy interface for this node, e.g. "iscsi". + type: str + inspect_interface: + description: + - The interface used for node inspection, e.g. "no-inspect". + type: str + management_interface: + description: + - The interface for out-of-band management of this node, e.g. + "ipmitool". + type: str + network_interface: + description: + - The network interface provider to use when describing + connections for this node. + type: str + power_interface: + description: + - The interface used to manage power actions on this node, e.g. + "ipmitool". + type: str + raid_interface: + description: + - Interface used for configuring raid on this node. + type: str + rescue_interface: + description: + - Interface used for node rescue, e.g. "no-rescue". + type: str + storage_interface: + description: + - Interface used for attaching and detaching volumes on this node, e.g. + "cinder". + type: str + vendor_interface: + description: + - Interface for all vendor-specific actions on this node, e.g. + "no-vendor". + type: str + driver_info: + description: + - Information for this server's driver. Will vary based on which + driver is in use. Any sub-field which is populated will be validated + during creation. For compatibility reasons sub-fields `power`, + `deploy`, `management` and `console` are flattened. + required: true + type: dict + nics: + description: + - 'A list of network interface cards, eg, " - mac: aa:bb:cc:aa:bb:cc"' + required: true + type: list + elements: dict + suboptions: + mac: + description: The MAC address of the network interface card. + type: str + required: true + properties: + description: + - Definition of the physical characteristics of this server, used for scheduling purposes + type: dict + suboptions: + cpu_arch: + description: + - CPU architecture (x86_64, i686, ...) + default: x86_64 + cpus: + description: + - Number of CPU cores this machine has + default: 1 + ram: + description: + - amount of RAM this machine has, in MB + default: 1 + disk_size: + description: + - size of first storage device in this machine (typically /dev/sda), in GB + default: 1 + capabilities: + description: + - special capabilities for the node, such as boot_option, node_role etc + (see U(https://docs.openstack.org/ironic/latest/install/advanced.html) + for more information) + default: "" + root_device: + description: + - Root disk device hints for deployment. + - See U(https://docs.openstack.org/ironic/latest/install/advanced.html#specifying-the-disk-for-deployment-root-device-hints) + for allowed hints. + default: "" + skip_update_of_masked_password: + description: + - Allows the code that would assert changes to nodes to skip the + update if the change is a single line consisting of the password + field. + - As of Kilo, by default, passwords are always masked to API + requests, which means the logic as a result always attempts to + re-assert the password field. + - C(skip_update_of_driver_password) is deprecated alias and will be removed in openstack.cloud 2.0.0. + type: bool + aliases: + - skip_update_of_driver_password +requirements: + - "python >= 3.6" + - "openstacksdk" + - "jsonpatch" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Enroll a node with some basic properties and driver info +- openstack.cloud.baremetal_node: + cloud: "devstack" + driver: "pxe_ipmitool" + uuid: "00000000-0000-0000-0000-000000000002" + properties: + cpus: 2 + cpu_arch: "x86_64" + ram: 8192 + disk_size: 64 + capabilities: "boot_option:local" + root_device: + wwn: "0x4000cca77fc4dba1" + nics: + - mac: "aa:bb:cc:aa:bb:cc" + - mac: "dd:ee:ff:dd:ee:ff" + driver_info: + ipmi_address: "1.2.3.4" + ipmi_username: "admin" + ipmi_password: "adminpass" + chassis_uuid: "00000000-0000-0000-0000-000000000001" + +''' + +try: + import jsonpatch + HAS_JSONPATCH = True +except ImportError: + HAS_JSONPATCH = False + + +from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( + IronicModule, + ironic_argument_spec, +) +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_module_kwargs, + openstack_cloud_from_module +) + + +_PROPERTIES = { + 'cpu_arch': 'cpu_arch', + 'cpus': 'cpus', + 'ram': 'memory_mb', + 'disk_size': 'local_gb', + 'capabilities': 'capabilities', + 'root_device': 'root_device', +} + + +def _parse_properties(module): + """Convert ansible properties into native ironic values. + + Also filter out any properties that are not set. + """ + p = module.params['properties'] + return {to_key: p[from_key] for (from_key, to_key) in _PROPERTIES.items() + if p.get(from_key) is not None} + + +def _parse_driver_info(sdk, module): + info = module.params['driver_info'].copy() + for deprecated in ('power', 'console', 'management', 'deploy'): + if deprecated in info: + info.update(info.pop(deprecated)) + module.deprecate("Suboption %s of the driver_info parameter of " + "'openstack.cloud.baremetal_node' is deprecated" + % deprecated, version='2.0.0', + collection_name='openstack.cloud') + return info + + +def _choose_id_value(module): + if module.params['uuid']: + return module.params['uuid'] + if module.params['name']: + return module.params['name'] + return None + + +def _choose_if_password_only(module, patch): + if len(patch) == 1: + if 'password' in patch[0]['path'] and module.params['skip_update_of_masked_password']: + # Return false to abort update as the password appears + # to be the only element in the patch. + return False + return True + + +def _exit_node_not_updated(module, server): + module.exit_json( + changed=False, + result="Node not updated", + uuid=server['uuid'], + provision_state=server['provision_state'] + ) + + +def main(): + argument_spec = ironic_argument_spec( + uuid=dict(required=False), + name=dict(required=False), + driver=dict(required=False), + resource_class=dict(required=False), + bios_interface=dict(required=False), + boot_interface=dict(required=False), + console_interface=dict(required=False), + deploy_interface=dict(required=False), + inspect_interface=dict(required=False), + management_interface=dict(required=False), + network_interface=dict(required=False), + power_interface=dict(required=False), + raid_interface=dict(required=False), + rescue_interface=dict(required=False), + storage_interface=dict(required=False), + vendor_interface=dict(required=False), + driver_info=dict(type='dict', required=True), + nics=dict(type='list', required=True, elements="dict"), + properties=dict(type='dict', default={}), + chassis_uuid=dict(required=False), + skip_update_of_masked_password=dict( + required=False, + type='bool', + aliases=['skip_update_of_driver_password'], + deprecated_aliases=[dict( + name='skip_update_of_driver_password', + version='2.0.0', + collection_name='openstack.cloud')] + ), + state=dict(required=False, default='present', choices=['present', 'absent']) + ) + module_kwargs = openstack_module_kwargs() + module = IronicModule(argument_spec, **module_kwargs) + + if not HAS_JSONPATCH: + module.fail_json(msg='jsonpatch is required for this module') + + node_id = _choose_id_value(module) + + sdk, cloud = openstack_cloud_from_module(module) + try: + server = cloud.get_machine(node_id) + if module.params['state'] == 'present': + if module.params['driver'] is None: + module.fail_json(msg="A driver must be defined in order " + "to set a node to present.") + + properties = _parse_properties(module) + driver_info = _parse_driver_info(sdk, module) + kwargs = dict( + driver=module.params['driver'], + properties=properties, + driver_info=driver_info, + name=module.params['name'], + ) + optional_field_names = ('resource_class', + 'bios_interface', + 'boot_interface', + 'console_interface', + 'deploy_interface', + 'inspect_interface', + 'management_interface', + 'network_interface', + 'power_interface', + 'raid_interface', + 'rescue_interface', + 'storage_interface', + 'vendor_interface') + for i in optional_field_names: + if module.params[i]: + kwargs[i] = module.params[i] + + if module.params['chassis_uuid']: + kwargs['chassis_uuid'] = module.params['chassis_uuid'] + + if server is None: + # Note(TheJulia): Add a specific UUID to the request if + # present in order to be able to re-use kwargs for if + # the node already exists logic, since uuid cannot be + # updated. + if module.params['uuid']: + kwargs['uuid'] = module.params['uuid'] + + server = cloud.register_machine(module.params['nics'], + **kwargs) + module.exit_json(changed=True, uuid=server['uuid'], + provision_state=server['provision_state']) + else: + # TODO(TheJulia): Presently this does not support updating + # nics. Support needs to be added. + # + # Note(TheJulia): This message should never get logged + # however we cannot realistically proceed if neither a + # name or uuid was supplied to begin with. + if not node_id: + module.fail_json(msg="A uuid or name value " + "must be defined") + + # Note(TheJulia): Constructing the configuration to compare + # against. The items listed in the server_config block can + # be updated via the API. + + server_config = dict( + driver=server['driver'], + properties=server['properties'], + driver_info=server['driver_info'], + name=server['name'], + ) + + # Add the pre-existing chassis_uuid only if + # it is present in the server configuration. + if hasattr(server, 'chassis_uuid'): + server_config['chassis_uuid'] = server['chassis_uuid'] + + # Note(TheJulia): If a password is defined and concealed, a + # patch will always be generated and re-asserted. + patch = jsonpatch.JsonPatch.from_diff(server_config, kwargs) + + if not patch: + _exit_node_not_updated(module, server) + elif _choose_if_password_only(module, list(patch)): + # Note(TheJulia): Normally we would allow the general + # exception catch below, however this allows a specific + # message. + try: + server = cloud.patch_machine( + server['uuid'], + list(patch)) + except Exception as e: + module.fail_json(msg="Failed to update node, " + "Error: %s" % e.message) + + # Enumerate out a list of changed paths. + change_list = [] + for change in list(patch): + change_list.append(change['path']) + module.exit_json(changed=True, + result="Node Updated", + changes=change_list, + uuid=server['uuid'], + provision_state=server['provision_state']) + + # Return not updated by default as the conditions were not met + # to update. + _exit_node_not_updated(module, server) + + if module.params['state'] == 'absent': + if not node_id: + module.fail_json(msg="A uuid or name value must be defined " + "in order to remove a node.") + + if server is not None: + cloud.unregister_machine(module.params['nics'], + server['uuid']) + module.exit_json(changed=True, result="deleted") + else: + module.exit_json(changed=False, result="Server not found") + + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_ironic_inspect.py b/ansible_collections/openstack/cloud/plugins/modules/os_ironic_inspect.py new file mode 100644 index 00000000..f7d90d1c --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_ironic_inspect.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2015-2016, Hewlett Packard Enterprise Development Company LP +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: baremetal_inspect +short_description: Explicitly triggers baremetal node introspection in ironic. +author: OpenStack Ansible SIG +description: + - Requests Ironic to set a node into inspect state in order to collect metadata regarding the node. + This command may be out of band or in-band depending on the ironic driver configuration. + This is only possible on nodes in 'manageable' and 'available' state. +options: + mac: + description: + - unique mac address that is used to attempt to identify the host. + type: str + uuid: + description: + - globally unique identifier (UUID) to identify the host. + type: str + name: + description: + - unique name identifier to identify the host in Ironic. + type: str + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the endpoint URL for the Ironic API. + Use with "auth" and "auth_type" settings set to None. + type: str + timeout: + description: + - A timeout in seconds to tell the role to wait for the node to complete introspection if wait is set to True. + default: 1200 + type: int + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +ansible_facts: + description: Dictionary of new facts representing discovered properties of the node.. + returned: changed + type: complex + contains: + memory_mb: + description: Amount of node memory as updated in the node properties + type: str + sample: "1024" + cpu_arch: + description: Detected CPU architecture type + type: str + sample: "x86_64" + local_gb: + description: Total size of local disk storage as updated in node properties. + type: str + sample: "10" + cpus: + description: Count of cpu cores defined in the updated node properties. + type: str + sample: "1" +''' + +EXAMPLES = ''' +# Invoke node inspection +- openstack.cloud.baremetal_inspect: + name: "testnode1" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( + IronicModule, + ironic_argument_spec, +) +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_module_kwargs, + openstack_cloud_from_module +) + + +def _choose_id_value(module): + if module.params['uuid']: + return module.params['uuid'] + if module.params['name']: + return module.params['name'] + return None + + +def main(): + argument_spec = ironic_argument_spec( + uuid=dict(required=False), + name=dict(required=False), + mac=dict(required=False), + timeout=dict(default=1200, type='int', required=False), + ) + module_kwargs = openstack_module_kwargs() + module = IronicModule(argument_spec, **module_kwargs) + + sdk, cloud = openstack_cloud_from_module(module) + try: + if module.params['name'] or module.params['uuid']: + server = cloud.get_machine(_choose_id_value(module)) + elif module.params['mac']: + server = cloud.get_machine_by_mac(module.params['mac']) + else: + module.fail_json(msg="The worlds did not align, " + "the host was not found as " + "no name, uuid, or mac was " + "defined.") + if server: + cloud.inspect_machine(server['uuid'], module.params['wait']) + # TODO(TheJulia): diff properties, ?and ports? and determine + # if a change occurred. In theory, the node is always changed + # if introspection is able to update the record. + module.exit_json(changed=True, + ansible_facts=server['properties']) + + else: + module.fail_json(msg="node not found.") + + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_ironic_node.py b/ansible_collections/openstack/cloud/plugins/modules/os_ironic_node.py new file mode 100644 index 00000000..267e4308 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_ironic_node.py @@ -0,0 +1,362 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2015, Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: baremetal_node_action +short_description: Activate/Deactivate Bare Metal Resources from OpenStack +author: OpenStack Ansible SIG +description: + - Deploy to nodes controlled by Ironic. +options: + name: + description: + - Name of the node to create. + type: str + state: + description: + - Indicates desired state of the resource. + - I(state) can be C('present'), C('absent'), C('maintenance') or C('off'). + default: present + type: str + deploy: + description: + - Indicates if the resource should be deployed. Allows for deployment + logic to be disengaged and control of the node power or maintenance + state to be changed. + type: str + default: 'yes' + uuid: + description: + - globally unique identifier (UUID) to be given to the resource. + type: str + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the + endpoint URL for the Ironic API. Use with "auth" and "auth_type" + settings set to None. + type: str + config_drive: + description: + - A configdrive file or HTTP(S) URL that will be passed along to the + node. + type: raw + instance_info: + description: + - Definition of the instance information which is used to deploy + the node. This information is only required when an instance is + set to present. + type: dict + suboptions: + image_source: + description: + - An HTTP(S) URL where the image can be retrieved from. + image_checksum: + description: + - The checksum of image_source. + image_disk_format: + description: + - The type of image that has been requested to be deployed. + power: + description: + - A setting to allow power state to be asserted allowing nodes + that are not yet deployed to be powered on, and nodes that + are deployed to be powered off. + - I(power) can be C('present'), C('absent'), C('maintenance') or C('off'). + default: present + type: str + maintenance: + description: + - A setting to allow the direct control if a node is in + maintenance mode. + - I(maintenance) can be C('yes'), C('no'), C('True'), or C('False'). + type: str + maintenance_reason: + description: + - A string expression regarding the reason a node is in a + maintenance mode. + type: str + wait: + description: + - A boolean value instructing the module to wait for node + activation or deactivation to complete before returning. + type: bool + default: 'no' + timeout: + description: + - An integer value representing the number of seconds to + wait for the node activation or deactivation to complete. + default: 1800 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Activate a node by booting an image with a configdrive attached +- openstack.cloud.baremetal_node_action: + cloud: "openstack" + uuid: "d44666e1-35b3-4f6b-acb0-88ab7052da69" + state: present + power: present + deploy: True + maintenance: False + config_drive: "http://192.168.1.1/host-configdrive.iso" + instance_info: + image_source: "http://192.168.1.1/deploy_image.img" + image_checksum: "356a6b55ecc511a20c33c946c4e678af" + image_disk_format: "qcow" + delegate_to: localhost + +# Activate a node by booting an image with a configdrive json object +- openstack.cloud.baremetal_node_action: + uuid: "d44666e1-35b3-4f6b-acb0-88ab7052da69" + auth_type: None + ironic_url: "http://192.168.1.1:6385/" + config_drive: + meta_data: + hostname: node1 + public_keys: + default: ssh-rsa AAA...BBB== + instance_info: + image_source: "http://192.168.1.1/deploy_image.img" + image_checksum: "356a6b55ecc511a20c33c946c4e678af" + image_disk_format: "qcow" + delegate_to: localhost +''' + + +from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( + IronicModule, + ironic_argument_spec, +) +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_module_kwargs, + openstack_cloud_from_module +) + + +def _choose_id_value(module): + if module.params['uuid']: + return module.params['uuid'] + if module.params['name']: + return module.params['name'] + return None + + +def _is_true(value): + true_values = [True, 'yes', 'Yes', 'True', 'true', 'present', 'on'] + if value in true_values: + return True + return False + + +def _is_false(value): + false_values = [False, None, 'no', 'No', 'False', 'false', 'absent', 'off'] + if value in false_values: + return True + return False + + +def _check_set_maintenance(module, cloud, node): + if _is_true(module.params['maintenance']): + if _is_false(node['maintenance']): + cloud.set_machine_maintenance_state( + node['uuid'], + True, + reason=module.params['maintenance_reason']) + module.exit_json(changed=True, msg="Node has been set into " + "maintenance mode") + else: + # User has requested maintenance state, node is already in the + # desired state, checking to see if the reason has changed. + if (str(node['maintenance_reason']) not in + str(module.params['maintenance_reason'])): + cloud.set_machine_maintenance_state( + node['uuid'], + True, + reason=module.params['maintenance_reason']) + module.exit_json(changed=True, msg="Node maintenance reason " + "updated, cannot take any " + "additional action.") + elif _is_false(module.params['maintenance']): + if node['maintenance'] is True: + cloud.remove_machine_from_maintenance(node['uuid']) + return True + else: + module.fail_json(msg="maintenance parameter was set but a valid " + "the value was not recognized.") + return False + + +def _check_set_power_state(module, cloud, node): + if 'power on' in str(node['power_state']): + if _is_false(module.params['power']): + # User has requested the node be powered off. + cloud.set_machine_power_off(node['uuid']) + module.exit_json(changed=True, msg="Power requested off") + if 'power off' in str(node['power_state']): + if ( + _is_false(module.params['power']) + and _is_false(module.params['state']) + ): + return False + if ( + _is_false(module.params['power']) + and _is_false(module.params['state']) + ): + module.exit_json( + changed=False, + msg="Power for node is %s, node must be reactivated " + "OR set to state absent" + ) + # In the event the power has been toggled on and + # deployment has been requested, we need to skip this + # step. + if ( + _is_true(module.params['power']) + and _is_false(module.params['deploy']) + ): + # Node is powered down when it is not awaiting to be provisioned + cloud.set_machine_power_on(node['uuid']) + return True + # Default False if no action has been taken. + return False + + +def main(): + argument_spec = ironic_argument_spec( + uuid=dict(required=False), + name=dict(required=False), + instance_info=dict(type='dict', required=False), + config_drive=dict(type='raw', required=False), + state=dict(required=False, default='present'), + maintenance=dict(required=False), + maintenance_reason=dict(required=False), + power=dict(required=False, default='present'), + deploy=dict(required=False, default='yes'), + wait=dict(type='bool', required=False, default=False), + timeout=dict(required=False, type='int', default=1800), + ) + module_kwargs = openstack_module_kwargs() + module = IronicModule(argument_spec, **module_kwargs) + + if ( + module.params['config_drive'] + and not isinstance(module.params['config_drive'], (str, dict)) + ): + config_drive_type = type(module.params['config_drive']) + msg = ('argument config_drive is of type %s and we expected' + ' str or dict') % config_drive_type + module.fail_json(msg=msg) + + node_id = _choose_id_value(module) + + if not node_id: + module.fail_json(msg="A uuid or name value must be defined " + "to use this module.") + sdk, cloud = openstack_cloud_from_module(module) + try: + node = cloud.get_machine(node_id) + + if node is None: + module.fail_json(msg="node not found") + + uuid = node['uuid'] + instance_info = module.params['instance_info'] + changed = False + wait = module.params['wait'] + timeout = module.params['timeout'] + + # User has requested desired state to be in maintenance state. + if module.params['state'] == 'maintenance': + module.params['maintenance'] = True + + if node['provision_state'] in [ + 'cleaning', + 'deleting', + 'wait call-back']: + module.fail_json(msg="Node is in %s state, cannot act upon the " + "request as the node is in a transition " + "state" % node['provision_state']) + # TODO(TheJulia) This is in-development code, that requires + # code in the shade library that is still in development. + if _check_set_maintenance(module, cloud, node): + if node['provision_state'] in 'active': + module.exit_json(changed=True, + result="Maintenance state changed") + changed = True + node = cloud.get_machine(node_id) + + if _check_set_power_state(module, cloud, node): + changed = True + node = cloud.get_machine(node_id) + + if _is_true(module.params['state']): + if _is_false(module.params['deploy']): + module.exit_json( + changed=changed, + result="User request has explicitly disabled " + "deployment logic" + ) + + if 'active' in node['provision_state']: + module.exit_json( + changed=changed, + result="Node already in an active state." + ) + + if instance_info is None: + module.fail_json( + changed=changed, + msg="When setting an instance to present, " + "instance_info is a required variable.") + + # TODO(TheJulia): Update instance info, however info is + # deployment specific. Perhaps consider adding rebuild + # support, although there is a known desire to remove + # rebuild support from Ironic at some point in the future. + cloud.update_machine(uuid, instance_info=instance_info) + cloud.validate_node(uuid) + if not wait: + cloud.activate_node(uuid, module.params['config_drive']) + else: + cloud.activate_node( + uuid, + configdrive=module.params['config_drive'], + wait=wait, + timeout=timeout) + # TODO(TheJulia): Add more error checking.. + module.exit_json(changed=changed, result="node activated") + + elif _is_false(module.params['state']): + if node['provision_state'] not in "deleted": + cloud.update_machine(uuid, instance_info={}) + if not wait: + cloud.deactivate_node(uuid) + else: + cloud.deactivate_node( + uuid, + wait=wait, + timeout=timeout) + + module.exit_json(changed=True, result="deleted") + else: + module.exit_json(changed=False, result="node not found") + else: + module.fail_json(msg="State must be present, absent, " + "maintenance, off") + + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keypair.py b/ansible_collections/openstack/cloud/plugins/modules/os_keypair.py new file mode 100644 index 00000000..232d4985 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keypair.py @@ -0,0 +1,156 @@ +#!/usr/bin/python + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# Copyright (c) 2013, John Dewey <john@dewey.ws> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: keypair +short_description: Add/Delete a keypair from OpenStack +author: OpenStack Ansible SIG +description: + - Add or Remove key pair from OpenStack +options: + name: + description: + - Name that has to be given to the key pair + required: true + type: str + public_key: + description: + - The public key that would be uploaded to nova and injected into VMs + upon creation. + type: str + public_key_file: + description: + - Path to local file containing ssh public key. Mutually exclusive + with public_key. + type: str + state: + description: + - Should the resource be present or absent. If state is replace and + the key exists but has different content, delete it and recreate it + with the new content. + choices: [present, absent, replace] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Creates a key pair with the running users public key +- openstack.cloud.keypair: + cloud: mordred + state: present + name: ansible_key + public_key_file: /home/me/.ssh/id_rsa.pub + +# Creates a new key pair and the private key returned after the run. +- openstack.cloud.keypair: + cloud: rax-dfw + state: present + name: ansible_key +''' + +RETURN = ''' +id: + description: Unique UUID. + returned: success + type: str +name: + description: Name given to the keypair. + returned: success + type: str +public_key: + description: The public key value for the keypair. + returned: success + type: str +private_key: + description: The private key value for the keypair. + returned: Only when a keypair is generated for the user (e.g., when creating one + and a public key is not specified). + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule) + + +class KeyPairModule(OpenStackModule): + deprecated_names = ('os_keypair', 'openstack.cloud.os_keypair') + + argument_spec = dict( + name=dict(required=True), + public_key=dict(default=None), + public_key_file=dict(default=None), + state=dict(default='present', + choices=['absent', 'present', 'replace']), + ) + + module_kwargs = dict( + mutually_exclusive=[['public_key', 'public_key_file']]) + + def _system_state_change(self, keypair): + state = self.params['state'] + if state == 'present' and not keypair: + return True + if state == 'absent' and keypair: + return True + return False + + def run(self): + + state = self.params['state'] + name = self.params['name'] + public_key = self.params['public_key'] + + if self.params['public_key_file']: + with open(self.params['public_key_file']) as public_key_fh: + public_key = public_key_fh.read() + + keypair = self.conn.get_keypair(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(keypair)) + + if state in ('present', 'replace'): + if keypair and keypair['name'] == name: + if public_key and (public_key != keypair['public_key']): + if state == 'present': + self.fail_json( + msg="Key name %s present but key hash not the same" + " as offered. Delete key first." % name + ) + else: + self.conn.delete_keypair(name) + keypair = self.conn.create_keypair(name, public_key) + changed = True + else: + changed = False + else: + keypair = self.conn.create_keypair(name, public_key) + changed = True + + self.exit_json(changed=changed, key=keypair, id=keypair['id']) + + elif state == 'absent': + if keypair: + self.conn.delete_keypair(name) + self.exit_json(changed=True) + self.exit_json(changed=False) + + +def main(): + module = KeyPairModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_domain.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_domain.py new file mode 100644 index 00000000..660748c4 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_domain.py @@ -0,0 +1,175 @@ +#!/usr/bin/python +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_domain +short_description: Manage OpenStack Identity Domains +author: OpenStack Ansible SIG +description: + - Create, update, or delete OpenStack Identity domains. If a domain + with the supplied name already exists, it will be updated with the + new description and enabled attributes. +options: + name: + description: + - Name that has to be given to the instance + required: true + type: str + description: + description: + - Description of the domain + type: str + enabled: + description: + - Is the domain enabled + type: bool + default: 'yes' + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a domain +- openstack.cloud.identity_domain: + cloud: mycloud + state: present + name: demo + description: Demo Domain + +# Delete a domain +- openstack.cloud.identity_domain: + cloud: mycloud + state: absent + name: demo +''' + +RETURN = ''' +domain: + description: Dictionary describing the domain. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Domain ID. + type: str + sample: "474acfe5-be34-494c-b339-50f06aa143e4" + name: + description: Domain name. + type: str + sample: "demo" + description: + description: Domain description. + type: str + sample: "Demo Domain" + enabled: + description: Domain description. + type: bool + sample: True + +id: + description: The domain ID. + returned: On success when I(state) is 'present' + type: str + sample: "474acfe5-be34-494c-b339-50f06aa143e4" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityDomainModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + description=dict(default=None), + enabled=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, domain): + if self.params['description'] is not None and \ + domain.description != self.params['description']: + return True + if domain.get( + "is_enabled", domain.get("enabled")) != self.params['enabled']: + return True + return False + + def _system_state_change(self, domain): + state = self.params['state'] + if state == 'absent' and domain: + return True + + if state == 'present': + if domain is None: + return True + return self._needs_update(domain) + + return False + + def run(self): + name = self.params['name'] + description = self.params['description'] + enabled = self.params['enabled'] + state = self.params['state'] + + domains = list(self.conn.identity.domains(name=name)) + + if len(domains) > 1: + self.fail_json(msg='Domain name %s is not unique' % name) + elif len(domains) == 1: + domain = domains[0] + else: + domain = None + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(domain)) + + if state == 'present': + if domain is None: + domain = self.conn.create_domain( + name=name, description=description, enabled=enabled) + changed = True + else: + if self._needs_update(domain): + domain = self.conn.update_domain( + domain.id, name=name, description=description, + enabled=enabled) + changed = True + else: + changed = False + if hasattr(domain, "to_dict"): + domain = domain.to_dict() + domain.pop("location") + self.exit_json(changed=changed, domain=domain, id=domain['id']) + + elif state == 'absent': + if domain is None: + changed = False + else: + self.conn.delete_domain(domain.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityDomainModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_domain_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_domain_info.py new file mode 100644 index 00000000..e0e33cde --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_domain_info.py @@ -0,0 +1,119 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_domain_info +short_description: Retrieve information about one or more OpenStack domains +author: OpenStack Ansible SIG +description: + - Retrieve information about a one or more OpenStack domains + - This module was called C(openstack.cloud.identity_domain_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.identity_domain_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the domain + type: str + filters: + description: + - A dictionary of meta data to use for filtering. Elements of + this dictionary may be additional dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about previously created domain +- openstack.cloud.identity_domain_info: + cloud: awesomecloud + register: result +- debug: + msg: "{{ result.openstack_domains }}" + +# Gather information about a previously created domain by name +- openstack.cloud.identity_domain_info: + cloud: awesomecloud + name: demodomain + register: result +- debug: + msg: "{{ result.openstack_domains }}" + +# Gather information about a previously created domain with filter +- openstack.cloud.identity_domain_info: + cloud: awesomecloud + name: demodomain + filters: + enabled: false + register: result +- debug: + msg: "{{ result.openstack_domains }}" +''' + + +RETURN = ''' +openstack_domains: + description: has all the OpenStack information about domains + returned: always, but can be null + type: list + elements: dict + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the domain. + returned: success + type: str + description: + description: Description of the domain. + returned: success + type: str + enabled: + description: Flag to indicate if the domain is enabled. + returned: success + type: bool +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityDomainInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + deprecated_names = ('openstack.cloud.identity_domain_facts') + + def run(self): + name = self.params['name'] + filters = self.params['filters'] or {} + + args = {} + if name: + args['name_or_id'] = name + args['filters'] = filters + + domains = self.conn.search_domains(**args) + self.exit_json(changed=False, openstack_domains=domains) + + +def main(): + module = IdentityDomainInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_endpoint.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_endpoint.py new file mode 100644 index 00000000..e7864ecf --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_endpoint.py @@ -0,0 +1,218 @@ +#!/usr/bin/python + +# Copyright: (c) 2017, VEXXHOST, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: endpoint +short_description: Manage OpenStack Identity service endpoints +author: OpenStack Ansible SIG +description: + - Create, update, or delete OpenStack Identity service endpoints. If a + service with the same combination of I(service), I(interface) and I(region) + exist, the I(url) and I(state) (C(present) or C(absent)) will be updated. +options: + service: + description: + - Name or id of the service. + required: true + type: str + endpoint_interface: + description: + - Interface of the service. + choices: [admin, public, internal] + required: true + type: str + url: + description: + - URL of the service. + required: true + type: str + region: + description: + - Region that the service belongs to. Note that I(region_name) is used for authentication. + type: str + enabled: + description: + - Is the service enabled. + default: True + type: bool + state: + description: + - Should the resource be C(present) or C(absent). + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.13.0" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a service for glance + openstack.cloud.endpoint: + cloud: mycloud + service: glance + endpoint_interface: public + url: http://controller:9292 + region: RegionOne + state: present + +- name: Delete a service for nova + openstack.cloud.endpoint: + cloud: mycloud + service: nova + endpoint_interface: public + region: RegionOne + state: absent +''' + +RETURN = ''' +endpoint: + description: Dictionary describing the endpoint. + returned: On success when I(state) is C(present) + type: complex + contains: + id: + description: Endpoint ID. + type: str + sample: 3292f020780b4d5baf27ff7e1d224c44 + interface: + description: Endpoint Interface. + type: str + sample: public + enabled: + description: Service status. + type: bool + sample: True + links: + description: Links for the endpoint + type: str + sample: http://controller/identity/v3/endpoints/123 + region: + description: Same as C(region_id). Deprecated. + type: str + sample: RegionOne + region_id: + description: Region ID. + type: str + sample: RegionOne + service_id: + description: Service ID. + type: str + sample: b91f1318f735494a825a55388ee118f3 + url: + description: Service URL. + type: str + sample: http://controller:9292 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityEndpointModule(OpenStackModule): + argument_spec = dict( + service=dict(type='str', required=True), + endpoint_interface=dict(type='str', required=True, choices=['admin', 'public', 'internal']), + url=dict(type='str', required=True), + region=dict(type='str'), + enabled=dict(type='bool', default=True), + state=dict(type='str', default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, endpoint): + if endpoint.enabled != self.params['enabled']: + return True + if endpoint.url != self.params['url']: + return True + return False + + def _system_state_change(self, endpoint): + state = self.params['state'] + if state == 'absent' and endpoint: + return True + + if state == 'present': + if endpoint is None: + return True + return self._needs_update(endpoint) + + return False + + def run(self): + service_name_or_id = self.params['service'] + interface = self.params['endpoint_interface'] + url = self.params['url'] + region = self.params['region'] + enabled = self.params['enabled'] + state = self.params['state'] + + service = self.conn.get_service(service_name_or_id) + + if service is None and state == 'absent': + self.exit_json(changed=False) + + if service is None and state == 'present': + self.fail_json(msg='Service %s does not exist' % service_name_or_id) + + filters = dict(service_id=service.id, interface=interface) + if region is not None: + filters['region'] = region + endpoints = self.conn.search_endpoints(filters=filters) + + endpoint = None + if len(endpoints) > 1: + self.fail_json(msg='Service %s, interface %s and region %s are ' + 'not unique' % + (service_name_or_id, interface, region)) + elif len(endpoints) == 1: + endpoint = endpoints[0] + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(endpoint)) + + if state == 'present': + if endpoint is None: + args = {'url': url, 'interface': interface, + 'service_name_or_id': service.id, 'enabled': enabled, + 'region': region} + endpoints = self.conn.create_endpoint(**args) + # safe because endpoints contains a single item when url is + # given to self.conn.create_endpoint() + endpoint = endpoints[0] + + changed = True + else: + if self._needs_update(endpoint): + endpoint = self.conn.update_endpoint( + endpoint.id, url=url, enabled=enabled) + changed = True + else: + changed = False + self.exit_json(changed=changed, + endpoint=endpoint) + + elif state == 'absent': + if endpoint is None: + changed = False + else: + self.conn.delete_endpoint(endpoint.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityEndpointModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_federation_protocol.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_federation_protocol.py new file mode 100644 index 00000000..5a33d8a3 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_federation_protocol.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: keystone_federation_protocol +short_description: manage a federation Protocol +author: OpenStack Ansible SIG +description: + - Manage a federation Protocol. +options: + name: + description: + - The name of the Protocol. + type: str + required: true + aliases: ['id'] + state: + description: + - Whether the protocol should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str + idp_id: + description: + - The name of the Identity Provider this Protocol is associated with. + aliases: ['idp_name'] + required: true + type: str + mapping_id: + description: + - The name of the Mapping to use for this Protocol.' + - Required when creating a new Protocol. + type: str + aliases: ['mapping_name'] +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a protocol + openstack.cloud.keystone_federation_protocol: + cloud: example_cloud + name: example_protocol + idp_id: example_idp + mapping_id: example_mapping + +- name: Delete a protocol + openstack.cloud.keystone_federation_protocol: + cloud: example_cloud + name: example_protocol + idp_id: example_idp + state: absent +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationProtocolModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True, aliases=['id']), + state=dict(default='present', choices=['absent', 'present']), + idp_id=dict(required=True, aliases=['idp_name']), + mapping_id=dict(aliases=['mapping_name']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def normalize_protocol(self, protocol): + """ + Normalizes the protocol definitions so that the outputs are consistent with the + parameters + + - "name" (parameter) == "id" (SDK) + """ + if protocol is None: + return None + + _protocol = protocol.to_dict() + _protocol['name'] = protocol['id'] + # As of 0.44 SDK doesn't copy the URI parameters over, so let's add them + _protocol['idp_id'] = protocol['idp_id'] + return _protocol + + def delete_protocol(self, protocol): + """ + Delete an existing Protocol + + returns: the "Changed" state + """ + if protocol is None: + return False + + if self.ansible.check_mode: + return True + + self.conn.identity.delete_federation_protocol(None, protocol) + return True + + def create_protocol(self, name): + """ + Create a new Protocol + + returns: the "Changed" state and the new protocol + """ + if self.ansible.check_mode: + return True, None + + idp_name = self.params.get('idp_id') + mapping_id = self.params.get('mapping_id') + + attributes = { + 'idp_id': idp_name, + 'mapping_id': mapping_id, + } + + protocol = self.conn.identity.create_federation_protocol(id=name, **attributes) + return (True, protocol) + + def update_protocol(self, protocol): + """ + Update an existing Protocol + + returns: the "Changed" state and the new protocol + """ + mapping_id = self.params.get('mapping_id') + + attributes = {} + + if (mapping_id is not None) and (mapping_id != protocol.mapping_id): + attributes['mapping_id'] = mapping_id + + if not attributes: + return False, protocol + + if self.ansible.check_mode: + return True, None + + new_protocol = self.conn.identity.update_federation_protocol(None, protocol, **attributes) + return (True, new_protocol) + + def run(self): + """ Module entry point """ + name = self.params.get('name') + state = self.params.get('state') + idp = self.params.get('idp_id') + changed = False + + protocol = self.conn.identity.find_federation_protocol(idp, name) + + if state == 'absent': + if protocol is not None: + changed = self.delete_protocol(protocol) + self.exit_json(changed=changed) + + # state == 'present' + else: + if protocol is None: + if self.params.get('mapping_id') is None: + self.fail_json( + msg='A mapping_id must be passed when creating' + ' a protocol') + (changed, protocol) = self.create_protocol(name) + protocol = self.normalize_protocol(protocol) + self.exit_json(changed=changed, protocol=protocol) + + else: + (changed, new_protocol) = self.update_protocol(protocol) + new_protocol = self.normalize_protocol(new_protocol) + self.exit_json(changed=changed, protocol=new_protocol) + + +def main(): + module = IdentityFederationProtocolModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_federation_protocol_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_federation_protocol_info.py new file mode 100644 index 00000000..b281b13e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_federation_protocol_info.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: keystone_federation_protocol_info +short_description: get information about federation Protocols +author: OpenStack Ansible SIG +description: + - Get information about federation Protocols. +options: + name: + description: + - The name of the Protocol. + type: str + aliases: ['id'] + idp_id: + description: + - The name of the Identity Provider this Protocol is associated with. + aliases: ['idp_name'] + required: true + type: str +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Describe a protocol + openstack.cloud.keystone_federation_protocol_info: + cloud: example_cloud + name: example_protocol + idp_id: example_idp + mapping_name: example_mapping + +- name: Describe all protocols attached to an IDP + openstack.cloud.keystone_federation_protocol_info: + cloud: example_cloud + idp_id: example_idp +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationProtocolInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(aliases=['id']), + idp_id=dict(required=True, aliases=['idp_name']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def normalize_protocol(self, protocol): + """ + Normalizes the protocol definitions so that the outputs are consistent with the + parameters + + - "name" (parameter) == "id" (SDK) + """ + if protocol is None: + return None + + _protocol = protocol.to_dict() + _protocol['name'] = protocol['id'] + # As of 0.44 SDK doesn't copy the URI parameters over, so let's add them + _protocol['idp_id'] = protocol['idp_id'] + return _protocol + + def run(self): + """ Module entry point """ + + name = self.params.get('name') + idp = self.params.get('idp_id') + + if name: + protocol = self.conn.identity.get_federation_protocol(idp, name) + protocol = self.normalize_protocol(protocol) + self.exit_json(changed=False, protocols=[protocol]) + + else: + protocols = list(map(self.normalize_protocol, self.conn.identity.federation_protocols(idp))) + self.exit_json(changed=False, protocols=protocols) + + +def main(): + module = IdentityFederationProtocolInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_identity_provider.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_identity_provider.py new file mode 100644 index 00000000..35606cca --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_identity_provider.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: federation_idp +short_description: manage a federation Identity Provider +author: OpenStack Ansible SIG +description: + - Manage a federation Identity Provider. +options: + name: + description: + - The name of the Identity Provider. + type: str + required: true + aliases: ['id'] + state: + description: + - Whether the Identity Provider should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str + description: + description: + - The description of the Identity Provider. + type: str + domain_id: + description: + - The ID of a domain that is associated with the Identity Provider. + Federated users that authenticate with the Identity Provider will be + created under the domain specified. + - Required when creating a new Identity Provider. + type: str + enabled: + description: + - Whether the Identity Provider is enabled or not. + - Will default to C(true) when creating a new Identity Provider. + type: bool + aliases: ['is_enabled'] + remote_ids: + description: + - "List of the unique Identity Provider's remote IDs." + - Will default to an empty list when creating a new Identity Provider. + type: list + elements: str +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create an identity provider + openstack.cloud.federation_idp: + cloud: example_cloud + name: example_provider + domain_id: 0123456789abcdef0123456789abcdef + description: 'My example IDP' + remote_ids: + - 'https://auth.example.com/auth/realms/ExampleRealm' + +- name: Delete an identity provider + openstack.cloud.federation_idp: + cloud: example_cloud + name: example_provider + state: absent +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationIdpModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True, aliases=['id']), + state=dict(default='present', choices=['absent', 'present']), + description=dict(), + domain_id=dict(), + enabled=dict(type='bool', aliases=['is_enabled']), + remote_ids=dict(type='list', elements='str'), + ) + module_kwargs = dict( + supports_check_mode=True, + ) + + def normalize_idp(self, idp): + """ + Normalizes the IDP definitions so that the outputs are consistent with the + parameters + + - "enabled" (parameter) == "is_enabled" (SDK) + - "name" (parameter) == "id" (SDK) + """ + if idp is None: + return None + + _idp = idp.to_dict() + _idp['enabled'] = idp['is_enabled'] + _idp['name'] = idp['id'] + return _idp + + def delete_identity_provider(self, idp): + """ + Delete an existing Identity Provider + + returns: the "Changed" state + """ + if idp is None: + return False + + if self.ansible.check_mode: + return True + + self.conn.identity.delete_identity_provider(idp) + return True + + def create_identity_provider(self, name): + """ + Create a new Identity Provider + + returns: the "Changed" state and the new identity provider + """ + + if self.ansible.check_mode: + return True, None + + description = self.params.get('description') + enabled = self.params.get('enabled') + domain_id = self.params.get('domain_id') + remote_ids = self.params.get('remote_ids') + + if enabled is None: + enabled = True + if remote_ids is None: + remote_ids = [] + + attributes = { + 'domain_id': domain_id, + 'enabled': enabled, + 'remote_ids': remote_ids, + } + if description is not None: + attributes['description'] = description + + idp = self.conn.identity.create_identity_provider(id=name, **attributes) + return (True, idp) + + def update_identity_provider(self, idp): + """ + Update an existing Identity Provider + + returns: the "Changed" state and the new identity provider + """ + + description = self.params.get('description') + enabled = self.params.get('enabled') + domain_id = self.params.get('domain_id') + remote_ids = self.params.get('remote_ids') + + attributes = {} + + if (description is not None) and (description != idp.description): + attributes['description'] = description + if (enabled is not None) and (enabled != idp.is_enabled): + attributes['enabled'] = enabled + if (domain_id is not None) and (domain_id != idp.domain_id): + attributes['domain_id'] = domain_id + if (remote_ids is not None) and (remote_ids != idp.remote_ids): + attributes['remote_ids'] = remote_ids + + if not attributes: + return False, idp + + if self.ansible.check_mode: + return True, None + + new_idp = self.conn.identity.update_identity_provider(idp, **attributes) + return (True, new_idp) + + def run(self): + """ Module entry point """ + + name = self.params.get('name') + state = self.params.get('state') + changed = False + + idp = self.conn.identity.find_identity_provider(name) + + if state == 'absent': + if idp is not None: + changed = self.delete_identity_provider(idp) + self.exit_json(changed=changed) + + # state == 'present' + else: + if idp is None: + if self.params.get('domain_id') is None: + self.fail_json(msg='A domain_id must be passed when creating' + ' an identity provider') + (changed, idp) = self.create_identity_provider(name) + idp = self.normalize_idp(idp) + self.exit_json(changed=changed, identity_provider=idp) + + (changed, new_idp) = self.update_identity_provider(idp) + new_idp = self.normalize_idp(new_idp) + self.exit_json(changed=changed, identity_provider=new_idp) + + +def main(): + module = IdentityFederationIdpModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_identity_provider_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_identity_provider_info.py new file mode 100644 index 00000000..4fe71949 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_identity_provider_info.py @@ -0,0 +1,89 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: federation_idp_info +short_description: Get the information about the available federation identity + providers +author: OpenStack Ansible SIG +description: + - Fetch a federation identity provider. +options: + name: + description: + - The name of the identity provider to fetch. + - If I(name) is specified, the module will return failed if the identity + provider doesn't exist. + type: str + aliases: ['id'] +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Fetch a specific identity provider + openstack.cloud.federation_idp_info: + cloud: example_cloud + name: example_provider + +- name: Fetch all providers + openstack.cloud.federation_idp_info: + cloud: example_cloud +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationIdpInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(aliases=['id']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def normalize_idp(self, idp): + """ + Normalizes the IDP definitions so that the outputs are consistent with the + parameters + + - "enabled" (parameter) == "is_enabled" (SDK) + - "name" (parameter) == "id" (SDK) + """ + if idp is None: + return + + _idp = idp.to_dict() + _idp['enabled'] = idp['is_enabled'] + _idp['name'] = idp['id'] + return _idp + + def run(self): + """ Module entry point """ + + name = self.params.get('name') + + if name: + idp = self.normalize_idp(self.conn.identity.get_identity_provider(name)) + self.exit_json(changed=False, identity_providers=[idp]) + + else: + providers = list(map(self.normalize_idp, self.conn.identity.identity_providers())) + self.exit_json(changed=False, identity_providers=providers) + + +def main(): + module = IdentityFederationIdpInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_mapping.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_mapping.py new file mode 100644 index 00000000..6c07a41d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_mapping.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: federation_mapping +short_description: Manage a federation mapping +author: OpenStack Ansible SIG +description: + - Manage a federation mapping. +options: + name: + description: + - The name of the mapping to manage. + required: true + type: str + aliases: ['id'] + state: + description: + - Whether the mapping should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str + rules: + description: + - The rules that comprise the mapping. These are pairs of I(local) and + I(remote) definitions. For more details on how these work please see + the OpenStack documentation + U(https://docs.openstack.org/keystone/latest/admin/federation/mapping_combinations.html). + - Required if I(state=present) + type: list + elements: dict + suboptions: + local: + description: + - Information on what local attributes will be mapped. + required: true + type: list + elements: dict + remote: + description: + - Information on what remote attributes will be mapped. + required: true + type: list + elements: dict +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a new mapping + openstack.cloud.federation_mapping: + cloud: example_cloud + name: example_mapping + rules: + - local: + - user: + name: '{0}' + - group: + id: '0cd5e9' + remote: + - type: UserName + - type: orgPersonType + any_one_of: + - Contractor + - SubContractor + +- name: Delete a mapping + openstack.cloud.federation_mapping: + name: example_mapping + state: absent +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationMappingModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True, aliases=['id']), + state=dict(default='present', choices=['absent', 'present']), + rules=dict(type='list', elements='dict', options=dict( + local=dict(required=True, type='list', elements='dict'), + remote=dict(required=True, type='list', elements='dict') + )), + ) + module_kwargs = dict( + required_if=[('state', 'present', ['rules'])], + supports_check_mode=True + ) + + def normalize_mapping(self, mapping): + """ + Normalizes the mapping definitions so that the outputs are consistent with + the parameters + + - "name" (parameter) == "id" (SDK) + """ + if mapping is None: + return None + + _mapping = mapping.to_dict() + _mapping['name'] = mapping['id'] + return _mapping + + def create_mapping(self, name): + """ + Attempt to create a Mapping + + returns: A tuple containing the "Changed" state and the created mapping + """ + + if self.ansible.check_mode: + return (True, None) + + rules = self.params.get('rules') + + mapping = self.conn.identity.create_mapping(id=name, rules=rules) + return (True, mapping) + + def delete_mapping(self, mapping): + """ + Attempt to delete a Mapping + + returns: the "Changed" state + """ + if mapping is None: + return False + + if self.ansible.check_mode: + return True + + self.conn.identity.delete_mapping(mapping) + return True + + def update_mapping(self, mapping): + """ + Attempt to delete a Mapping + + returns: The "Changed" state and the the new mapping + """ + + current_rules = mapping.rules + new_rules = self.params.get('rules') + + # Nothing to do + if current_rules == new_rules: + return (False, mapping) + + if self.ansible.check_mode: + return (True, None) + + new_mapping = self.conn.identity.update_mapping(mapping, rules=new_rules) + return (True, new_mapping) + + def run(self): + """ Module entry point """ + + name = self.params.get('name') + state = self.params.get('state') + changed = False + + mapping = self.conn.identity.find_mapping(name) + + if state == 'absent': + if mapping is not None: + changed = self.delete_mapping(mapping) + self.exit_json(changed=changed) + + # state == 'present' + else: + if len(self.params.get('rules')) < 1: + self.fail_json(msg='At least one rule must be passed') + + if mapping is None: + (changed, mapping) = self.create_mapping(name) + mapping = self.normalize_mapping(mapping) + self.exit_json(changed=changed, mapping=mapping) + else: + (changed, new_mapping) = self.update_mapping(mapping) + new_mapping = self.normalize_mapping(new_mapping) + self.exit_json(mapping=new_mapping, changed=changed) + + +def main(): + module = IdentityFederationMappingModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_mapping_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_mapping_info.py new file mode 100644 index 00000000..2ba317c9 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_mapping_info.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: federation_mapping_info +short_description: Get the information about the available federation mappings +author: OpenStack Ansible SIG +description: + - Fetch a federation mapping. +options: + name: + description: + - The name of the mapping to fetch. + - If I(name) is specified, the module will return failed if the mapping + doesn't exist. + type: str + aliases: ['id'] +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Fetch a specific mapping + openstack.cloud.federation_mapping_info: + cloud: example_cloud + name: example_mapping + +- name: Fetch all mappings + openstack.cloud.federation_mapping_info: + cloud: example_cloud +''' + +RETURN = ''' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityFederationMappingInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(aliases=['id']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + module_min_sdk_version = "0.44" + + def normalize_mapping(self, mapping): + """ + Normalizes the mapping definitions so that the outputs are consistent with the + parameters + + - "name" (parameter) == "id" (SDK) + """ + if mapping is None: + return None + + _mapping = mapping.to_dict() + _mapping['name'] = mapping['id'] + return _mapping + + def run(self): + """ Module entry point """ + name = self.params.get('name') + + if name: + mapping = self.normalize_mapping( + self.conn.identity.get_mapping(name)) + self.exit_json(changed=False, mappings=[mapping]) + else: + mappings = list(map( + self.normalize_mapping, self.conn.identity.mappings())) + self.exit_json(changed=False, mappings=mappings) + + +def main(): + module = IdentityFederationMappingInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_role.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_role.py new file mode 100644 index 00000000..272d9821 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_role.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# Copyright (c) 2016 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_role +short_description: Manage OpenStack Identity Roles +author: OpenStack Ansible SIG +description: + - Manage OpenStack Identity Roles. +options: + name: + description: + - Role Name + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a role named "demo" +- openstack.cloud.identity_role: + cloud: mycloud + state: present + name: demo + +# Delete the role named "demo" +- openstack.cloud.identity_role: + cloud: mycloud + state: absent + name: demo +''' + +RETURN = ''' +role: + description: Dictionary describing the role. + returned: On success when I(state) is 'present'. + type: complex + contains: + domain_id: + description: Domain to which the role belongs + type: str + sample: default + id: + description: Unique role ID. + type: str + sample: "677bfab34c844a01b88a217aa12ec4c2" + name: + description: Role name. + type: str + sample: "demo" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityRoleModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _system_state_change(self, state, role): + if state == 'present' and not role: + return True + if state == 'absent' and role: + return True + return False + + def run(self): + name = self.params.get('name') + state = self.params.get('state') + + role = self.conn.get_role(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, role)) + + changed = False + if state == 'present': + if role is None: + role = self.conn.create_role(name=name) + changed = True + self.exit_json(changed=changed, role=role) + elif state == 'absent' and role is not None: + self.conn.identity.delete_role(role['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityRoleModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_keystone_service.py b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_service.py new file mode 100644 index 00000000..6d1962f3 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_keystone_service.py @@ -0,0 +1,190 @@ +#!/usr/bin/python +# Copyright 2016 Sam Yaple +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: catalog_service +short_description: Manage OpenStack Identity services +author: OpenStack Ansible SIG +description: + - Create, update, or delete OpenStack Identity service. If a service + with the supplied name already exists, it will be updated with the + new description and enabled attributes. +options: + name: + description: + - Name of the service + required: true + type: str + description: + description: + - Description of the service + type: str + enabled: + description: + - Is the service enabled + type: bool + default: 'yes' + aliases: ['is_enabled'] + type: + description: + - The type of service + required: true + type: str + aliases: ['service_type'] + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a service for glance +- openstack.cloud.catalog_service: + cloud: mycloud + state: present + name: glance + type: image + description: OpenStack Image Service +# Delete a service +- openstack.cloud.catalog_service: + cloud: mycloud + state: absent + name: glance + type: image +''' + +RETURN = ''' +service: + description: Dictionary describing the service. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Service ID. + type: str + sample: "3292f020780b4d5baf27ff7e1d224c44" + name: + description: Service name. + type: str + sample: "glance" + type: + description: Service type. + type: str + sample: "image" + service_type: + description: Service type. + type: str + sample: "image" + description: + description: Service description. + type: str + sample: "OpenStack Image Service" + enabled: + description: Service status. + type: bool + sample: True +id: + description: The service ID. + returned: On success when I(state) is 'present' + type: str + sample: "3292f020780b4d5baf27ff7e1d224c44" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityCatalogServiceModule(OpenStackModule): + argument_spec = dict( + description=dict(default=None), + enabled=dict(default=True, aliases=['is_enabled'], type='bool'), + name=dict(required=True), + type=dict(required=True, aliases=['service_type']), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, service): + for parameter in ('enabled', 'description', 'type'): + if service[parameter] != self.params[parameter]: + return True + return False + + def _system_state_change(self, service): + state = self.params['state'] + if state == 'absent' and service: + return True + + if state == 'present': + if service is None: + return True + return self._needs_update(service) + + return False + + def run(self): + description = self.params['description'] + enabled = self.params['enabled'] + name = self.params['name'] + state = self.params['state'] + type = self.params['type'] + + services = self.conn.search_services( + name_or_id=name, filters=(dict(type=type) if type else None)) + + service = None + if len(services) > 1: + self.fail_json( + msg='Service name %s and type %s are not unique' + % (name, type)) + elif len(services) == 1: + service = services[0] + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(service)) + + args = {'name': name, 'enabled': enabled, 'type': type} + if description: + args['description'] = description + + if state == 'present': + if service is None: + service = self.conn.create_service(**args) + changed = True + else: + if self._needs_update(service): + service = self.conn.update_service(service, + **args) + changed = True + else: + changed = False + self.exit_json(changed=changed, service=service, id=service.id) + + elif state == 'absent': + if service is None: + changed = False + else: + self.conn.identity.delete_service(service.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityCatalogServiceModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_listener.py b/ansible_collections/openstack/cloud/plugins/modules/os_listener.py new file mode 100644 index 00000000..f4cdad48 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_listener.py @@ -0,0 +1,287 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst Cloud Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: lb_listener +short_description: Add/Delete a listener for a load balancer from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove a listener for a load balancer from the OpenStack load-balancer service. +options: + name: + description: + - Name that has to be given to the listener + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + loadbalancer: + description: + - The name or id of the load balancer that this listener belongs to. + required: true + type: str + protocol: + description: + - The protocol for the listener. + choices: [HTTP, HTTPS, TCP, TERMINATED_HTTPS, UDP, SCTP] + default: HTTP + type: str + protocol_port: + description: + - The protocol port number for the listener. + default: 80 + type: int + timeout_client_data: + description: + - Client inactivity timeout in milliseconds. + default: 50000 + type: int + timeout_member_data: + description: + - Member inactivity timeout in milliseconds. + default: 50000 + type: int + wait: + description: + - If the module should wait for the load balancer to be ACTIVE. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the load balancer to get + into ACTIVE state. + default: 180 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The listener UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +listener: + description: Dictionary describing the listener. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + name: + description: Name given to the listener. + type: str + sample: "test" + description: + description: The listener description. + type: str + sample: "description" + load_balancer_id: + description: The load balancer UUID this listener belongs to. + type: str + sample: "b32eef7e-d2a6-4ea4-a301-60a873f89b3b" + loadbalancers: + description: A list of load balancer IDs.. + type: list + sample: [{"id": "b32eef7e-d2a6-4ea4-a301-60a873f89b3b"}] + provisioning_status: + description: The provisioning status of the listener. + type: str + sample: "ACTIVE" + operating_status: + description: The operating status of the listener. + type: str + sample: "ONLINE" + is_admin_state_up: + description: The administrative state of the listener. + type: bool + sample: true + protocol: + description: The protocol for the listener. + type: str + sample: "HTTP" + protocol_port: + description: The protocol port number for the listener. + type: int + sample: 80 + timeout_client_data: + description: Client inactivity timeout in milliseconds. + type: int + sample: 50000 + timeout_member_data: + description: Member inactivity timeout in milliseconds. + type: int + sample: 50000 +''' + +EXAMPLES = ''' +# Create a listener, wait for the loadbalancer to be active. +- openstack.cloud.lb_listener: + cloud: mycloud + endpoint_type: admin + state: present + name: test-listener + loadbalancer: test-loadbalancer + protocol: HTTP + protocol_port: 8080 + +# Create a listener, do not wait for the loadbalancer to be active. +- openstack.cloud.lb_listener: + cloud: mycloud + endpoint_type: admin + state: present + name: test-listener + loadbalancer: test-loadbalancer + protocol: HTTP + protocol_port: 8080 + wait: no + +# Delete a listener +- openstack.cloud.lb_listener: + cloud: mycloud + endpoint_type: admin + state: absent + name: test-listener + loadbalancer: test-loadbalancer + +# Create a listener, increase timeouts for connection persistence (for SSH for example). +- openstack.cloud.lb_listener: + cloud: mycloud + endpoint_type: admin + state: present + name: test-listener + loadbalancer: test-loadbalancer + protocol: TCP + protocol_port: 22 + timeout_client_data: 1800000 + timeout_member_data: 1800000 +''' + +import time + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class LoadbalancerListenerModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + loadbalancer=dict(required=True), + protocol=dict(default='HTTP', + choices=['HTTP', 'HTTPS', 'TCP', 'TERMINATED_HTTPS', 'UDP', 'SCTP']), + protocol_port=dict(default=80, type='int', required=False), + timeout_client_data=dict(default=50000, type='int', required=False), + timeout_member_data=dict(default=50000, type='int', required=False), + ) + module_kwargs = dict() + + def _lb_wait_for_status(self, lb, status, failures, interval=5): + """Wait for load balancer to be in a particular provisioning status.""" + timeout = self.params['timeout'] + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + lb = self.conn.load_balancer.get_load_balancer(lb.id) + if lb.provisioning_status == status: + return None + if lb.provisioning_status in failures: + self.fail_json( + msg="Load Balancer %s transitioned to failure state %s" % + (lb.id, lb.provisioning_status) + ) + + time.sleep(interval) + total_sleep += interval + + self.fail_json( + msg="Timeout waiting for Load Balancer %s to transition to %s" % + (lb.id, status) + ) + + def run(self): + loadbalancer = self.params['loadbalancer'] + loadbalancer_id = None + + changed = False + listener = self.conn.load_balancer.find_listener( + name_or_id=self.params['name']) + + if self.params['state'] == 'present': + if not listener: + lb = self.conn.load_balancer.find_load_balancer(loadbalancer) + if not lb: + self.fail_json( + msg='load balancer %s is not found' % loadbalancer + ) + loadbalancer_id = lb.id + + listener = self.conn.load_balancer.create_listener( + name=self.params['name'], + loadbalancer_id=loadbalancer_id, + protocol=self.params['protocol'], + protocol_port=self.params['protocol_port'], + timeout_client_data=self.params['timeout_client_data'], + timeout_member_data=self.params['timeout_member_data'], + ) + changed = True + + if not self.params['wait']: + self.exit_json( + changed=changed, listener=listener.to_dict(), + id=listener.id) + + if self.params['wait']: + # Check in case the listener already exists. + lb = self.conn.load_balancer.find_load_balancer(loadbalancer) + if not lb: + self.fail_json( + msg='load balancer %s is not found' % loadbalancer + ) + self._lb_wait_for_status(lb, "ACTIVE", ["ERROR"]) + + self.exit_json( + changed=changed, listener=listener.to_dict(), id=listener.id) + elif self.params['state'] == 'absent': + if not listener: + changed = False + else: + self.conn.load_balancer.delete_listener(listener) + changed = True + + if self.params['wait']: + # Wait for the load balancer to be active after deleting + # the listener. + lb = self.conn.load_balancer.find_load_balancer(loadbalancer) + if not lb: + self.fail_json( + msg='load balancer %s is not found' % loadbalancer + ) + self._lb_wait_for_status(lb, "ACTIVE", ["ERROR"]) + + self.exit_json(changed=changed) + + +def main(): + module = LoadbalancerListenerModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_loadbalancer.py b/ansible_collections/openstack/cloud/plugins/modules/os_loadbalancer.py new file mode 100644 index 00000000..336da966 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_loadbalancer.py @@ -0,0 +1,691 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst Cloud Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: loadbalancer +short_description: Add/Delete load balancer from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove load balancer from the OpenStack load-balancer + service(Octavia). Load balancer update is not supported for now. +options: + name: + description: + - The name of the load balancer. + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + flavor: + description: + - The flavor of the load balancer. + type: str + vip_network: + description: + - The name or id of the network for the virtual IP of the load balancer. + One of I(vip_network), I(vip_subnet), or I(vip_port) must be specified + for creation. + type: str + vip_subnet: + description: + - The name or id of the subnet for the virtual IP of the load balancer. + One of I(vip_network), I(vip_subnet), or I(vip_port) must be specified + for creation. + type: str + vip_port: + description: + - The name or id of the load balancer virtual IP port. One of + I(vip_network), I(vip_subnet), or I(vip_port) must be specified for + creation. + type: str + vip_address: + description: + - IP address of the load balancer virtual IP. + type: str + public_ip_address: + description: + - Public IP address associated with the VIP. + type: str + auto_public_ip: + description: + - Allocate a public IP address and associate with the VIP automatically. + type: bool + default: 'no' + public_network: + description: + - The name or ID of a Neutron external network. + type: str + delete_public_ip: + description: + - When C(state=absent) and this option is true, any public IP address + associated with the VIP will be deleted along with the load balancer. + type: bool + default: 'no' + listeners: + description: + - A list of listeners that attached to the load balancer. + suboptions: + name: + description: + - The listener name or ID. + protocol: + description: + - The protocol for the listener. + default: HTTP + protocol_port: + description: + - The protocol port number for the listener. + default: 80 + allowed_cidrs: + description: + - A list of IPv4, IPv6 or mix of both CIDRs to be allowed access to the listener. The default is all allowed. + When a list of CIDRs is provided, the default switches to deny all. + Ignored on unsupported Octavia versions (less than 2.12) + default: [] + pool: + description: + - The pool attached to the listener. + suboptions: + name: + description: + - The pool name or ID. + protocol: + description: + - The protocol for the pool. + default: HTTP + lb_algorithm: + description: + - The load balancing algorithm for the pool. + default: ROUND_ROBIN + members: + description: + - A list of members that added to the pool. + suboptions: + name: + description: + - The member name or ID. + address: + description: + - The IP address of the member. + protocol_port: + description: + - The protocol port number for the member. + default: 80 + subnet: + description: + - The name or ID of the subnet the member service is + accessible from. + elements: dict + type: list + wait: + description: + - If the module should wait for the load balancer to be created or + deleted. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait. + default: 180 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The load balancer UUID. + returned: On success when C(state=present) + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +loadbalancer: + description: Dictionary describing the load balancer. + returned: On success when C(state=present) + type: complex + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + name: + description: Name given to the load balancer. + type: str + sample: "lingxian_test" + vip_network_id: + description: Network ID the load balancer virtual IP port belongs in. + type: str + sample: "f171db43-56fd-41cf-82d7-4e91d741762e" + vip_subnet_id: + description: Subnet ID the load balancer virtual IP port belongs in. + type: str + sample: "c53e3c70-9d62-409a-9f71-db148e7aa853" + vip_port_id: + description: The load balancer virtual IP port ID. + type: str + sample: "2061395c-1c01-47ab-b925-c91b93df9c1d" + vip_address: + description: The load balancer virtual IP address. + type: str + sample: "192.168.2.88" + public_vip_address: + description: The load balancer public VIP address. + type: str + sample: "10.17.8.254" + provisioning_status: + description: The provisioning status of the load balancer. + type: str + sample: "ACTIVE" + operating_status: + description: The operating status of the load balancer. + type: str + sample: "ONLINE" + is_admin_state_up: + description: The administrative state of the load balancer. + type: bool + sample: true + listeners: + description: The associated listener IDs, if any. + type: list + sample: [{"id": "7aa1b380-beec-459c-a8a7-3a4fb6d30645"}, {"id": "692d06b8-c4f8-4bdb-b2a3-5a263cc23ba6"}] + pools: + description: The associated pool IDs, if any. + type: list + sample: [{"id": "27b78d92-cee1-4646-b831-e3b90a7fa714"}, {"id": "befc1fb5-1992-4697-bdb9-eee330989344"}] +''' + +EXAMPLES = ''' +# Create a load balancer by specifying the VIP subnet. +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + state: present + name: my_lb + vip_subnet: my_subnet + timeout: 150 + +# Create a load balancer by specifying the VIP network and the IP address. +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + state: present + name: my_lb + vip_network: my_network + vip_address: 192.168.0.11 + +# Create a load balancer together with its sub-resources in the 'all in one' +# way. A public IP address is also allocated to the load balancer VIP. +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + name: lingxian_test + state: present + vip_subnet: kong_subnet + auto_public_ip: yes + public_network: public + listeners: + - name: lingxian_80 + protocol: TCP + protocol_port: 80 + pool: + name: lingxian_80_pool + protocol: TCP + members: + - name: mywebserver1 + address: 192.168.2.81 + protocol_port: 80 + subnet: webserver_subnet + - name: lingxian_8080 + protocol: TCP + protocol_port: 8080 + pool: + name: lingxian_8080-pool + protocol: TCP + members: + - name: mywebserver2 + address: 192.168.2.82 + protocol_port: 8080 + wait: yes + timeout: 600 + +# Delete a load balancer(and all its related resources) +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + state: absent + name: my_lb + +# Delete a load balancer(and all its related resources) together with the +# public IP address(if any) attached to it. +- openstack.cloud.loadbalancer: + auth: + auth_url: https://identity.example.com + username: admin + password: passme + project_name: admin + state: absent + name: my_lb + delete_public_ip: yes +''' + +import time +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class LoadBalancerModule(OpenStackModule): + + def _wait_for_pool(self, pool, provisioning_status, operating_status, failures, interval=5): + """Wait for pool to be in a particular provisioning and operating status.""" + timeout = self.params['timeout'] # reuse loadbalancer timeout + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + pool = self.conn.load_balancer.find_pool(name_or_id=pool.id) + if pool: + if pool.provisioning_status == provisioning_status and pool.operating_status == operating_status: + return None + if pool.provisioning_status in failures: + self.fail_json( + msg="Pool %s transitioned to failure state %s" % + (pool.id, pool.provisioning_status) + ) + else: + if provisioning_status == "DELETED": + return None + else: + self.fail_json( + msg="Pool %s transitioned to DELETED" % pool.id + ) + + time.sleep(interval) + total_sleep += interval + + def _wait_for_lb(self, lb, status, failures, interval=5): + """Wait for load balancer to be in a particular provisioning status.""" + timeout = self.params['timeout'] + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + lb = self.conn.load_balancer.find_load_balancer(lb.id) + + if lb: + if lb.provisioning_status == status: + return None + if lb.provisioning_status in failures: + self.fail_json( + msg="Load Balancer %s transitioned to failure state %s" % + (lb.id, lb.provisioning_status) + ) + else: + if status == "DELETED": + return None + else: + self.fail_json( + msg="Load Balancer %s transitioned to DELETED" % lb.id + ) + + time.sleep(interval) + total_sleep += interval + + self.fail_json( + msg="Timeout waiting for Load Balancer %s to transition to %s" % + (lb.id, status) + ) + + argument_spec = dict( + name=dict(required=True), + flavor=dict(required=False), + state=dict(default='present', choices=['absent', 'present']), + vip_network=dict(required=False), + vip_subnet=dict(required=False), + vip_port=dict(required=False), + vip_address=dict(required=False), + listeners=dict(type='list', default=[], elements='dict'), + public_ip_address=dict(required=False, default=None), + auto_public_ip=dict(required=False, default=False, type='bool'), + public_network=dict(required=False), + delete_public_ip=dict(required=False, default=False, type='bool'), + ) + module_kwargs = dict(supports_check_mode=True) + + def run(self): + flavor = self.params['flavor'] + vip_network = self.params['vip_network'] + vip_subnet = self.params['vip_subnet'] + vip_port = self.params['vip_port'] + listeners = self.params['listeners'] + public_vip_address = self.params['public_ip_address'] + allocate_fip = self.params['auto_public_ip'] + delete_fip = self.params['delete_public_ip'] + public_network = self.params['public_network'] + + vip_network_id = None + vip_subnet_id = None + vip_port_id = None + flavor_id = None + + try: + max_microversion = 1 + max_majorversion = 2 + changed = False + lb = self.conn.load_balancer.find_load_balancer( + name_or_id=self.params['name']) + + if self.params['state'] == 'present': + if lb and self.ansible.check_mode: + self.exit_json(changed=False) + if lb: + self.exit_json(changed=False) + ver_data = self.conn.load_balancer.get_all_version_data() + region = list(ver_data.keys())[0] + interface_type = list(ver_data[region].keys())[0] + versions = ver_data[region][interface_type]['load-balancer'] + for ver in versions: + if ver['status'] == 'CURRENT': + curversion = ver['version'].split(".") + max_majorversion = int(curversion[0]) + max_microversion = int(curversion[1]) + + if not lb: + if self.ansible.check_mode: + self.exit_json(changed=True) + + if not (vip_network or vip_subnet or vip_port): + self.fail_json( + msg="One of vip_network, vip_subnet, or vip_port must " + "be specified for load balancer creation" + ) + + if flavor: + _flavor = self.conn.load_balancer.find_flavor(flavor) + if not _flavor: + self.fail_json( + msg='flavor %s not found' % flavor + ) + flavor_id = _flavor.id + + if vip_network: + network = self.conn.get_network(vip_network) + if not network: + self.fail_json( + msg='network %s is not found' % vip_network + ) + vip_network_id = network.id + if vip_subnet: + subnet = self.conn.get_subnet(vip_subnet) + if not subnet: + self.fail_json( + msg='subnet %s is not found' % vip_subnet + ) + vip_subnet_id = subnet.id + if vip_port: + port = self.conn.get_port(vip_port) + + if not port: + self.fail_json( + msg='port %s is not found' % vip_port + ) + vip_port_id = port.id + lbargs = {"name": self.params['name'], + "vip_network_id": vip_network_id, + "vip_subnet_id": vip_subnet_id, + "vip_port_id": vip_port_id, + "vip_address": self.params['vip_address'] + } + if flavor_id is not None: + lbargs["flavor_id"] = flavor_id + + lb = self.conn.load_balancer.create_load_balancer(**lbargs) + + changed = True + + if not listeners and not self.params['wait']: + self.exit_json( + changed=changed, + loadbalancer=lb.to_dict(), + id=lb.id + ) + + self._wait_for_lb(lb, "ACTIVE", ["ERROR"]) + + for listener_def in listeners: + listener_name = listener_def.get("name") + pool_def = listener_def.get("pool") + + if not listener_name: + self.fail_json(msg='listener name is required') + + listener = self.conn.load_balancer.find_listener( + name_or_id=listener_name + ) + + if not listener: + self._wait_for_lb(lb, "ACTIVE", ["ERROR"]) + + protocol = listener_def.get("protocol", "HTTP") + protocol_port = listener_def.get("protocol_port", 80) + allowed_cidrs = listener_def.get("allowed_cidrs", []) + listenerargs = {"name": listener_name, + "loadbalancer_id": lb.id, + "protocol": protocol, + "protocol_port": protocol_port + } + if max_microversion >= 12 and max_majorversion >= 2: + listenerargs['allowed_cidrs'] = allowed_cidrs + listener = self.conn.load_balancer.create_listener(**listenerargs) + changed = True + + # Ensure pool in the listener. + if pool_def: + pool_name = pool_def.get("name") + members = pool_def.get('members', []) + + if not pool_name: + self.fail_json(msg='pool name is required') + + pool = self.conn.load_balancer.find_pool(name_or_id=pool_name) + + if not pool: + self._wait_for_lb(lb, "ACTIVE", ["ERROR"]) + + protocol = pool_def.get("protocol", "HTTP") + lb_algorithm = pool_def.get("lb_algorithm", + "ROUND_ROBIN") + + pool = self.conn.load_balancer.create_pool( + name=pool_name, + listener_id=listener.id, + protocol=protocol, + lb_algorithm=lb_algorithm + ) + self._wait_for_pool(pool, "ACTIVE", "ONLINE", ["ERROR"]) + changed = True + + # Ensure members in the pool + for member_def in members: + member_name = member_def.get("name") + if not member_name: + self.fail_json(msg='member name is required') + + member = self.conn.load_balancer.find_member(member_name, + pool.id + ) + + if not member: + self._wait_for_lb(lb, "ACTIVE", ["ERROR"]) + + address = member_def.get("address") + if not address: + self.fail_json( + msg='member address for member %s is ' + 'required' % member_name + ) + + subnet_id = member_def.get("subnet") + if subnet_id: + subnet = self.conn.get_subnet(subnet_id) + if not subnet: + self.fail_json( + msg='subnet %s for member %s is not ' + 'found' % (subnet_id, member_name) + ) + subnet_id = subnet.id + + protocol_port = member_def.get("protocol_port", 80) + + member = self.conn.load_balancer.create_member( + pool, + name=member_name, + address=address, + protocol_port=protocol_port, + subnet_id=subnet_id + ) + self._wait_for_pool(pool, "ACTIVE", "ONLINE", ["ERROR"]) + changed = True + + # Associate public ip to the load balancer VIP. If + # public_vip_address is provided, use that IP, otherwise, either + # find an available public ip or create a new one. + fip = None + orig_public_ip = None + new_public_ip = None + if public_vip_address or allocate_fip: + ips = self.conn.network.ips( + port_id=lb.vip_port_id, + fixed_ip_address=lb.vip_address + ) + ips = list(ips) + if ips: + orig_public_ip = ips[0] + new_public_ip = orig_public_ip.floating_ip_address + + if public_vip_address and public_vip_address != orig_public_ip: + fip = self.conn.network.find_ip(public_vip_address) + + if not fip: + self.fail_json( + msg='Public IP %s is unavailable' % public_vip_address + ) + + # Release origin public ip first + self.conn.network.update_ip( + orig_public_ip, + fixed_ip_address=None, + port_id=None + ) + + # Associate new public ip + self.conn.network.update_ip( + fip, + fixed_ip_address=lb.vip_address, + port_id=lb.vip_port_id + ) + + new_public_ip = public_vip_address + changed = True + elif allocate_fip and not orig_public_ip: + fip = self.conn.network.find_available_ip() + if not fip: + if not public_network: + self.fail_json(msg="Public network is not provided") + + pub_net = self.conn.network.find_network(public_network) + if not pub_net: + self.fail_json( + msg='Public network %s not found' % + public_network + ) + fip = self.conn.network.create_ip( + floating_network_id=pub_net.id + ) + + self.conn.network.update_ip( + fip, + fixed_ip_address=lb.vip_address, + port_id=lb.vip_port_id + ) + + new_public_ip = fip.floating_ip_address + changed = True + + # Include public_vip_address in the result. + lb = self.conn.load_balancer.find_load_balancer(name_or_id=lb.id) + lb_dict = lb.to_dict() + lb_dict.update({"public_vip_address": new_public_ip}) + + self.exit_json( + changed=changed, + loadbalancer=lb_dict, + id=lb.id + ) + elif self.params['state'] == 'absent': + changed = False + public_vip_address = None + + if lb: + if self.ansible.check_mode: + self.exit_json(changed=True) + if delete_fip: + ips = self.conn.network.ips( + port_id=lb.vip_port_id, + fixed_ip_address=lb.vip_address + ) + ips = list(ips) + if ips: + public_vip_address = ips[0] + + # Deleting load balancer with `cascade=False` does not make + # sense because the deletion will always fail if there are + # sub-resources. + self.conn.load_balancer.delete_load_balancer(lb, cascade=True) + changed = True + + if self.params['wait']: + self._wait_for_lb(lb, "DELETED", ["ERROR"]) + + if delete_fip and public_vip_address: + self.conn.network.delete_ip(public_vip_address) + changed = True + elif self.ansible.check_mode: + self.exit_json(changed=False) + + self.exit_json(changed=changed) + except Exception as e: + self.fail_json(msg=str(e)) + + +def main(): + module = LoadBalancerModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_member.py b/ansible_collections/openstack/cloud/plugins/modules/os_member.py new file mode 100644 index 00000000..264f2b8e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_member.py @@ -0,0 +1,235 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst Cloud Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: lb_member +short_description: Add/Delete a member for a pool in load balancer from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove a member for a pool from the OpenStack load-balancer service. +options: + name: + description: + - Name that has to be given to the member + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + pool: + description: + - The name or id of the pool that this member belongs to. + required: true + type: str + protocol_port: + description: + - The protocol port number for the member. + default: 80 + type: int + address: + description: + - The IP address of the member. + type: str + subnet_id: + description: + - The subnet ID the member service is accessible from. + type: str + wait: + description: + - If the module should wait for the load balancer to be ACTIVE. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the load balancer to get + into ACTIVE state. + default: 180 + type: int + monitor_address: + description: + - IP address used to monitor this member + type: str + monitor_port: + description: + - Port used to monitor this member + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The member UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +member: + description: Dictionary describing the member. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + name: + description: Name given to the member. + type: str + sample: "test" + description: + description: The member description. + type: str + sample: "description" + provisioning_status: + description: The provisioning status of the member. + type: str + sample: "ACTIVE" + operating_status: + description: The operating status of the member. + type: str + sample: "ONLINE" + is_admin_state_up: + description: The administrative state of the member. + type: bool + sample: true + protocol_port: + description: The protocol port number for the member. + type: int + sample: 80 + subnet_id: + description: The subnet ID the member service is accessible from. + type: str + sample: "489247fa-9c25-11e8-9679-00224d6b7bc1" + address: + description: The IP address of the backend member server. + type: str + sample: "192.168.2.10" +''' + +EXAMPLES = ''' +# Create a member, wait for the member to be created. +- openstack.cloud.lb_member: + cloud: mycloud + endpoint_type: admin + state: present + name: test-member + pool: test-pool + address: 192.168.10.3 + protocol_port: 8080 + +# Delete a listener +- openstack.cloud.lb_member: + cloud: mycloud + endpoint_type: admin + state: absent + name: test-member + pool: test-pool +''' + +import time + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class LoadbalancerMemberModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + pool=dict(required=True), + address=dict(default=None), + protocol_port=dict(default=80, type='int'), + subnet_id=dict(default=None), + monitor_address=dict(default=None), + monitor_port=dict(default=None, type='int') + ) + module_kwargs = dict() + + def _wait_for_member_status(self, pool_id, member_id, status, + failures, interval=5): + timeout = self.params['timeout'] + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + member = self.conn.load_balancer.get_member(member_id, pool_id) + provisioning_status = member.provisioning_status + if provisioning_status == status: + return member + if provisioning_status in failures: + self.fail_json( + msg="Member %s transitioned to failure state %s" % + (member_id, provisioning_status) + ) + + time.sleep(interval) + total_sleep += interval + + self.fail_json( + msg="Timeout waiting for member %s to transition to %s" % + (member_id, status) + ) + + def run(self): + name = self.params['name'] + pool = self.params['pool'] + + changed = False + + pool_ret = self.conn.load_balancer.find_pool(name_or_id=pool) + if not pool_ret: + self.fail_json(msg='pool %s is not found' % pool) + + pool_id = pool_ret.id + member = self.conn.load_balancer.find_member(name, pool_id) + + if self.params['state'] == 'present': + if not member: + member = self.conn.load_balancer.create_member( + pool_ret, + address=self.params['address'], + name=name, + protocol_port=self.params['protocol_port'], + subnet_id=self.params['subnet_id'], + monitor_address=self.params['monitor_address'], + monitor_port=self.params['monitor_port'] + ) + changed = True + + if not self.params['wait']: + self.exit_json( + changed=changed, member=member.to_dict(), id=member.id) + + if self.params['wait']: + member = self._wait_for_member_status( + pool_id, member.id, "ACTIVE", ["ERROR"]) + + self.exit_json( + changed=changed, member=member.to_dict(), id=member.id) + + elif self.params['state'] == 'absent': + if member: + self.conn.load_balancer.delete_member(member, pool_ret) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = LoadbalancerMemberModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_network.py b/ansible_collections/openstack/cloud/plugins/modules/os_network.py new file mode 100644 index 00000000..780d49ba --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_network.py @@ -0,0 +1,245 @@ +#!/usr/bin/python + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: network +short_description: Creates/removes networks from OpenStack +author: OpenStack Ansible SIG +description: + - Add or remove network from OpenStack. +options: + name: + description: + - Name to be assigned to the network. + required: true + type: str + shared: + description: + - Whether this network is shared or not. + type: bool + default: 'no' + admin_state_up: + description: + - Whether the state should be marked as up or down. + type: bool + default: 'yes' + external: + description: + - Whether this network is externally accessible. + type: bool + default: 'no' + state: + description: + - Indicate desired state of the resource. + choices: ['present', 'absent'] + default: present + type: str + provider_physical_network: + description: + - The physical network where this network object is implemented. + type: str + provider_network_type: + description: + - The type of physical network that maps to this network resource. + type: str + provider_segmentation_id: + description: + - An isolated segment on the physical network. The I(network_type) + attribute defines the segmentation model. For example, if the + I(network_type) value is vlan, this ID is a vlan identifier. If + the I(network_type) value is gre, this ID is a gre key. + type: int + project: + description: + - Project name or ID containing the network (name admin-only) + type: str + port_security_enabled: + description: + - Whether port security is enabled on the network or not. + Network will use OpenStack defaults if this option is + not utilised. Requires openstacksdk>=0.18. + type: bool + mtu_size: + description: + - The maximum transmission unit (MTU) value to address fragmentation. + Network will use OpenStack defaults if this option is + not provided. Requires openstacksdk>=0.18. + type: int + aliases: ['mtu'] + dns_domain: + description: + - The DNS domain value to set. Requires openstacksdk>=0.29. + Network will use Openstack defaults if this option is + not provided. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create an externally accessible network named 'ext_network'. +- openstack.cloud.network: + cloud: mycloud + state: present + name: ext_network + external: true +''' + +RETURN = ''' +network: + description: Dictionary describing the network. + returned: On success when I(state) is 'present'. + type: complex + contains: + id: + description: Network ID. + type: str + sample: "4bb4f9a5-3bd2-4562-bf6a-d17a6341bb56" + name: + description: Network name. + type: str + sample: "ext_network" + shared: + description: Indicates whether this network is shared across all tenants. + type: bool + sample: false + status: + description: Network status. + type: str + sample: "ACTIVE" + mtu: + description: The MTU of a network resource. + type: int + sample: 0 + dns_domain: + description: The DNS domain of a network resource. + type: str + sample: "sample.openstack.org." + admin_state_up: + description: The administrative state of the network. + type: bool + sample: true + port_security_enabled: + description: The port security status + type: bool + sample: true + router:external: + description: Indicates whether this network is externally accessible. + type: bool + sample: true + tenant_id: + description: The tenant ID. + type: str + sample: "06820f94b9f54b119636be2728d216fc" + subnets: + description: The associated subnets. + type: list + sample: [] + "provider:physical_network": + description: The physical network where this network object is implemented. + type: str + sample: my_vlan_net + "provider:network_type": + description: The type of physical network that maps to this network resource. + type: str + sample: vlan + "provider:segmentation_id": + description: An isolated segment on the physical network. + type: str + sample: 101 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class NetworkModule(OpenStackModule): + + argument_spec = dict( + name=dict(required=True), + shared=dict(default=False, type='bool'), + admin_state_up=dict(default=True, type='bool'), + external=dict(default=False, type='bool'), + provider_physical_network=dict(required=False), + provider_network_type=dict(required=False), + provider_segmentation_id=dict(required=False, type='int'), + state=dict(default='present', choices=['absent', 'present']), + project=dict(default=None), + port_security_enabled=dict(type='bool', min_ver='0.18.0'), + mtu_size=dict(required=False, type='int', min_ver='0.18.0', aliases=['mtu']), + dns_domain=dict(required=False, min_ver='0.29.0') + ) + + def run(self): + + state = self.params['state'] + name = self.params['name'] + shared = self.params['shared'] + admin_state_up = self.params['admin_state_up'] + external = self.params['external'] + provider_physical_network = self.params['provider_physical_network'] + provider_network_type = self.params['provider_network_type'] + provider_segmentation_id = self.params['provider_segmentation_id'] + project = self.params['project'] + + kwargs = self.check_versioned( + mtu_size=self.params['mtu_size'], port_security_enabled=self.params['port_security_enabled'], + dns_domain=self.params['dns_domain'] + ) + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail_json(msg='Project %s could not be found' % project) + project_id = proj['id'] + filters = {'tenant_id': project_id} + else: + project_id = None + filters = None + net = self.conn.get_network(name, filters=filters) + + if state == 'present': + if not net: + provider = {} + if provider_physical_network: + provider['physical_network'] = provider_physical_network + if provider_network_type: + provider['network_type'] = provider_network_type + if provider_segmentation_id: + provider['segmentation_id'] = provider_segmentation_id + + if project_id is not None: + net = self.conn.create_network(name, shared, admin_state_up, + external, provider, project_id, + **kwargs) + else: + net = self.conn.create_network(name, shared, admin_state_up, + external, provider, + **kwargs) + changed = True + else: + changed = False + self.exit(changed=changed, network=net, id=net['id']) + + elif state == 'absent': + if not net: + self.exit(changed=False) + else: + self.conn.delete_network(name) + self.exit(changed=True) + + +def main(): + module = NetworkModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_networks_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_networks_info.py new file mode 100644 index 00000000..251af3e7 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_networks_info.py @@ -0,0 +1,149 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: networks_info +short_description: Retrieve information about one or more OpenStack networks. +author: OpenStack Ansible SIG +description: + - Retrieve information about one or more networks from OpenStack. + - This module was called C(openstack.cloud.networks_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.networks_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the Network + required: false + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Gather information about previously created networks + openstack.cloud.networks_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + register: result + +- name: Show openstack networks + debug: + msg: "{{ result.openstack_networks }}" + +- name: Gather information about a previously created network by name + openstack.cloud.networks_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + name: network1 + register: result + +- name: Show openstack networks + debug: + msg: "{{ result.openstack_networks }}" + +- name: Gather information about a previously created network with filter + # Note: name and filters parameters are Not mutually exclusive + openstack.cloud.networks_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + filters: + tenant_id: 55e2ce24b2a245b09f181bf025724cbe + subnets: + - 057d4bdf-6d4d-4728-bb0f-5ac45a6f7400 + - 443d4dc0-91d4-4998-b21c-357d10433483 + register: result + +- name: Show openstack networks + debug: + msg: "{{ result.openstack_networks }}" +''' + +RETURN = ''' +openstack_networks: + description: has all the openstack information about the networks + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the network. + returned: success + type: str + status: + description: Network status. + returned: success + type: str + subnets: + description: Subnet(s) included in this network. + returned: success + type: list + elements: str + tenant_id: + description: Tenant id associated with this network. + returned: success + type: str + shared: + description: Network shared flag. + returned: success + type: bool +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class NetworkInfoModule(OpenStackModule): + + deprecated_names = ('networks_facts', 'openstack.cloud.networks_facts') + + argument_spec = dict( + name=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None) + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + + kwargs = self.check_versioned( + filters=self.params['filters'] + ) + if self.params['name']: + kwargs['name_or_id'] = self.params['name'] + networks = self.conn.search_networks(**kwargs) + + self.exit(changed=False, openstack_networks=networks) + + +def main(): + module = NetworkInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_nova_flavor.py b/ansible_collections/openstack/cloud/plugins/modules/os_nova_flavor.py new file mode 100644 index 00000000..8a993ca5 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_nova_flavor.py @@ -0,0 +1,274 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: compute_flavor +short_description: Manage OpenStack compute flavors +author: OpenStack Ansible SIG +description: + - Add or remove flavors from OpenStack. +options: + state: + description: + - Indicate desired state of the resource. When I(state) is 'present', + then I(ram), I(vcpus), and I(disk) are all required. There are no + default values for those parameters. + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Flavor name. + required: true + type: str + ram: + description: + - Amount of memory, in MB. + type: int + vcpus: + description: + - Number of virtual CPUs. + type: int + disk: + description: + - Size of local disk, in GB. + default: 0 + type: int + ephemeral: + description: + - Ephemeral space size, in GB. + default: 0 + type: int + swap: + description: + - Swap space size, in MB. + default: 0 + type: int + rxtx_factor: + description: + - RX/TX factor. + default: 1.0 + type: float + is_public: + description: + - Make flavor accessible to the public. + type: bool + default: 'yes' + flavorid: + description: + - ID for the flavor. This is optional as a unique UUID will be + assigned if a value is not specified. + default: "auto" + type: str + extra_specs: + description: + - Metadata dictionary + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: "Create 'tiny' flavor with 1024MB of RAM, 1 virtual CPU, and 10GB of local disk, and 10GB of ephemeral." + openstack.cloud.compute_flavor: + cloud: mycloud + state: present + name: tiny + ram: 1024 + vcpus: 1 + disk: 10 + ephemeral: 10 + +- name: "Delete 'tiny' flavor" + openstack.cloud.compute_flavor: + cloud: mycloud + state: absent + name: tiny + +- name: Create flavor with metadata + openstack.cloud.compute_flavor: + cloud: mycloud + state: present + name: tiny + ram: 1024 + vcpus: 1 + disk: 10 + extra_specs: + "quota:disk_read_iops_sec": 5000 + "aggregate_instance_extra_specs:pinned": false +''' + +RETURN = ''' +flavor: + description: Dictionary describing the flavor. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Flavor ID. + returned: success + type: str + sample: "515256b8-7027-4d73-aa54-4e30a4a4a339" + name: + description: Flavor name. + returned: success + type: str + sample: "tiny" + disk: + description: Size of local disk, in GB. + returned: success + type: int + sample: 10 + ephemeral: + description: Ephemeral space size, in GB. + returned: success + type: int + sample: 10 + ram: + description: Amount of memory, in MB. + returned: success + type: int + sample: 1024 + swap: + description: Swap space size, in MB. + returned: success + type: int + sample: 100 + vcpus: + description: Number of virtual CPUs. + returned: success + type: int + sample: 2 + is_public: + description: Make flavor accessible to the public. + returned: success + type: bool + sample: true + extra_specs: + description: Flavor metadata + returned: success + type: dict + sample: + "quota:disk_read_iops_sec": 5000 + "aggregate_instance_extra_specs:pinned": false +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ComputeFlavorModule(OpenStackModule): + argument_spec = dict( + state=dict(required=False, default='present', + choices=['absent', 'present']), + name=dict(required=True), + + # required when state is 'present' + ram=dict(required=False, type='int'), + vcpus=dict(required=False, type='int'), + + disk=dict(required=False, default=0, type='int'), + ephemeral=dict(required=False, default=0, type='int'), + swap=dict(required=False, default=0, type='int'), + rxtx_factor=dict(required=False, default=1.0, type='float'), + is_public=dict(required=False, default=True, type='bool'), + flavorid=dict(required=False, default="auto"), + extra_specs=dict(required=False, default=None, type='dict'), + ) + + module_kwargs = dict( + required_if=[ + ('state', 'present', ['ram', 'vcpus', 'disk']) + ], + supports_check_mode=True + ) + + def _system_state_change(self, flavor): + state = self.params['state'] + if state == 'present' and not flavor: + return True + if state == 'absent' and flavor: + return True + return False + + def run(self): + state = self.params['state'] + name = self.params['name'] + extra_specs = self.params['extra_specs'] or {} + + flavor = self.conn.get_flavor(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(flavor)) + + if state == 'present': + old_extra_specs = {} + require_update = False + + if flavor: + old_extra_specs = flavor['extra_specs'] + if flavor['swap'] == "": + flavor['swap'] = 0 + for param_key in ['ram', 'vcpus', 'disk', 'ephemeral', + 'swap', 'rxtx_factor', 'is_public']: + if self.params[param_key] != flavor[param_key]: + require_update = True + break + flavorid = self.params['flavorid'] + if flavor and require_update: + self.conn.delete_flavor(name) + old_extra_specs = {} + if flavorid == 'auto': + flavorid = flavor['id'] + flavor = None + + if not flavor: + flavor = self.conn.create_flavor( + name=name, + ram=self.params['ram'], + vcpus=self.params['vcpus'], + disk=self.params['disk'], + flavorid=flavorid, + ephemeral=self.params['ephemeral'], + swap=self.params['swap'], + rxtx_factor=self.params['rxtx_factor'], + is_public=self.params['is_public'] + ) + changed = True + else: + changed = False + + new_extra_specs = dict([(k, str(v)) for k, v in extra_specs.items()]) + unset_keys = set(old_extra_specs.keys()) - set(extra_specs.keys()) + + if unset_keys and not require_update: + self.conn.unset_flavor_specs(flavor['id'], unset_keys) + + if old_extra_specs != new_extra_specs: + self.conn.set_flavor_specs(flavor['id'], extra_specs) + + changed = (changed or old_extra_specs != new_extra_specs) + + self.exit_json( + changed=changed, flavor=flavor, id=flavor['id']) + + elif state == 'absent': + if flavor: + self.conn.delete_flavor(name) + self.exit_json(changed=True) + self.exit_json(changed=False) + + +def main(): + module = ComputeFlavorModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_nova_host_aggregate.py b/ansible_collections/openstack/cloud/plugins/modules/os_nova_host_aggregate.py new file mode 100644 index 00000000..4c95fd29 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_nova_host_aggregate.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# Copyright 2016 Jakub Jursa <jakub.jursa1@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: host_aggregate +short_description: Manage OpenStack host aggregates +author: OpenStack Ansible SIG +description: + - Create, update, or delete OpenStack host aggregates. If a aggregate + with the supplied name already exists, it will be updated with the + new name, new availability zone, new metadata and new list of hosts. +options: + name: + description: Name of the aggregate. + required: true + type: str + metadata: + description: Metadata dict. + type: dict + availability_zone: + description: Availability zone to create aggregate into. + type: str + hosts: + description: List of hosts to set for an aggregate. + type: list + elements: str + purge_hosts: + description: Whether hosts not in I(hosts) should be removed from the aggregate + type: bool + default: true + state: + description: Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a host aggregate +- openstack.cloud.host_aggregate: + cloud: mycloud + state: present + name: db_aggregate + hosts: + - host1 + - host2 + metadata: + type: dbcluster + +# Add an additional host to the aggregate +- openstack.cloud.host_aggregate: + cloud: mycloud + state: present + name: db_aggregate + hosts: + - host3 + purge_hosts: false + metadata: + type: dbcluster + +# Delete an aggregate +- openstack.cloud.host_aggregate: + cloud: mycloud + state: absent + name: db_aggregate +''' + +RETURN = r''' +aggregate: + description: A host aggregate resource. + type: complex + returned: On success, when I(state) is present + contains: + availability_zone: + description: Availability zone of the aggregate + type: str + returned: always + deleted: + description: Whether or not the resource is deleted + type: bool + returned: always + hosts: + description: Hosts belonging to the aggregate + type: str + returned: always + id: + description: The UUID of the aggregate. + type: str + returned: always + metadata: + description: Metadata attached to the aggregate + type: str + returned: always + name: + description: Name of the aggregate + type: str + returned: always +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ComputeHostAggregateModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + metadata=dict(required=False, default=None, type='dict'), + availability_zone=dict(required=False, default=None), + hosts=dict(required=False, default=None, type='list', elements='str'), + purge_hosts=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _find_aggregate(self, name_or_id): + aggregates = self.conn.search_aggregates(name_or_id=name_or_id) + if len(aggregates) == 1: + return aggregates[0] + elif len(aggregates) == 0: + return None + raise Exception("Aggregate is not unique, this should be impossible") + + def _needs_update(self, aggregate): + new_metadata = self.params['metadata'] or {} + + if self.params['availability_zone'] is not None: + new_metadata['availability_zone'] = self.params['availability_zone'] + + if self.params['hosts'] is not None: + if self.params['purge_hosts']: + if set(self.params['hosts']) != set(aggregate.hosts): + return True + else: + intersection = set(self.params['hosts']).intersection(set(aggregate.hosts)) + if set(self.params['hosts']) != intersection: + return True + + for param in ('availability_zone', 'metadata'): + if self.params[param] is not None and \ + self.params[param] != aggregate[param]: + return True + + return False + + def _system_state_change(self, aggregate): + state = self.params['state'] + if state == 'absent' and aggregate: + return True + + if state == 'present': + if aggregate is None: + return True + return self._needs_update(aggregate) + + return False + + def _update_hosts(self, aggregate, hosts, purge_hosts): + if hosts is None: + return + + hosts_to_add = set(hosts) - set(aggregate['hosts'] or []) + for host in hosts_to_add: + self.conn.add_host_to_aggregate(aggregate.id, host) + + if not purge_hosts: + return + + hosts_to_remove = set(aggregate["hosts"] or []) - set(hosts) + for host in hosts_to_remove: + self.conn.remove_host_from_aggregate(aggregate.id, host) + + def run(self): + name = self.params['name'] + metadata = self.params['metadata'] + availability_zone = self.params['availability_zone'] + hosts = self.params['hosts'] + purge_hosts = self.params['purge_hosts'] + state = self.params['state'] + + if metadata is not None: + metadata.pop('availability_zone', None) + + aggregate = self._find_aggregate(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(aggregate)) + + changed = False + if state == 'present': + if aggregate is None: + aggregate = self.conn.create_aggregate( + name=name, availability_zone=availability_zone) + self._update_hosts(aggregate, hosts, False) + if metadata: + self.conn.set_aggregate_metadata(aggregate.id, metadata) + changed = True + elif self._needs_update(aggregate): + if availability_zone is not None: + aggregate = self.conn.update_aggregate( + aggregate.id, name=name, + availability_zone=availability_zone) + if metadata is not None: + metas = metadata + for i in set(aggregate.metadata.keys() - set(metadata.keys())): + if i != 'availability_zone': + metas[i] = None + self.conn.set_aggregate_metadata(aggregate.id, metas) + self._update_hosts(aggregate, hosts, purge_hosts) + changed = True + aggregate = self._find_aggregate(name) + self.exit_json(changed=changed, aggregate=aggregate) + + elif state == 'absent' and aggregate is not None: + self._update_hosts(aggregate, [], True) + self.conn.delete_aggregate(aggregate.id) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = ComputeHostAggregateModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_object.py b/ansible_collections/openstack/cloud/plugins/modules/os_object.py new file mode 100644 index 00000000..4a22604e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_object.py @@ -0,0 +1,120 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: object +short_description: Create or Delete objects and containers from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Delete objects and containers from OpenStack +options: + container: + description: + - The name of the container in which to create the object + required: true + type: str + name: + description: + - Name to be give to the object. If omitted, operations will be on + the entire container + required: false + type: str + filename: + description: + - Path to local file to be uploaded. + required: false + type: str + container_access: + description: + - desired container access level. + required: false + choices: ['private', 'public'] + default: private + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: "Create a object named 'fstab' in the 'config' container" + openstack.cloud.object: + cloud: mordred + state: present + name: fstab + container: config + filename: /etc/fstab + +- name: Delete a container called config and all of its contents + openstack.cloud.object: + cloud: rax-iad + state: absent + container: config +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class SwiftObjectModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + container=dict(required=True), + filename=dict(required=False, default=None), + container_access=dict(default='private', choices=['private', 'public']), + state=dict(default='present', choices=['absent', 'present']), + ) + module_kwargs = dict() + + def process_object( + self, container, name, filename, container_access, **kwargs + ): + changed = False + container_obj = self.conn.get_container(container) + if kwargs['state'] == 'present': + if not container_obj: + container_obj = self.conn.create_container(container) + changed = True + if self.conn.get_container_access(container) != container_access: + self.conn.set_container_access(container, container_access) + changed = True + if name: + if self.conn.is_object_stale(container, name, filename): + self.conn.create_object(container, name, filename) + changed = True + else: + if container_obj: + if name: + if self.conn.get_object_metadata(container, name): + self.conn.delete_object(container, name) + changed = True + else: + self.conn.delete_container(container) + changed = True + return changed + + def run(self): + changed = self.process_object(**self.params) + + self.exit_json(changed=changed) + + +def main(): + module = SwiftObjectModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_pool.py b/ansible_collections/openstack/cloud/plugins/modules/os_pool.py new file mode 100644 index 00000000..6f73ea1c --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_pool.py @@ -0,0 +1,263 @@ +#!/usr/bin/python + +# Copyright (c) 2018 Catalyst Cloud Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: lb_pool +short_description: Add/Delete a pool in the load balancing service from OpenStack Cloud +author: OpenStack Ansible SIG +description: + - Add or Remove a pool from the OpenStack load-balancer service. +options: + name: + description: + - Name that has to be given to the pool + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + loadbalancer: + description: + - The name or id of the load balancer that this pool belongs to. + Either loadbalancer or listener must be specified for pool creation. + type: str + listener: + description: + - The name or id of the listener that this pool belongs to. + Either loadbalancer or listener must be specified for pool creation. + type: str + protocol: + description: + - The protocol for the pool. + choices: [HTTP, HTTPS, PROXY, TCP, UDP] + default: HTTP + type: str + lb_algorithm: + description: + - The load balancing algorithm for the pool. + choices: [LEAST_CONNECTIONS, ROUND_ROBIN, SOURCE_IP] + default: ROUND_ROBIN + type: str + wait: + description: + - If the module should wait for the pool to be ACTIVE. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the pool to get + into ACTIVE state. + default: 180 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +id: + description: The pool UUID. + returned: On success when I(state) is 'present' + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +listener: + description: Dictionary describing the pool. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + name: + description: Name given to the pool. + type: str + sample: "test" + description: + description: The pool description. + type: str + sample: "description" + loadbalancers: + description: A list of load balancer IDs. + type: list + sample: [{"id": "b32eef7e-d2a6-4ea4-a301-60a873f89b3b"}] + listeners: + description: A list of listener IDs. + type: list + sample: [{"id": "b32eef7e-d2a6-4ea4-a301-60a873f89b3b"}] + members: + description: A list of member IDs. + type: list + sample: [{"id": "b32eef7e-d2a6-4ea4-a301-60a873f89b3b"}] + loadbalancer_id: + description: The load balancer ID the pool belongs to. This field is set when the pool doesn't belong to any listener in the load balancer. + type: str + sample: "7c4be3f8-9c2f-11e8-83b3-44a8422643a4" + listener_id: + description: The listener ID the pool belongs to. + type: str + sample: "956aa716-9c2f-11e8-83b3-44a8422643a4" + provisioning_status: + description: The provisioning status of the pool. + type: str + sample: "ACTIVE" + operating_status: + description: The operating status of the pool. + type: str + sample: "ONLINE" + is_admin_state_up: + description: The administrative state of the pool. + type: bool + sample: true + protocol: + description: The protocol for the pool. + type: str + sample: "HTTP" + lb_algorithm: + description: The load balancing algorithm for the pool. + type: str + sample: "ROUND_ROBIN" +''' + +EXAMPLES = ''' +# Create a pool, wait for the pool to be active. +- openstack.cloud.lb_pool: + cloud: mycloud + endpoint_type: admin + state: present + name: test-pool + loadbalancer: test-loadbalancer + protocol: HTTP + lb_algorithm: ROUND_ROBIN + +# Delete a pool +- openstack.cloud.lb_pool: + cloud: mycloud + endpoint_type: admin + state: absent + name: test-pool +''' + +import time + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class LoadbalancerPoolModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + loadbalancer=dict(default=None), + listener=dict(default=None), + protocol=dict(default='HTTP', + choices=['HTTP', 'HTTPS', 'TCP', 'UDP', 'PROXY']), + lb_algorithm=dict( + default='ROUND_ROBIN', + choices=['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'] + ) + ) + module_kwargs = dict( + mutually_exclusive=[['loadbalancer', 'listener']] + ) + + def _wait_for_pool_status(self, pool_id, status, failures, + interval=5): + timeout = self.params['timeout'] + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < timeout: + pool = self.conn.load_balancer.get_pool(pool_id) + provisioning_status = pool.provisioning_status + if provisioning_status == status: + return pool + if provisioning_status in failures: + self.fail_json( + msg="pool %s transitioned to failure state %s" % + (pool_id, provisioning_status) + ) + + time.sleep(interval) + total_sleep += interval + + self.fail_json( + msg="timeout waiting for pool %s to transition to %s" % + (pool_id, status) + ) + + def run(self): + loadbalancer = self.params['loadbalancer'] + listener = self.params['listener'] + + changed = False + pool = self.conn.load_balancer.find_pool(name_or_id=self.params['name']) + + if self.params['state'] == 'present': + if not pool: + loadbalancer_id = None + if not (loadbalancer or listener): + self.fail_json( + msg="either loadbalancer or listener must be provided" + ) + + if loadbalancer: + lb = self.conn.load_balancer.find_load_balancer(loadbalancer) + if not lb: + self.fail_json( + msg='load balancer %s is not found' % loadbalancer) + loadbalancer_id = lb.id + + listener_id = None + if listener: + listener_ret = self.conn.load_balancer.find_listener(listener) + if not listener_ret: + self.fail_json( + msg='listener %s is not found' % listener) + listener_id = listener_ret.id + + pool = self.conn.load_balancer.create_pool( + name=self.params['name'], + loadbalancer_id=loadbalancer_id, + listener_id=listener_id, + protocol=self.params['protocol'], + lb_algorithm=self.params['lb_algorithm'] + ) + changed = True + + if not self.params['wait']: + self.exit_json( + changed=changed, pool=pool.to_dict(), id=pool.id) + + if self.params['wait']: + pool = self._wait_for_pool_status( + pool.id, "ACTIVE", ["ERROR"]) + + self.exit_json( + changed=changed, pool=pool.to_dict(), id=pool.id) + + elif self.params['state'] == 'absent': + if pool: + self.conn.load_balancer.delete_pool(pool) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = LoadbalancerPoolModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_port.py b/ansible_collections/openstack/cloud/plugins/modules/os_port.py new file mode 100644 index 00000000..accef4fc --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_port.py @@ -0,0 +1,530 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: port +short_description: Add/Update/Delete ports from an OpenStack cloud. +author: OpenStack Ansible SIG +description: + - Add, Update or Remove ports from an OpenStack cloud. A I(state) of + 'present' will ensure the port is created or updated if required. +options: + network: + description: + - Network ID or name this port belongs to. + - Required when creating a new port. + type: str + name: + description: + - Name that has to be given to the port. + type: str + fixed_ips: + description: + - Desired IP and/or subnet for this port. Subnet is referenced by + subnet_id and IP is referenced by ip_address. + type: list + elements: dict + suboptions: + ip_address: + description: The fixed IP address to attempt to allocate. + required: true + type: str + subnet_id: + description: The subnet to attach the IP address to. + type: str + admin_state_up: + description: + - Sets admin state. + type: bool + mac_address: + description: + - MAC address of this port. + type: str + security_groups: + description: + - Security group(s) ID(s) or name(s) associated with the port (comma + separated string or YAML list) + type: list + elements: str + no_security_groups: + description: + - Do not associate a security group with this port. + type: bool + default: 'no' + allowed_address_pairs: + description: + - "Allowed address pairs list. Allowed address pairs are supported with + dictionary structure. + e.g. allowed_address_pairs: + - ip_address: 10.1.0.12 + mac_address: ab:cd:ef:12:34:56 + - ip_address: ..." + type: list + elements: dict + suboptions: + ip_address: + description: The IP address. + type: str + mac_address: + description: The MAC address. + type: str + extra_dhcp_opts: + description: + - "Extra dhcp options to be assigned to this port. Extra options are + supported with dictionary structure. Note that options cannot be removed + only updated. + e.g. extra_dhcp_opts: + - opt_name: opt name1 + opt_value: value1 + ip_version: 4 + - opt_name: ..." + type: list + elements: dict + suboptions: + opt_name: + description: The name of the DHCP option to set. + type: str + required: true + opt_value: + description: The value of the DHCP option to set. + type: str + required: true + ip_version: + description: The IP version this DHCP option is for. + type: int + required: true + device_owner: + description: + - The ID of the entity that uses this port. + type: str + device_id: + description: + - Device ID of device using this port. + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + vnic_type: + description: + - The type of the port that should be created + choices: [normal, direct, direct-physical, macvtap, baremetal, virtio-forwarder] + type: str + port_security_enabled: + description: + - Whether to enable or disable the port security on the network. + type: bool + binding_profile: + description: + - Binding profile dict that the port should be created with. + type: dict + dns_name: + description: + - The dns name of the port ( only with dns-integration enabled ) + type: str + dns_domain: + description: + - The dns domain of the port ( only with dns-integration enabled ) + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a port +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + +# Create a port with a static IP +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + fixed_ips: + - ip_address: 10.1.0.21 + +# Create a port with No security groups +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + no_security_groups: True + +# Update the existing 'port1' port with multiple security groups (version 1) +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + security_groups: 1496e8c7-4918-482a-9172-f4f00fc4a3a5,057d4bdf-6d4d-472... + +# Update the existing 'port1' port with multiple security groups (version 2) +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + security_groups: + - 1496e8c7-4918-482a-9172-f4f00fc4a3a5 + - 057d4bdf-6d4d-472... + +# Create port of type 'direct' +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + vnic_type: direct + +# Create a port with binding profile +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + binding_profile: + "pci_slot": "0000:03:11.1" + "physical_network": "provider" +''' + +RETURN = ''' +id: + description: Unique UUID. + returned: success + type: str +name: + description: Name given to the port. + returned: success + type: str +network_id: + description: Network ID this port belongs in. + returned: success + type: str +security_groups: + description: Security group(s) associated with this port. + returned: success + type: list +status: + description: Port's status. + returned: success + type: str +fixed_ips: + description: Fixed ip(s) associated with this port. + returned: success + type: list +tenant_id: + description: Tenant id associated with this port. + returned: success + type: str +allowed_address_pairs: + description: Allowed address pairs with this port. + returned: success + type: list +admin_state_up: + description: Admin state up flag for this port. + returned: success + type: bool +vnic_type: + description: Type of the created port + returned: success + type: str +port_security_enabled: + description: Port security state on the network. + returned: success + type: bool +binding:profile: + description: Port binded profile + returned: success + type: dict +''' + +from ansible.module_utils.basic import missing_required_lib +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + +try: + from collections import OrderedDict + HAS_ORDEREDDICT = True +except ImportError: + try: + from ordereddict import OrderedDict + HAS_ORDEREDDICT = True + except ImportError: + HAS_ORDEREDDICT = False + + +class NetworkPortModule(OpenStackModule): + argument_spec = dict( + network=dict(required=False), + name=dict(required=False), + fixed_ips=dict(type='list', default=None, elements='dict'), + admin_state_up=dict(type='bool', default=None), + mac_address=dict(default=None), + security_groups=dict(default=None, type='list', elements='str'), + no_security_groups=dict(default=False, type='bool'), + allowed_address_pairs=dict(type='list', default=None, elements='dict'), + extra_dhcp_opts=dict(type='list', default=None, elements='dict'), + device_owner=dict(default=None), + device_id=dict(default=None), + state=dict(default='present', choices=['absent', 'present']), + vnic_type=dict(default=None, + choices=['normal', 'direct', 'direct-physical', + 'macvtap', 'baremetal', 'virtio-forwarder']), + port_security_enabled=dict(default=None, type='bool'), + binding_profile=dict(default=None, type='dict'), + dns_name=dict(type='str', default=None), + dns_domain=dict(type='str', default=None) + ) + + module_kwargs = dict( + mutually_exclusive=[ + ['no_security_groups', 'security_groups'], + ], + supports_check_mode=True + ) + + def _is_dns_integration_enabled(self): + """ Check if dns-integraton is enabled """ + for ext in self.conn.network.extensions(): + if ext.alias == 'dns-integration': + return True + return False + + def _needs_update(self, port): + """Check for differences in the updatable values. + + NOTE: We don't currently allow name updates. + """ + compare_simple = ['admin_state_up', + 'mac_address', + 'device_owner', + 'device_id', + 'binding:vnic_type', + 'port_security_enabled', + 'binding:profile'] + compare_dns = ['dns_name', 'dns_domain'] + compare_list_dict = ['allowed_address_pairs', + 'extra_dhcp_opts'] + compare_list = ['security_groups'] + + if self.conn.has_service('dns') and \ + self._is_dns_integration_enabled(): + for key in compare_dns: + if self.params[key] is not None and \ + self.params[key] != port[key]: + return True + + for key in compare_simple: + if self.params[key] is not None and self.params[key] != port[key]: + return True + for key in compare_list: + if ( + self.params[key] is not None + and set(self.params[key]) != set(port[key]) + ): + return True + + for key in compare_list_dict: + if not self.params[key]: + if port.get(key): + return True + + if self.params[key]: + if not port.get(key): + return True + + # sort dicts in list + port_ordered = [OrderedDict(sorted(d.items())) for d in port[key]] + param_ordered = [OrderedDict(sorted(d.items())) for d in self.params[key]] + + for d in param_ordered: + if d not in port_ordered: + return True + + for d in port_ordered: + if d not in param_ordered: + return True + + # NOTE: if port was created or updated with 'no_security_groups=True', + # subsequent updates without 'no_security_groups' flag or + # 'no_security_groups=False' and no specified 'security_groups', will not + # result in an update to the port where the default security group is + # applied. + if self.params['no_security_groups'] and port['security_groups'] != []: + return True + + if self.params['fixed_ips'] is not None: + for item in self.params['fixed_ips']: + if 'ip_address' in item: + # if ip_address in request does not match any in existing port, + # update is required. + if not any(match['ip_address'] == item['ip_address'] + for match in port['fixed_ips']): + return True + if 'subnet_id' in item: + return True + for item in port['fixed_ips']: + # if ip_address in existing port does not match any in request, + # update is required. + if not any(match.get('ip_address') == item['ip_address'] + for match in self.params['fixed_ips']): + return True + + return False + + def _system_state_change(self, port): + state = self.params['state'] + if state == 'present': + if not port: + return True + return self._needs_update(port) + if state == 'absent' and port: + return True + return False + + def _compose_port_args(self): + port_kwargs = {} + optional_parameters = ['name', + 'fixed_ips', + 'admin_state_up', + 'mac_address', + 'security_groups', + 'allowed_address_pairs', + 'extra_dhcp_opts', + 'device_owner', + 'device_id', + 'binding:vnic_type', + 'port_security_enabled', + 'binding:profile'] + + if self.conn.has_service('dns') and \ + self._is_dns_integration_enabled(): + optional_parameters.extend(['dns_name', 'dns_domain']) + + for optional_param in optional_parameters: + if self.params[optional_param] is not None: + port_kwargs[optional_param] = self.params[optional_param] + + if self.params['no_security_groups']: + port_kwargs['security_groups'] = [] + + return port_kwargs + + def get_security_group_id(self, security_group_name_or_id): + security_group = self.conn.get_security_group(security_group_name_or_id) + if not security_group: + self.fail_json(msg="Security group: %s, was not found" + % security_group_name_or_id) + return security_group['id'] + + def run(self): + if not HAS_ORDEREDDICT: + self.fail_json(msg=missing_required_lib('ordereddict')) + + name = self.params['name'] + state = self.params['state'] + + if self.params['security_groups']: + # translate security_groups to UUID's if names where provided + self.params['security_groups'] = [ + self.get_security_group_id(v) + for v in self.params['security_groups'] + ] + + # Neutron API accept 'binding:vnic_type' as an argument + # for the port type. + self.params['binding:vnic_type'] = self.params.pop('vnic_type') + # Neutron API accept 'binding:profile' as an argument + # for the port binding profile type. + self.params['binding:profile'] = self.params.pop('binding_profile') + + port = None + network_id = None + if name: + port = self.conn.get_port(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(port)) + + changed = False + if state == 'present': + if not port: + network = self.params['network'] + if not network: + self.fail_json( + msg="Parameter 'network' is required in Port Create" + ) + port_kwargs = self._compose_port_args() + network_object = self.conn.get_network(network) + + if network_object: + network_id = network_object['id'] + else: + self.fail_json( + msg="Specified network was not found." + ) + + port_kwargs['network_id'] = network_id + port = self.conn.network.create_port(**port_kwargs) + changed = True + else: + if self._needs_update(port): + port_kwargs = self._compose_port_args() + port = self.conn.network.update_port(port['id'], + **port_kwargs) + changed = True + self.exit_json(changed=changed, id=port['id'], port=port) + + if state == 'absent': + if port: + self.conn.delete_port(port['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = NetworkPortModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_port_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_port_info.py new file mode 100644 index 00000000..0ed3f059 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_port_info.py @@ -0,0 +1,210 @@ +#!/usr/bin/python + +# Copyright (c) 2016 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +module: port_info +short_description: Retrieve information about ports within OpenStack. +author: OpenStack Ansible SIG +description: + - Retrieve information about ports from OpenStack. + - This module was called C(openstack.cloud.port_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.port_info) module no longer returns C(ansible_facts)! +options: + port: + description: + - Unique name or ID of a port. + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements + of this dictionary will be matched against the returned port + dictionaries. Matching is currently limited to strings within + the port dictionary, or strings within nested dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about all ports +- openstack.cloud.port_info: + cloud: mycloud + register: result + +- debug: + msg: "{{ result.openstack_ports }}" + +# Gather information about a single port +- openstack.cloud.port_info: + cloud: mycloud + port: 6140317d-e676-31e1-8a4a-b1913814a471 + +# Gather information about all ports that have device_id set to a specific value +# and with a status of ACTIVE. +- openstack.cloud.port_info: + cloud: mycloud + filters: + device_id: 1038a010-3a37-4a9d-82ea-652f1da36597 + status: ACTIVE +''' + +RETURN = ''' +openstack_ports: + description: List of port dictionaries. A subset of the dictionary keys + listed below may be returned, depending on your cloud provider. + returned: always, but can be null + type: complex + contains: + admin_state_up: + description: The administrative state of the router, which is + up (true) or down (false). + returned: success + type: bool + sample: true + allowed_address_pairs: + description: A set of zero or more allowed address pairs. An + address pair consists of an IP address and MAC address. + returned: success + type: list + sample: [] + "binding:host_id": + description: The UUID of the host where the port is allocated. + returned: success + type: str + sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759" + "binding:profile": + description: A dictionary the enables the application running on + the host to pass and receive VIF port-specific + information to the plug-in. + returned: success + type: dict + sample: {} + "binding:vif_details": + description: A dictionary that enables the application to pass + information about functions that the Networking API + provides. + returned: success + type: dict + sample: {"port_filter": true} + "binding:vif_type": + description: The VIF type for the port. + returned: success + type: dict + sample: "ovs" + "binding:vnic_type": + description: The virtual network interface card (vNIC) type that is + bound to the neutron port. + returned: success + type: str + sample: "normal" + device_id: + description: The UUID of the device that uses this port. + returned: success + type: str + sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759" + device_owner: + description: The UUID of the entity that uses this port. + returned: success + type: str + sample: "network:router_interface" + dns_assignment: + description: DNS assignment information. + returned: success + type: list + dns_name: + description: DNS name + returned: success + type: str + sample: "" + extra_dhcp_opts: + description: A set of zero or more extra DHCP option pairs. + An option pair consists of an option value and name. + returned: success + type: list + sample: [] + fixed_ips: + description: The IP addresses for the port. Includes the IP address + and UUID of the subnet. + returned: success + type: list + id: + description: The UUID of the port. + returned: success + type: str + sample: "3ec25c97-7052-4ab8-a8ba-92faf84148de" + ip_address: + description: The IP address. + returned: success + type: str + sample: "127.0.0.1" + mac_address: + description: The MAC address. + returned: success + type: str + sample: "00:00:5E:00:53:42" + name: + description: The port name. + returned: success + type: str + sample: "port_name" + network_id: + description: The UUID of the attached network. + returned: success + type: str + sample: "dd1ede4f-3952-4131-aab6-3b8902268c7d" + port_security_enabled: + description: The port security status. The status is enabled (true) or disabled (false). + returned: success + type: bool + sample: false + security_groups: + description: The UUIDs of any attached security groups. + returned: success + type: list + status: + description: The port status. + returned: success + type: str + sample: "ACTIVE" + tenant_id: + description: The UUID of the tenant who owns the network. + returned: success + type: str + sample: "51fce036d7984ba6af4f6c849f65ef00" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class NetworkPortInfoModule(OpenStackModule): + argument_spec = dict( + port=dict(required=False), + filters=dict(type='dict', required=False), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + deprecated_names = ('openstack.cloud.port_facts') + + def run(self): + port = self.params.get('port') + filters = self.params.get('filters') + + ports = self.conn.search_ports(port, filters) + self.exit_json(changed=False, openstack_ports=ports) + + +def main(): + module = NetworkPortInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_project.py b/ansible_collections/openstack/cloud/plugins/modules/os_project.py new file mode 100644 index 00000000..9719452d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_project.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# Copyright (c) 2015 IBM Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: project +short_description: Manage OpenStack Projects +author: OpenStack Ansible SIG +description: + - Manage OpenStack Projects. Projects can be created, + updated or deleted using this module. A project will be updated + if I(name) matches an existing project and I(state) is present. + The value for I(name) cannot be updated without deleting and + re-creating the project. +options: + name: + description: + - Name for the project + required: true + type: str + description: + description: + - Description for the project + type: str + domain_id: + description: + - Domain id to create the project in if the cloud supports domains. + aliases: ['domain'] + type: str + enabled: + description: + - Is the project enabled + type: bool + default: 'yes' + properties: + description: + - Additional properties to be associated with this project. Requires + openstacksdk>0.45. + type: dict + required: false + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a project +- openstack.cloud.project: + cloud: mycloud + endpoint_type: admin + state: present + name: demoproject + description: demodescription + domain_id: demoid + enabled: True + properties: + internal_alias: demo_project + +# Delete a project +- openstack.cloud.project: + cloud: mycloud + endpoint_type: admin + state: absent + name: demoproject +''' + + +RETURN = ''' +project: + description: Dictionary describing the project. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Project ID + type: str + sample: "f59382db809c43139982ca4189404650" + name: + description: Project name + type: str + sample: "demoproject" + description: + description: Project description + type: str + sample: "demodescription" + enabled: + description: Boolean to indicate if project is enabled + type: bool + sample: True +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityProjectModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + description=dict(required=False), + domain_id=dict(required=False, aliases=['domain']), + properties=dict(required=False, type='dict', min_ver='0.45.1'), + enabled=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']) + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, project): + keys = ('description', 'enabled') + for key in keys: + if self.params[key] is not None and self.params[key] != project.get(key): + return True + + properties = self.params['properties'] + if properties: + project_properties = project.get('properties') + for k, v in properties.items(): + if v is not None and (k not in project_properties or v != project_properties[k]): + return True + + return False + + def _system_state_change(self, project): + state = self.params['state'] + if state == 'present': + if project is None: + changed = True + else: + if self._needs_update(project): + changed = True + else: + changed = False + + elif state == 'absent': + changed = project is not None + + return changed + + def run(self): + name = self.params['name'] + description = self.params['description'] + domain = self.params['domain_id'] + enabled = self.params['enabled'] + properties = self.params['properties'] or {} + state = self.params['state'] + + if domain: + try: + # We assume admin is passing domain id + dom = self.conn.get_domain(domain)['id'] + domain = dom + except Exception: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + try: + dom = self.conn.search_domains(filters={'name': domain})[0]['id'] + domain = dom + except Exception: + # Ok, let's hope the user is non-admin and passing a sane id + pass + + if domain: + project = self.conn.get_project(name, domain_id=domain) + else: + project = self.conn.get_project(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(project)) + + if state == 'present': + if project is None: + project = self.conn.create_project( + name=name, description=description, + domain_id=domain, + enabled=enabled) + changed = True + + project = self.conn.update_project( + project['id'], + description=description, + enabled=enabled, + **properties) + else: + if self._needs_update(project): + project = self.conn.update_project( + project['id'], + description=description, + enabled=enabled, + **properties) + changed = True + else: + changed = False + self.exit_json(changed=changed, project=project) + + elif state == 'absent': + if project is None: + changed = False + else: + self.conn.delete_project(project['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityProjectModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_project_access.py b/ansible_collections/openstack/cloud/plugins/modules/os_project_access.py new file mode 100644 index 00000000..c49a8449 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_project_access.py @@ -0,0 +1,193 @@ +#!/usr/bin/python + +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see <http://www.gnu.org/licenses/>. + +DOCUMENTATION = ''' +--- +module: project_access +short_description: Manage OpenStack compute flavors access +author: OpenStack Ansible SIG +description: + - Add or remove flavor, volume_type or other resources access + from OpenStack. +options: + state: + description: + - Indicate desired state of the resource. + choices: ['present', 'absent'] + required: false + default: present + type: str + target_project_id: + description: + - Project id. + required: true + type: str + resource_type: + description: + - The resource type (eg. nova_flavor, cinder_volume_type). + required: true + type: str + resource_name: + description: + - The resource name (eg. tiny). + required: true + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: "Enable access to tiny flavor to your tenant." + openstack.cloud.project_access: + cloud: mycloud + state: present + target_project_id: f0f1f2f3f4f5f67f8f9e0e1 + resource_name: tiny + resource_type: nova_flavor + + +- name: "Disable access to the given flavor to project" + openstack.cloud.project_access: + cloud: mycloud + state: absent + target_project_id: f0f1f2f3f4f5f67f8f9e0e1 + resource_name: tiny + resource_type: nova_flavor +''' + +RETURN = ''' +flavor: + description: Dictionary describing the flavor. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Flavor ID. + returned: success + type: str + sample: "515256b8-7027-4d73-aa54-4e30a4a4a339" + name: + description: Flavor name. + returned: success + type: str + sample: "tiny" + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityProjectAccess(OpenStackModule): + argument_spec = dict( + state=dict(required=False, default='present', + choices=['absent', 'present']), + target_project_id=dict(required=True, type='str'), + resource_type=dict(required=True, type='str'), + resource_name=dict(required=True, type='str'), + ) + + module_kwargs = dict( + supports_check_mode=True, + required_if=[ + ('state', 'present', ['target_project_id']) + ] + ) + + def run(self): + state = self.params['state'] + resource_name = self.params['resource_name'] + resource_type = self.params['resource_type'] + target_project_id = self.params['target_project_id'] + + if resource_type == 'nova_flavor': + # returns Munch({'NAME_ATTR': 'name', + # 'tenant_id': u'37e55da59ec842649d84230f3a24eed5', + # 'HUMAN_ID': False, + # 'flavor_id': u'6d4d37b9-0480-4a8c-b8c9-f77deaad73f9', + # 'request_ids': [], 'human_id': None}), + _get_resource = self.conn.get_flavor + _list_resource_access = self.conn.list_flavor_access + _add_resource_access = self.conn.add_flavor_access + _remove_resource_access = self.conn.remove_flavor_access + elif resource_type == 'cinder_volume_type': + # returns [Munch({ + # 'project_id': u'178cdb9955b047eea7afbe582038dc94', + # 'properties': {'request_ids': [], 'NAME_ATTR': 'name', + # 'human_id': None, + # 'HUMAN_ID': False}, + # 'id': u'd5573023-b290-42c8-b232-7c5ca493667f'}), + _get_resource = self.conn.get_volume_type + _list_resource_access = self.conn.get_volume_type_access + _add_resource_access = self.conn.add_volume_type_access + _remove_resource_access = self.conn.remove_volume_type_access + else: + self.exit_json( + changed=False, + resource_name=resource_name, + resource_type=resource_type, + error="Not implemented.") + + resource = _get_resource(resource_name) + if not resource: + self.exit_json( + changed=False, + resource_name=resource_name, + resource_type=resource_type, + error="Not found.") + resource_id = getattr(resource, 'id', resource['id']) + # _list_resource_access returns a list of dicts containing 'project_id' + acls = _list_resource_access(resource_id) + + if not all(acl.get('project_id') for acl in acls): + self.exit_json( + changed=False, + resource_name=resource_name, + resource_type=resource_type, + error="Missing project_id in resource output.") + allowed_tenants = [acl['project_id'] for acl in acls] + + changed_access = any(( + state == 'present' and target_project_id not in allowed_tenants, + state == 'absent' and target_project_id in allowed_tenants + )) + if self.ansible.check_mode or not changed_access: + self.exit_json( + changed=changed_access, resource=resource, id=resource_id) + + if state == 'present': + _add_resource_access( + resource_id, target_project_id + ) + elif state == 'absent': + _remove_resource_access( + resource_id, target_project_id + ) + + self.exit_json( + changed=True, resource=resource, id=resource_id) + + +def main(): + module = IdentityProjectAccess() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_project_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_project_info.py new file mode 100644 index 00000000..fb1e2767 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_project_info.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: project_info +short_description: Retrieve information about one or more OpenStack projects +author: OpenStack Ansible SIG +description: + - Retrieve information about a one or more OpenStack projects + - This module was called C(openstack.cloud.project_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.project_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the project + type: str + domain: + description: + - Name or ID of the domain containing the project if the cloud supports domains + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about previously created projects +- openstack.cloud.project_info: + cloud: awesomecloud + register: result +- debug: + msg: "{{ result.openstack_projects }}" + +# Gather information about a previously created project by name +- openstack.cloud.project_info: + cloud: awesomecloud + name: demoproject + register: result +- debug: + msg: "{{ result.openstack_projects }}" + +# Gather information about a previously created project in a specific domain +- openstack.cloud.project_info: + cloud: awesomecloud + name: demoproject + domain: admindomain + register: result +- debug: + msg: "{{ result.openstack_projects }}" + +# Gather information about a previously created project in a specific domain with filter +- openstack.cloud.project_info: + cloud: awesomecloud + name: demoproject + domain: admindomain + filters: + enabled: False + register: result +- debug: + msg: "{{ result.openstack_projects }}" +''' + + +RETURN = ''' +openstack_projects: + description: has all the OpenStack information about projects + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the project. + returned: success + type: str + description: + description: Description of the project + returned: success + type: str + enabled: + description: Flag to indicate if the project is enabled + returned: success + type: bool + domain_id: + description: Domain ID containing the project (keystone v3 clouds only) + returned: success + type: bool +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityProjectInfoModule(OpenStackModule): + deprecated_names = ('project_facts', 'openstack.cloud.project_facts') + + argument_spec = dict( + name=dict(required=False), + domain=dict(required=False), + filters=dict(required=False, type='dict'), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + name = self.params['name'] + domain = self.params['domain'] + filters = self.params['filters'] + is_old_facts = self.module_name == 'openstack.cloud.project_facts' + + if domain: + try: + # We assume admin is passing domain id + dom = self.conn.get_domain(domain)['id'] + domain = dom + except Exception: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + dom = self.conn.search_domains(filters={'name': domain}) + if dom: + domain = dom[0]['id'] + else: + self.fail_json(msg='Domain name or ID does not exist') + + if not filters: + filters = {} + + filters['domain_id'] = domain + + projects = self.conn.search_projects(name, filters) + if is_old_facts: + self.exit_json(changed=False, ansible_facts=dict( + openstack_projects=projects)) + else: + self.exit_json(changed=False, openstack_projects=projects) + + +def main(): + module = IdentityProjectInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_quota.py b/ansible_collections/openstack/cloud/plugins/modules/os_quota.py new file mode 100644 index 00000000..0d6a4f04 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_quota.py @@ -0,0 +1,466 @@ +#!/usr/bin/python +# Copyright (c) 2016 Pason System Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: quota +short_description: Manage OpenStack Quotas +author: OpenStack Ansible SIG +description: + - Manage OpenStack Quotas. Quotas can be created, + updated or deleted using this module. A quota will be updated + if matches an existing project and is present. +options: + name: + description: + - Name of the OpenStack Project to manage. + required: true + type: str + state: + description: + - A value of present sets the quota and a value of absent resets the quota to system defaults. + default: present + type: str + choices: ['absent', 'present'] + backup_gigabytes: + description: Maximum size of backups in GB's. + type: int + backups: + description: Maximum number of backups allowed. + type: int + cores: + description: Maximum number of CPU's per project. + type: int + fixed_ips: + description: Number of fixed IP's to allow. + type: int + floating_ips: + description: Number of floating IP's to allow in Compute. + aliases: ['compute_floating_ips'] + type: int + floatingip: + description: Number of floating IP's to allow in Network. + aliases: ['network_floating_ips'] + type: int + gigabytes: + description: Maximum volume storage allowed for project. + type: int + gigabytes_types: + description: + - Per driver volume storage quotas. Keys should be + prefixed with C(gigabytes_) values should be ints. + type: dict + injected_file_size: + description: Maximum file size in bytes. + type: int + injected_files: + description: Number of injected files to allow. + type: int + injected_path_size: + description: Maximum path size. + type: int + instances: + description: Maximum number of instances allowed. + type: int + key_pairs: + description: Number of key pairs to allow. + type: int + loadbalancer: + description: Number of load balancers to allow. + type: int + metadata_items: + description: Number of metadata items allowed per instance. + type: int + network: + description: Number of networks to allow. + type: int + per_volume_gigabytes: + description: Maximum size in GB's of individual volumes. + type: int + pool: + description: Number of load balancer pools to allow. + type: int + port: + description: Number of Network ports to allow, this needs to be greater than the instances limit. + type: int + properties: + description: Number of properties to allow. + type: int + ram: + description: Maximum amount of ram in MB to allow. + type: int + rbac_policy: + description: Number of policies to allow. + type: int + router: + description: Number of routers to allow. + type: int + security_group_rule: + description: Number of rules per security group to allow. + type: int + security_group: + description: Number of security groups to allow. + type: int + server_group_members: + description: Number of server group members to allow. + type: int + server_groups: + description: Number of server groups to allow. + type: int + snapshots: + description: Number of snapshots to allow. + type: int + snapshots_types: + description: + - Per-driver volume snapshot quotas. Keys should be + prefixed with C(snapshots_) values should be ints. + type: dict + subnet: + description: Number of subnets to allow. + type: int + subnetpool: + description: Number of subnet pools to allow. + type: int + volumes: + description: Number of volumes to allow. + type: int + volumes_types: + description: + - Per-driver volume count quotas. Keys should be + prefixed with C(volumes_) values should be ints. + type: dict + project: + description: Unused, kept for compatability + type: int + +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.13.0" + - "keystoneauth1 >= 3.4.0" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# List a Project Quota +- openstack.cloud.quota: + cloud: mycloud + name: demoproject + +# Set a Project back to the defaults +- openstack.cloud.quota: + cloud: mycloud + name: demoproject + state: absent + +# Update a Project Quota for cores +- openstack.cloud.quota: + cloud: mycloud + name: demoproject + cores: 100 + +# Update a Project Quota +- openstack.cloud.quota: + name: demoproject + cores: 1000 + volumes: 20 + volumes_type: + - volume_lvm: 10 + +# Complete example based on list of projects +- name: Update quotas + openstack.cloud.quota: + name: "{{ item.name }}" + backup_gigabytes: "{{ item.backup_gigabytes }}" + backups: "{{ item.backups }}" + cores: "{{ item.cores }}" + fixed_ips: "{{ item.fixed_ips }}" + floating_ips: "{{ item.floating_ips }}" + floatingip: "{{ item.floatingip }}" + gigabytes: "{{ item.gigabytes }}" + injected_file_size: "{{ item.injected_file_size }}" + injected_files: "{{ item.injected_files }}" + injected_path_size: "{{ item.injected_path_size }}" + instances: "{{ item.instances }}" + key_pairs: "{{ item.key_pairs }}" + loadbalancer: "{{ item.loadbalancer }}" + metadata_items: "{{ item.metadata_items }}" + per_volume_gigabytes: "{{ item.per_volume_gigabytes }}" + pool: "{{ item.pool }}" + port: "{{ item.port }}" + properties: "{{ item.properties }}" + ram: "{{ item.ram }}" + security_group_rule: "{{ item.security_group_rule }}" + security_group: "{{ item.security_group }}" + server_group_members: "{{ item.server_group_members }}" + server_groups: "{{ item.server_groups }}" + snapshots: "{{ item.snapshots }}" + volumes: "{{ item.volumes }}" + volumes_types: + volumes_lvm: "{{ item.volumes_lvm }}" + snapshots_types: + snapshots_lvm: "{{ item.snapshots_lvm }}" + gigabytes_types: + gigabytes_lvm: "{{ item.gigabytes_lvm }}" + with_items: + - "{{ projects }}" + when: item.state == "present" +''' + +RETURN = ''' +openstack_quotas: + description: Dictionary describing the project quota. + returned: Regardless if changes where made or not + type: dict + sample: + openstack_quotas: { + compute: { + cores: 150, + fixed_ips: -1, + floating_ips: 10, + injected_file_content_bytes: 10240, + injected_file_path_bytes: 255, + injected_files: 5, + instances: 100, + key_pairs: 100, + metadata_items: 128, + ram: 153600, + security_group_rules: 20, + security_groups: 10, + server_group_members: 10, + server_groups: 10 + }, + network: { + floatingip: 50, + loadbalancer: 10, + network: 10, + pool: 10, + port: 160, + rbac_policy: 10, + router: 10, + security_group: 10, + security_group_rule: 100, + subnet: 10, + subnetpool: -1 + }, + volume: { + backup_gigabytes: 1000, + backups: 10, + gigabytes: 1000, + gigabytes_lvm: -1, + per_volume_gigabytes: -1, + snapshots: 10, + snapshots_lvm: -1, + volumes: 10, + volumes_lvm: -1 + } + } + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class QuotaModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + backup_gigabytes=dict(required=False, type='int', default=None), + backups=dict(required=False, type='int', default=None), + cores=dict(required=False, type='int', default=None), + fixed_ips=dict(required=False, type='int', default=None), + floating_ips=dict(required=False, type='int', default=None, aliases=['compute_floating_ips']), + floatingip=dict(required=False, type='int', default=None, aliases=['network_floating_ips']), + gigabytes=dict(required=False, type='int', default=None), + gigabytes_types=dict(required=False, type='dict', default={}), + injected_file_size=dict(required=False, type='int', default=None), + injected_files=dict(required=False, type='int', default=None), + injected_path_size=dict(required=False, type='int', default=None), + instances=dict(required=False, type='int', default=None), + key_pairs=dict(required=False, type='int', default=None, no_log=False), + loadbalancer=dict(required=False, type='int', default=None), + metadata_items=dict(required=False, type='int', default=None), + network=dict(required=False, type='int', default=None), + per_volume_gigabytes=dict(required=False, type='int', default=None), + pool=dict(required=False, type='int', default=None), + port=dict(required=False, type='int', default=None), + project=dict(required=False, type='int', default=None), + properties=dict(required=False, type='int', default=None), + ram=dict(required=False, type='int', default=None), + rbac_policy=dict(required=False, type='int', default=None), + router=dict(required=False, type='int', default=None), + security_group_rule=dict(required=False, type='int', default=None), + security_group=dict(required=False, type='int', default=None), + server_group_members=dict(required=False, type='int', default=None), + server_groups=dict(required=False, type='int', default=None), + snapshots=dict(required=False, type='int', default=None), + snapshots_types=dict(required=False, type='dict', default={}), + subnet=dict(required=False, type='int', default=None), + subnetpool=dict(required=False, type='int', default=None), + volumes=dict(required=False, type='int', default=None), + volumes_types=dict(required=False, type='dict', default={}) + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _get_volume_quotas(self, project): + return self.conn.get_volume_quotas(project) + + def _get_network_quotas(self, project): + return self.conn.get_network_quotas(project) + + def _get_compute_quotas(self, project): + return self.conn.get_compute_quotas(project) + + def _get_quotas(self, project): + quota = {} + try: + quota['volume'] = self._get_volume_quotas(project) + except Exception: + self.warn("No public endpoint for volumev2 service was found. Ignoring volume quotas.") + + try: + quota['network'] = self._get_network_quotas(project) + except Exception: + self.warn("No public endpoint for network service was found. Ignoring network quotas.") + + quota['compute'] = self._get_compute_quotas(project) + + for quota_type in quota.keys(): + quota[quota_type] = self._scrub_results(quota[quota_type]) + + return quota + + def _scrub_results(self, quota): + filter_attr = [ + 'HUMAN_ID', + 'NAME_ATTR', + 'human_id', + 'request_ids', + 'x_openstack_request_ids', + ] + + for attr in filter_attr: + if attr in quota: + del quota[attr] + + return quota + + def _system_state_change_details(self, project_quota_output): + quota_change_request = {} + changes_required = False + + for quota_type in project_quota_output.keys(): + for quota_option in project_quota_output[quota_type].keys(): + if quota_option in self.params and self.params[quota_option] is not None: + if project_quota_output[quota_type][quota_option] != self.params[quota_option]: + changes_required = True + + if quota_type not in quota_change_request: + quota_change_request[quota_type] = {} + + quota_change_request[quota_type][quota_option] = self.params[quota_option] + + return (changes_required, quota_change_request) + + def _system_state_change(self, project_quota_output): + """ + Determine if changes are required to the current project quota. + + This is done by comparing the current project_quota_output against + the desired quota settings set on the module params. + """ + + changes_required, quota_change_request = self._system_state_change_details( + project_quota_output + ) + + if changes_required: + return True + else: + return False + + def run(self): + cloud_params = dict(self.params) + + # In order to handle the different volume types we update module params after. + dynamic_types = [ + 'gigabytes_types', + 'snapshots_types', + 'volumes_types', + ] + + for dynamic_type in dynamic_types: + for k, v in self.params[dynamic_type].items(): + self.params[k] = int(v) + + # Get current quota values + project_quota_output = self._get_quotas(cloud_params['name']) + changes_required = False + + if self.params['state'] == "absent": + # If a quota state is set to absent we should assume there will be changes. + # The default quota values are not accessible so we can not determine if + # no changes will occur or not. + if self.ansible.check_mode: + self.exit_json(changed=True) + + # Calling delete_network_quotas when a quota has not been set results + # in an error, according to the sdk docs it should return the + # current quota. + # The following error string is returned: + # network client call failed: Quota for tenant 69dd91d217e949f1a0b35a4b901741dc could not be found. + neutron_msg1 = "network client call failed: Quota for tenant" + neutron_msg2 = "could not be found" + + for quota_type in project_quota_output.keys(): + quota_call = getattr(self.conn, 'delete_%s_quotas' % (quota_type)) + try: + quota_call(cloud_params['name']) + except Exception as e: + error_msg = str(e) + if error_msg.find(neutron_msg1) > -1 and error_msg.find(neutron_msg2) > -1: + pass + else: + self.fail_json(msg=str(e), extra_data=e.extra_data) + + project_quota_output = self._get_quotas(cloud_params['name']) + changes_required = True + + elif self.params['state'] == "present": + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change( + project_quota_output)) + + changes_required, quota_change_request = self._system_state_change_details( + project_quota_output + ) + + if changes_required: + for quota_type in quota_change_request.keys(): + quota_call = getattr(self.conn, 'set_%s_quotas' % (quota_type)) + quota_call(cloud_params['name'], **quota_change_request[quota_type]) + + # Get quota state post changes for validation + project_quota_update = self._get_quotas(cloud_params['name']) + + if project_quota_output == project_quota_update: + self.fail_json(msg='Could not apply quota update') + + project_quota_output = project_quota_update + + self.exit_json( + changed=changes_required, openstack_quotas=project_quota_output) + + +def main(): + module = QuotaModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_recordset.py b/ansible_collections/openstack/cloud/plugins/modules/os_recordset.py new file mode 100644 index 00000000..921d6efa --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_recordset.py @@ -0,0 +1,260 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: recordset +short_description: Manage OpenStack DNS recordsets +author: OpenStack Ansible SIG +description: + - Manage OpenStack DNS recordsets. Recordsets can be created, deleted or + updated. Only the I(records), I(description), and I(ttl) values + can be updated. +options: + description: + description: + - Description of the recordset + type: str + name: + description: + - Name of the recordset. It must be ended with name of dns zone. + required: true + type: str + records: + description: + - List of recordset definitions. + - Required when I(state=present). + type: list + elements: str + recordset_type: + description: + - Recordset type + - Required when I(state=present). + choices: ['a', 'aaaa', 'mx', 'cname', 'txt', 'ns', 'srv', 'ptr', 'caa'] + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + ttl: + description: + - TTL (Time To Live) value in seconds + type: int + zone: + description: + - Name or ID of the zone which manages the recordset + required: true + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a recordset named "www.example.net." +- openstack.cloud.recordset: + cloud: mycloud + state: present + zone: example.net. + name: www.example.net. + recordset_type: "a" + records: ['10.1.1.1'] + description: test recordset + ttl: 3600 + +# Update the TTL on existing "www.example.net." recordset +- openstack.cloud.recordset: + cloud: mycloud + state: present + zone: example.net. + name: www.example.net. + recordset_type: "a" + records: ['10.1.1.1'] + ttl: 7200 + +# Delete recordset named "www.example.net." +- openstack.cloud.recordset: + cloud: mycloud + state: absent + zone: example.net. + name: www.example.net. +''' + +RETURN = ''' +recordset: + description: Dictionary describing the recordset. + returned: On success when I(state) is 'present'. + type: dict + contains: + action: + description: Current action in progress on the resource + type: str + returned: always + created_at: + description: Timestamp when the zone was created + type: str + returned: always + description: + description: Recordset description + type: str + sample: "Test description" + returned: always + id: + description: Unique recordset ID + type: str + sample: "c1c530a3-3619-46f3-b0f6-236927b2618c" + links: + description: Links related to the resource + type: dict + returned: always + name: + description: Recordset name + type: str + sample: "www.example.net." + returned: always + project_id: + description: ID of the proect to which the recordset belongs + type: str + returned: always + records: + description: Recordset records + type: list + sample: ['10.0.0.1'] + returned: always + status: + description: + - Recordset status + - Valid values include `PENDING_CREATE`, `ACTIVE`,`PENDING_DELETE`, + `ERROR` + type: str + returned: always + ttl: + description: Zone TTL value + type: int + sample: 3600 + returned: always + type: + description: + - Recordset type + - Valid values include `A`, `AAAA`, `MX`, `CNAME`, `TXT`, `NS`, + `SSHFP`, `SPF`, `SRV`, `PTR` + type: str + sample: "A" + returned: always + zone_id: + description: The id of the Zone which this recordset belongs to + type: str + sample: 9508e177-41d8-434e-962c-6fe6ca880af7 + returned: always + zone_name: + description: The name of the Zone which this recordset belongs to + type: str + sample: "example.com." + returned: always +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class DnsRecordsetModule(OpenStackModule): + argument_spec = dict( + description=dict(required=False, default=None), + name=dict(required=True), + records=dict(required=False, type='list', elements='str'), + recordset_type=dict(required=False, choices=['a', 'aaaa', 'mx', 'cname', 'txt', 'ns', 'srv', 'ptr', 'caa']), + state=dict(default='present', choices=['absent', 'present']), + ttl=dict(required=False, type='int'), + zone=dict(required=True), + ) + + module_kwargs = dict( + required_if=[ + ('state', 'present', + ['recordset_type', 'records'])], + supports_check_mode=True + ) + + module_min_sdk_version = '0.28.0' + + def _needs_update(self, params, recordset): + for k in ('description', 'records', 'ttl'): + if k not in params: + continue + if params[k] is not None and params[k] != recordset[k]: + return True + return False + + def _system_state_change(self, state, recordset): + if state == 'present': + if recordset is None: + return True + kwargs = self._build_params() + return self._needs_update(kwargs, recordset) + if state == 'absent' and recordset: + return True + return False + + def _build_params(self): + recordset_type = self.params['recordset_type'] + records = self.params['records'] + description = self.params['description'] + ttl = self.params['ttl'] + params = { + 'description': description, + 'records': records, + 'type': recordset_type.upper(), + 'ttl': ttl, + } + return {k: v for k, v in params.items() if v is not None} + + def run(self): + zone = self.params.get('zone') + name = self.params.get('name') + state = self.params.get('state') + ttl = self.params.get('ttl') + + recordsets = self.conn.search_recordsets(zone, name_or_id=name) + + recordset = None + if recordsets: + recordset = recordsets[0] + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, recordset)) + + changed = False + if state == 'present': + kwargs = self._build_params() + if recordset is None: + kwargs['ttl'] = ttl or 300 + type = kwargs.pop('type', None) + if type is not None: + kwargs['recordset_type'] = type + recordset = self.conn.create_recordset(zone=zone, name=name, + **kwargs) + changed = True + elif self._needs_update(kwargs, recordset): + type = kwargs.pop('type', None) + recordset = self.conn.update_recordset(zone, recordset['id'], + **kwargs) + changed = True + self.exit_json(changed=changed, recordset=recordset) + elif state == 'absent' and recordset is not None: + self.conn.delete_recordset(zone, recordset['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = DnsRecordsetModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_router.py b/ansible_collections/openstack/cloud/plugins/modules/os_router.py new file mode 100644 index 00000000..58c5c124 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_router.py @@ -0,0 +1,571 @@ +#!/usr/bin/python +# +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: router +short_description: Create or delete routers from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Delete routers from OpenStack. Although Neutron allows + routers to share the same name, this module enforces name uniqueness + to be more user friendly. +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Name to be give to the router + required: true + type: str + admin_state_up: + description: + - Desired admin state of the created or existing router. + type: bool + default: 'yes' + enable_snat: + description: + - Enable Source NAT (SNAT) attribute. + type: bool + network: + description: + - Unique name or ID of the external gateway network. + - required I(interfaces) or I(enable_snat) are provided. + type: str + project: + description: + - Unique name or ID of the project. + type: str + external_fixed_ips: + description: + - The IP address parameters for the external gateway network. Each + is a dictionary with the subnet name or ID (subnet) and the IP + address to assign on the subnet (ip). If no IP is specified, + one is automatically assigned from that subnet. + type: list + elements: dict + suboptions: + ip: + description: The fixed IP address to attempt to allocate. + required: true + type: str + subnet: + description: The subnet to attach the IP address to. + type: str + interfaces: + description: + - List of subnets to attach to the router internal interface. Default + gateway associated with the subnet will be automatically attached + with the router's internal interface. + In order to provide an ip address different from the default + gateway,parameters are passed as dictionary with keys as network + name or ID (I(net)), subnet name or ID (I(subnet)) and the IP of + port (I(portip)) from the network. + User defined portip is often required when a multiple router need + to be connected to a single subnet for which the default gateway has + been already used. + type: list + elements: raw +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a simple router, not attached to a gateway or subnets. +- openstack.cloud.router: + cloud: mycloud + state: present + name: simple_router + +# Create a simple router, not attached to a gateway or subnets for a given project. +- openstack.cloud.router: + cloud: mycloud + state: present + name: simple_router + project: myproj + +# Creates a router attached to ext_network1 on an IPv4 subnet and one +# internal subnet interface. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router1 + network: ext_network1 + external_fixed_ips: + - subnet: public-subnet + ip: 172.24.4.2 + interfaces: + - private-subnet + +# Create another router with two internal subnet interfaces.One with user defined port +# ip and another with default gateway. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router2 + network: ext_network1 + interfaces: + - net: private-net + subnet: private-subnet + portip: 10.1.1.10 + - project-subnet + +# Create another router with two internal subnet interface.One with user defined port +# ip and and another with default gateway. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router2 + network: ext_network1 + interfaces: + - net: private-net + subnet: private-subnet + portip: 10.1.1.10 + - project-subnet + +# Create another router with two internal subnet interface. one with user defined port +# ip and and another with default gateway. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router2 + network: ext_network1 + interfaces: + - net: private-net + subnet: private-subnet + portip: 10.1.1.10 + - project-subnet + +# Update existing router1 external gateway to include the IPv6 subnet. +# Note that since 'interfaces' is not provided, any existing internal +# interfaces on an existing router will be left intact. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router1 + network: ext_network1 + external_fixed_ips: + - subnet: public-subnet + ip: 172.24.4.2 + - subnet: ipv6-public-subnet + ip: 2001:db8::3 + +# Delete router1 +- openstack.cloud.router: + cloud: mycloud + state: absent + name: router1 +''' + +RETURN = ''' +router: + description: Dictionary describing the router. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Router ID. + type: str + sample: "474acfe5-be34-494c-b339-50f06aa143e4" + name: + description: Router name. + type: str + sample: "router1" + admin_state_up: + description: Administrative state of the router. + type: bool + sample: true + status: + description: The router status. + type: str + sample: "ACTIVE" + tenant_id: + description: The tenant ID. + type: str + sample: "861174b82b43463c9edc5202aadc60ef" + external_gateway_info: + description: The external gateway parameters. + type: dict + sample: { + "enable_snat": true, + "external_fixed_ips": [ + { + "ip_address": "10.6.6.99", + "subnet_id": "4272cb52-a456-4c20-8f3c-c26024ecfa81" + } + ] + } + routes: + description: The extra routes configuration for L3 router. + type: list +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule +import itertools + + +class RouterModule(OpenStackModule): + argument_spec = dict( + state=dict(default='present', choices=['absent', 'present']), + name=dict(required=True), + admin_state_up=dict(type='bool', default=True), + enable_snat=dict(type='bool'), + network=dict(default=None), + interfaces=dict(type='list', default=None, elements='raw'), + external_fixed_ips=dict(type='list', default=None, elements='dict'), + project=dict(default=None) + ) + + def _get_subnet_ids_from_ports(self, ports): + return [fixed_ip['subnet_id'] for fixed_ip in + itertools.chain.from_iterable(port['fixed_ips'] for port in ports if 'fixed_ips' in port)] + + def _needs_update(self, router, net, + missing_port_ids, + requested_subnet_ids, + existing_subnet_ids, + router_ifs_cfg): + """Decide if the given router needs an update.""" + if router['admin_state_up'] != self.params['admin_state_up']: + return True + if router['external_gateway_info']: + # check if enable_snat is set in module params + if self.params['enable_snat'] is not None: + if router['external_gateway_info'].get('enable_snat', True) != self.params['enable_snat']: + return True + if net: + if not router['external_gateway_info']: + return True + elif router['external_gateway_info']['network_id'] != net['id']: + return True + + # check if external_fixed_ip has to be added + for external_fixed_ip in router_ifs_cfg['external_fixed_ips']: + exists = False + + # compare the requested interface with existing, looking for an existing match + for existing_if in router['external_gateway_info']['external_fixed_ips']: + if existing_if['subnet_id'] == external_fixed_ip['subnet_id']: + if 'ip' in external_fixed_ip: + if existing_if['ip_address'] == external_fixed_ip['ip']: + # both subnet id and ip address match + exists = True + break + else: + # only the subnet was given, so ip doesn't matter + exists = True + break + + # this interface isn't present on the existing router + if not exists: + return True + + # check if external_fixed_ip has to be removed + if router_ifs_cfg['external_fixed_ips']: + for external_fixed_ip in router['external_gateway_info']['external_fixed_ips']: + obsolete = True + + # compare the existing interface with requested, looking for an requested match + for requested_if in router_ifs_cfg['external_fixed_ips']: + if external_fixed_ip['subnet_id'] == requested_if['subnet_id']: + if 'ip' in requested_if: + if external_fixed_ip['ip_address'] == requested_if['ip']: + # both subnet id and ip address match + obsolete = False + break + else: + # only the subnet was given, so ip doesn't matter + obsolete = False + break + + # this interface isn't present on the existing router + if obsolete: + return True + else: + # no external fixed ips requested + if router['external_gateway_info'] \ + and router['external_gateway_info']['external_fixed_ips'] \ + and len(router['external_gateway_info']['external_fixed_ips']) > 1: + # but router has several external fixed ips + return True + + # check if internal port has to be added + if router_ifs_cfg['internal_ports_missing']: + return True + + if missing_port_ids: + return True + + # check if internal subnet has to be added or removed + if set(requested_subnet_ids) != set(existing_subnet_ids): + return True + + return False + + def _build_kwargs(self, router, net): + kwargs = { + 'admin_state_up': self.params['admin_state_up'], + } + + if router: + kwargs['name_or_id'] = router['id'] + else: + kwargs['name'] = self.params['name'] + + if net: + kwargs['ext_gateway_net_id'] = net['id'] + # can't send enable_snat unless we have a network + if self.params.get('enable_snat') is not None: + kwargs['enable_snat'] = self.params['enable_snat'] + + if self.params['external_fixed_ips']: + kwargs['ext_fixed_ips'] = [] + for iface in self.params['external_fixed_ips']: + subnet = self.conn.get_subnet(iface['subnet']) + d = {'subnet_id': subnet['id']} + if 'ip' in iface: + d['ip_address'] = iface['ip'] + kwargs['ext_fixed_ips'].append(d) + else: + # no external fixed ips requested + if router \ + and router['external_gateway_info'] \ + and router['external_gateway_info']['external_fixed_ips'] \ + and len(router['external_gateway_info']['external_fixed_ips']) > 1: + # but router has several external fixed ips + # keep first external fixed ip only + fip = router['external_gateway_info']['external_fixed_ips'][0] + kwargs['ext_fixed_ips'] = [fip] + + return kwargs + + def _build_router_interface_config(self, filters=None): + external_fixed_ips = [] + internal_subnets = [] + internal_ports = [] + internal_ports_missing = [] + + # Build external interface configuration + if self.params['external_fixed_ips']: + for iface in self.params['external_fixed_ips']: + subnet = self.conn.get_subnet(iface['subnet'], filters) + if not subnet: + self.fail(msg='subnet %s not found' % iface['subnet']) + new_external_fixed_ip = {'subnet_name': subnet.name, 'subnet_id': subnet.id} + if 'ip' in iface: + new_external_fixed_ip['ip'] = iface['ip'] + external_fixed_ips.append(new_external_fixed_ip) + + # Build internal interface configuration + if self.params['interfaces']: + internal_ips = [] + for iface in self.params['interfaces']: + if isinstance(iface, str): + subnet = self.conn.get_subnet(iface, filters) + if not subnet: + self.fail(msg='subnet %s not found' % iface) + internal_subnets.append(subnet) + + elif isinstance(iface, dict): + subnet = self.conn.get_subnet(iface['subnet'], filters) + if not subnet: + self.fail(msg='subnet %s not found' % iface['subnet']) + + net = self.conn.get_network(iface['net']) + if not net: + self.fail(msg='net %s not found' % iface['net']) + + if "portip" not in iface: + # portip not set, add any ip from subnet + internal_subnets.append(subnet) + elif not iface['portip']: + # portip is set but has invalid value + self.fail(msg='put an ip in portip or remove it from list to assign default port to router') + else: + # portip has valid value + # look for ports whose fixed_ips.ip_address matchs portip + for existing_port in self.conn.list_ports(filters={'network_id': net.id}): + for fixed_ip in existing_port['fixed_ips']: + if iface['portip'] == fixed_ip['ip_address']: + # portip exists in net already + internal_ports.append(existing_port) + internal_ips.append(fixed_ip['ip_address']) + if iface['portip'] not in internal_ips: + # no port with portip exists hence create a new port + internal_ports_missing.append({ + 'network_id': net.id, + 'fixed_ips': [{'ip_address': iface['portip'], 'subnet_id': subnet.id}] + }) + + return { + 'external_fixed_ips': external_fixed_ips, + 'internal_subnets': internal_subnets, + 'internal_ports': internal_ports, + 'internal_ports_missing': internal_ports_missing + } + + def run(self): + + state = self.params['state'] + name = self.params['name'] + network = self.params['network'] + project = self.params['project'] + + if self.params['external_fixed_ips'] and not network: + self.fail(msg='network is required when supplying external_fixed_ips') + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail(msg='Project %s could not be found' % project) + project_id = proj['id'] + filters = {'tenant_id': project_id} + else: + project_id = None + filters = None + + router = self.conn.get_router(name, filters=filters) + net = None + if network: + net = self.conn.get_network(network) + if not net: + self.fail(msg='network %s not found' % network) + + # Validate and cache the subnet IDs so we can avoid duplicate checks + # and expensive API calls. + router_ifs_cfg = self._build_router_interface_config(filters) + requested_subnet_ids = [subnet.id for subnet in router_ifs_cfg['internal_subnets']] + \ + self._get_subnet_ids_from_ports(router_ifs_cfg['internal_ports']) + requested_port_ids = [i['id'] for i in router_ifs_cfg['internal_ports']] + + if router: + router_ifs_internal = self.conn.list_router_interfaces(router, 'internal') + existing_subnet_ids = self._get_subnet_ids_from_ports(router_ifs_internal) + obsolete_subnet_ids = set(existing_subnet_ids) - set(requested_subnet_ids) + existing_port_ids = [i['id'] for i in router_ifs_internal] + + else: + router_ifs_internal = [] + existing_subnet_ids = [] + obsolete_subnet_ids = [] + existing_port_ids = [] + + missing_port_ids = set(requested_port_ids) - set(existing_port_ids) + + if self.ansible.check_mode: + # Check if the system state would be changed + if state == 'absent' and router: + changed = True + elif state == 'absent' and not router: + changed = False + elif state == 'present' and not router: + changed = True + else: # if state == 'present' and router + changed = self._needs_update(router, net, + missing_port_ids, + requested_subnet_ids, + existing_subnet_ids, + router_ifs_cfg) + self.exit_json(changed=changed) + + if state == 'present': + changed = False + + if not router: + changed = True + + kwargs = self._build_kwargs(router, net) + if project_id: + kwargs['project_id'] = project_id + router = self.conn.create_router(**kwargs) + + # add interface by subnet id, because user did not specify a port id + for subnet in router_ifs_cfg['internal_subnets']: + self.conn.add_router_interface(router, subnet_id=subnet.id) + + # add interface by port id if user did specify a valid port id + for port in router_ifs_cfg['internal_ports']: + self.conn.add_router_interface(router, port_id=port.id) + + # add port and interface if user did specify an ip address but port is missing yet + for missing_internal_port in router_ifs_cfg['internal_ports_missing']: + p = self.conn.create_port(**missing_internal_port) + if p: + self.conn.add_router_interface(router, port_id=p.id) + + else: + if self._needs_update(router, net, + missing_port_ids, + requested_subnet_ids, + existing_subnet_ids, + router_ifs_cfg): + changed = True + kwargs = self._build_kwargs(router, net) + updated_router = self.conn.update_router(**kwargs) + + # Protect against update_router() not actually updating the router. + if not updated_router: + changed = False + else: + router = updated_router + + # delete internal subnets i.e. ports + if obsolete_subnet_ids: + for port in router_ifs_internal: + if 'fixed_ips' in port: + for fip in port['fixed_ips']: + if fip['subnet_id'] in obsolete_subnet_ids: + self.conn.remove_router_interface(router, port_id=port['id']) + changed = True + + # add new internal interface by subnet id, because user did not specify a port id + for subnet in router_ifs_cfg['internal_subnets']: + if subnet.id not in existing_subnet_ids: + self.conn.add_router_interface(router, subnet_id=subnet.id) + changed = True + + # add new internal interface by port id if user did specify a valid port id + for port_id in missing_port_ids: + self.conn.add_router_interface(router, port_id=port_id) + changed = True + + # add new port and new internal interface if user did specify an ip address but port is missing yet + for missing_internal_port in router_ifs_cfg['internal_ports_missing']: + p = self.conn.create_port(**missing_internal_port) + if p: + self.conn.add_router_interface(router, port_id=p.id) + changed = True + + self.exit_json(changed=changed, router=router) + + elif state == 'absent': + if not router: + self.exit_json(changed=False) + else: + # We need to detach all internal interfaces on a router + # before we will be allowed to delete it. Deletion can + # still fail if e.g. floating ips are attached to the + # router. + for port in router_ifs_internal: + self.conn.remove_router_interface(router, port_id=port['id']) + self.conn.delete_router(router['id']) + self.exit_json(changed=True, router=router) + + +def main(): + module = RouterModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_routers_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_routers_info.py new file mode 100644 index 00000000..990eef8d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_routers_info.py @@ -0,0 +1,194 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Bram Verschueren <verschueren.bram@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: routers_info +short_description: Retrieve information about one or more OpenStack routers. +author: OpenStack Ansible SIG +description: + - Retrieve information about one or more routers from OpenStack. +options: + name: + description: + - Name or ID of the router + required: false + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + type: dict + suboptions: + project_id: + description: + - Filter the list result by the ID of the project that owns the resource. + type: str + aliases: + - tenant_id + name: + description: + - Filter the list result by the human-readable name of the resource. + type: str + description: + description: + - Filter the list result by the human-readable description of the resource. + type: str + admin_state_up: + description: + - Filter the list result by the administrative state of the resource, which is up (true) or down (false). + type: bool + revision_number: + description: + - Filter the list result by the revision number of the resource. + type: int + tags: + description: + - A list of tags to filter the list result by. Resources that match all tags in this list will be returned. + type: list + elements: str +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Gather information about routers + openstack.cloud.routers_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + register: result + +- name: Show openstack routers + debug: + msg: "{{ result.openstack_routers }}" + +- name: Gather information about a router by name + openstack.cloud.routers_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + name: router1 + register: result + +- name: Show openstack routers + debug: + msg: "{{ result.openstack_routers }}" + +- name: Gather information about a router with filter + openstack.cloud.routers_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + filters: + tenant_id: bc3ea709c96849d6b81f54640400a19f + register: result + +- name: Show openstack routers + debug: + msg: "{{ result.openstack_routers }}" +''' + +RETURN = ''' +openstack_routers: + description: has all the openstack information about the routers + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the router. + returned: success + type: str + status: + description: Router status. + returned: success + type: str + external_gateway_info: + description: The external gateway information of the router. + returned: success + type: dict + interfaces_info: + description: List of connected interfaces. + returned: success + type: list + distributed: + description: Indicates a distributed router. + returned: success + type: bool + ha: + description: Indicates a highly-available router. + returned: success + type: bool + project_id: + description: Project id associated with this router. + returned: success + type: str + routes: + description: The extra routes configuration for L3 router. + returned: success + type: list +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class RouterInfoModule(OpenStackModule): + + deprecated_names = ('os_routers_info', 'openstack.cloud.os_routers_info') + + argument_spec = dict( + name=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None) + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + + kwargs = self.check_versioned( + filters=self.params['filters'] + ) + if self.params['name']: + kwargs['name_or_id'] = self.params['name'] + routers = self.conn.search_routers(**kwargs) + + for router in routers: + interfaces_info = [] + for port in self.conn.list_router_interfaces(router): + if port.device_owner != "network:router_gateway": + for ip_spec in port.fixed_ips: + int_info = { + 'port_id': port.id, + 'ip_address': ip_spec.get('ip_address'), + 'subnet_id': ip_spec.get('subnet_id') + } + interfaces_info.append(int_info) + router['interfaces_info'] = interfaces_info + + self.exit(changed=False, openstack_routers=routers) + + +def main(): + module = RouterInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_security_group.py b/ansible_collections/openstack/cloud/plugins/modules/os_security_group.py new file mode 100644 index 00000000..8208a1c2 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_security_group.py @@ -0,0 +1,153 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: security_group +short_description: Add/Delete security groups from an OpenStack cloud. +author: OpenStack Ansible SIG +description: + - Add or Remove security groups from an OpenStack cloud. +options: + name: + description: + - Name that has to be given to the security group. This module + requires that security group names be unique. + required: true + type: str + description: + description: + - Long description of the purpose of the security group + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + project: + description: + - Unique name or ID of the project. + required: false + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a security group +- openstack.cloud.security_group: + cloud: mordred + state: present + name: foo + description: security group for foo servers + +# Update the existing 'foo' security group description +- openstack.cloud.security_group: + cloud: mordred + state: present + name: foo + description: updated description for the foo security group + +# Create a security group for a given project +- openstack.cloud.security_group: + cloud: mordred + state: present + name: foo + project: myproj +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class SecurityGroupModule(OpenStackModule): + + argument_spec = dict( + name=dict(required=True), + description=dict(default=''), + state=dict(default='present', choices=['absent', 'present']), + project=dict(default=None), + ) + + def _needs_update(self, secgroup): + """Check for differences in the updatable values. + + NOTE: We don't currently allow name updates. + """ + if secgroup['description'] != self.params['description']: + return True + return False + + def _system_state_change(self, secgroup): + state = self.params['state'] + if state == 'present': + if not secgroup: + return True + return self._needs_update(secgroup) + if state == 'absent' and secgroup: + return True + return False + + def run(self): + + name = self.params['name'] + state = self.params['state'] + description = self.params['description'] + project = self.params['project'] + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail_json(msg='Project %s could not be found' % project) + project_id = proj['id'] + else: + project_id = self.conn.current_project_id + + if project_id: + filters = {'tenant_id': project_id} + else: + filters = None + + secgroup = self.conn.get_security_group(name, filters=filters) + + if self.ansible.check_mode: + self.exit(changed=self._system_state_change(secgroup)) + + changed = False + if state == 'present': + if not secgroup: + kwargs = {} + if project_id: + kwargs['project_id'] = project_id + secgroup = self.conn.create_security_group(name, description, + **kwargs) + changed = True + else: + if self._needs_update(secgroup): + secgroup = self.conn.update_security_group( + secgroup['id'], description=description) + changed = True + self.exit( + changed=changed, id=secgroup['id'], secgroup=secgroup) + + if state == 'absent': + if secgroup: + self.conn.delete_security_group(secgroup['id']) + changed = True + self.exit(changed=changed) + + +def main(): + module = SecurityGroupModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_security_group_rule.py b/ansible_collections/openstack/cloud/plugins/modules/os_security_group_rule.py new file mode 100644 index 00000000..53fe6f59 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_security_group_rule.py @@ -0,0 +1,389 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: security_group_rule +short_description: Add/Delete rule from an existing security group +author: OpenStack Ansible SIG +description: + - Add or Remove rule from an existing security group +options: + security_group: + description: + - Name or ID of the security group + required: true + type: str + protocol: + description: + - IP protocols ANY TCP UDP ICMP and others, also number in range 0-255 + type: str + port_range_min: + description: + - Starting port + type: int + port_range_max: + description: + - Ending port + type: int + remote_ip_prefix: + description: + - Source IP address(es) in CIDR notation (exclusive with remote_group) + type: str + remote_group: + description: + - Name or ID of the Security group to link (exclusive with + remote_ip_prefix) + type: str + ethertype: + description: + - Must be IPv4 or IPv6, and addresses represented in CIDR must + match the ingress or egress rules. Not all providers support IPv6. + choices: ['IPv4', 'IPv6'] + default: IPv4 + type: str + direction: + description: + - The direction in which the security group rule is applied. Not + all providers support egress. + choices: ['egress', 'ingress'] + default: ingress + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + project: + description: + - Unique name or ID of the project. + required: false + type: str + description: + required: false + description: + - Description of the rule. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a security group rule +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 + +# Create a security group rule for ping +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 + +# Another way to create the ping rule +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: icmp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 + +# Create a TCP rule covering all ports +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: tcp + port_range_min: 1 + port_range_max: 65535 + remote_ip_prefix: 0.0.0.0/0 + +# Another way to create the TCP rule above (defaults to all ports) +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + +# Create a rule for VRRP with numbered protocol 112 +- openstack.cloud.security_group_rule: + security_group: loadbalancer_sg + protocol: 112 + remote_group: loadbalancer-node_sg + +# Create a security group rule for a given project +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 + project: myproj + +# Remove the default created egress rule for IPv4 +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: any + remote_ip_prefix: 0.0.0.0/0 +''' + +RETURN = ''' +id: + description: Unique rule UUID. + type: str + returned: state == present +direction: + description: The direction in which the security group rule is applied. + type: str + sample: 'egress' + returned: state == present +ethertype: + description: One of IPv4 or IPv6. + type: str + sample: 'IPv4' + returned: state == present +port_range_min: + description: The minimum port number in the range that is matched by + the security group rule. + type: int + sample: 8000 + returned: state == present +port_range_max: + description: The maximum port number in the range that is matched by + the security group rule. + type: int + sample: 8000 + returned: state == present +protocol: + description: The protocol that is matched by the security group rule. + type: str + sample: 'tcp' + returned: state == present +remote_ip_prefix: + description: The remote IP prefix to be associated with this security group rule. + type: str + sample: '0.0.0.0/0' + returned: state == present +security_group_id: + description: The security group ID to associate with this security group rule. + type: str + returned: state == present +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule) + + +def _ports_match(protocol, module_min, module_max, rule_min, rule_max): + """ + Capture the complex port matching logic. + + The port values coming in for the module might be -1 (for ICMP), + which will work only for Nova, but this is handled by sdk. Likewise, + they might be None, which works for Neutron, but not Nova. This too is + handled by sdk. Since sdk will consistently return these port + values as None, we need to convert any -1 values input to the module + to None here for comparison. + + For TCP and UDP protocols, None values for both min and max are + represented as the range 1-65535 for Nova, but remain None for + Neutron. sdk returns the full range when Nova is the backend (since + that is how Nova stores them), and None values for Neutron. If None + values are input to the module for both values, then we need to adjust + for comparison. + """ + + # Check if the user is supplying -1 for ICMP. + if protocol in ['icmp', 'ipv6-icmp']: + if module_min and int(module_min) == -1: + module_min = None + if module_max and int(module_max) == -1: + module_max = None + + # Rules with 'any' protocol do not match ports + if protocol == 'any': + return True + + # Check if the user is supplying -1, 1 to 65535 or None values for full TPC/UDP port range. + if protocol in ['tcp', 'udp'] or protocol is None: + if ( + not module_min and not module_max + or (int(module_min) in [-1, 1] + and int(module_max) in [-1, 65535]) + ): + if ( + not rule_min and not rule_max + or (int(rule_min) in [-1, 1] + and int(rule_max) in [-1, 65535]) + ): + # (None, None) == (1, 65535) == (-1, -1) + return True + + # Sanity check to make sure we don't have type comparison issues. + if module_min: + module_min = int(module_min) + if module_max: + module_max = int(module_max) + if rule_min: + rule_min = int(rule_min) + if rule_max: + rule_max = int(rule_max) + + return module_min == rule_min and module_max == rule_max + + +class SecurityGroupRuleModule(OpenStackModule): + deprecated_names = ('os_security_group_rule', 'openstack.cloud.os_security_group_rule') + + argument_spec = dict( + security_group=dict(required=True), + protocol=dict(type='str'), + port_range_min=dict(required=False, type='int'), + port_range_max=dict(required=False, type='int'), + remote_ip_prefix=dict(required=False), + remote_group=dict(required=False), + ethertype=dict(default='IPv4', + choices=['IPv4', 'IPv6']), + direction=dict(default='ingress', + choices=['egress', 'ingress']), + state=dict(default='present', + choices=['absent', 'present']), + description=dict(required=False, default=None), + project=dict(default=None), + ) + + module_kwargs = dict( + mutually_exclusive=[ + ['remote_ip_prefix', 'remote_group'], + ] + ) + + def _find_matching_rule(self, secgroup, remotegroup): + """ + Find a rule in the group that matches the module parameters. + :returns: The matching rule dict, or None if no matches. + """ + protocol = self.params['protocol'] + remote_ip_prefix = self.params['remote_ip_prefix'] + ethertype = self.params['ethertype'] + direction = self.params['direction'] + remote_group_id = remotegroup['id'] + + for rule in secgroup['security_group_rules']: + if ( + protocol == rule['protocol'] + and remote_ip_prefix == rule['remote_ip_prefix'] + and ethertype == rule['ethertype'] + and direction == rule['direction'] + and remote_group_id == rule['remote_group_id'] + and _ports_match( + protocol, + self.params['port_range_min'], + self.params['port_range_max'], + rule['port_range_min'], + rule['port_range_max']) + ): + return rule + return None + + def _system_state_change(self, secgroup, remotegroup): + state = self.params['state'] + if secgroup: + rule_exists = self._find_matching_rule(secgroup, remotegroup) + else: + return False + + if state == 'present' and not rule_exists: + return True + if state == 'absent' and rule_exists: + return True + return False + + def run(self): + + state = self.params['state'] + security_group = self.params['security_group'] + remote_group = self.params['remote_group'] + project = self.params['project'] + changed = False + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail_json(msg='Project %s could not be found' % project) + project_id = proj['id'] + else: + project_id = self.conn.current_project_id + + if project_id and not remote_group: + filters = {'tenant_id': project_id} + else: + filters = None + + secgroup = self.conn.get_security_group(security_group, filters=filters) + + if remote_group: + remotegroup = self.conn.get_security_group(remote_group, filters=filters) + else: + remotegroup = {'id': None} + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(secgroup, remotegroup)) + + if state == 'present': + if self.params['protocol'] == 'any': + self.params['protocol'] = None + + if not secgroup: + self.fail_json(msg='Could not find security group %s' % security_group) + + rule = self._find_matching_rule(secgroup, remotegroup) + if not rule: + kwargs = {} + if project_id: + kwargs['project_id'] = project_id + if self.params["description"] is not None: + kwargs["description"] = self.params['description'] + rule = self.conn.network.create_security_group_rule( + security_group_id=secgroup['id'], + port_range_min=None if self.params['port_range_min'] == -1 else self.params['port_range_min'], + port_range_max=None if self.params['port_range_max'] == -1 else self.params['port_range_max'], + protocol=self.params['protocol'], + remote_ip_prefix=self.params['remote_ip_prefix'], + remote_group_id=remotegroup['id'], + direction=self.params['direction'], + ethertype=self.params['ethertype'], + **kwargs + ) + changed = True + self.exit_json(changed=changed, rule=rule, id=rule['id']) + + if state == 'absent' and secgroup: + rule = self._find_matching_rule(secgroup, remotegroup) + if rule: + self.conn.delete_security_group_rule(rule['id']) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = SecurityGroupRuleModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_server.py b/ansible_collections/openstack/cloud/plugins/modules/os_server.py new file mode 100644 index 00000000..a3ca7d05 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_server.py @@ -0,0 +1,805 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright 2019 Red Hat, Inc. +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# Copyright (c) 2013, John Dewey <john@dewey.ws> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server +short_description: Create/Delete Compute Instances from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Remove compute instances from OpenStack. +options: + name: + description: + - Name that has to be given to the instance. It is also possible to + specify the ID of the instance instead of its name if I(state) is I(absent). + required: true + type: str + image: + description: + - The name or id of the base image to boot. + - Required when I(boot_from_volume=true) + type: str + image_exclude: + description: + - Text to use to filter image names, for the case, such as HP, where + there are multiple image names matching the common identifying + portions. image_exclude is a negative match filter - it is text that + may not exist in the image name. + type: str + default: "(deprecated)" + flavor: + description: + - The name or id of the flavor in which the new instance has to be + created. + - Exactly one of I(flavor) and I(flavor_ram) must be defined when + I(state=present). + type: str + flavor_ram: + description: + - The minimum amount of ram in MB that the flavor in which the new + instance has to be created must have. + - Exactly one of I(flavor) and I(flavor_ram) must be defined when + I(state=present). + type: int + flavor_include: + description: + - Text to use to filter flavor names, for the case, such as Rackspace, + where there are multiple flavors that have the same ram count. + flavor_include is a positive match filter - it must exist in the + flavor name. + type: str + key_name: + description: + - The key pair name to be used when creating a instance + type: str + security_groups: + description: + - Names of the security groups to which the instance should be + added. This may be a YAML list or a comma separated string. + type: list + default: ['default'] + elements: str + network: + description: + - Name or ID of a network to attach this instance to. A simpler + version of the nics parameter, only one of network or nics should + be supplied. + type: str + nics: + description: + - A list of networks to which the instance's interface should + be attached. Networks may be referenced by net-id/net-name/port-id + or port-name. + - 'Also this accepts a string containing a list of (net/port)-(id/name) + Eg: nics: "net-id=uuid-1,port-name=myport" + Only one of network or nics should be supplied.' + type: list + elements: raw + suboptions: + tag: + description: + - 'A "tag" for the specific port to be passed via metadata. + Eg: tag: test_tag' + auto_ip: + description: + - Ensure instance has public ip however the cloud wants to do that + type: bool + default: 'yes' + aliases: ['auto_floating_ip', 'public_ip'] + floating_ips: + description: + - list of valid floating IPs that pre-exist to assign to this node + type: list + elements: str + floating_ip_pools: + description: + - Name of floating IP pool from which to choose a floating IP + type: list + elements: str + meta: + description: + - 'A list of key value pairs that should be provided as a metadata to + the new instance or a string containing a list of key-value pairs. + Eg: meta: "key1=value1,key2=value2"' + type: raw + wait: + description: + - If the module should wait for the instance to be created. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the instance to get + into active state. + default: 180 + type: int + config_drive: + description: + - Whether to boot the server with config drive enabled + type: bool + default: 'no' + userdata: + description: + - Opaque blob of data which is made available to the instance + type: str + aliases: ['user_data'] + boot_from_volume: + description: + - Should the instance boot from a persistent volume created based on + the image given. Mutually exclusive with boot_volume. + type: bool + default: 'no' + volume_size: + description: + - The size of the volume to create in GB if booting from volume based + on an image. + type: int + boot_volume: + description: + - Volume name or id to use as the volume to boot from. Implies + boot_from_volume. Mutually exclusive with image and boot_from_volume. + aliases: ['root_volume'] + type: str + terminate_volume: + description: + - If C(yes), delete volume when deleting instance (if booted from volume) + type: bool + default: 'no' + volumes: + description: + - A list of preexisting volumes names or ids to attach to the instance + default: [] + type: list + elements: str + scheduler_hints: + description: + - Arbitrary key/value pairs to the scheduler for custom use + type: dict + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + delete_fip: + description: + - When I(state) is absent and this option is true, any floating IP + associated with the instance will be deleted along with the instance. + type: bool + default: 'no' + reuse_ips: + description: + - When I(auto_ip) is true and this option is true, the I(auto_ip) code + will attempt to re-use unassigned floating ips in the project before + creating a new one. It is important to note that it is impossible + to safely do this concurrently, so if your use case involves + concurrent server creation, it is highly recommended to set this to + false and to delete the floating ip associated with a server when + the server is deleted using I(delete_fip). + type: bool + default: 'yes' + availability_zone: + description: + - Availability zone in which to create the server. + type: str + description: + description: + - Description of the server. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a new instance and attaches to a network and passes metadata to the instance + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: + - net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723 + - net-name: another_network + meta: + hostname: test1 + group: uge_master + +# Create a new instance in HP Cloud AE1 region availability zone az2 and +# automatically assigns a floating IP +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: username + password: Equality7-2521 + project_name: username-project1 + name: vm1 + region_name: region-b.geo-1 + availability_zone: az2 + image: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + timeout: 200 + flavor: 101 + security_groups: default + auto_ip: yes + +# Create a new instance in named cloud mordred availability zone az2 +# and assigns a pre-known floating IP +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + state: present + cloud: mordred + name: vm1 + availability_zone: az2 + image: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + timeout: 200 + flavor: 101 + floating_ips: + - 12.34.56.79 + +# Create a new instance with 4G of RAM on Ubuntu Trusty, ignoring +# deprecated images +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + cloud: mordred + region_name: region-b.geo-1 + image: Ubuntu Server 14.04 + image_exclude: deprecated + flavor_ram: 4096 + +# Create a new instance with 4G of RAM on Ubuntu Trusty on a Performance node +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + cloud: rax-dfw + state: present + image: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM) + flavor_ram: 4096 + flavor_include: Performance + +# Creates a new instance and attaches to multiple network +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance with a string + openstack.cloud.server: + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: "net-id=4cb08b20-62fe-11e5-9d70-feff819cdc9f,net-id=542f0430-62fe-11e5-9d70-feff819cdc9f..." + +- name: Creates a new instance and attaches to a network and passes metadata to the instance + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: + - net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723 + - net-name: another_network + meta: "hostname=test1,group=uge_master" + +- name: Creates a new instance and attaches to a specific network + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + network: another_network + +# Create a new instance with 4G of RAM on a 75G Ubuntu Trusty volume +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + boot_from_volume: True + volume_size: 75 + +# Creates a new instance with 2 volumes attached +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + volumes: + - photos + - music + +# Creates a new instance with provisioning userdata using Cloud-Init +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + image: "Ubuntu Server 14.04" + flavor: "P-1" + network: "Production" + userdata: | + #cloud-config + chpasswd: + list: | + ubuntu:{{ default_password }} + expire: False + packages: + - ansible + package_upgrade: true + +# Creates a new instance with provisioning userdata using Bash Scripts +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + image: "Ubuntu Server 14.04" + flavor: "P-1" + network: "Production" + userdata: | + {%- raw -%}#!/bin/bash + echo " up ip route add 10.0.0.0/8 via {% endraw -%}{{ intra_router }}{%- raw -%}" >> /etc/network/interfaces.d/eth0.conf + echo " down ip route del 10.0.0.0/8" >> /etc/network/interfaces.d/eth0.conf + ifdown eth0 && ifup eth0 + {% endraw %} + +# Create a new instance with server group for (anti-)affinity +# server group ID is returned from openstack.cloud.server_group module. +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + state: present + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + flavor: 4 + scheduler_hints: + group: f5c8c61a-9230-400a-8ed2-3b023c190a7f + +# Create an instance with "tags" for the nic +- name: Create instance with nics "tags" + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + flavor: 4 + nics: + - port-name: net1_port1 + tag: test_tag + - net-name: another_network + +# Deletes an instance via its ID +- name: remove an instance + hosts: localhost + tasks: + - name: remove an instance + openstack.cloud.server: + name: abcdef01-2345-6789-0abc-def0123456789 + state: absent + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_find_nova_addresses, OpenStackModule) + + +def _parse_nics(nics): + for net in nics: + if isinstance(net, str): + for nic in net.split(','): + yield dict((nic.split('='),)) + else: + yield net + + +def _parse_meta(meta): + if isinstance(meta, str): + metas = {} + for kv_str in meta.split(","): + k, v = kv_str.split("=") + metas[k] = v + return metas + if not meta: + return {} + return meta + + +class ServerModule(OpenStackModule): + deprecated_names = ('os_server', 'openstack.cloud.os_server') + + argument_spec = dict( + name=dict(required=True), + image=dict(default=None), + image_exclude=dict(default='(deprecated)'), + flavor=dict(default=None), + flavor_ram=dict(default=None, type='int'), + flavor_include=dict(default=None), + key_name=dict(default=None), + security_groups=dict(default=['default'], type='list', elements='str'), + network=dict(default=None), + nics=dict(default=[], type='list', elements='raw'), + meta=dict(default=None, type='raw'), + userdata=dict(default=None, aliases=['user_data']), + config_drive=dict(default=False, type='bool'), + auto_ip=dict(default=True, type='bool', aliases=['auto_floating_ip', 'public_ip']), + floating_ips=dict(default=None, type='list', elements='str'), + floating_ip_pools=dict(default=None, type='list', elements='str'), + volume_size=dict(default=None, type='int'), + boot_from_volume=dict(default=False, type='bool'), + boot_volume=dict(default=None, aliases=['root_volume']), + terminate_volume=dict(default=False, type='bool'), + volumes=dict(default=[], type='list', elements='str'), + scheduler_hints=dict(default=None, type='dict'), + state=dict(default='present', choices=['absent', 'present']), + delete_fip=dict(default=False, type='bool'), + reuse_ips=dict(default=True, type='bool'), + description=dict(default=None, type='str'), + ) + module_kwargs = dict( + mutually_exclusive=[ + ['auto_ip', 'floating_ips'], + ['auto_ip', 'floating_ip_pools'], + ['floating_ips', 'floating_ip_pools'], + ['flavor', 'flavor_ram'], + ['image', 'boot_volume'], + ['boot_from_volume', 'boot_volume'], + ['nics', 'network'], + ], + required_if=[ + ('boot_from_volume', True, ['volume_size', 'image']), + ], + ) + + def run(self): + + state = self.params['state'] + image = self.params['image'] + boot_volume = self.params['boot_volume'] + flavor = self.params['flavor'] + flavor_ram = self.params['flavor_ram'] + + if state == 'present': + if not (image or boot_volume): + self.fail( + msg="Parameter 'image' or 'boot_volume' is required " + "if state == 'present'" + ) + if not flavor and not flavor_ram: + self.fail( + msg="Parameter 'flavor' or 'flavor_ram' is required " + "if state == 'present'" + ) + + if state == 'present': + self._get_server_state() + self._create_server() + elif state == 'absent': + self._get_server_state() + self._delete_server() + + def _exit_hostvars(self, server, changed=True): + hostvars = self.conn.get_openstack_vars(server) + self.exit( + changed=changed, server=server, id=server.id, openstack=hostvars) + + def _get_server_state(self): + state = self.params['state'] + server = self.conn.get_server(self.params['name']) + if server and state == 'present': + if server.status not in ('ACTIVE', 'SHUTOFF', 'PAUSED', 'SUSPENDED'): + self.fail( + msg="The instance is available but not Active state: " + server.status) + (ip_changed, server) = self._check_ips(server) + (sg_changed, server) = self._check_security_groups(server) + (server_changed, server) = self._update_server(server) + self._exit_hostvars(server, ip_changed or sg_changed or server_changed) + if server and state == 'absent': + return True + if state == 'absent': + self.exit(changed=False, result="not present") + return True + + def _create_server(self): + flavor = self.params['flavor'] + flavor_ram = self.params['flavor_ram'] + flavor_include = self.params['flavor_include'] + + image_id = None + if not self.params['boot_volume']: + image_id = self.conn.get_image_id( + self.params['image'], self.params['image_exclude']) + if not image_id: + self.fail( + msg="Could not find image %s" % self.params['image']) + + if flavor: + flavor_dict = self.conn.get_flavor(flavor) + if not flavor_dict: + self.fail(msg="Could not find flavor %s" % flavor) + else: + flavor_dict = self.conn.get_flavor_by_ram(flavor_ram, flavor_include) + if not flavor_dict: + self.fail(msg="Could not find any matching flavor") + + nics = self._network_args() + + self.params['meta'] = _parse_meta(self.params['meta']) + + bootkwargs = self.check_versioned( + name=self.params['name'], + image=image_id, + flavor=flavor_dict['id'], + nics=nics, + meta=self.params['meta'], + security_groups=self.params['security_groups'], + userdata=self.params['userdata'], + config_drive=self.params['config_drive'], + ) + for optional_param in ( + 'key_name', 'availability_zone', 'network', + 'scheduler_hints', 'volume_size', 'volumes', + 'description'): + if self.params[optional_param]: + bootkwargs[optional_param] = self.params[optional_param] + + server = self.conn.create_server( + ip_pool=self.params['floating_ip_pools'], + ips=self.params['floating_ips'], + auto_ip=self.params['auto_ip'], + boot_volume=self.params['boot_volume'], + boot_from_volume=self.params['boot_from_volume'], + terminate_volume=self.params['terminate_volume'], + reuse_ips=self.params['reuse_ips'], + wait=self.params['wait'], timeout=self.params['timeout'], + **bootkwargs + ) + + self._exit_hostvars(server) + + def _update_server(self, server): + changed = False + + self.params['meta'] = _parse_meta(self.params['meta']) + + # self.conn.set_server_metadata only updates the key=value pairs, it doesn't + # touch existing ones + update_meta = {} + for (k, v) in self.params['meta'].items(): + if k not in server.metadata or server.metadata[k] != v: + update_meta[k] = v + + if update_meta: + self.conn.set_server_metadata(server, update_meta) + changed = True + # Refresh server vars + server = self.conn.get_server(self.params['name']) + + return (changed, server) + + def _delete_server(self): + try: + self.conn.delete_server( + self.params['name'], wait=self.params['wait'], + timeout=self.params['timeout'], + delete_ips=self.params['delete_fip']) + except Exception as e: + self.fail(msg="Error in deleting vm: %s" % e) + self.exit(changed=True, result='deleted') + + def _network_args(self): + args = [] + nics = self.params['nics'] + + if not isinstance(nics, list): + self.fail(msg='The \'nics\' parameter must be a list.') + + for num, net in enumerate(_parse_nics(nics)): + if not isinstance(net, dict): + self.fail( + msg='Each entry in the \'nics\' parameter must be a dict.') + + if net.get('net-id'): + args.append(net) + elif net.get('net-name'): + by_name = self.conn.get_network(net['net-name']) + if not by_name: + self.fail( + msg='Could not find network by net-name: %s' % + net['net-name']) + resolved_net = net.copy() + del resolved_net['net-name'] + resolved_net['net-id'] = by_name['id'] + args.append(resolved_net) + elif net.get('port-id'): + args.append(net) + elif net.get('port-name'): + by_name = self.conn.get_port(net['port-name']) + if not by_name: + self.fail( + msg='Could not find port by port-name: %s' % + net['port-name']) + resolved_net = net.copy() + del resolved_net['port-name'] + resolved_net['port-id'] = by_name['id'] + args.append(resolved_net) + + if 'tag' in net: + args[num]['tag'] = net['tag'] + return args + + def _detach_ip_list(self, server, extra_ips): + for ip in extra_ips: + ip_id = self.conn.get_floating_ip( + id=None, filters={'floating_ip_address': ip}) + self.conn.detach_ip_from_server( + server_id=server.id, floating_ip_id=ip_id) + + def _check_ips(self, server): + changed = False + + auto_ip = self.params['auto_ip'] + floating_ips = self.params['floating_ips'] + floating_ip_pools = self.params['floating_ip_pools'] + + if floating_ip_pools or floating_ips: + ips = openstack_find_nova_addresses(server.addresses, 'floating') + if not ips: + # If we're configured to have a floating but we don't have one, + # let's add one + server = self.conn.add_ips_to_server( + server, + auto_ip=auto_ip, + ips=floating_ips, + ip_pool=floating_ip_pools, + wait=self.params['wait'], + timeout=self.params['timeout'], + ) + changed = True + elif floating_ips: + # we were configured to have specific ips, let's make sure we have + # those + missing_ips = [] + for ip in floating_ips: + if ip not in ips: + missing_ips.append(ip) + if missing_ips: + server = self.conn.add_ip_list(server, missing_ips, + wait=self.params['wait'], + timeout=self.params['timeout']) + changed = True + extra_ips = [] + for ip in ips: + if ip not in floating_ips: + extra_ips.append(ip) + if extra_ips: + self._detach_ip_list(server, extra_ips) + changed = True + elif auto_ip: + if server['interface_ip']: + changed = False + else: + # We're configured for auto_ip but we're not showing an + # interface_ip. Maybe someone deleted an IP out from under us. + server = self.conn.add_ips_to_server( + server, + auto_ip=auto_ip, + ips=floating_ips, + ip_pool=floating_ip_pools, + wait=self.params['wait'], + timeout=self.params['timeout'], + ) + changed = True + return (changed, server) + + def _check_security_groups(self, server): + changed = False + + # server security groups were added to shade in 1.19. Until then this + # module simply ignored trying to update security groups and only set them + # on newly created hosts. + if not ( + hasattr(self.conn, 'add_server_security_groups') + and hasattr(self.conn, 'remove_server_security_groups') + ): + return changed, server + + module_security_groups = set(self.params['security_groups']) + server_security_groups = set(sg['name'] for sg in server.security_groups) + + add_sgs = module_security_groups - server_security_groups + remove_sgs = server_security_groups - module_security_groups + + if add_sgs: + self.conn.add_server_security_groups(server, list(add_sgs)) + changed = True + + if remove_sgs: + self.conn.remove_server_security_groups(server, list(remove_sgs)) + changed = True + + return (changed, server) + + +def main(): + module = ServerModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_server_action.py b/ansible_collections/openstack/cloud/plugins/modules/os_server_action.py new file mode 100644 index 00000000..341ff374 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_server_action.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2015, Jesse Keating <jlk@derpops.bike> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_action +short_description: Perform actions on Compute Instances from OpenStack +author: OpenStack Ansible SIG +description: + - Perform server actions on an existing compute instance from OpenStack. + This module does not return any data other than changed true/false. + When I(action) is 'rebuild', then I(image) parameter is required. +options: + server: + description: + - Name or ID of the instance + required: true + type: str + wait: + description: + - If the module should wait for the instance action to be performed. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the instance to perform + the requested action. + default: 180 + type: int + action: + description: + - Perform the given action. The lock and unlock actions always return + changed as the servers API does not provide lock status. + choices: [stop, start, pause, unpause, lock, unlock, suspend, resume, + rebuild, shelve, shelve_offload, unshelve] + type: str + required: true + image: + description: + - Image the server should be rebuilt with + type: str + admin_password: + description: + - Admin password for server to rebuild + type: str + all_projects: + description: + - Whether to search for server in all projects or just the current + auth scoped project. + type: bool + default: 'no' + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Pauses a compute instance +- openstack.cloud.server_action: + action: pause + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + server: vm1 + timeout: 200 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + +# If I(action) is set to C(shelve) then according to OpenStack's Compute API, the shelved +# server is in one of two possible states: +# +# SHELVED: The server is in shelved state. Depends on the shelve offload time, +# the server will be automatically shelved off loaded. +# SHELVED_OFFLOADED: The shelved server is offloaded (removed from the compute host) and +# it needs unshelved action to be used again. +# +# But wait_for_server can only wait for a single server state. If a shelved server is offloaded +# immediately, then a exceptions.ResourceTimeout will be raised if I(action) is set to C(shelve). +# This is likely to happen because shelved_offload_time in Nova's config is set to 0 by default. +# This also applies if you boot the server from volumes. +# +# Calling C(shelve_offload) instead of C(shelve) will also fail most likely because the default +# policy does not allow C(shelve_offload) for non-admin users while C(shelve) is allowed for +# admin users and server owners. +# +# As we cannot retrieve shelved_offload_time from Nova's config, we fall back to waiting for +# one state and if that fails then we fetch the server's state and match it against the other +# valid states from _action_map. +# +# Ref.: https://docs.openstack.org/api-guide/compute/server_concepts.html + +_action_map = {'stop': ['SHUTOFF'], + 'start': ['ACTIVE'], + 'pause': ['PAUSED'], + 'unpause': ['ACTIVE'], + 'lock': ['ACTIVE'], # API doesn't show lock/unlock status + 'unlock': ['ACTIVE'], + 'suspend': ['SUSPENDED'], + 'resume': ['ACTIVE'], + 'rebuild': ['ACTIVE'], + 'shelve': ['SHELVED_OFFLOADED', 'SHELVED'], + 'shelve_offload': ['SHELVED_OFFLOADED'], + 'unshelve': ['ACTIVE']} + +_admin_actions = ['pause', 'unpause', 'suspend', 'resume', 'lock', 'unlock', 'shelve_offload'] + + +class ServerActionModule(OpenStackModule): + deprecated_names = ('os_server_action', 'openstack.cloud.os_server_action') + + argument_spec = dict( + server=dict(required=True, type='str'), + action=dict(required=True, type='str', + choices=['stop', 'start', 'pause', 'unpause', + 'lock', 'unlock', 'suspend', 'resume', + 'rebuild', 'shelve', 'shelve_offload', 'unshelve']), + image=dict(required=False, type='str'), + admin_password=dict(required=False, type='str', no_log=True), + all_projects=dict(required=False, type='bool', default=False), + ) + module_kwargs = dict( + required_if=[('action', 'rebuild', ['image'])], + supports_check_mode=True, + ) + + def run(self): + os_server = self._preliminary_checks() + self._execute_server_action(os_server) + # for some reason we don't wait for lock and unlock before exit + if self.params['action'] not in ('lock', 'unlock'): + if self.params['wait']: + self._wait(os_server) + self.exit_json(changed=True) + + def _preliminary_checks(self): + # Using Munch object for getting information about a server + os_server = self.conn.get_server( + self.params['server'], + all_projects=self.params['all_projects'], + ) + if not os_server: + self.fail_json(msg='Could not find server %s' % self.params['server']) + # check mode + if self.ansible.check_mode: + self.exit_json(changed=self.__system_state_change(os_server)) + # examine special cases + # lock, unlock and rebuild don't depend on state, just do it + if self.params['action'] not in ('lock', 'unlock', 'rebuild'): + if not self.__system_state_change(os_server): + self.exit_json(changed=False) + return os_server + + def _execute_server_action(self, os_server): + if self.params['action'] == 'rebuild': + return self._rebuild_server(os_server) + if self.params['action'] == 'shelve_offload': + # shelve_offload is not supported in OpenstackSDK + return self._action(os_server, json={'shelveOffload': None}) + action_name = self.params['action'] + "_server" + try: + func_name = getattr(self.conn.compute, action_name) + except AttributeError: + self.fail_json( + msg="Method %s wasn't found in OpenstackSDK compute" % action_name) + func_name(os_server) + + def _rebuild_server(self, os_server): + # rebuild should ensure images exists + try: + image = self.conn.get_image(self.params['image']) + except Exception as e: + self.fail_json( + msg="Can't find the image %s: %s" % (self.params['image'], e)) + if not image: + self.fail_json(msg="Image %s was not found!" % self.params['image']) + # admin_password is required by SDK, but not required by Nova API + if self.params['admin_password']: + self.conn.compute.rebuild_server( + server=os_server, + name=os_server['name'], + image=image['id'], + admin_password=self.params['admin_password'] + ) + else: + self._action(os_server, json={'rebuild': {'imageRef': image['id']}}) + + def _action(self, os_server, json): + response = self.conn.compute.post( + '/servers/{server_id}/action'.format(server_id=os_server['id']), + json=json) + self.sdk.exceptions.raise_from_response(response) + return response + + def _wait(self, os_server): + """Wait for the server to reach the desired state for the given action.""" + # The wait_for_server function needs a Server object instead of the + # Munch object returned by self.conn.get_server + server = self.conn.compute.get_server(os_server['id']) + states = _action_map[self.params['action']] + + try: + self.conn.compute.wait_for_server( + server, + status=states[0], + wait=self.params['timeout']) + except self.sdk.exceptions.ResourceTimeout: + # raise if there is only one valid state + if len(states) < 2: + raise + # fetch current server status and compare to other valid states + server = self.conn.compute.get_server(os_server['id']) + if server.status not in states: + raise + + def __system_state_change(self, os_server): + """Check if system state would change.""" + return os_server.status not in _action_map[self.params['action']] + + +def main(): + module = ServerActionModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_server_group.py b/ansible_collections/openstack/cloud/plugins/modules/os_server_group.py new file mode 100644 index 00000000..84f59e6c --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_server_group.py @@ -0,0 +1,162 @@ +#!/usr/bin/python + +# Copyright (c) 2016 Catalyst IT Limited +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_group +short_description: Manage OpenStack server groups +author: OpenStack Ansible SIG +description: + - Add or remove server groups from OpenStack. +options: + state: + description: + - Indicate desired state of the resource. When I(state) is 'present', + then I(policies) is required. + choices: ['present', 'absent'] + required: false + default: present + type: str + name: + description: + - Server group name. + required: true + type: str + policies: + description: + - A list of one or more policy names to associate with the server + group. The list must contain at least one policy name. The current + valid policy names are anti-affinity, affinity, soft-anti-affinity + and soft-affinity. + required: false + type: list + elements: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a server group with 'affinity' policy. +- openstack.cloud.server_group: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: my_server_group + policies: + - affinity + +# Delete 'my_server_group' server group. +- openstack.cloud.server_group: + state: absent + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: my_server_group +''' + +RETURN = ''' +id: + description: Unique UUID. + returned: success + type: str +name: + description: The name of the server group. + returned: success + type: str +policies: + description: A list of one or more policy names of the server group. + returned: success + type: list +members: + description: A list of members in the server group. + returned: success + type: list +metadata: + description: Metadata key and value pairs. + returned: success + type: dict +project_id: + description: The project ID who owns the server group. + returned: success + type: str +user_id: + description: The user ID who owns the server group. + returned: success + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ServerGroupModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + policies=dict(required=False, type='list', elements='str'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + def _system_state_change(self, state, server_group): + if state == 'present' and not server_group: + return True + if state == 'absent' and server_group: + return True + + return False + + def run(self): + name = self.params['name'] + policies = self.params['policies'] + state = self.params['state'] + + server_group = self.conn.get_server_group(name) + + if self.ansible.check_mode: + self.exit_json( + changed=self._system_state_change(state, server_group) + ) + + changed = False + if state == 'present': + if not server_group: + if not policies: + self.fail_json( + msg="Parameter 'policies' is required in Server Group " + "Create" + ) + server_group = self.conn.create_server_group(name, policies) + changed = True + + self.exit_json( + changed=changed, + id=server_group['id'], + server_group=server_group + ) + if state == 'absent': + if server_group: + self.conn.delete_server_group(server_group['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = ServerGroupModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_server_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_server_info.py new file mode 100644 index 00000000..bac1d211 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_server_info.py @@ -0,0 +1,96 @@ +#!/usr/bin/python + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_info +short_description: Retrieve information about one or more compute instances +author: OpenStack Ansible SIG +description: + - Retrieve information about server instances from OpenStack. + - This module was called C(os_server_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.server_info) module no longer returns C(ansible_facts)! +notes: + - The result contains a list of servers. +options: + server: + description: + - restrict results to servers with names or UUID matching + this glob expression (e.g., <web*>). + type: str + detailed: + description: + - when true, return additional detail about servers at the expense + of additional API calls. + type: bool + default: 'no' + filters: + description: + - restrict results to servers matching a dictionary of + filters + type: dict + all_projects: + description: + - Whether to list servers from all projects or just the current auth + scoped project. + type: bool + default: 'no' +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about all servers named <web*> that are in an active state: +- openstack.cloud.server_info: + cloud: rax-dfw + server: web* + filters: + vm_state: active + register: result +- debug: + msg: "{{ result.openstack_servers }}" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ServerInfoModule(OpenStackModule): + + deprecated_names = ('os_server_info', 'openstack.cloud.os_server_info') + + argument_spec = dict( + server=dict(required=False), + detailed=dict(required=False, type='bool', default=False), + filters=dict(required=False, type='dict', default=None), + all_projects=dict(required=False, type='bool', default=False), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + + kwargs = self.check_versioned( + detailed=self.params['detailed'], + filters=self.params['filters'], + all_projects=self.params['all_projects'] + ) + if self.params['server']: + kwargs['name_or_id'] = self.params['server'] + openstack_servers = self.conn.search_servers(**kwargs) + self.exit(changed=False, openstack_servers=openstack_servers) + + +def main(): + module = ServerInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_server_metadata.py b/ansible_collections/openstack/cloud/plugins/modules/os_server_metadata.py new file mode 100644 index 00000000..a1207e3b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_server_metadata.py @@ -0,0 +1,165 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2016, Mario Santos <mario.rf.santos@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_metadata +short_description: Add/Update/Delete Metadata in Compute Instances from OpenStack +author: OpenStack Ansible SIG +description: + - Add, Update or Remove metadata in compute instances from OpenStack. +options: + server: + description: + - Name of the instance to update the metadata + required: true + aliases: ['name'] + type: str + meta: + description: + - 'A list of key value pairs that should be provided as a metadata to + the instance or a string containing a list of key-value pairs. + Eg: meta: "key1=value1,key2=value2"' + required: true + type: dict + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + availability_zone: + description: + - Availability zone in which to create the snapshot. + required: false + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Creates or updates hostname=test1 as metadata of the server instance vm1 +- name: add metadata to compute instance + hosts: localhost + tasks: + - name: add metadata to instance + openstack.cloud.server_metadata: + state: present + auth: + auth_url: https://openstack-api.example.com:35357/v2.0/ + username: admin + password: admin + project_name: admin + name: vm1 + meta: + hostname: test1 + group: group1 + +# Removes the keys under meta from the instance named vm1 +- name: delete metadata from compute instance + hosts: localhost + tasks: + - name: delete metadata from instance + openstack.cloud.server_metadata: + state: absent + auth: + auth_url: https://openstack-api.example.com:35357/v2.0/ + username: admin + password: admin + project_name: admin + name: vm1 + meta: + hostname: + group: +''' + +RETURN = ''' +server_id: + description: The compute instance id where the change was made + returned: success + type: str + sample: "324c4e91-3e03-4f62-9a4d-06119a8a8d16" +metadata: + description: The metadata of compute instance after the change + returned: success + type: dict + sample: {'key1': 'value1', 'key2': 'value2'} +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ServerMetadataModule(OpenStackModule): + argument_spec = dict( + server=dict(required=True, aliases=['name']), + meta=dict(required=True, type='dict'), + state=dict(default='present', choices=['absent', 'present']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, server_metadata=None, metadata=None): + if server_metadata is None: + server_metadata = {} + if metadata is None: + metadata = {} + return len(set(metadata.items()) - set(server_metadata.items())) != 0 + + def _get_keys_to_delete(self, server_metadata_keys=None, metadata_keys=None): + if server_metadata_keys is None: + server_metadata_keys = [] + if metadata_keys is None: + metadata_keys = [] + return set(server_metadata_keys) & set(metadata_keys) + + def run(self): + state = self.params['state'] + server_param = self.params['server'] + meta_param = self.params['meta'] + changed = False + + server = self.conn.get_server(server_param) + if not server: + self.fail_json( + msg='Could not find server {0}'.format(server_param)) + + if state == 'present': + # check if it needs update + if self._needs_update( + server_metadata=server.metadata, metadata=meta_param + ): + if not self.ansible.check_mode: + self.conn.set_server_metadata(server_param, meta_param) + changed = True + elif state == 'absent': + # remove from params the keys that do not exist in the server + keys_to_delete = self._get_keys_to_delete( + server.metadata.keys(), meta_param.keys()) + if len(keys_to_delete) > 0: + if not self.ansible.check_mode: + self.conn.delete_server_metadata( + server_param, keys_to_delete) + changed = True + + if changed: + server = self.conn.get_server(server_param) + + self.exit_json( + changed=changed, server_id=server.id, metadata=server.metadata) + + +def main(): + module = ServerMetadataModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_server_volume.py b/ansible_collections/openstack/cloud/plugins/modules/os_server_volume.py new file mode 100644 index 00000000..1deb8fa6 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_server_volume.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_volume +short_description: Attach/Detach Volumes from OpenStack VM's +author: OpenStack Ansible SIG +description: + - Attach or Detach volumes from OpenStack VM's +options: + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + required: false + type: str + server: + description: + - Name or ID of server you want to attach a volume to + required: true + type: str + volume: + description: + - Name or id of volume you want to attach to a server + required: true + type: str + device: + description: + - Device you want to attach. Defaults to auto finding a device name. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Attaches a volume to a compute host +- name: attach a volume + hosts: localhost + tasks: + - name: attach volume to host + openstack.cloud.server_volume: + state: present + cloud: mordred + server: Mysql-server + volume: mysql-data + device: /dev/vdb +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +def _system_state_change(state, device): + """Check if system state would change.""" + if state == 'present': + if device: + return False + return True + if state == 'absent': + if device: + return True + return False + return False + + +class ServerVolumeModule(OpenStackModule): + + argument_spec = dict( + server=dict(required=True), + volume=dict(required=True), + device=dict(default=None), # None == auto choose device name + state=dict(default='present', choices=['absent', 'present']), + ) + + def run(self): + + state = self.params['state'] + wait = self.params['wait'] + timeout = self.params['timeout'] + + server = self.conn.get_server(self.params['server']) + volume = self.conn.get_volume(self.params['volume']) + + if not server: + self.fail(msg='server %s is not found' % self.params['server']) + + if not volume: + self.fail(msg='volume %s is not found' % self.params['volume']) + + dev = self.conn.get_volume_attach_device(volume, server.id) + + if self.ansible.check_mode: + self.exit(changed=_system_state_change(state, dev)) + + if state == 'present': + changed = False + if not dev: + changed = True + self.conn.attach_volume(server, volume, self.params['device'], + wait=wait, timeout=timeout) + + server = self.conn.get_server(self.params['server']) # refresh + volume = self.conn.get_volume(self.params['volume']) # refresh + hostvars = self.conn.get_openstack_vars(server) + + self.exit( + changed=changed, + id=volume['id'], + attachments=volume['attachments'], + openstack=hostvars + ) + + elif state == 'absent': + if not dev: + # Volume is not attached to this server + self.exit(changed=False) + + self.conn.detach_volume(server, volume, wait=wait, timeout=timeout) + self.exit( + changed=True, + result='Detached volume from server' + ) + + +def main(): + module = ServerVolumeModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_stack.py b/ansible_collections/openstack/cloud/plugins/modules/os_stack.py new file mode 100644 index 00000000..95b7bef5 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_stack.py @@ -0,0 +1,248 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2016, Mathieu Bultel <mbultel@redhat.com> +# (c) 2016, Steve Baker <sbaker@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: stack +short_description: Add/Remove Heat Stack +author: OpenStack Ansible SIG +description: + - Add or Remove a Stack to an OpenStack Heat +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Name of the stack that should be created, name could be char and digit, no space + required: true + type: str + tag: + description: + - Tag for the stack that should be created, name could be char and digit, no space + type: str + template: + description: + - Path of the template file to use for the stack creation + type: str + environment: + description: + - List of environment files that should be used for the stack creation + type: list + elements: str + parameters: + description: + - Dictionary of parameters for the stack creation + type: dict + rollback: + description: + - Rollback stack creation + type: bool + default: false + timeout: + description: + - Maximum number of seconds to wait for the stack creation + default: 3600 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' +EXAMPLES = ''' +--- +- name: create stack + ignore_errors: True + register: stack_create + openstack.cloud.stack: + name: "{{ stack_name }}" + tag: "{{ tag_name }}" + state: present + template: "/path/to/my_stack.yaml" + environment: + - /path/to/resource-registry.yaml + - /path/to/environment.yaml + parameters: + bmc_flavor: m1.medium + bmc_image: CentOS + key_name: default + private_net: "{{ private_net_param }}" + node_count: 2 + name: undercloud + image: CentOS + my_flavor: m1.large + external_net: "{{ external_net_param }}" +''' + +RETURN = ''' +id: + description: Stack ID. + type: str + sample: "97a3f543-8136-4570-920e-fd7605c989d6" + returned: always + +stack: + description: stack info + type: complex + returned: always + contains: + action: + description: Action, could be Create or Update. + type: str + sample: "CREATE" + creation_time: + description: Time when the action has been made. + type: str + sample: "2016-07-05T17:38:12Z" + description: + description: Description of the Stack provided in the heat template. + type: str + sample: "HOT template to create a new instance and networks" + id: + description: Stack ID. + type: str + sample: "97a3f543-8136-4570-920e-fd7605c989d6" + name: + description: Name of the Stack + type: str + sample: "test-stack" + identifier: + description: Identifier of the current Stack action. + type: str + sample: "test-stack/97a3f543-8136-4570-920e-fd7605c989d6" + links: + description: Links to the current Stack. + type: list + elements: dict + sample: "[{'href': 'http://foo:8004/v1/7f6a/stacks/test-stack/97a3f543-8136-4570-920e-fd7605c989d6']" + outputs: + description: Output returned by the Stack. + type: list + elements: dict + sample: "{'description': 'IP address of server1 in private network', + 'output_key': 'server1_private_ip', + 'output_value': '10.1.10.103'}" + parameters: + description: Parameters of the current Stack + type: dict + sample: "{'OS::project_id': '7f6a3a3e01164a4eb4eecb2ab7742101', + 'OS::stack_id': '97a3f543-8136-4570-920e-fd7605c989d6', + 'OS::stack_name': 'test-stack', + 'stack_status': 'CREATE_COMPLETE', + 'stack_status_reason': 'Stack CREATE completed successfully', + 'status': 'COMPLETE', + 'template_description': 'HOT template to create a new instance and networks', + 'timeout_mins': 60, + 'updated_time': null}" +''' + + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class StackModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + tag=dict(required=False, default=None, min_ver='0.28.0'), + template=dict(default=None), + environment=dict(default=None, type='list', elements='str'), + parameters=dict(default={}, type='dict'), + rollback=dict(default=False, type='bool'), + timeout=dict(default=3600, type='int'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _create_stack(self, stack, parameters): + stack = self.conn.create_stack( + self.params['name'], + template_file=self.params['template'], + environment_files=self.params['environment'], + timeout=self.params['timeout'], + wait=True, + rollback=self.params['rollback'], + **parameters) + + stack = self.conn.get_stack(stack.id, None) + if stack.stack_status == 'CREATE_COMPLETE': + return stack + else: + self.fail_json(msg="Failure in creating stack: {0}".format(stack)) + + def _update_stack(self, stack, parameters): + stack = self.conn.update_stack( + self.params['name'], + template_file=self.params['template'], + environment_files=self.params['environment'], + timeout=self.params['timeout'], + rollback=self.params['rollback'], + wait=self.params['wait'], + **parameters) + + if stack['stack_status'] == 'UPDATE_COMPLETE': + return stack + else: + self.fail_json(msg="Failure in updating stack: %s" % + stack['stack_status_reason']) + + def _system_state_change(self, stack): + state = self.params['state'] + if state == 'present': + if not stack: + return True + if state == 'absent' and stack: + return True + return False + + def run(self): + state = self.params['state'] + name = self.params['name'] + # Check for required parameters when state == 'present' + if state == 'present': + for p in ['template']: + if not self.params[p]: + self.fail_json(msg='%s required with present state' % p) + + stack = self.conn.get_stack(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(stack)) + + if state == 'present': + parameters = self.params['parameters'] + if not stack: + stack = self._create_stack(stack, parameters) + else: + stack = self._update_stack(stack, parameters) + self.exit_json(changed=True, + stack=stack, + id=stack.id) + elif state == 'absent': + if not stack: + changed = False + else: + changed = True + if not self.conn.delete_stack(name, wait=self.params['wait']): + self.fail_json(msg='delete stack failed for stack: %s' % name) + self.exit_json(changed=changed) + + +def main(): + module = StackModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_subnet.py b/ansible_collections/openstack/cloud/plugins/modules/os_subnet.py new file mode 100644 index 00000000..dfe1eaca --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_subnet.py @@ -0,0 +1,364 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: subnet +short_description: Add/Remove subnet to an OpenStack network +author: OpenStack Ansible SIG +description: + - Add or Remove a subnet to an OpenStack network +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + network_name: + description: + - Name of the network to which the subnet should be attached + - Required when I(state) is 'present' + type: str + name: + description: + - The name of the subnet that should be created. Although Neutron + allows for non-unique subnet names, this module enforces subnet + name uniqueness. + required: true + type: str + cidr: + description: + - The CIDR representation of the subnet that should be assigned to + the subnet. Required when I(state) is 'present' and a subnetpool + is not specified. + type: str + ip_version: + description: + - The IP version of the subnet 4 or 6 + default: '4' + type: str + choices: ['4', '6'] + enable_dhcp: + description: + - Whether DHCP should be enabled for this subnet. + type: bool + default: 'yes' + gateway_ip: + description: + - The ip that would be assigned to the gateway for this subnet + type: str + no_gateway_ip: + description: + - The gateway IP would not be assigned for this subnet + type: bool + default: 'no' + dns_nameservers: + description: + - List of DNS nameservers for this subnet. + type: list + elements: str + allocation_pool_start: + description: + - From the subnet pool the starting address from which the IP should + be allocated. + type: str + allocation_pool_end: + description: + - From the subnet pool the last IP that should be assigned to the + virtual machines. + type: str + host_routes: + description: + - A list of host route dictionaries for the subnet. + type: list + elements: dict + suboptions: + destination: + description: The destination network (CIDR). + type: str + required: true + nexthop: + description: The next hop (aka gateway) for the I(destination). + type: str + required: true + ipv6_ra_mode: + description: + - IPv6 router advertisement mode + choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] + type: str + ipv6_address_mode: + description: + - IPv6 address mode + choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] + type: str + use_default_subnetpool: + description: + - Use the default subnetpool for I(ip_version) to obtain a CIDR. + type: bool + default: 'no' + project: + description: + - Project name or ID containing the subnet (name admin-only) + type: str + extra_specs: + description: + - Dictionary with extra key/value pairs passed to the API + required: false + default: {} + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a new (or update an existing) subnet on the specified network +- openstack.cloud.subnet: + state: present + network_name: network1 + name: net1subnet + cidr: 192.168.0.0/24 + dns_nameservers: + - 8.8.8.7 + - 8.8.8.8 + host_routes: + - destination: 0.0.0.0/0 + nexthop: 12.34.56.78 + - destination: 192.168.0.0/24 + nexthop: 192.168.0.1 + +# Delete a subnet +- openstack.cloud.subnet: + state: absent + name: net1subnet + +# Create an ipv6 stateless subnet +- openstack.cloud.subnet: + state: present + name: intv6 + network_name: internal + ip_version: 6 + cidr: 2db8:1::/64 + dns_nameservers: + - 2001:4860:4860::8888 + - 2001:4860:4860::8844 + ipv6_ra_mode: dhcpv6-stateless + ipv6_address_mode: dhcpv6-stateless +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class SubnetModule(OpenStackModule): + ipv6_mode_choices = ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] + argument_spec = dict( + name=dict(type='str', required=True), + network_name=dict(type='str'), + cidr=dict(type='str'), + ip_version=dict(type='str', default='4', choices=['4', '6']), + enable_dhcp=dict(type='bool', default=True), + gateway_ip=dict(type='str'), + no_gateway_ip=dict(type='bool', default=False), + dns_nameservers=dict(type='list', default=None, elements='str'), + allocation_pool_start=dict(type='str'), + allocation_pool_end=dict(type='str'), + host_routes=dict(type='list', default=None, elements='dict'), + ipv6_ra_mode=dict(type='str', choices=ipv6_mode_choices), + ipv6_address_mode=dict(type='str', choices=ipv6_mode_choices), + use_default_subnetpool=dict(type='bool', default=False), + extra_specs=dict(type='dict', default=dict()), + state=dict(type='str', default='present', choices=['absent', 'present']), + project=dict(type='str'), + ) + + module_kwargs = dict( + supports_check_mode=True, + required_together=[['allocation_pool_end', 'allocation_pool_start']] + ) + + def _can_update(self, subnet, filters=None): + """Check for differences in non-updatable values""" + network_name = self.params['network_name'] + ip_version = int(self.params['ip_version']) + ipv6_ra_mode = self.params['ipv6_ra_mode'] + ipv6_a_mode = self.params['ipv6_address_mode'] + + if network_name: + network = self.conn.get_network(network_name, filters) + if network: + netid = network['id'] + if netid != subnet['network_id']: + self.fail_json(msg='Cannot update network_name in existing subnet') + else: + self.fail_json(msg='No network found for %s' % network_name) + + if ip_version and subnet['ip_version'] != ip_version: + self.fail_json(msg='Cannot update ip_version in existing subnet') + if ipv6_ra_mode and subnet.get('ipv6_ra_mode', None) != ipv6_ra_mode: + self.fail_json(msg='Cannot update ipv6_ra_mode in existing subnet') + if ipv6_a_mode and subnet.get('ipv6_address_mode', None) != ipv6_a_mode: + self.fail_json(msg='Cannot update ipv6_address_mode in existing subnet') + + def _needs_update(self, subnet, filters=None): + """Check for differences in the updatable values.""" + + # First check if we are trying to update something we're not allowed to + self._can_update(subnet, filters) + + # now check for the things we are allowed to update + enable_dhcp = self.params['enable_dhcp'] + subnet_name = self.params['name'] + pool_start = self.params['allocation_pool_start'] + pool_end = self.params['allocation_pool_end'] + gateway_ip = self.params['gateway_ip'] + no_gateway_ip = self.params['no_gateway_ip'] + dns = self.params['dns_nameservers'] + host_routes = self.params['host_routes'] + if pool_start and pool_end: + pool = dict(start=pool_start, end=pool_end) + else: + pool = None + + changes = dict() + if subnet['enable_dhcp'] != enable_dhcp: + changes['enable_dhcp'] = enable_dhcp + if subnet_name and subnet['name'] != subnet_name: + changes['subnet_name'] = subnet_name + if pool and (not subnet['allocation_pools'] or subnet['allocation_pools'] != [pool]): + changes['allocation_pools'] = [pool] + if gateway_ip and subnet['gateway_ip'] != gateway_ip: + changes['gateway_ip'] = gateway_ip + if dns and sorted(subnet['dns_nameservers']) != sorted(dns): + changes['dns_nameservers'] = dns + if host_routes: + curr_hr = sorted(subnet['host_routes'], key=lambda t: t.keys()) + new_hr = sorted(host_routes, key=lambda t: t.keys()) + if curr_hr != new_hr: + changes['host_routes'] = host_routes + if no_gateway_ip and subnet['gateway_ip']: + changes['disable_gateway_ip'] = no_gateway_ip + return changes + + def _system_state_change(self, subnet, filters=None): + state = self.params['state'] + if state == 'present': + if not subnet: + return True + return bool(self._needs_update(subnet, filters)) + if state == 'absent' and subnet: + return True + return False + + def run(self): + + state = self.params['state'] + network_name = self.params['network_name'] + cidr = self.params['cidr'] + ip_version = self.params['ip_version'] + enable_dhcp = self.params['enable_dhcp'] + subnet_name = self.params['name'] + gateway_ip = self.params['gateway_ip'] + no_gateway_ip = self.params['no_gateway_ip'] + dns = self.params['dns_nameservers'] + pool_start = self.params['allocation_pool_start'] + pool_end = self.params['allocation_pool_end'] + host_routes = self.params['host_routes'] + ipv6_ra_mode = self.params['ipv6_ra_mode'] + ipv6_a_mode = self.params['ipv6_address_mode'] + use_default_subnetpool = self.params['use_default_subnetpool'] + project = self.params.pop('project') + extra_specs = self.params['extra_specs'] + + # Check for required parameters when state == 'present' + if state == 'present': + if not self.params['network_name']: + self.fail(msg='network_name required with present state') + if ( + not self.params['cidr'] + and not use_default_subnetpool + and not extra_specs.get('subnetpool_id', False) + ): + self.fail(msg='cidr or use_default_subnetpool or ' + 'subnetpool_id required with present state') + + if pool_start and pool_end: + pool = [dict(start=pool_start, end=pool_end)] + else: + pool = None + + if no_gateway_ip and gateway_ip: + self.fail_json(msg='no_gateway_ip is not allowed with gateway_ip') + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail_json(msg='Project %s could not be found' % project) + project_id = proj['id'] + filters = {'tenant_id': project_id} + else: + project_id = None + filters = None + + subnet = self.conn.get_subnet(subnet_name, filters=filters) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(subnet, filters)) + + if state == 'present': + if not subnet: + kwargs = dict( + cidr=cidr, + ip_version=ip_version, + enable_dhcp=enable_dhcp, + subnet_name=subnet_name, + gateway_ip=gateway_ip, + disable_gateway_ip=no_gateway_ip, + dns_nameservers=dns, + allocation_pools=pool, + host_routes=host_routes, + ipv6_ra_mode=ipv6_ra_mode, + ipv6_address_mode=ipv6_a_mode, + tenant_id=project_id) + dup_args = set(kwargs.keys()) & set(extra_specs.keys()) + if dup_args: + raise ValueError('Duplicate key(s) {0} in extra_specs' + .format(list(dup_args))) + if use_default_subnetpool: + kwargs['use_default_subnetpool'] = use_default_subnetpool + kwargs = dict(kwargs, **extra_specs) + subnet = self.conn.create_subnet(network_name, **kwargs) + changed = True + else: + changes = self._needs_update(subnet, filters) + if changes: + subnet = self.conn.update_subnet(subnet['id'], **changes) + changed = True + else: + changed = False + self.exit_json(changed=changed, + subnet=subnet, + id=subnet['id']) + + elif state == 'absent': + if not subnet: + changed = False + else: + changed = True + self.conn.delete_subnet(subnet_name) + self.exit_json(changed=changed) + + +def main(): + module = SubnetModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_subnets_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_subnets_info.py new file mode 100644 index 00000000..7a771b53 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_subnets_info.py @@ -0,0 +1,164 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: subnets_info +short_description: Retrieve information about one or more OpenStack subnets. +author: OpenStack Ansible SIG +description: + - Retrieve information about one or more subnets from OpenStack. + - This module was called C(openstack.cloud.subnets_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.subnets_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the subnet. + - Alias 'subnet' added in version 2.8. + required: false + aliases: ['subnet'] + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Gather information about previously created subnets + openstack.cloud.subnets_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + register: result + +- name: Show openstack subnets + debug: + msg: "{{ result.openstack_subnets }}" + +- name: Gather information about a previously created subnet by name + openstack.cloud.subnets_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + name: subnet1 + register: result + +- name: Show openstack subnets + debug: + msg: "{{ result.openstack_subnets }}" + +- name: Gather information about a previously created subnet with filter + # Note: name and filters parameters are not mutually exclusive + openstack.cloud.subnets_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + filters: + tenant_id: 55e2ce24b2a245b09f181bf025724cbe + register: result + +- name: Show openstack subnets + debug: + msg: "{{ result.openstack_subnets }}" +''' + +RETURN = ''' +openstack_subnets: + description: has all the openstack information about the subnets + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the subnet. + returned: success + type: str + network_id: + description: Network ID this subnet belongs in. + returned: success + type: str + cidr: + description: Subnet's CIDR. + returned: success + type: str + gateway_ip: + description: Subnet's gateway ip. + returned: success + type: str + enable_dhcp: + description: DHCP enable flag for this subnet. + returned: success + type: bool + ip_version: + description: IP version for this subnet. + returned: success + type: int + tenant_id: + description: Tenant id associated with this subnet. + returned: success + type: str + dns_nameservers: + description: DNS name servers for this subnet. + returned: success + type: list + elements: str + allocation_pools: + description: Allocation pools associated with this subnet. + returned: success + type: list + elements: dict +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class SubnetInfoModule(OpenStackModule): + + deprecated_names = ('subnets_facts', 'openstack.cloud.subnets_facts') + + argument_spec = dict( + name=dict(required=False, default=None, aliases=['subnet']), + filters=dict(required=False, type='dict', default=None) + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + kwargs = self.check_versioned( + filters=self.params['filters'] + ) + if self.params['name']: + kwargs['name_or_id'] = self.params['name'] + subnets = self.conn.search_subnets(**kwargs) + + self.exit(changed=False, openstack_subnets=subnets) + + +def main(): + module = SubnetInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_user.py b/ansible_collections/openstack/cloud/plugins/modules/os_user.py new file mode 100644 index 00000000..047b3ed8 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_user.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_user +short_description: Manage OpenStack Identity Users +author: OpenStack Ansible SIG +description: + - Manage OpenStack Identity users. Users can be created, + updated or deleted using this module. A user will be updated + if I(name) matches an existing user and I(state) is present. + The value for I(name) cannot be updated without deleting and + re-creating the user. +options: + name: + description: + - Username for the user + required: true + type: str + password: + description: + - Password for the user + type: str + update_password: + required: false + choices: ['always', 'on_create'] + default: on_create + description: + - C(always) will attempt to update password. C(on_create) will only + set the password for newly created users. + type: str + email: + description: + - Email address for the user + type: str + description: + description: + - Description about the user + type: str + default_project: + description: + - Project name or ID that the user should be associated with by default + type: str + domain: + description: + - Domain to create the user in if the cloud supports domains + type: str + enabled: + description: + - Is the user enabled + type: bool + default: 'yes' + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a user +- openstack.cloud.identity_user: + cloud: mycloud + state: present + name: demouser + password: secret + email: demo@example.com + domain: default + default_project: demo + +# Delete a user +- openstack.cloud.identity_user: + cloud: mycloud + state: absent + name: demouser + +# Create a user but don't update password if user exists +- openstack.cloud.identity_user: + cloud: mycloud + state: present + name: demouser + password: secret + update_password: on_create + email: demo@example.com + domain: default + default_project: demo + +# Create a user without password +- openstack.cloud.identity_user: + cloud: mycloud + state: present + name: demouser + email: demo@example.com + domain: default + default_project: demo +''' + + +RETURN = ''' +user: + description: Dictionary describing the user. + returned: On success when I(state) is 'present' + type: dict + contains: + default_project_id: + description: User default project ID. Only present with Keystone >= v3. + returned: success + type: str + sample: "4427115787be45f08f0ec22a03bfc735" + description: + description: The description of this user + returned: success + type: str + sample: "a user" + domain_id: + description: User domain ID. Only present with Keystone >= v3. + returned: success + type: str + sample: "default" + email: + description: User email address + returned: success + type: str + sample: "demo@example.com" + id: + description: User ID + returned: success + type: str + sample: "f59382db809c43139982ca4189404650" + enabled: + description: Indicates whether the user is enabled + type: bool + name: + description: Unique user name, within the owning domain + returned: success + type: str + sample: "demouser" + username: + description: Username with Identity API v2 (OpenStack Pike or earlier) else Null + returned: success + type: str + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityUserModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + password=dict(required=False, default=None, no_log=True), + email=dict(required=False, default=None), + default_project=dict(required=False, default=None), + description=dict(type='str'), + domain=dict(required=False, default=None), + enabled=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + update_password=dict(default='on_create', choices=['always', 'on_create']), + ) + + module_kwargs = dict() + + def _needs_update(self, params_dict, user): + for k in params_dict: + # We don't get password back in the user object, so assume any supplied + # password is a change. + if k == 'password': + return True + if k == 'default_project': + if user['default_project_id'] != params_dict['default_project']: + return True + else: + continue + if user[k] != params_dict[k]: + return True + return False + + def _get_domain_id(self, domain): + dom_obj = self.conn.identity.find_domain(domain) + if dom_obj is None: + # Ok, let's hope the user is non-admin and passing a sane id + return domain + return dom_obj.id + + def _get_default_project_id(self, default_project, domain_id): + project = self.conn.identity.find_project(default_project, domain_id=domain_id) + if not project: + self.fail_json(msg='Default project %s is not valid' % default_project) + return project['id'] + + def run(self): + name = self.params['name'] + password = self.params.get('password') + email = self.params['email'] + default_project = self.params['default_project'] + domain = self.params['domain'] + enabled = self.params['enabled'] + state = self.params['state'] + update_password = self.params['update_password'] + description = self.params['description'] + + if domain: + domain_id = self._get_domain_id(domain) + user = self.conn.get_user(name, domain_id=domain_id) + else: + domain_id = None + user = self.conn.get_user(name) + + changed = False + if state == 'present': + user_args = { + 'name': name, + 'email': email, + 'domain_id': domain_id, + 'description': description, + 'enabled': enabled, + } + if default_project: + default_project_id = self._get_default_project_id( + default_project, domain_id) + user_args['default_project'] = default_project_id + user_args = {k: v for k, v in user_args.items() if v is not None} + + changed = False + if user is None: + if password: + user_args['password'] = password + + user = self.conn.create_user(**user_args) + changed = True + else: + if update_password == 'always': + if not password: + self.fail_json(msg="update_password is always but a password value is missing") + user_args['password'] = password + + if self._needs_update(user_args, user): + user = self.conn.update_user(user['id'], **user_args) + changed = True + + self.exit_json(changed=changed, user=user) + elif state == 'absent' and user is not None: + self.conn.identity.delete_user(user['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityUserModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_user_group.py b/ansible_collections/openstack/cloud/plugins/modules/os_user_group.py new file mode 100644 index 00000000..ce8f28e1 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_user_group.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: group_assignment +short_description: Associate OpenStack Identity users and groups +author: OpenStack Ansible SIG +description: + - Add and remove users from groups +options: + user: + description: + - Name or id for the user + required: true + type: str + group: + description: + - Name or id for the group. + required: true + type: str + state: + description: + - Should the user be present or absent in the group + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Add the demo user to the demo group +- openstack.cloud.group_assignment: + cloud: mycloud + user: demo + group: demo +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityGroupAssignment(OpenStackModule): + argument_spec = dict( + user=dict(required=True), + group=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _system_state_change(self, state, in_group): + if state == 'present' and not in_group: + return True + if state == 'absent' and in_group: + return True + return False + + def run(self): + user = self.params['user'] + group = self.params['group'] + state = self.params['state'] + + in_group = self.conn.is_user_in_group(user, group) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, in_group)) + + changed = False + if state == 'present': + if not in_group: + self.conn.add_user_to_group(user, group) + changed = True + + elif state == 'absent': + if in_group: + self.conn.remove_user_from_group(user, group) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = IdentityGroupAssignment() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_user_info.py b/ansible_collections/openstack/cloud/plugins/modules/os_user_info.py new file mode 100644 index 00000000..c0e0d949 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_user_info.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: identity_user_info +short_description: Retrieve information about one or more OpenStack users +author: OpenStack Ansible SIG +description: + - Retrieve information about a one or more OpenStack users + - This module was called C(openstack.cloud.identity_user_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.identity_user_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the user + type: str + domain: + description: + - Name or ID of the domain containing the user if the cloud supports domains + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about previously created users +- openstack.cloud.identity_user_info: + cloud: awesomecloud + register: result +- debug: + msg: "{{ result.openstack_users }}" + +# Gather information about a previously created user by name +- openstack.cloud.identity_user_info: + cloud: awesomecloud + name: demouser + register: result +- debug: + msg: "{{ result.openstack_users }}" + +# Gather information about a previously created user in a specific domain +- openstack.cloud.identity_user_info: + cloud: awesomecloud + name: demouser + domain: admindomain + register: result +- debug: + msg: "{{ result.openstack_users }}" + +# Gather information about a previously created user in a specific domain with filter +- openstack.cloud.identity_user_info: + cloud: awesomecloud + name: demouser + domain: admindomain + filters: + enabled: False + register: result +- debug: + msg: "{{ result.openstack_users }}" +''' + + +RETURN = ''' +openstack_users: + description: has all the OpenStack information about users + returned: always + type: list + elements: dict + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Username of the user. + returned: success + type: str + default_project_id: + description: Default project ID of the user + returned: success + type: str + description: + description: The description of this user + returned: success + type: str + domain_id: + description: Domain ID containing the user + returned: success + type: str + email: + description: Email of the user + returned: success + type: str + enabled: + description: Flag to indicate if the user is enabled + returned: success + type: bool + username: + description: Username with Identity API v2 (OpenStack Pike or earlier) else Null + returned: success + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityUserInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(required=False, default=None), + domain=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + deprecated_names = ('openstack.cloud.identity_user_facts') + + def run(self): + name = self.params['name'] + domain = self.params['domain'] + filters = self.params['filters'] + + args = {} + if domain: + dom_obj = self.conn.identity.find_domain(domain) + if dom_obj is None: + self.fail_json( + msg="Domain name or ID '{0}' does not exist".format(domain)) + args['domain_id'] = dom_obj.id + + users = self.conn.search_users(name, filters, **args) + self.exit_json(changed=False, openstack_users=users) + + +def main(): + module = IdentityUserInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_user_role.py b/ansible_collections/openstack/cloud/plugins/modules/os_user_role.py new file mode 100644 index 00000000..5ad7dce4 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_user_role.py @@ -0,0 +1,190 @@ +#!/usr/bin/python +# Copyright (c) 2016 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: role_assignment +short_description: Associate OpenStack Identity users and roles +author: OpenStack Ansible SIG +description: + - Grant and revoke roles in either project or domain context for + OpenStack Identity Users. +options: + role: + description: + - Name or ID for the role. + required: true + type: str + user: + description: + - Name or ID for the user. If I(user) is not specified, then + I(group) is required. Both may not be specified. + type: str + group: + description: + - Name or ID for the group. Valid only with keystone version 3. + If I(group) is not specified, then I(user) is required. Both + may not be specified. + type: str + project: + description: + - Name or ID of the project to scope the role association to. + If you are using keystone version 2, then this value is required. + type: str + domain: + description: + - Name or ID of the domain to scope the role association to. Valid only + with keystone version 3, and required if I(project) is not specified. + type: str + state: + description: + - Should the roles be present or absent on the user. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Grant an admin role on the user admin in the project project1 +- openstack.cloud.role_assignment: + cloud: mycloud + user: admin + role: admin + project: project1 + +# Revoke the admin role from the user barney in the newyork domain +- openstack.cloud.role_assignment: + cloud: mycloud + state: absent + user: barney + role: admin + domain: newyork +''' + +RETURN = ''' +# +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityRoleAssignmentModule(OpenStackModule): + argument_spec = dict( + role=dict(required=True), + user=dict(required=False), + group=dict(required=False), + project=dict(required=False), + domain=dict(required=False), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + required_one_of=[ + ['user', 'group'] + ], + supports_check_mode=True + ) + + def _system_state_change(self, state, assignment): + if state == 'present' and not assignment: + return True + elif state == 'absent' and assignment: + return True + return False + + def _build_kwargs(self, user, group, project, domain): + kwargs = {} + if user: + kwargs['user'] = user + if group: + kwargs['group'] = group + if project: + kwargs['project'] = project + if domain: + kwargs['domain'] = domain + return kwargs + + def run(self): + role = self.params.get('role') + user = self.params.get('user') + group = self.params.get('group') + project = self.params.get('project') + domain = self.params.get('domain') + state = self.params.get('state') + + filters = {} + find_filters = {} + domain_id = None + + r = self.conn.identity.find_role(role) + if r is None: + self.fail_json(msg="Role %s is not valid" % role) + filters['role'] = r['id'] + + if domain: + d = self.conn.identity.find_domain(domain) + if d is None: + self.fail_json(msg="Domain %s is not valid" % domain) + domain_id = d['id'] + find_filters['domain_id'] = domain_id + if user: + u = self.conn.identity.find_user(user, **find_filters) + if u is None: + self.fail_json(msg="User %s is not valid" % user) + filters['user'] = u['id'] + + if group: + # self.conn.identity.find_group() does not accept + # a domain_id argument in Train's openstacksdk + g = self.conn.get_group(group, **find_filters) + if g is None: + self.fail_json(msg="Group %s is not valid" % group) + filters['group'] = g['id'] + if project: + p = self.conn.identity.find_project(project, **find_filters) + if p is None: + self.fail_json(msg="Project %s is not valid" % project) + filters['project'] = p['id'] + + # Keeping the self.conn.list_role_assignments because it calls directly + # the identity.role_assignments and there are some logics for the + # filters that won't worth rewrite here. + assignment = self.conn.list_role_assignments(filters=filters) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, assignment)) + + changed = False + + # Both grant_role and revoke_role calls directly the proxy layer, and + # has some logic that won't worth to rewrite here so keeping it is a + # good idea + if state == 'present': + if not assignment: + kwargs = self._build_kwargs(user, group, project, domain_id) + self.conn.grant_role(role, **kwargs) + changed = True + + elif state == 'absent': + if assignment: + kwargs = self._build_kwargs(user, group, project, domain_id) + self.conn.revoke_role(role, **kwargs) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = IdentityRoleAssignmentModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_volume.py b/ansible_collections/openstack/cloud/plugins/modules/os_volume.py new file mode 100644 index 00000000..3a50c05a --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_volume.py @@ -0,0 +1,263 @@ +#!/usr/bin/python + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: volume +short_description: Create/Delete Cinder Volumes +author: OpenStack Ansible SIG +description: + - Create or Remove cinder block storage volumes +options: + size: + description: + - Size of volume in GB. This parameter is required when the + I(state) parameter is 'present'. + type: int + display_name: + description: + - Name of volume + required: true + type: str + aliases: [name] + display_description: + description: + - String describing the volume + type: str + aliases: [description] + volume_type: + description: + - Volume type for volume + type: str + image: + description: + - Image name or id for boot from volume + type: str + snapshot_id: + description: + - Volume snapshot id to create from + type: str + volume: + description: + - Volume name or id to create from + type: str + bootable: + description: + - Bootable flag for volume. + type: bool + default: False + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + scheduler_hints: + description: + - Scheduler hints passed to volume API in form of dict + type: dict + metadata: + description: + - Metadata for the volume + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Creates a new volume +- name: create a volume + hosts: localhost + tasks: + - name: create 40g test volume + openstack.cloud.volume: + state: present + cloud: mordred + availability_zone: az2 + size: 40 + display_name: test_volume + scheduler_hints: + same_host: 243e8d3c-8f47-4a61-93d6-7215c344b0c0 +''' + +RETURNS = ''' +id: + description: Cinder's unique ID for this volume + returned: always + type: str + sample: fcc4ac1c-e249-4fe7-b458-2138bfb44c06 + +volume: + description: Cinder's representation of the volume object + returned: always + type: dict + sample: {'...'} +''' +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class VolumeModule(OpenStackModule): + + argument_spec = dict( + size=dict(type='int'), + volume_type=dict(type='str'), + display_name=dict(required=True, aliases=['name'], type='str'), + display_description=dict(aliases=['description'], type='str'), + image=dict(type='str'), + snapshot_id=dict(type='str'), + volume=dict(type='str'), + state=dict(default='present', choices=['absent', 'present'], type='str'), + scheduler_hints=dict(type='dict'), + metadata=dict(type='dict'), + bootable=dict(type='bool', default=False) + ) + + module_kwargs = dict( + mutually_exclusive=[ + ['image', 'snapshot_id', 'volume'], + ], + required_if=[ + ['state', 'present', ['size']], + ], + ) + + def _needs_update(self, volume): + ''' + check for differences in updatable values, at the moment + openstacksdk only supports extending the volume size, this + may change in the future. + :returns: bool + ''' + compare_simple = ['size'] + + for k in compare_simple: + if self.params[k] is not None and self.params[k] != volume.get(k): + return True + + return False + + def _modify_volume(self, volume): + ''' + modify volume, the only modification to an existing volume + available at the moment is extending the size, this is + limited by the openstacksdk and may change whenever the + functionality is extended. + ''' + volume = self.conn.get_volume(self.params['display_name']) + diff = {'before': volume, 'after': ''} + size = self.params['size'] + + if size < volume.get('size'): + self.fail_json( + msg='Cannot shrink volumes, size: {0} < {1}'.format(size, volume.get('size')) + ) + + if not self._needs_update(volume): + diff['after'] = volume + self.exit_json(changed=False, id=volume['id'], volume=volume, diff=diff) + + if self.ansible.check_mode: + diff['after'] = volume + self.exit_json(changed=True, id=volume['id'], volume=volume, diff=diff) + + self.conn.volume.extend_volume( + volume.id, + size + ) + diff['after'] = self.conn.get_volume(self.params['display_name']) + self.exit_json(changed=True, id=volume['id'], volume=volume, diff=diff) + + def _present_volume(self): + + diff = {'before': '', 'after': ''} + + volume_args = dict( + size=self.params['size'], + volume_type=self.params['volume_type'], + display_name=self.params['display_name'], + display_description=self.params['display_description'], + snapshot_id=self.params['snapshot_id'], + bootable=self.params['bootable'], + availability_zone=self.params['availability_zone'], + ) + if self.params['image']: + image_id = self.conn.get_image_id(self.params['image']) + if not image_id: + self.fail_json(msg="Failed to find image '%s'" % self.params['image']) + volume_args['imageRef'] = image_id + + if self.params['volume']: + volume_id = self.conn.get_volume_id(self.params['volume']) + if not volume_id: + self.fail_json(msg="Failed to find volume '%s'" % self.params['volume']) + volume_args['source_volid'] = volume_id + + if self.params['scheduler_hints']: + volume_args['scheduler_hints'] = self.params['scheduler_hints'] + + if self.params['metadata']: + volume_args['metadata'] = self.params['metadata'] + + if self.ansible.check_mode: + diff['after'] = volume_args + self.exit_json(changed=True, id=None, volume=volume_args, diff=diff) + + volume = self.conn.create_volume( + wait=self.params['wait'], timeout=self.params['timeout'], + **volume_args) + diff['after'] = volume + self.exit_json(changed=True, id=volume['id'], volume=volume, diff=diff) + + def _absent_volume(self, volume): + changed = False + diff = {'before': '', 'after': ''} + + if self.conn.volume_exists(self.params['display_name']): + volume = self.conn.get_volume(self.params['display_name']) + diff['before'] = volume + + if self.ansible.check_mode: + self.exit_json(changed=True, diff=diff) + + try: + changed = self.conn.delete_volume(name_or_id=self.params['display_name'], + wait=self.params['wait'], + timeout=self.params['timeout']) + except self.sdk.exceptions.ResourceTimeout: + diff['after'] = volume + self.exit_json(changed=changed, diff=diff) + + self.exit_json(changed=changed, diff=diff) + + def run(self): + + state = self.params['state'] + if self.conn.volume_exists(self.params['display_name']): + volume = self.conn.get_volume(self.params['display_name']) + else: + volume = None + + if state == 'present': + if not volume: + self._present_volume() + elif self._needs_update(volume): + self._modify_volume(volume) + else: + self.exit_json(changed=False, id=volume['id'], volume=volume) + if state == 'absent': + self._absent_volume(volume) + + +def main(): + module = VolumeModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_volume_snapshot.py b/ansible_collections/openstack/cloud/plugins/modules/os_volume_snapshot.py new file mode 100644 index 00000000..8625984c --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_volume_snapshot.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2016, Mario Santos <mario.rf.santos@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: volume_snapshot +short_description: Create/Delete Cinder Volume Snapshots +author: OpenStack Ansible SIG +description: + - Create or Delete cinder block storage volume snapshots +options: + display_name: + description: + - Name of the snapshot + required: true + aliases: ['name'] + type: str + display_description: + description: + - String describing the snapshot + aliases: ['description'] + type: str + volume: + description: + - The volume name or id to create/delete the snapshot + required: True + type: str + force: + description: + - Allows or disallows snapshot of a volume to be created when the volume + is attached to an instance. + type: bool + default: 'no' + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Creates a snapshot on volume 'test_volume' +- name: create and delete snapshot + hosts: localhost + tasks: + - name: create snapshot + openstack.cloud.volume_snapshot: + state: present + cloud: mordred + availability_zone: az2 + display_name: test_snapshot + volume: test_volume + - name: delete snapshot + openstack.cloud.volume_snapshot: + state: absent + cloud: mordred + availability_zone: az2 + display_name: test_snapshot + volume: test_volume +''' + +RETURN = ''' +snapshot: + description: The snapshot instance after the change + returned: success + type: dict + sample: + id: 837aca54-c0ee-47a2-bf9a-35e1b4fdac0c + name: test_snapshot + volume_id: ec646a7c-6a35-4857-b38b-808105a24be6 + size: 2 + status: available + display_name: test_snapshot +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class VolumeSnapshotModule(OpenStackModule): + argument_spec = dict( + display_name=dict(required=True, aliases=['name']), + display_description=dict(default=None, aliases=['description']), + volume=dict(required=True), + force=dict(required=False, default=False, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _present_volume_snapshot(self): + volume = self.conn.get_volume(self.params['volume']) + snapshot = self.conn.get_volume_snapshot( + self.params['display_name'], filters={'volume_id': volume.id}) + if not snapshot: + snapshot = self.conn.create_volume_snapshot( + volume.id, + force=self.params['force'], + wait=self.params['wait'], + timeout=self.params['timeout'], + name=self.params['display_name'], + description=self.params.get('display_description') + ) + self.exit_json(changed=True, snapshot=snapshot) + else: + self.exit_json(changed=False, snapshot=snapshot) + + def _absent_volume_snapshot(self): + volume = self.conn.get_volume(self.params['volume']) + snapshot = self.conn.get_volume_snapshot( + self.params['display_name'], filters={'volume_id': volume.id}) + if not snapshot: + self.exit_json(changed=False) + else: + self.conn.delete_volume_snapshot( + name_or_id=snapshot.id, + wait=self.params['wait'], + timeout=self.params['timeout'], + ) + self.exit_json(changed=True, snapshot_id=snapshot.id) + + def _system_state_change(self): + volume = self.conn.get_volume(self.params['volume']) + snapshot = self.conn.get_volume_snapshot( + self.params['display_name'], + filters={'volume_id': volume.id}) + state = self.params['state'] + + if state == 'present': + return snapshot is None + if state == 'absent': + return snapshot is not None + + def run(self): + state = self.params['state'] + + if self.conn.volume_exists(self.params['volume']): + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change()) + if state == 'present': + self._present_volume_snapshot() + if state == 'absent': + self._absent_volume_snapshot() + else: + self.fail_json( + msg="No volume with name or id '{0}' was found.".format( + self.params['volume'])) + + +def main(): + module = VolumeSnapshotModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/os_zone.py b/ansible_collections/openstack/cloud/plugins/modules/os_zone.py new file mode 100644 index 00000000..98cf655e --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/os_zone.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: dns_zone +short_description: Manage OpenStack DNS zones +author: OpenStack Ansible SIG +description: + - Manage OpenStack DNS zones. Zones can be created, deleted or + updated. Only the I(email), I(description), I(ttl) and I(masters) values + can be updated. +options: + name: + description: + - Zone name + required: true + type: str + zone_type: + description: + - Zone type + choices: [primary, secondary] + type: str + email: + description: + - Email of the zone owner (only applies if zone_type is primary) + type: str + description: + description: + - Zone description + type: str + ttl: + description: + - TTL (Time To Live) value in seconds + type: int + masters: + description: + - Master nameservers (only applies if zone_type is secondary) + type: list + elements: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a zone named "example.net" +- openstack.cloud.dns_zone: + cloud: mycloud + state: present + name: example.net. + zone_type: primary + email: test@example.net + description: Test zone + ttl: 3600 + +# Update the TTL on existing "example.net." zone +- openstack.cloud.dns_zone: + cloud: mycloud + state: present + name: example.net. + ttl: 7200 + +# Delete zone named "example.net." +- openstack.cloud.dns_zone: + cloud: mycloud + state: absent + name: example.net. +''' + +RETURN = ''' +zone: + description: Dictionary describing the zone. + returned: On success when I(state) is 'present'. + type: complex + contains: + id: + description: Unique zone ID + type: str + sample: "c1c530a3-3619-46f3-b0f6-236927b2618c" + name: + description: Zone name + type: str + sample: "example.net." + type: + description: Zone type + type: str + sample: "PRIMARY" + email: + description: Zone owner email + type: str + sample: "test@example.net" + description: + description: Zone description + type: str + sample: "Test description" + ttl: + description: Zone TTL value + type: int + sample: 3600 + masters: + description: Zone master nameservers + type: list + sample: [] +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class DnsZoneModule(OpenStackModule): + + argument_spec = dict( + name=dict(required=True, type='str'), + zone_type=dict(required=False, choices=['primary', 'secondary'], type='str'), + email=dict(required=False, type='str'), + description=dict(required=False, type='str'), + ttl=dict(required=False, type='int'), + masters=dict(required=False, type='list', elements='str'), + state=dict(default='present', choices=['absent', 'present'], type='str'), + ) + + def _system_state_change(self, state, email, description, ttl, masters, zone): + if state == 'present': + if not zone: + return True + if email is not None and zone.email != email: + return True + if description is not None and zone.description != description: + return True + if ttl is not None and zone.ttl != ttl: + return True + if masters is not None and zone.masters != masters: + return True + if state == 'absent' and zone: + return True + return False + + def _wait(self, timeout, zone, state): + """Wait for a zone to reach the desired state for the given state.""" + + for count in self.sdk.utils.iterate_timeout( + timeout, + "Timeout waiting for zone to be %s" % state): + + if (state == 'absent' and zone is None) or (state == 'present' and zone and zone.status == 'ACTIVE'): + return + + try: + zone = self.conn.get_zone(zone.id) + except Exception: + continue + + if zone and zone.status == 'ERROR': + self.fail_json(msg="Zone reached ERROR state while waiting for it to be %s" % state) + + def run(self): + + name = self.params['name'] + state = self.params['state'] + wait = self.params['wait'] + timeout = self.params['timeout'] + + zone = self.conn.get_zone(name) + + if state == 'present': + + zone_type = self.params['zone_type'] + email = self.params['email'] + description = self.params['description'] + ttl = self.params['ttl'] + masters = self.params['masters'] + + kwargs = {} + + if email: + kwargs['email'] = email + if description: + kwargs['description'] = description + if ttl: + kwargs['ttl'] = ttl + if masters: + kwargs['masters'] = masters + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, email, + description, ttl, + masters, zone)) + + if zone is None: + zone = self.conn.create_zone( + name=name, zone_type=zone_type, **kwargs) + changed = True + else: + if masters is None: + masters = [] + + pre_update_zone = zone + changed = self._system_state_change(state, email, + description, ttl, + masters, pre_update_zone) + if changed: + zone = self.conn.update_zone( + name, **kwargs) + + if wait: + self._wait(timeout, zone, state) + + self.exit_json(changed=changed, zone=zone) + + elif state == 'absent': + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, None, + None, None, + None, zone)) + + if zone is None: + changed = False + else: + self.conn.delete_zone(name) + changed = True + + if wait: + self._wait(timeout, zone, state) + + self.exit_json(changed=changed) + + +def main(): + module = DnsZoneModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/port.py b/ansible_collections/openstack/cloud/plugins/modules/port.py new file mode 100644 index 00000000..accef4fc --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/port.py @@ -0,0 +1,530 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: port +short_description: Add/Update/Delete ports from an OpenStack cloud. +author: OpenStack Ansible SIG +description: + - Add, Update or Remove ports from an OpenStack cloud. A I(state) of + 'present' will ensure the port is created or updated if required. +options: + network: + description: + - Network ID or name this port belongs to. + - Required when creating a new port. + type: str + name: + description: + - Name that has to be given to the port. + type: str + fixed_ips: + description: + - Desired IP and/or subnet for this port. Subnet is referenced by + subnet_id and IP is referenced by ip_address. + type: list + elements: dict + suboptions: + ip_address: + description: The fixed IP address to attempt to allocate. + required: true + type: str + subnet_id: + description: The subnet to attach the IP address to. + type: str + admin_state_up: + description: + - Sets admin state. + type: bool + mac_address: + description: + - MAC address of this port. + type: str + security_groups: + description: + - Security group(s) ID(s) or name(s) associated with the port (comma + separated string or YAML list) + type: list + elements: str + no_security_groups: + description: + - Do not associate a security group with this port. + type: bool + default: 'no' + allowed_address_pairs: + description: + - "Allowed address pairs list. Allowed address pairs are supported with + dictionary structure. + e.g. allowed_address_pairs: + - ip_address: 10.1.0.12 + mac_address: ab:cd:ef:12:34:56 + - ip_address: ..." + type: list + elements: dict + suboptions: + ip_address: + description: The IP address. + type: str + mac_address: + description: The MAC address. + type: str + extra_dhcp_opts: + description: + - "Extra dhcp options to be assigned to this port. Extra options are + supported with dictionary structure. Note that options cannot be removed + only updated. + e.g. extra_dhcp_opts: + - opt_name: opt name1 + opt_value: value1 + ip_version: 4 + - opt_name: ..." + type: list + elements: dict + suboptions: + opt_name: + description: The name of the DHCP option to set. + type: str + required: true + opt_value: + description: The value of the DHCP option to set. + type: str + required: true + ip_version: + description: The IP version this DHCP option is for. + type: int + required: true + device_owner: + description: + - The ID of the entity that uses this port. + type: str + device_id: + description: + - Device ID of device using this port. + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + vnic_type: + description: + - The type of the port that should be created + choices: [normal, direct, direct-physical, macvtap, baremetal, virtio-forwarder] + type: str + port_security_enabled: + description: + - Whether to enable or disable the port security on the network. + type: bool + binding_profile: + description: + - Binding profile dict that the port should be created with. + type: dict + dns_name: + description: + - The dns name of the port ( only with dns-integration enabled ) + type: str + dns_domain: + description: + - The dns domain of the port ( only with dns-integration enabled ) + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a port +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + +# Create a port with a static IP +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + fixed_ips: + - ip_address: 10.1.0.21 + +# Create a port with No security groups +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + no_security_groups: True + +# Update the existing 'port1' port with multiple security groups (version 1) +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + security_groups: 1496e8c7-4918-482a-9172-f4f00fc4a3a5,057d4bdf-6d4d-472... + +# Update the existing 'port1' port with multiple security groups (version 2) +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + security_groups: + - 1496e8c7-4918-482a-9172-f4f00fc4a3a5 + - 057d4bdf-6d4d-472... + +# Create port of type 'direct' +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + vnic_type: direct + +# Create a port with binding profile +- openstack.cloud.port: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: port1 + network: foo + binding_profile: + "pci_slot": "0000:03:11.1" + "physical_network": "provider" +''' + +RETURN = ''' +id: + description: Unique UUID. + returned: success + type: str +name: + description: Name given to the port. + returned: success + type: str +network_id: + description: Network ID this port belongs in. + returned: success + type: str +security_groups: + description: Security group(s) associated with this port. + returned: success + type: list +status: + description: Port's status. + returned: success + type: str +fixed_ips: + description: Fixed ip(s) associated with this port. + returned: success + type: list +tenant_id: + description: Tenant id associated with this port. + returned: success + type: str +allowed_address_pairs: + description: Allowed address pairs with this port. + returned: success + type: list +admin_state_up: + description: Admin state up flag for this port. + returned: success + type: bool +vnic_type: + description: Type of the created port + returned: success + type: str +port_security_enabled: + description: Port security state on the network. + returned: success + type: bool +binding:profile: + description: Port binded profile + returned: success + type: dict +''' + +from ansible.module_utils.basic import missing_required_lib +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + +try: + from collections import OrderedDict + HAS_ORDEREDDICT = True +except ImportError: + try: + from ordereddict import OrderedDict + HAS_ORDEREDDICT = True + except ImportError: + HAS_ORDEREDDICT = False + + +class NetworkPortModule(OpenStackModule): + argument_spec = dict( + network=dict(required=False), + name=dict(required=False), + fixed_ips=dict(type='list', default=None, elements='dict'), + admin_state_up=dict(type='bool', default=None), + mac_address=dict(default=None), + security_groups=dict(default=None, type='list', elements='str'), + no_security_groups=dict(default=False, type='bool'), + allowed_address_pairs=dict(type='list', default=None, elements='dict'), + extra_dhcp_opts=dict(type='list', default=None, elements='dict'), + device_owner=dict(default=None), + device_id=dict(default=None), + state=dict(default='present', choices=['absent', 'present']), + vnic_type=dict(default=None, + choices=['normal', 'direct', 'direct-physical', + 'macvtap', 'baremetal', 'virtio-forwarder']), + port_security_enabled=dict(default=None, type='bool'), + binding_profile=dict(default=None, type='dict'), + dns_name=dict(type='str', default=None), + dns_domain=dict(type='str', default=None) + ) + + module_kwargs = dict( + mutually_exclusive=[ + ['no_security_groups', 'security_groups'], + ], + supports_check_mode=True + ) + + def _is_dns_integration_enabled(self): + """ Check if dns-integraton is enabled """ + for ext in self.conn.network.extensions(): + if ext.alias == 'dns-integration': + return True + return False + + def _needs_update(self, port): + """Check for differences in the updatable values. + + NOTE: We don't currently allow name updates. + """ + compare_simple = ['admin_state_up', + 'mac_address', + 'device_owner', + 'device_id', + 'binding:vnic_type', + 'port_security_enabled', + 'binding:profile'] + compare_dns = ['dns_name', 'dns_domain'] + compare_list_dict = ['allowed_address_pairs', + 'extra_dhcp_opts'] + compare_list = ['security_groups'] + + if self.conn.has_service('dns') and \ + self._is_dns_integration_enabled(): + for key in compare_dns: + if self.params[key] is not None and \ + self.params[key] != port[key]: + return True + + for key in compare_simple: + if self.params[key] is not None and self.params[key] != port[key]: + return True + for key in compare_list: + if ( + self.params[key] is not None + and set(self.params[key]) != set(port[key]) + ): + return True + + for key in compare_list_dict: + if not self.params[key]: + if port.get(key): + return True + + if self.params[key]: + if not port.get(key): + return True + + # sort dicts in list + port_ordered = [OrderedDict(sorted(d.items())) for d in port[key]] + param_ordered = [OrderedDict(sorted(d.items())) for d in self.params[key]] + + for d in param_ordered: + if d not in port_ordered: + return True + + for d in port_ordered: + if d not in param_ordered: + return True + + # NOTE: if port was created or updated with 'no_security_groups=True', + # subsequent updates without 'no_security_groups' flag or + # 'no_security_groups=False' and no specified 'security_groups', will not + # result in an update to the port where the default security group is + # applied. + if self.params['no_security_groups'] and port['security_groups'] != []: + return True + + if self.params['fixed_ips'] is not None: + for item in self.params['fixed_ips']: + if 'ip_address' in item: + # if ip_address in request does not match any in existing port, + # update is required. + if not any(match['ip_address'] == item['ip_address'] + for match in port['fixed_ips']): + return True + if 'subnet_id' in item: + return True + for item in port['fixed_ips']: + # if ip_address in existing port does not match any in request, + # update is required. + if not any(match.get('ip_address') == item['ip_address'] + for match in self.params['fixed_ips']): + return True + + return False + + def _system_state_change(self, port): + state = self.params['state'] + if state == 'present': + if not port: + return True + return self._needs_update(port) + if state == 'absent' and port: + return True + return False + + def _compose_port_args(self): + port_kwargs = {} + optional_parameters = ['name', + 'fixed_ips', + 'admin_state_up', + 'mac_address', + 'security_groups', + 'allowed_address_pairs', + 'extra_dhcp_opts', + 'device_owner', + 'device_id', + 'binding:vnic_type', + 'port_security_enabled', + 'binding:profile'] + + if self.conn.has_service('dns') and \ + self._is_dns_integration_enabled(): + optional_parameters.extend(['dns_name', 'dns_domain']) + + for optional_param in optional_parameters: + if self.params[optional_param] is not None: + port_kwargs[optional_param] = self.params[optional_param] + + if self.params['no_security_groups']: + port_kwargs['security_groups'] = [] + + return port_kwargs + + def get_security_group_id(self, security_group_name_or_id): + security_group = self.conn.get_security_group(security_group_name_or_id) + if not security_group: + self.fail_json(msg="Security group: %s, was not found" + % security_group_name_or_id) + return security_group['id'] + + def run(self): + if not HAS_ORDEREDDICT: + self.fail_json(msg=missing_required_lib('ordereddict')) + + name = self.params['name'] + state = self.params['state'] + + if self.params['security_groups']: + # translate security_groups to UUID's if names where provided + self.params['security_groups'] = [ + self.get_security_group_id(v) + for v in self.params['security_groups'] + ] + + # Neutron API accept 'binding:vnic_type' as an argument + # for the port type. + self.params['binding:vnic_type'] = self.params.pop('vnic_type') + # Neutron API accept 'binding:profile' as an argument + # for the port binding profile type. + self.params['binding:profile'] = self.params.pop('binding_profile') + + port = None + network_id = None + if name: + port = self.conn.get_port(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(port)) + + changed = False + if state == 'present': + if not port: + network = self.params['network'] + if not network: + self.fail_json( + msg="Parameter 'network' is required in Port Create" + ) + port_kwargs = self._compose_port_args() + network_object = self.conn.get_network(network) + + if network_object: + network_id = network_object['id'] + else: + self.fail_json( + msg="Specified network was not found." + ) + + port_kwargs['network_id'] = network_id + port = self.conn.network.create_port(**port_kwargs) + changed = True + else: + if self._needs_update(port): + port_kwargs = self._compose_port_args() + port = self.conn.network.update_port(port['id'], + **port_kwargs) + changed = True + self.exit_json(changed=changed, id=port['id'], port=port) + + if state == 'absent': + if port: + self.conn.delete_port(port['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = NetworkPortModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/port_info.py b/ansible_collections/openstack/cloud/plugins/modules/port_info.py new file mode 100644 index 00000000..0ed3f059 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/port_info.py @@ -0,0 +1,210 @@ +#!/usr/bin/python + +# Copyright (c) 2016 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +module: port_info +short_description: Retrieve information about ports within OpenStack. +author: OpenStack Ansible SIG +description: + - Retrieve information about ports from OpenStack. + - This module was called C(openstack.cloud.port_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.port_info) module no longer returns C(ansible_facts)! +options: + port: + description: + - Unique name or ID of a port. + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements + of this dictionary will be matched against the returned port + dictionaries. Matching is currently limited to strings within + the port dictionary, or strings within nested dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about all ports +- openstack.cloud.port_info: + cloud: mycloud + register: result + +- debug: + msg: "{{ result.openstack_ports }}" + +# Gather information about a single port +- openstack.cloud.port_info: + cloud: mycloud + port: 6140317d-e676-31e1-8a4a-b1913814a471 + +# Gather information about all ports that have device_id set to a specific value +# and with a status of ACTIVE. +- openstack.cloud.port_info: + cloud: mycloud + filters: + device_id: 1038a010-3a37-4a9d-82ea-652f1da36597 + status: ACTIVE +''' + +RETURN = ''' +openstack_ports: + description: List of port dictionaries. A subset of the dictionary keys + listed below may be returned, depending on your cloud provider. + returned: always, but can be null + type: complex + contains: + admin_state_up: + description: The administrative state of the router, which is + up (true) or down (false). + returned: success + type: bool + sample: true + allowed_address_pairs: + description: A set of zero or more allowed address pairs. An + address pair consists of an IP address and MAC address. + returned: success + type: list + sample: [] + "binding:host_id": + description: The UUID of the host where the port is allocated. + returned: success + type: str + sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759" + "binding:profile": + description: A dictionary the enables the application running on + the host to pass and receive VIF port-specific + information to the plug-in. + returned: success + type: dict + sample: {} + "binding:vif_details": + description: A dictionary that enables the application to pass + information about functions that the Networking API + provides. + returned: success + type: dict + sample: {"port_filter": true} + "binding:vif_type": + description: The VIF type for the port. + returned: success + type: dict + sample: "ovs" + "binding:vnic_type": + description: The virtual network interface card (vNIC) type that is + bound to the neutron port. + returned: success + type: str + sample: "normal" + device_id: + description: The UUID of the device that uses this port. + returned: success + type: str + sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759" + device_owner: + description: The UUID of the entity that uses this port. + returned: success + type: str + sample: "network:router_interface" + dns_assignment: + description: DNS assignment information. + returned: success + type: list + dns_name: + description: DNS name + returned: success + type: str + sample: "" + extra_dhcp_opts: + description: A set of zero or more extra DHCP option pairs. + An option pair consists of an option value and name. + returned: success + type: list + sample: [] + fixed_ips: + description: The IP addresses for the port. Includes the IP address + and UUID of the subnet. + returned: success + type: list + id: + description: The UUID of the port. + returned: success + type: str + sample: "3ec25c97-7052-4ab8-a8ba-92faf84148de" + ip_address: + description: The IP address. + returned: success + type: str + sample: "127.0.0.1" + mac_address: + description: The MAC address. + returned: success + type: str + sample: "00:00:5E:00:53:42" + name: + description: The port name. + returned: success + type: str + sample: "port_name" + network_id: + description: The UUID of the attached network. + returned: success + type: str + sample: "dd1ede4f-3952-4131-aab6-3b8902268c7d" + port_security_enabled: + description: The port security status. The status is enabled (true) or disabled (false). + returned: success + type: bool + sample: false + security_groups: + description: The UUIDs of any attached security groups. + returned: success + type: list + status: + description: The port status. + returned: success + type: str + sample: "ACTIVE" + tenant_id: + description: The UUID of the tenant who owns the network. + returned: success + type: str + sample: "51fce036d7984ba6af4f6c849f65ef00" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class NetworkPortInfoModule(OpenStackModule): + argument_spec = dict( + port=dict(required=False), + filters=dict(type='dict', required=False), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + deprecated_names = ('openstack.cloud.port_facts') + + def run(self): + port = self.params.get('port') + filters = self.params.get('filters') + + ports = self.conn.search_ports(port, filters) + self.exit_json(changed=False, openstack_ports=ports) + + +def main(): + module = NetworkPortInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/project.py b/ansible_collections/openstack/cloud/plugins/modules/project.py new file mode 100644 index 00000000..9719452d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/project.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# Copyright (c) 2015 IBM Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: project +short_description: Manage OpenStack Projects +author: OpenStack Ansible SIG +description: + - Manage OpenStack Projects. Projects can be created, + updated or deleted using this module. A project will be updated + if I(name) matches an existing project and I(state) is present. + The value for I(name) cannot be updated without deleting and + re-creating the project. +options: + name: + description: + - Name for the project + required: true + type: str + description: + description: + - Description for the project + type: str + domain_id: + description: + - Domain id to create the project in if the cloud supports domains. + aliases: ['domain'] + type: str + enabled: + description: + - Is the project enabled + type: bool + default: 'yes' + properties: + description: + - Additional properties to be associated with this project. Requires + openstacksdk>0.45. + type: dict + required: false + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a project +- openstack.cloud.project: + cloud: mycloud + endpoint_type: admin + state: present + name: demoproject + description: demodescription + domain_id: demoid + enabled: True + properties: + internal_alias: demo_project + +# Delete a project +- openstack.cloud.project: + cloud: mycloud + endpoint_type: admin + state: absent + name: demoproject +''' + + +RETURN = ''' +project: + description: Dictionary describing the project. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Project ID + type: str + sample: "f59382db809c43139982ca4189404650" + name: + description: Project name + type: str + sample: "demoproject" + description: + description: Project description + type: str + sample: "demodescription" + enabled: + description: Boolean to indicate if project is enabled + type: bool + sample: True +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityProjectModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + description=dict(required=False), + domain_id=dict(required=False, aliases=['domain']), + properties=dict(required=False, type='dict', min_ver='0.45.1'), + enabled=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']) + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, project): + keys = ('description', 'enabled') + for key in keys: + if self.params[key] is not None and self.params[key] != project.get(key): + return True + + properties = self.params['properties'] + if properties: + project_properties = project.get('properties') + for k, v in properties.items(): + if v is not None and (k not in project_properties or v != project_properties[k]): + return True + + return False + + def _system_state_change(self, project): + state = self.params['state'] + if state == 'present': + if project is None: + changed = True + else: + if self._needs_update(project): + changed = True + else: + changed = False + + elif state == 'absent': + changed = project is not None + + return changed + + def run(self): + name = self.params['name'] + description = self.params['description'] + domain = self.params['domain_id'] + enabled = self.params['enabled'] + properties = self.params['properties'] or {} + state = self.params['state'] + + if domain: + try: + # We assume admin is passing domain id + dom = self.conn.get_domain(domain)['id'] + domain = dom + except Exception: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + try: + dom = self.conn.search_domains(filters={'name': domain})[0]['id'] + domain = dom + except Exception: + # Ok, let's hope the user is non-admin and passing a sane id + pass + + if domain: + project = self.conn.get_project(name, domain_id=domain) + else: + project = self.conn.get_project(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(project)) + + if state == 'present': + if project is None: + project = self.conn.create_project( + name=name, description=description, + domain_id=domain, + enabled=enabled) + changed = True + + project = self.conn.update_project( + project['id'], + description=description, + enabled=enabled, + **properties) + else: + if self._needs_update(project): + project = self.conn.update_project( + project['id'], + description=description, + enabled=enabled, + **properties) + changed = True + else: + changed = False + self.exit_json(changed=changed, project=project) + + elif state == 'absent': + if project is None: + changed = False + else: + self.conn.delete_project(project['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = IdentityProjectModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/project_access.py b/ansible_collections/openstack/cloud/plugins/modules/project_access.py new file mode 100644 index 00000000..c49a8449 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/project_access.py @@ -0,0 +1,193 @@ +#!/usr/bin/python + +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see <http://www.gnu.org/licenses/>. + +DOCUMENTATION = ''' +--- +module: project_access +short_description: Manage OpenStack compute flavors access +author: OpenStack Ansible SIG +description: + - Add or remove flavor, volume_type or other resources access + from OpenStack. +options: + state: + description: + - Indicate desired state of the resource. + choices: ['present', 'absent'] + required: false + default: present + type: str + target_project_id: + description: + - Project id. + required: true + type: str + resource_type: + description: + - The resource type (eg. nova_flavor, cinder_volume_type). + required: true + type: str + resource_name: + description: + - The resource name (eg. tiny). + required: true + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: "Enable access to tiny flavor to your tenant." + openstack.cloud.project_access: + cloud: mycloud + state: present + target_project_id: f0f1f2f3f4f5f67f8f9e0e1 + resource_name: tiny + resource_type: nova_flavor + + +- name: "Disable access to the given flavor to project" + openstack.cloud.project_access: + cloud: mycloud + state: absent + target_project_id: f0f1f2f3f4f5f67f8f9e0e1 + resource_name: tiny + resource_type: nova_flavor +''' + +RETURN = ''' +flavor: + description: Dictionary describing the flavor. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Flavor ID. + returned: success + type: str + sample: "515256b8-7027-4d73-aa54-4e30a4a4a339" + name: + description: Flavor name. + returned: success + type: str + sample: "tiny" + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityProjectAccess(OpenStackModule): + argument_spec = dict( + state=dict(required=False, default='present', + choices=['absent', 'present']), + target_project_id=dict(required=True, type='str'), + resource_type=dict(required=True, type='str'), + resource_name=dict(required=True, type='str'), + ) + + module_kwargs = dict( + supports_check_mode=True, + required_if=[ + ('state', 'present', ['target_project_id']) + ] + ) + + def run(self): + state = self.params['state'] + resource_name = self.params['resource_name'] + resource_type = self.params['resource_type'] + target_project_id = self.params['target_project_id'] + + if resource_type == 'nova_flavor': + # returns Munch({'NAME_ATTR': 'name', + # 'tenant_id': u'37e55da59ec842649d84230f3a24eed5', + # 'HUMAN_ID': False, + # 'flavor_id': u'6d4d37b9-0480-4a8c-b8c9-f77deaad73f9', + # 'request_ids': [], 'human_id': None}), + _get_resource = self.conn.get_flavor + _list_resource_access = self.conn.list_flavor_access + _add_resource_access = self.conn.add_flavor_access + _remove_resource_access = self.conn.remove_flavor_access + elif resource_type == 'cinder_volume_type': + # returns [Munch({ + # 'project_id': u'178cdb9955b047eea7afbe582038dc94', + # 'properties': {'request_ids': [], 'NAME_ATTR': 'name', + # 'human_id': None, + # 'HUMAN_ID': False}, + # 'id': u'd5573023-b290-42c8-b232-7c5ca493667f'}), + _get_resource = self.conn.get_volume_type + _list_resource_access = self.conn.get_volume_type_access + _add_resource_access = self.conn.add_volume_type_access + _remove_resource_access = self.conn.remove_volume_type_access + else: + self.exit_json( + changed=False, + resource_name=resource_name, + resource_type=resource_type, + error="Not implemented.") + + resource = _get_resource(resource_name) + if not resource: + self.exit_json( + changed=False, + resource_name=resource_name, + resource_type=resource_type, + error="Not found.") + resource_id = getattr(resource, 'id', resource['id']) + # _list_resource_access returns a list of dicts containing 'project_id' + acls = _list_resource_access(resource_id) + + if not all(acl.get('project_id') for acl in acls): + self.exit_json( + changed=False, + resource_name=resource_name, + resource_type=resource_type, + error="Missing project_id in resource output.") + allowed_tenants = [acl['project_id'] for acl in acls] + + changed_access = any(( + state == 'present' and target_project_id not in allowed_tenants, + state == 'absent' and target_project_id in allowed_tenants + )) + if self.ansible.check_mode or not changed_access: + self.exit_json( + changed=changed_access, resource=resource, id=resource_id) + + if state == 'present': + _add_resource_access( + resource_id, target_project_id + ) + elif state == 'absent': + _remove_resource_access( + resource_id, target_project_id + ) + + self.exit_json( + changed=True, resource=resource, id=resource_id) + + +def main(): + module = IdentityProjectAccess() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/project_info.py b/ansible_collections/openstack/cloud/plugins/modules/project_info.py new file mode 100644 index 00000000..fb1e2767 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/project_info.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: project_info +short_description: Retrieve information about one or more OpenStack projects +author: OpenStack Ansible SIG +description: + - Retrieve information about a one or more OpenStack projects + - This module was called C(openstack.cloud.project_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.project_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the project + type: str + domain: + description: + - Name or ID of the domain containing the project if the cloud supports domains + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about previously created projects +- openstack.cloud.project_info: + cloud: awesomecloud + register: result +- debug: + msg: "{{ result.openstack_projects }}" + +# Gather information about a previously created project by name +- openstack.cloud.project_info: + cloud: awesomecloud + name: demoproject + register: result +- debug: + msg: "{{ result.openstack_projects }}" + +# Gather information about a previously created project in a specific domain +- openstack.cloud.project_info: + cloud: awesomecloud + name: demoproject + domain: admindomain + register: result +- debug: + msg: "{{ result.openstack_projects }}" + +# Gather information about a previously created project in a specific domain with filter +- openstack.cloud.project_info: + cloud: awesomecloud + name: demoproject + domain: admindomain + filters: + enabled: False + register: result +- debug: + msg: "{{ result.openstack_projects }}" +''' + + +RETURN = ''' +openstack_projects: + description: has all the OpenStack information about projects + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the project. + returned: success + type: str + description: + description: Description of the project + returned: success + type: str + enabled: + description: Flag to indicate if the project is enabled + returned: success + type: bool + domain_id: + description: Domain ID containing the project (keystone v3 clouds only) + returned: success + type: bool +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityProjectInfoModule(OpenStackModule): + deprecated_names = ('project_facts', 'openstack.cloud.project_facts') + + argument_spec = dict( + name=dict(required=False), + domain=dict(required=False), + filters=dict(required=False, type='dict'), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + name = self.params['name'] + domain = self.params['domain'] + filters = self.params['filters'] + is_old_facts = self.module_name == 'openstack.cloud.project_facts' + + if domain: + try: + # We assume admin is passing domain id + dom = self.conn.get_domain(domain)['id'] + domain = dom + except Exception: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + dom = self.conn.search_domains(filters={'name': domain}) + if dom: + domain = dom[0]['id'] + else: + self.fail_json(msg='Domain name or ID does not exist') + + if not filters: + filters = {} + + filters['domain_id'] = domain + + projects = self.conn.search_projects(name, filters) + if is_old_facts: + self.exit_json(changed=False, ansible_facts=dict( + openstack_projects=projects)) + else: + self.exit_json(changed=False, openstack_projects=projects) + + +def main(): + module = IdentityProjectInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/quota.py b/ansible_collections/openstack/cloud/plugins/modules/quota.py new file mode 100644 index 00000000..0d6a4f04 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/quota.py @@ -0,0 +1,466 @@ +#!/usr/bin/python +# Copyright (c) 2016 Pason System Corporation +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: quota +short_description: Manage OpenStack Quotas +author: OpenStack Ansible SIG +description: + - Manage OpenStack Quotas. Quotas can be created, + updated or deleted using this module. A quota will be updated + if matches an existing project and is present. +options: + name: + description: + - Name of the OpenStack Project to manage. + required: true + type: str + state: + description: + - A value of present sets the quota and a value of absent resets the quota to system defaults. + default: present + type: str + choices: ['absent', 'present'] + backup_gigabytes: + description: Maximum size of backups in GB's. + type: int + backups: + description: Maximum number of backups allowed. + type: int + cores: + description: Maximum number of CPU's per project. + type: int + fixed_ips: + description: Number of fixed IP's to allow. + type: int + floating_ips: + description: Number of floating IP's to allow in Compute. + aliases: ['compute_floating_ips'] + type: int + floatingip: + description: Number of floating IP's to allow in Network. + aliases: ['network_floating_ips'] + type: int + gigabytes: + description: Maximum volume storage allowed for project. + type: int + gigabytes_types: + description: + - Per driver volume storage quotas. Keys should be + prefixed with C(gigabytes_) values should be ints. + type: dict + injected_file_size: + description: Maximum file size in bytes. + type: int + injected_files: + description: Number of injected files to allow. + type: int + injected_path_size: + description: Maximum path size. + type: int + instances: + description: Maximum number of instances allowed. + type: int + key_pairs: + description: Number of key pairs to allow. + type: int + loadbalancer: + description: Number of load balancers to allow. + type: int + metadata_items: + description: Number of metadata items allowed per instance. + type: int + network: + description: Number of networks to allow. + type: int + per_volume_gigabytes: + description: Maximum size in GB's of individual volumes. + type: int + pool: + description: Number of load balancer pools to allow. + type: int + port: + description: Number of Network ports to allow, this needs to be greater than the instances limit. + type: int + properties: + description: Number of properties to allow. + type: int + ram: + description: Maximum amount of ram in MB to allow. + type: int + rbac_policy: + description: Number of policies to allow. + type: int + router: + description: Number of routers to allow. + type: int + security_group_rule: + description: Number of rules per security group to allow. + type: int + security_group: + description: Number of security groups to allow. + type: int + server_group_members: + description: Number of server group members to allow. + type: int + server_groups: + description: Number of server groups to allow. + type: int + snapshots: + description: Number of snapshots to allow. + type: int + snapshots_types: + description: + - Per-driver volume snapshot quotas. Keys should be + prefixed with C(snapshots_) values should be ints. + type: dict + subnet: + description: Number of subnets to allow. + type: int + subnetpool: + description: Number of subnet pools to allow. + type: int + volumes: + description: Number of volumes to allow. + type: int + volumes_types: + description: + - Per-driver volume count quotas. Keys should be + prefixed with C(volumes_) values should be ints. + type: dict + project: + description: Unused, kept for compatability + type: int + +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.13.0" + - "keystoneauth1 >= 3.4.0" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# List a Project Quota +- openstack.cloud.quota: + cloud: mycloud + name: demoproject + +# Set a Project back to the defaults +- openstack.cloud.quota: + cloud: mycloud + name: demoproject + state: absent + +# Update a Project Quota for cores +- openstack.cloud.quota: + cloud: mycloud + name: demoproject + cores: 100 + +# Update a Project Quota +- openstack.cloud.quota: + name: demoproject + cores: 1000 + volumes: 20 + volumes_type: + - volume_lvm: 10 + +# Complete example based on list of projects +- name: Update quotas + openstack.cloud.quota: + name: "{{ item.name }}" + backup_gigabytes: "{{ item.backup_gigabytes }}" + backups: "{{ item.backups }}" + cores: "{{ item.cores }}" + fixed_ips: "{{ item.fixed_ips }}" + floating_ips: "{{ item.floating_ips }}" + floatingip: "{{ item.floatingip }}" + gigabytes: "{{ item.gigabytes }}" + injected_file_size: "{{ item.injected_file_size }}" + injected_files: "{{ item.injected_files }}" + injected_path_size: "{{ item.injected_path_size }}" + instances: "{{ item.instances }}" + key_pairs: "{{ item.key_pairs }}" + loadbalancer: "{{ item.loadbalancer }}" + metadata_items: "{{ item.metadata_items }}" + per_volume_gigabytes: "{{ item.per_volume_gigabytes }}" + pool: "{{ item.pool }}" + port: "{{ item.port }}" + properties: "{{ item.properties }}" + ram: "{{ item.ram }}" + security_group_rule: "{{ item.security_group_rule }}" + security_group: "{{ item.security_group }}" + server_group_members: "{{ item.server_group_members }}" + server_groups: "{{ item.server_groups }}" + snapshots: "{{ item.snapshots }}" + volumes: "{{ item.volumes }}" + volumes_types: + volumes_lvm: "{{ item.volumes_lvm }}" + snapshots_types: + snapshots_lvm: "{{ item.snapshots_lvm }}" + gigabytes_types: + gigabytes_lvm: "{{ item.gigabytes_lvm }}" + with_items: + - "{{ projects }}" + when: item.state == "present" +''' + +RETURN = ''' +openstack_quotas: + description: Dictionary describing the project quota. + returned: Regardless if changes where made or not + type: dict + sample: + openstack_quotas: { + compute: { + cores: 150, + fixed_ips: -1, + floating_ips: 10, + injected_file_content_bytes: 10240, + injected_file_path_bytes: 255, + injected_files: 5, + instances: 100, + key_pairs: 100, + metadata_items: 128, + ram: 153600, + security_group_rules: 20, + security_groups: 10, + server_group_members: 10, + server_groups: 10 + }, + network: { + floatingip: 50, + loadbalancer: 10, + network: 10, + pool: 10, + port: 160, + rbac_policy: 10, + router: 10, + security_group: 10, + security_group_rule: 100, + subnet: 10, + subnetpool: -1 + }, + volume: { + backup_gigabytes: 1000, + backups: 10, + gigabytes: 1000, + gigabytes_lvm: -1, + per_volume_gigabytes: -1, + snapshots: 10, + snapshots_lvm: -1, + volumes: 10, + volumes_lvm: -1 + } + } + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class QuotaModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + backup_gigabytes=dict(required=False, type='int', default=None), + backups=dict(required=False, type='int', default=None), + cores=dict(required=False, type='int', default=None), + fixed_ips=dict(required=False, type='int', default=None), + floating_ips=dict(required=False, type='int', default=None, aliases=['compute_floating_ips']), + floatingip=dict(required=False, type='int', default=None, aliases=['network_floating_ips']), + gigabytes=dict(required=False, type='int', default=None), + gigabytes_types=dict(required=False, type='dict', default={}), + injected_file_size=dict(required=False, type='int', default=None), + injected_files=dict(required=False, type='int', default=None), + injected_path_size=dict(required=False, type='int', default=None), + instances=dict(required=False, type='int', default=None), + key_pairs=dict(required=False, type='int', default=None, no_log=False), + loadbalancer=dict(required=False, type='int', default=None), + metadata_items=dict(required=False, type='int', default=None), + network=dict(required=False, type='int', default=None), + per_volume_gigabytes=dict(required=False, type='int', default=None), + pool=dict(required=False, type='int', default=None), + port=dict(required=False, type='int', default=None), + project=dict(required=False, type='int', default=None), + properties=dict(required=False, type='int', default=None), + ram=dict(required=False, type='int', default=None), + rbac_policy=dict(required=False, type='int', default=None), + router=dict(required=False, type='int', default=None), + security_group_rule=dict(required=False, type='int', default=None), + security_group=dict(required=False, type='int', default=None), + server_group_members=dict(required=False, type='int', default=None), + server_groups=dict(required=False, type='int', default=None), + snapshots=dict(required=False, type='int', default=None), + snapshots_types=dict(required=False, type='dict', default={}), + subnet=dict(required=False, type='int', default=None), + subnetpool=dict(required=False, type='int', default=None), + volumes=dict(required=False, type='int', default=None), + volumes_types=dict(required=False, type='dict', default={}) + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _get_volume_quotas(self, project): + return self.conn.get_volume_quotas(project) + + def _get_network_quotas(self, project): + return self.conn.get_network_quotas(project) + + def _get_compute_quotas(self, project): + return self.conn.get_compute_quotas(project) + + def _get_quotas(self, project): + quota = {} + try: + quota['volume'] = self._get_volume_quotas(project) + except Exception: + self.warn("No public endpoint for volumev2 service was found. Ignoring volume quotas.") + + try: + quota['network'] = self._get_network_quotas(project) + except Exception: + self.warn("No public endpoint for network service was found. Ignoring network quotas.") + + quota['compute'] = self._get_compute_quotas(project) + + for quota_type in quota.keys(): + quota[quota_type] = self._scrub_results(quota[quota_type]) + + return quota + + def _scrub_results(self, quota): + filter_attr = [ + 'HUMAN_ID', + 'NAME_ATTR', + 'human_id', + 'request_ids', + 'x_openstack_request_ids', + ] + + for attr in filter_attr: + if attr in quota: + del quota[attr] + + return quota + + def _system_state_change_details(self, project_quota_output): + quota_change_request = {} + changes_required = False + + for quota_type in project_quota_output.keys(): + for quota_option in project_quota_output[quota_type].keys(): + if quota_option in self.params and self.params[quota_option] is not None: + if project_quota_output[quota_type][quota_option] != self.params[quota_option]: + changes_required = True + + if quota_type not in quota_change_request: + quota_change_request[quota_type] = {} + + quota_change_request[quota_type][quota_option] = self.params[quota_option] + + return (changes_required, quota_change_request) + + def _system_state_change(self, project_quota_output): + """ + Determine if changes are required to the current project quota. + + This is done by comparing the current project_quota_output against + the desired quota settings set on the module params. + """ + + changes_required, quota_change_request = self._system_state_change_details( + project_quota_output + ) + + if changes_required: + return True + else: + return False + + def run(self): + cloud_params = dict(self.params) + + # In order to handle the different volume types we update module params after. + dynamic_types = [ + 'gigabytes_types', + 'snapshots_types', + 'volumes_types', + ] + + for dynamic_type in dynamic_types: + for k, v in self.params[dynamic_type].items(): + self.params[k] = int(v) + + # Get current quota values + project_quota_output = self._get_quotas(cloud_params['name']) + changes_required = False + + if self.params['state'] == "absent": + # If a quota state is set to absent we should assume there will be changes. + # The default quota values are not accessible so we can not determine if + # no changes will occur or not. + if self.ansible.check_mode: + self.exit_json(changed=True) + + # Calling delete_network_quotas when a quota has not been set results + # in an error, according to the sdk docs it should return the + # current quota. + # The following error string is returned: + # network client call failed: Quota for tenant 69dd91d217e949f1a0b35a4b901741dc could not be found. + neutron_msg1 = "network client call failed: Quota for tenant" + neutron_msg2 = "could not be found" + + for quota_type in project_quota_output.keys(): + quota_call = getattr(self.conn, 'delete_%s_quotas' % (quota_type)) + try: + quota_call(cloud_params['name']) + except Exception as e: + error_msg = str(e) + if error_msg.find(neutron_msg1) > -1 and error_msg.find(neutron_msg2) > -1: + pass + else: + self.fail_json(msg=str(e), extra_data=e.extra_data) + + project_quota_output = self._get_quotas(cloud_params['name']) + changes_required = True + + elif self.params['state'] == "present": + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change( + project_quota_output)) + + changes_required, quota_change_request = self._system_state_change_details( + project_quota_output + ) + + if changes_required: + for quota_type in quota_change_request.keys(): + quota_call = getattr(self.conn, 'set_%s_quotas' % (quota_type)) + quota_call(cloud_params['name'], **quota_change_request[quota_type]) + + # Get quota state post changes for validation + project_quota_update = self._get_quotas(cloud_params['name']) + + if project_quota_output == project_quota_update: + self.fail_json(msg='Could not apply quota update') + + project_quota_output = project_quota_update + + self.exit_json( + changed=changes_required, openstack_quotas=project_quota_output) + + +def main(): + module = QuotaModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/recordset.py b/ansible_collections/openstack/cloud/plugins/modules/recordset.py new file mode 100644 index 00000000..921d6efa --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/recordset.py @@ -0,0 +1,260 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: recordset +short_description: Manage OpenStack DNS recordsets +author: OpenStack Ansible SIG +description: + - Manage OpenStack DNS recordsets. Recordsets can be created, deleted or + updated. Only the I(records), I(description), and I(ttl) values + can be updated. +options: + description: + description: + - Description of the recordset + type: str + name: + description: + - Name of the recordset. It must be ended with name of dns zone. + required: true + type: str + records: + description: + - List of recordset definitions. + - Required when I(state=present). + type: list + elements: str + recordset_type: + description: + - Recordset type + - Required when I(state=present). + choices: ['a', 'aaaa', 'mx', 'cname', 'txt', 'ns', 'srv', 'ptr', 'caa'] + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + ttl: + description: + - TTL (Time To Live) value in seconds + type: int + zone: + description: + - Name or ID of the zone which manages the recordset + required: true + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a recordset named "www.example.net." +- openstack.cloud.recordset: + cloud: mycloud + state: present + zone: example.net. + name: www.example.net. + recordset_type: "a" + records: ['10.1.1.1'] + description: test recordset + ttl: 3600 + +# Update the TTL on existing "www.example.net." recordset +- openstack.cloud.recordset: + cloud: mycloud + state: present + zone: example.net. + name: www.example.net. + recordset_type: "a" + records: ['10.1.1.1'] + ttl: 7200 + +# Delete recordset named "www.example.net." +- openstack.cloud.recordset: + cloud: mycloud + state: absent + zone: example.net. + name: www.example.net. +''' + +RETURN = ''' +recordset: + description: Dictionary describing the recordset. + returned: On success when I(state) is 'present'. + type: dict + contains: + action: + description: Current action in progress on the resource + type: str + returned: always + created_at: + description: Timestamp when the zone was created + type: str + returned: always + description: + description: Recordset description + type: str + sample: "Test description" + returned: always + id: + description: Unique recordset ID + type: str + sample: "c1c530a3-3619-46f3-b0f6-236927b2618c" + links: + description: Links related to the resource + type: dict + returned: always + name: + description: Recordset name + type: str + sample: "www.example.net." + returned: always + project_id: + description: ID of the proect to which the recordset belongs + type: str + returned: always + records: + description: Recordset records + type: list + sample: ['10.0.0.1'] + returned: always + status: + description: + - Recordset status + - Valid values include `PENDING_CREATE`, `ACTIVE`,`PENDING_DELETE`, + `ERROR` + type: str + returned: always + ttl: + description: Zone TTL value + type: int + sample: 3600 + returned: always + type: + description: + - Recordset type + - Valid values include `A`, `AAAA`, `MX`, `CNAME`, `TXT`, `NS`, + `SSHFP`, `SPF`, `SRV`, `PTR` + type: str + sample: "A" + returned: always + zone_id: + description: The id of the Zone which this recordset belongs to + type: str + sample: 9508e177-41d8-434e-962c-6fe6ca880af7 + returned: always + zone_name: + description: The name of the Zone which this recordset belongs to + type: str + sample: "example.com." + returned: always +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class DnsRecordsetModule(OpenStackModule): + argument_spec = dict( + description=dict(required=False, default=None), + name=dict(required=True), + records=dict(required=False, type='list', elements='str'), + recordset_type=dict(required=False, choices=['a', 'aaaa', 'mx', 'cname', 'txt', 'ns', 'srv', 'ptr', 'caa']), + state=dict(default='present', choices=['absent', 'present']), + ttl=dict(required=False, type='int'), + zone=dict(required=True), + ) + + module_kwargs = dict( + required_if=[ + ('state', 'present', + ['recordset_type', 'records'])], + supports_check_mode=True + ) + + module_min_sdk_version = '0.28.0' + + def _needs_update(self, params, recordset): + for k in ('description', 'records', 'ttl'): + if k not in params: + continue + if params[k] is not None and params[k] != recordset[k]: + return True + return False + + def _system_state_change(self, state, recordset): + if state == 'present': + if recordset is None: + return True + kwargs = self._build_params() + return self._needs_update(kwargs, recordset) + if state == 'absent' and recordset: + return True + return False + + def _build_params(self): + recordset_type = self.params['recordset_type'] + records = self.params['records'] + description = self.params['description'] + ttl = self.params['ttl'] + params = { + 'description': description, + 'records': records, + 'type': recordset_type.upper(), + 'ttl': ttl, + } + return {k: v for k, v in params.items() if v is not None} + + def run(self): + zone = self.params.get('zone') + name = self.params.get('name') + state = self.params.get('state') + ttl = self.params.get('ttl') + + recordsets = self.conn.search_recordsets(zone, name_or_id=name) + + recordset = None + if recordsets: + recordset = recordsets[0] + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, recordset)) + + changed = False + if state == 'present': + kwargs = self._build_params() + if recordset is None: + kwargs['ttl'] = ttl or 300 + type = kwargs.pop('type', None) + if type is not None: + kwargs['recordset_type'] = type + recordset = self.conn.create_recordset(zone=zone, name=name, + **kwargs) + changed = True + elif self._needs_update(kwargs, recordset): + type = kwargs.pop('type', None) + recordset = self.conn.update_recordset(zone, recordset['id'], + **kwargs) + changed = True + self.exit_json(changed=changed, recordset=recordset) + elif state == 'absent' and recordset is not None: + self.conn.delete_recordset(zone, recordset['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = DnsRecordsetModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/role_assignment.py b/ansible_collections/openstack/cloud/plugins/modules/role_assignment.py new file mode 100644 index 00000000..5ad7dce4 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/role_assignment.py @@ -0,0 +1,190 @@ +#!/usr/bin/python +# Copyright (c) 2016 IBM +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: role_assignment +short_description: Associate OpenStack Identity users and roles +author: OpenStack Ansible SIG +description: + - Grant and revoke roles in either project or domain context for + OpenStack Identity Users. +options: + role: + description: + - Name or ID for the role. + required: true + type: str + user: + description: + - Name or ID for the user. If I(user) is not specified, then + I(group) is required. Both may not be specified. + type: str + group: + description: + - Name or ID for the group. Valid only with keystone version 3. + If I(group) is not specified, then I(user) is required. Both + may not be specified. + type: str + project: + description: + - Name or ID of the project to scope the role association to. + If you are using keystone version 2, then this value is required. + type: str + domain: + description: + - Name or ID of the domain to scope the role association to. Valid only + with keystone version 3, and required if I(project) is not specified. + type: str + state: + description: + - Should the roles be present or absent on the user. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Grant an admin role on the user admin in the project project1 +- openstack.cloud.role_assignment: + cloud: mycloud + user: admin + role: admin + project: project1 + +# Revoke the admin role from the user barney in the newyork domain +- openstack.cloud.role_assignment: + cloud: mycloud + state: absent + user: barney + role: admin + domain: newyork +''' + +RETURN = ''' +# +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class IdentityRoleAssignmentModule(OpenStackModule): + argument_spec = dict( + role=dict(required=True), + user=dict(required=False), + group=dict(required=False), + project=dict(required=False), + domain=dict(required=False), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + required_one_of=[ + ['user', 'group'] + ], + supports_check_mode=True + ) + + def _system_state_change(self, state, assignment): + if state == 'present' and not assignment: + return True + elif state == 'absent' and assignment: + return True + return False + + def _build_kwargs(self, user, group, project, domain): + kwargs = {} + if user: + kwargs['user'] = user + if group: + kwargs['group'] = group + if project: + kwargs['project'] = project + if domain: + kwargs['domain'] = domain + return kwargs + + def run(self): + role = self.params.get('role') + user = self.params.get('user') + group = self.params.get('group') + project = self.params.get('project') + domain = self.params.get('domain') + state = self.params.get('state') + + filters = {} + find_filters = {} + domain_id = None + + r = self.conn.identity.find_role(role) + if r is None: + self.fail_json(msg="Role %s is not valid" % role) + filters['role'] = r['id'] + + if domain: + d = self.conn.identity.find_domain(domain) + if d is None: + self.fail_json(msg="Domain %s is not valid" % domain) + domain_id = d['id'] + find_filters['domain_id'] = domain_id + if user: + u = self.conn.identity.find_user(user, **find_filters) + if u is None: + self.fail_json(msg="User %s is not valid" % user) + filters['user'] = u['id'] + + if group: + # self.conn.identity.find_group() does not accept + # a domain_id argument in Train's openstacksdk + g = self.conn.get_group(group, **find_filters) + if g is None: + self.fail_json(msg="Group %s is not valid" % group) + filters['group'] = g['id'] + if project: + p = self.conn.identity.find_project(project, **find_filters) + if p is None: + self.fail_json(msg="Project %s is not valid" % project) + filters['project'] = p['id'] + + # Keeping the self.conn.list_role_assignments because it calls directly + # the identity.role_assignments and there are some logics for the + # filters that won't worth rewrite here. + assignment = self.conn.list_role_assignments(filters=filters) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(state, assignment)) + + changed = False + + # Both grant_role and revoke_role calls directly the proxy layer, and + # has some logic that won't worth to rewrite here so keeping it is a + # good idea + if state == 'present': + if not assignment: + kwargs = self._build_kwargs(user, group, project, domain_id) + self.conn.grant_role(role, **kwargs) + changed = True + + elif state == 'absent': + if assignment: + kwargs = self._build_kwargs(user, group, project, domain_id) + self.conn.revoke_role(role, **kwargs) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = IdentityRoleAssignmentModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/router.py b/ansible_collections/openstack/cloud/plugins/modules/router.py new file mode 100644 index 00000000..58c5c124 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/router.py @@ -0,0 +1,571 @@ +#!/usr/bin/python +# +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: router +short_description: Create or delete routers from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Delete routers from OpenStack. Although Neutron allows + routers to share the same name, this module enforces name uniqueness + to be more user friendly. +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Name to be give to the router + required: true + type: str + admin_state_up: + description: + - Desired admin state of the created or existing router. + type: bool + default: 'yes' + enable_snat: + description: + - Enable Source NAT (SNAT) attribute. + type: bool + network: + description: + - Unique name or ID of the external gateway network. + - required I(interfaces) or I(enable_snat) are provided. + type: str + project: + description: + - Unique name or ID of the project. + type: str + external_fixed_ips: + description: + - The IP address parameters for the external gateway network. Each + is a dictionary with the subnet name or ID (subnet) and the IP + address to assign on the subnet (ip). If no IP is specified, + one is automatically assigned from that subnet. + type: list + elements: dict + suboptions: + ip: + description: The fixed IP address to attempt to allocate. + required: true + type: str + subnet: + description: The subnet to attach the IP address to. + type: str + interfaces: + description: + - List of subnets to attach to the router internal interface. Default + gateway associated with the subnet will be automatically attached + with the router's internal interface. + In order to provide an ip address different from the default + gateway,parameters are passed as dictionary with keys as network + name or ID (I(net)), subnet name or ID (I(subnet)) and the IP of + port (I(portip)) from the network. + User defined portip is often required when a multiple router need + to be connected to a single subnet for which the default gateway has + been already used. + type: list + elements: raw +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a simple router, not attached to a gateway or subnets. +- openstack.cloud.router: + cloud: mycloud + state: present + name: simple_router + +# Create a simple router, not attached to a gateway or subnets for a given project. +- openstack.cloud.router: + cloud: mycloud + state: present + name: simple_router + project: myproj + +# Creates a router attached to ext_network1 on an IPv4 subnet and one +# internal subnet interface. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router1 + network: ext_network1 + external_fixed_ips: + - subnet: public-subnet + ip: 172.24.4.2 + interfaces: + - private-subnet + +# Create another router with two internal subnet interfaces.One with user defined port +# ip and another with default gateway. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router2 + network: ext_network1 + interfaces: + - net: private-net + subnet: private-subnet + portip: 10.1.1.10 + - project-subnet + +# Create another router with two internal subnet interface.One with user defined port +# ip and and another with default gateway. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router2 + network: ext_network1 + interfaces: + - net: private-net + subnet: private-subnet + portip: 10.1.1.10 + - project-subnet + +# Create another router with two internal subnet interface. one with user defined port +# ip and and another with default gateway. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router2 + network: ext_network1 + interfaces: + - net: private-net + subnet: private-subnet + portip: 10.1.1.10 + - project-subnet + +# Update existing router1 external gateway to include the IPv6 subnet. +# Note that since 'interfaces' is not provided, any existing internal +# interfaces on an existing router will be left intact. +- openstack.cloud.router: + cloud: mycloud + state: present + name: router1 + network: ext_network1 + external_fixed_ips: + - subnet: public-subnet + ip: 172.24.4.2 + - subnet: ipv6-public-subnet + ip: 2001:db8::3 + +# Delete router1 +- openstack.cloud.router: + cloud: mycloud + state: absent + name: router1 +''' + +RETURN = ''' +router: + description: Dictionary describing the router. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Router ID. + type: str + sample: "474acfe5-be34-494c-b339-50f06aa143e4" + name: + description: Router name. + type: str + sample: "router1" + admin_state_up: + description: Administrative state of the router. + type: bool + sample: true + status: + description: The router status. + type: str + sample: "ACTIVE" + tenant_id: + description: The tenant ID. + type: str + sample: "861174b82b43463c9edc5202aadc60ef" + external_gateway_info: + description: The external gateway parameters. + type: dict + sample: { + "enable_snat": true, + "external_fixed_ips": [ + { + "ip_address": "10.6.6.99", + "subnet_id": "4272cb52-a456-4c20-8f3c-c26024ecfa81" + } + ] + } + routes: + description: The extra routes configuration for L3 router. + type: list +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule +import itertools + + +class RouterModule(OpenStackModule): + argument_spec = dict( + state=dict(default='present', choices=['absent', 'present']), + name=dict(required=True), + admin_state_up=dict(type='bool', default=True), + enable_snat=dict(type='bool'), + network=dict(default=None), + interfaces=dict(type='list', default=None, elements='raw'), + external_fixed_ips=dict(type='list', default=None, elements='dict'), + project=dict(default=None) + ) + + def _get_subnet_ids_from_ports(self, ports): + return [fixed_ip['subnet_id'] for fixed_ip in + itertools.chain.from_iterable(port['fixed_ips'] for port in ports if 'fixed_ips' in port)] + + def _needs_update(self, router, net, + missing_port_ids, + requested_subnet_ids, + existing_subnet_ids, + router_ifs_cfg): + """Decide if the given router needs an update.""" + if router['admin_state_up'] != self.params['admin_state_up']: + return True + if router['external_gateway_info']: + # check if enable_snat is set in module params + if self.params['enable_snat'] is not None: + if router['external_gateway_info'].get('enable_snat', True) != self.params['enable_snat']: + return True + if net: + if not router['external_gateway_info']: + return True + elif router['external_gateway_info']['network_id'] != net['id']: + return True + + # check if external_fixed_ip has to be added + for external_fixed_ip in router_ifs_cfg['external_fixed_ips']: + exists = False + + # compare the requested interface with existing, looking for an existing match + for existing_if in router['external_gateway_info']['external_fixed_ips']: + if existing_if['subnet_id'] == external_fixed_ip['subnet_id']: + if 'ip' in external_fixed_ip: + if existing_if['ip_address'] == external_fixed_ip['ip']: + # both subnet id and ip address match + exists = True + break + else: + # only the subnet was given, so ip doesn't matter + exists = True + break + + # this interface isn't present on the existing router + if not exists: + return True + + # check if external_fixed_ip has to be removed + if router_ifs_cfg['external_fixed_ips']: + for external_fixed_ip in router['external_gateway_info']['external_fixed_ips']: + obsolete = True + + # compare the existing interface with requested, looking for an requested match + for requested_if in router_ifs_cfg['external_fixed_ips']: + if external_fixed_ip['subnet_id'] == requested_if['subnet_id']: + if 'ip' in requested_if: + if external_fixed_ip['ip_address'] == requested_if['ip']: + # both subnet id and ip address match + obsolete = False + break + else: + # only the subnet was given, so ip doesn't matter + obsolete = False + break + + # this interface isn't present on the existing router + if obsolete: + return True + else: + # no external fixed ips requested + if router['external_gateway_info'] \ + and router['external_gateway_info']['external_fixed_ips'] \ + and len(router['external_gateway_info']['external_fixed_ips']) > 1: + # but router has several external fixed ips + return True + + # check if internal port has to be added + if router_ifs_cfg['internal_ports_missing']: + return True + + if missing_port_ids: + return True + + # check if internal subnet has to be added or removed + if set(requested_subnet_ids) != set(existing_subnet_ids): + return True + + return False + + def _build_kwargs(self, router, net): + kwargs = { + 'admin_state_up': self.params['admin_state_up'], + } + + if router: + kwargs['name_or_id'] = router['id'] + else: + kwargs['name'] = self.params['name'] + + if net: + kwargs['ext_gateway_net_id'] = net['id'] + # can't send enable_snat unless we have a network + if self.params.get('enable_snat') is not None: + kwargs['enable_snat'] = self.params['enable_snat'] + + if self.params['external_fixed_ips']: + kwargs['ext_fixed_ips'] = [] + for iface in self.params['external_fixed_ips']: + subnet = self.conn.get_subnet(iface['subnet']) + d = {'subnet_id': subnet['id']} + if 'ip' in iface: + d['ip_address'] = iface['ip'] + kwargs['ext_fixed_ips'].append(d) + else: + # no external fixed ips requested + if router \ + and router['external_gateway_info'] \ + and router['external_gateway_info']['external_fixed_ips'] \ + and len(router['external_gateway_info']['external_fixed_ips']) > 1: + # but router has several external fixed ips + # keep first external fixed ip only + fip = router['external_gateway_info']['external_fixed_ips'][0] + kwargs['ext_fixed_ips'] = [fip] + + return kwargs + + def _build_router_interface_config(self, filters=None): + external_fixed_ips = [] + internal_subnets = [] + internal_ports = [] + internal_ports_missing = [] + + # Build external interface configuration + if self.params['external_fixed_ips']: + for iface in self.params['external_fixed_ips']: + subnet = self.conn.get_subnet(iface['subnet'], filters) + if not subnet: + self.fail(msg='subnet %s not found' % iface['subnet']) + new_external_fixed_ip = {'subnet_name': subnet.name, 'subnet_id': subnet.id} + if 'ip' in iface: + new_external_fixed_ip['ip'] = iface['ip'] + external_fixed_ips.append(new_external_fixed_ip) + + # Build internal interface configuration + if self.params['interfaces']: + internal_ips = [] + for iface in self.params['interfaces']: + if isinstance(iface, str): + subnet = self.conn.get_subnet(iface, filters) + if not subnet: + self.fail(msg='subnet %s not found' % iface) + internal_subnets.append(subnet) + + elif isinstance(iface, dict): + subnet = self.conn.get_subnet(iface['subnet'], filters) + if not subnet: + self.fail(msg='subnet %s not found' % iface['subnet']) + + net = self.conn.get_network(iface['net']) + if not net: + self.fail(msg='net %s not found' % iface['net']) + + if "portip" not in iface: + # portip not set, add any ip from subnet + internal_subnets.append(subnet) + elif not iface['portip']: + # portip is set but has invalid value + self.fail(msg='put an ip in portip or remove it from list to assign default port to router') + else: + # portip has valid value + # look for ports whose fixed_ips.ip_address matchs portip + for existing_port in self.conn.list_ports(filters={'network_id': net.id}): + for fixed_ip in existing_port['fixed_ips']: + if iface['portip'] == fixed_ip['ip_address']: + # portip exists in net already + internal_ports.append(existing_port) + internal_ips.append(fixed_ip['ip_address']) + if iface['portip'] not in internal_ips: + # no port with portip exists hence create a new port + internal_ports_missing.append({ + 'network_id': net.id, + 'fixed_ips': [{'ip_address': iface['portip'], 'subnet_id': subnet.id}] + }) + + return { + 'external_fixed_ips': external_fixed_ips, + 'internal_subnets': internal_subnets, + 'internal_ports': internal_ports, + 'internal_ports_missing': internal_ports_missing + } + + def run(self): + + state = self.params['state'] + name = self.params['name'] + network = self.params['network'] + project = self.params['project'] + + if self.params['external_fixed_ips'] and not network: + self.fail(msg='network is required when supplying external_fixed_ips') + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail(msg='Project %s could not be found' % project) + project_id = proj['id'] + filters = {'tenant_id': project_id} + else: + project_id = None + filters = None + + router = self.conn.get_router(name, filters=filters) + net = None + if network: + net = self.conn.get_network(network) + if not net: + self.fail(msg='network %s not found' % network) + + # Validate and cache the subnet IDs so we can avoid duplicate checks + # and expensive API calls. + router_ifs_cfg = self._build_router_interface_config(filters) + requested_subnet_ids = [subnet.id for subnet in router_ifs_cfg['internal_subnets']] + \ + self._get_subnet_ids_from_ports(router_ifs_cfg['internal_ports']) + requested_port_ids = [i['id'] for i in router_ifs_cfg['internal_ports']] + + if router: + router_ifs_internal = self.conn.list_router_interfaces(router, 'internal') + existing_subnet_ids = self._get_subnet_ids_from_ports(router_ifs_internal) + obsolete_subnet_ids = set(existing_subnet_ids) - set(requested_subnet_ids) + existing_port_ids = [i['id'] for i in router_ifs_internal] + + else: + router_ifs_internal = [] + existing_subnet_ids = [] + obsolete_subnet_ids = [] + existing_port_ids = [] + + missing_port_ids = set(requested_port_ids) - set(existing_port_ids) + + if self.ansible.check_mode: + # Check if the system state would be changed + if state == 'absent' and router: + changed = True + elif state == 'absent' and not router: + changed = False + elif state == 'present' and not router: + changed = True + else: # if state == 'present' and router + changed = self._needs_update(router, net, + missing_port_ids, + requested_subnet_ids, + existing_subnet_ids, + router_ifs_cfg) + self.exit_json(changed=changed) + + if state == 'present': + changed = False + + if not router: + changed = True + + kwargs = self._build_kwargs(router, net) + if project_id: + kwargs['project_id'] = project_id + router = self.conn.create_router(**kwargs) + + # add interface by subnet id, because user did not specify a port id + for subnet in router_ifs_cfg['internal_subnets']: + self.conn.add_router_interface(router, subnet_id=subnet.id) + + # add interface by port id if user did specify a valid port id + for port in router_ifs_cfg['internal_ports']: + self.conn.add_router_interface(router, port_id=port.id) + + # add port and interface if user did specify an ip address but port is missing yet + for missing_internal_port in router_ifs_cfg['internal_ports_missing']: + p = self.conn.create_port(**missing_internal_port) + if p: + self.conn.add_router_interface(router, port_id=p.id) + + else: + if self._needs_update(router, net, + missing_port_ids, + requested_subnet_ids, + existing_subnet_ids, + router_ifs_cfg): + changed = True + kwargs = self._build_kwargs(router, net) + updated_router = self.conn.update_router(**kwargs) + + # Protect against update_router() not actually updating the router. + if not updated_router: + changed = False + else: + router = updated_router + + # delete internal subnets i.e. ports + if obsolete_subnet_ids: + for port in router_ifs_internal: + if 'fixed_ips' in port: + for fip in port['fixed_ips']: + if fip['subnet_id'] in obsolete_subnet_ids: + self.conn.remove_router_interface(router, port_id=port['id']) + changed = True + + # add new internal interface by subnet id, because user did not specify a port id + for subnet in router_ifs_cfg['internal_subnets']: + if subnet.id not in existing_subnet_ids: + self.conn.add_router_interface(router, subnet_id=subnet.id) + changed = True + + # add new internal interface by port id if user did specify a valid port id + for port_id in missing_port_ids: + self.conn.add_router_interface(router, port_id=port_id) + changed = True + + # add new port and new internal interface if user did specify an ip address but port is missing yet + for missing_internal_port in router_ifs_cfg['internal_ports_missing']: + p = self.conn.create_port(**missing_internal_port) + if p: + self.conn.add_router_interface(router, port_id=p.id) + changed = True + + self.exit_json(changed=changed, router=router) + + elif state == 'absent': + if not router: + self.exit_json(changed=False) + else: + # We need to detach all internal interfaces on a router + # before we will be allowed to delete it. Deletion can + # still fail if e.g. floating ips are attached to the + # router. + for port in router_ifs_internal: + self.conn.remove_router_interface(router, port_id=port['id']) + self.conn.delete_router(router['id']) + self.exit_json(changed=True, router=router) + + +def main(): + module = RouterModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/routers_info.py b/ansible_collections/openstack/cloud/plugins/modules/routers_info.py new file mode 100644 index 00000000..990eef8d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/routers_info.py @@ -0,0 +1,194 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Bram Verschueren <verschueren.bram@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: routers_info +short_description: Retrieve information about one or more OpenStack routers. +author: OpenStack Ansible SIG +description: + - Retrieve information about one or more routers from OpenStack. +options: + name: + description: + - Name or ID of the router + required: false + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + type: dict + suboptions: + project_id: + description: + - Filter the list result by the ID of the project that owns the resource. + type: str + aliases: + - tenant_id + name: + description: + - Filter the list result by the human-readable name of the resource. + type: str + description: + description: + - Filter the list result by the human-readable description of the resource. + type: str + admin_state_up: + description: + - Filter the list result by the administrative state of the resource, which is up (true) or down (false). + type: bool + revision_number: + description: + - Filter the list result by the revision number of the resource. + type: int + tags: + description: + - A list of tags to filter the list result by. Resources that match all tags in this list will be returned. + type: list + elements: str +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Gather information about routers + openstack.cloud.routers_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + register: result + +- name: Show openstack routers + debug: + msg: "{{ result.openstack_routers }}" + +- name: Gather information about a router by name + openstack.cloud.routers_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + name: router1 + register: result + +- name: Show openstack routers + debug: + msg: "{{ result.openstack_routers }}" + +- name: Gather information about a router with filter + openstack.cloud.routers_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + filters: + tenant_id: bc3ea709c96849d6b81f54640400a19f + register: result + +- name: Show openstack routers + debug: + msg: "{{ result.openstack_routers }}" +''' + +RETURN = ''' +openstack_routers: + description: has all the openstack information about the routers + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the router. + returned: success + type: str + status: + description: Router status. + returned: success + type: str + external_gateway_info: + description: The external gateway information of the router. + returned: success + type: dict + interfaces_info: + description: List of connected interfaces. + returned: success + type: list + distributed: + description: Indicates a distributed router. + returned: success + type: bool + ha: + description: Indicates a highly-available router. + returned: success + type: bool + project_id: + description: Project id associated with this router. + returned: success + type: str + routes: + description: The extra routes configuration for L3 router. + returned: success + type: list +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class RouterInfoModule(OpenStackModule): + + deprecated_names = ('os_routers_info', 'openstack.cloud.os_routers_info') + + argument_spec = dict( + name=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None) + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + + kwargs = self.check_versioned( + filters=self.params['filters'] + ) + if self.params['name']: + kwargs['name_or_id'] = self.params['name'] + routers = self.conn.search_routers(**kwargs) + + for router in routers: + interfaces_info = [] + for port in self.conn.list_router_interfaces(router): + if port.device_owner != "network:router_gateway": + for ip_spec in port.fixed_ips: + int_info = { + 'port_id': port.id, + 'ip_address': ip_spec.get('ip_address'), + 'subnet_id': ip_spec.get('subnet_id') + } + interfaces_info.append(int_info) + router['interfaces_info'] = interfaces_info + + self.exit(changed=False, openstack_routers=routers) + + +def main(): + module = RouterInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/security_group.py b/ansible_collections/openstack/cloud/plugins/modules/security_group.py new file mode 100644 index 00000000..8208a1c2 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/security_group.py @@ -0,0 +1,153 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: security_group +short_description: Add/Delete security groups from an OpenStack cloud. +author: OpenStack Ansible SIG +description: + - Add or Remove security groups from an OpenStack cloud. +options: + name: + description: + - Name that has to be given to the security group. This module + requires that security group names be unique. + required: true + type: str + description: + description: + - Long description of the purpose of the security group + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + project: + description: + - Unique name or ID of the project. + required: false + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a security group +- openstack.cloud.security_group: + cloud: mordred + state: present + name: foo + description: security group for foo servers + +# Update the existing 'foo' security group description +- openstack.cloud.security_group: + cloud: mordred + state: present + name: foo + description: updated description for the foo security group + +# Create a security group for a given project +- openstack.cloud.security_group: + cloud: mordred + state: present + name: foo + project: myproj +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class SecurityGroupModule(OpenStackModule): + + argument_spec = dict( + name=dict(required=True), + description=dict(default=''), + state=dict(default='present', choices=['absent', 'present']), + project=dict(default=None), + ) + + def _needs_update(self, secgroup): + """Check for differences in the updatable values. + + NOTE: We don't currently allow name updates. + """ + if secgroup['description'] != self.params['description']: + return True + return False + + def _system_state_change(self, secgroup): + state = self.params['state'] + if state == 'present': + if not secgroup: + return True + return self._needs_update(secgroup) + if state == 'absent' and secgroup: + return True + return False + + def run(self): + + name = self.params['name'] + state = self.params['state'] + description = self.params['description'] + project = self.params['project'] + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail_json(msg='Project %s could not be found' % project) + project_id = proj['id'] + else: + project_id = self.conn.current_project_id + + if project_id: + filters = {'tenant_id': project_id} + else: + filters = None + + secgroup = self.conn.get_security_group(name, filters=filters) + + if self.ansible.check_mode: + self.exit(changed=self._system_state_change(secgroup)) + + changed = False + if state == 'present': + if not secgroup: + kwargs = {} + if project_id: + kwargs['project_id'] = project_id + secgroup = self.conn.create_security_group(name, description, + **kwargs) + changed = True + else: + if self._needs_update(secgroup): + secgroup = self.conn.update_security_group( + secgroup['id'], description=description) + changed = True + self.exit( + changed=changed, id=secgroup['id'], secgroup=secgroup) + + if state == 'absent': + if secgroup: + self.conn.delete_security_group(secgroup['id']) + changed = True + self.exit(changed=changed) + + +def main(): + module = SecurityGroupModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/security_group_info.py b/ansible_collections/openstack/cloud/plugins/modules/security_group_info.py new file mode 100644 index 00000000..bc05356a --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/security_group_info.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2020 by Open Telekom Cloud, operated by T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: security_group_info +short_description: Lists security groups +extends_documentation_fragment: openstack.cloud.openstack +author: OpenStack Ansible SIG +description: + - List security groups +options: + description: + description: + - Description of the security group + type: str + name: + description: + - Name or id of the security group. + type: str + project_id: + description: + - Specifies the project id as filter criteria + type: str + revision_number: + description: + - Filter the list result by the revision number of the + - resource. + type: int + tags: + description: + - A list of tags to filter the list result by. + - Resources that match all tags in this list will be returned. + type: list + elements: str + any_tags: + description: + - A list of tags to filter the list result by. + - Resources that match any tag in this list will be returned. + type: list + elements: str + not_tags: + description: + - A list of tags to filter the list result by. + - Resources that match all tags in this list will be excluded. + type: list + elements: str + not_any_tags: + description: + - A list of tags to filter the list result by. + - Resources that match any tag in this list will be excluded. + type: list + elements: str + +requirements: ["openstacksdk"] +''' + +RETURN = ''' +security_groups: + description: List of dictionaries describing security groups. + type: complex + returned: On Success. + contains: + created_at: + description: Creation time of the security group + type: str + sample: "yyyy-mm-dd hh:mm:ss" + description: + description: Description of the security group + type: str + sample: "My security group" + id: + description: ID of the security group + type: str + sample: "d90e55ba-23bd-4d97-b722-8cb6fb485d69" + name: + description: Name of the security group. + type: str + sample: "my-sg" + project_id: + description: Project ID where the security group is located in. + type: str + sample: "25d24fc8-d019-4a34-9fff-0a09fde6a567" + security_group_rules: + description: Specifies the security group rule list + type: list + sample: [ + { + "id": "d90e55ba-23bd-4d97-b722-8cb6fb485d69", + "direction": "ingress", + "protocol": null, + "ethertype": "IPv4", + "description": null, + "remote_group_id": "0431c9c5-1660-42e0-8a00-134bec7f03e2", + "remote_ip_prefix": null, + "tenant_id": "bbfe8c41dd034a07bebd592bf03b4b0c", + "port_range_max": null, + "port_range_min": null, + "security_group_id": "0431c9c5-1660-42e0-8a00-134bec7f03e2" + }, + { + "id": "aecff4d4-9ce9-489c-86a3-803aedec65f7", + "direction": "egress", + "protocol": null, + "ethertype": "IPv4", + "description": null, + "remote_group_id": null, + "remote_ip_prefix": null, + "tenant_id": "bbfe8c41dd034a07bebd592bf03b4b0c", + "port_range_max": null, + "port_range_min": null, + "security_group_id": "0431c9c5-1660-42e0-8a00-134bec7f03e2" + } + ] + updated_at: + description: Update time of the security group + type: str + sample: "yyyy-mm-dd hh:mm:ss" +''' + +EXAMPLES = ''' +# Get specific security group +- openstack.cloud.security_group_info: + cloud: "{{ cloud }}" + name: "{{ my_sg }}" + register: sg +# Get all security groups +- openstack.cloud.security_group_info: + cloud: "{{ cloud }}" + register: sg +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule) + + +class SecurityGroupInfoModule(OpenStackModule): + argument_spec = dict( + description=dict(required=False, type='str'), + name=dict(required=False, type='str'), + project_id=dict(required=False, type='str'), + revision_number=dict(required=False, type='int'), + tags=dict(required=False, type='list', elements='str'), + any_tags=dict(required=False, type='list', elements='str'), + not_tags=dict(required=False, type='list', elements='str'), + not_any_tags=dict(required=False, type='list', elements='str') + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + description = self.params['description'] + name = self.params['name'] + project_id = self.params['project_id'] + revision_number = self.params['revision_number'] + tags = self.params['tags'] + any_tags = self.params['any_tags'] + not_tags = self.params['not_tags'] + not_any_tags = self.params['not_any_tags'] + + attrs = {} + + if description: + attrs['description'] = description + if project_id: + attrs['project_id'] = project_id + if revision_number: + attrs['revision_number'] = revision_number + if tags: + attrs['tags'] = ','.join(tags) + if any_tags: + attrs['any_tags'] = ','.join(any_tags) + if not_tags: + attrs['not_tags'] = ','.join(not_tags) + if not_any_tags: + attrs['not_any_tags'] = ','.join(not_any_tags) + + attrs = self.check_versioned(**attrs) + result = self.conn.network.security_groups(**attrs) + result = [item if isinstance(item, dict) else item.to_dict() for item in result] + if name: + result = [item for item in result if name in (item['id'], item['name'])] + self.results.update({'security_groups': result}) + + +def main(): + module = SecurityGroupInfoModule() + module() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/security_group_rule.py b/ansible_collections/openstack/cloud/plugins/modules/security_group_rule.py new file mode 100644 index 00000000..53fe6f59 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/security_group_rule.py @@ -0,0 +1,389 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: security_group_rule +short_description: Add/Delete rule from an existing security group +author: OpenStack Ansible SIG +description: + - Add or Remove rule from an existing security group +options: + security_group: + description: + - Name or ID of the security group + required: true + type: str + protocol: + description: + - IP protocols ANY TCP UDP ICMP and others, also number in range 0-255 + type: str + port_range_min: + description: + - Starting port + type: int + port_range_max: + description: + - Ending port + type: int + remote_ip_prefix: + description: + - Source IP address(es) in CIDR notation (exclusive with remote_group) + type: str + remote_group: + description: + - Name or ID of the Security group to link (exclusive with + remote_ip_prefix) + type: str + ethertype: + description: + - Must be IPv4 or IPv6, and addresses represented in CIDR must + match the ingress or egress rules. Not all providers support IPv6. + choices: ['IPv4', 'IPv6'] + default: IPv4 + type: str + direction: + description: + - The direction in which the security group rule is applied. Not + all providers support egress. + choices: ['egress', 'ingress'] + default: ingress + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + project: + description: + - Unique name or ID of the project. + required: false + type: str + description: + required: false + description: + - Description of the rule. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a security group rule +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 + +# Create a security group rule for ping +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 + +# Another way to create the ping rule +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: icmp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 + +# Create a TCP rule covering all ports +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: tcp + port_range_min: 1 + port_range_max: 65535 + remote_ip_prefix: 0.0.0.0/0 + +# Another way to create the TCP rule above (defaults to all ports) +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + +# Create a rule for VRRP with numbered protocol 112 +- openstack.cloud.security_group_rule: + security_group: loadbalancer_sg + protocol: 112 + remote_group: loadbalancer-node_sg + +# Create a security group rule for a given project +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 + project: myproj + +# Remove the default created egress rule for IPv4 +- openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: any + remote_ip_prefix: 0.0.0.0/0 +''' + +RETURN = ''' +id: + description: Unique rule UUID. + type: str + returned: state == present +direction: + description: The direction in which the security group rule is applied. + type: str + sample: 'egress' + returned: state == present +ethertype: + description: One of IPv4 or IPv6. + type: str + sample: 'IPv4' + returned: state == present +port_range_min: + description: The minimum port number in the range that is matched by + the security group rule. + type: int + sample: 8000 + returned: state == present +port_range_max: + description: The maximum port number in the range that is matched by + the security group rule. + type: int + sample: 8000 + returned: state == present +protocol: + description: The protocol that is matched by the security group rule. + type: str + sample: 'tcp' + returned: state == present +remote_ip_prefix: + description: The remote IP prefix to be associated with this security group rule. + type: str + sample: '0.0.0.0/0' + returned: state == present +security_group_id: + description: The security group ID to associate with this security group rule. + type: str + returned: state == present +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule) + + +def _ports_match(protocol, module_min, module_max, rule_min, rule_max): + """ + Capture the complex port matching logic. + + The port values coming in for the module might be -1 (for ICMP), + which will work only for Nova, but this is handled by sdk. Likewise, + they might be None, which works for Neutron, but not Nova. This too is + handled by sdk. Since sdk will consistently return these port + values as None, we need to convert any -1 values input to the module + to None here for comparison. + + For TCP and UDP protocols, None values for both min and max are + represented as the range 1-65535 for Nova, but remain None for + Neutron. sdk returns the full range when Nova is the backend (since + that is how Nova stores them), and None values for Neutron. If None + values are input to the module for both values, then we need to adjust + for comparison. + """ + + # Check if the user is supplying -1 for ICMP. + if protocol in ['icmp', 'ipv6-icmp']: + if module_min and int(module_min) == -1: + module_min = None + if module_max and int(module_max) == -1: + module_max = None + + # Rules with 'any' protocol do not match ports + if protocol == 'any': + return True + + # Check if the user is supplying -1, 1 to 65535 or None values for full TPC/UDP port range. + if protocol in ['tcp', 'udp'] or protocol is None: + if ( + not module_min and not module_max + or (int(module_min) in [-1, 1] + and int(module_max) in [-1, 65535]) + ): + if ( + not rule_min and not rule_max + or (int(rule_min) in [-1, 1] + and int(rule_max) in [-1, 65535]) + ): + # (None, None) == (1, 65535) == (-1, -1) + return True + + # Sanity check to make sure we don't have type comparison issues. + if module_min: + module_min = int(module_min) + if module_max: + module_max = int(module_max) + if rule_min: + rule_min = int(rule_min) + if rule_max: + rule_max = int(rule_max) + + return module_min == rule_min and module_max == rule_max + + +class SecurityGroupRuleModule(OpenStackModule): + deprecated_names = ('os_security_group_rule', 'openstack.cloud.os_security_group_rule') + + argument_spec = dict( + security_group=dict(required=True), + protocol=dict(type='str'), + port_range_min=dict(required=False, type='int'), + port_range_max=dict(required=False, type='int'), + remote_ip_prefix=dict(required=False), + remote_group=dict(required=False), + ethertype=dict(default='IPv4', + choices=['IPv4', 'IPv6']), + direction=dict(default='ingress', + choices=['egress', 'ingress']), + state=dict(default='present', + choices=['absent', 'present']), + description=dict(required=False, default=None), + project=dict(default=None), + ) + + module_kwargs = dict( + mutually_exclusive=[ + ['remote_ip_prefix', 'remote_group'], + ] + ) + + def _find_matching_rule(self, secgroup, remotegroup): + """ + Find a rule in the group that matches the module parameters. + :returns: The matching rule dict, or None if no matches. + """ + protocol = self.params['protocol'] + remote_ip_prefix = self.params['remote_ip_prefix'] + ethertype = self.params['ethertype'] + direction = self.params['direction'] + remote_group_id = remotegroup['id'] + + for rule in secgroup['security_group_rules']: + if ( + protocol == rule['protocol'] + and remote_ip_prefix == rule['remote_ip_prefix'] + and ethertype == rule['ethertype'] + and direction == rule['direction'] + and remote_group_id == rule['remote_group_id'] + and _ports_match( + protocol, + self.params['port_range_min'], + self.params['port_range_max'], + rule['port_range_min'], + rule['port_range_max']) + ): + return rule + return None + + def _system_state_change(self, secgroup, remotegroup): + state = self.params['state'] + if secgroup: + rule_exists = self._find_matching_rule(secgroup, remotegroup) + else: + return False + + if state == 'present' and not rule_exists: + return True + if state == 'absent' and rule_exists: + return True + return False + + def run(self): + + state = self.params['state'] + security_group = self.params['security_group'] + remote_group = self.params['remote_group'] + project = self.params['project'] + changed = False + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail_json(msg='Project %s could not be found' % project) + project_id = proj['id'] + else: + project_id = self.conn.current_project_id + + if project_id and not remote_group: + filters = {'tenant_id': project_id} + else: + filters = None + + secgroup = self.conn.get_security_group(security_group, filters=filters) + + if remote_group: + remotegroup = self.conn.get_security_group(remote_group, filters=filters) + else: + remotegroup = {'id': None} + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(secgroup, remotegroup)) + + if state == 'present': + if self.params['protocol'] == 'any': + self.params['protocol'] = None + + if not secgroup: + self.fail_json(msg='Could not find security group %s' % security_group) + + rule = self._find_matching_rule(secgroup, remotegroup) + if not rule: + kwargs = {} + if project_id: + kwargs['project_id'] = project_id + if self.params["description"] is not None: + kwargs["description"] = self.params['description'] + rule = self.conn.network.create_security_group_rule( + security_group_id=secgroup['id'], + port_range_min=None if self.params['port_range_min'] == -1 else self.params['port_range_min'], + port_range_max=None if self.params['port_range_max'] == -1 else self.params['port_range_max'], + protocol=self.params['protocol'], + remote_ip_prefix=self.params['remote_ip_prefix'], + remote_group_id=remotegroup['id'], + direction=self.params['direction'], + ethertype=self.params['ethertype'], + **kwargs + ) + changed = True + self.exit_json(changed=changed, rule=rule, id=rule['id']) + + if state == 'absent' and secgroup: + rule = self._find_matching_rule(secgroup, remotegroup) + if rule: + self.conn.delete_security_group_rule(rule['id']) + changed = True + + self.exit_json(changed=changed) + + +def main(): + module = SecurityGroupRuleModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/security_group_rule_info.py b/ansible_collections/openstack/cloud/plugins/modules/security_group_rule_info.py new file mode 100644 index 00000000..b00f7192 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/security_group_rule_info.py @@ -0,0 +1,251 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2020 by Tino Schreiber (Open Telekom Cloud), operated by T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: security_group_rule_info +short_description: Querying security group rules +author: OpenStack Ansible SIG +description: + - Querying security group rules +options: + description: + description: + - Filter the list result by the human-readable description of + the resource. + type: str + direction: + description: + - Filter the security group rule list result by the direction in + which the security group rule is applied. + choices: ['egress', 'ingress'] + type: str + ethertype: + description: + - Filter the security group rule list result by the ethertype of + network traffic. The value must be IPv4 or IPv6. + choices: ['IPv4', 'IPv6'] + type: str + port_range_min: + description: + - Starting port + type: int + port_range_max: + description: + - Ending port + type: int + project: + description: + - Unique name or ID of the project. + required: false + type: str + protocol: + description: + - Filter the security group rule list result by the IP protocol. + type: str + choices: ['any', 'tcp', 'udp', 'icmp', '112', '132'] + remote_group: + description: + - Filter the security group rule list result by the name or ID of the + remote group that associates with this security group rule. + type: str + remote_ip_prefix: + description: + - Source IP address(es) in CIDR notation (exclusive with remote_group) + type: str + revision_number: + description: + - Filter the list result by the revision number of the resource. + type: int + rule: + description: + - Filter the list result by the ID of the security group rule. + type: str + security_group: + description: + - Name or ID of the security group + type: str + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Get all security group rules +- openstack.cloud.security_group_rule_info: + cloud: "{{ cloud }}" + register: sg + +# Filter security group rules for port 80 and name +- openstack.cloud.security_group_rule_info: + cloud: "{{ cloud }}" + security_group: "{{ rule_name }}" + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 + +# Filter for ICMP rules +- openstack.cloud.security_group_rule_info: + cloud: "{{ cloud }}" + protocol: icmp +''' + +RETURN = ''' +security_group_rules: + description: List of dictionaries describing security group rules. + type: complex + returned: On Success. + contains: + id: + description: Unique rule UUID. + type: str + description: + description: Human-readable description of the resource. + type: str + sample: 'My description.' + direction: + description: The direction in which the security group rule is applied. + type: str + sample: 'egress' + ethertype: + description: One of IPv4 or IPv6. + type: str + sample: 'IPv4' + port_range_min: + description: The minimum port number in the range that is matched by + the security group rule. + type: int + sample: 8000 + port_range_max: + description: The maximum port number in the range that is matched by + the security group rule. + type: int + sample: 8000 + project: + description: + - Unique ID of the project. + type: str + sample: '16d53a84a13b49529d2e2c3646691123' + protocol: + description: The protocol that is matched by the security group rule. + type: str + sample: 'tcp' + remote_ip_prefix: + description: The remote IP prefix to be associated with this security group rule. + type: str + sample: '0.0.0.0/0' + security_group_id: + description: The security group ID to associate with this security group rule. + type: str + sample: '729b9660-a20a-41fe-bae6-ed8fa7f69123' +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule) + + +class SecurityGroupRuleInfoModule(OpenStackModule): + argument_spec = dict( + description=dict(required=False, type='str'), + direction=dict(required=False, + type='str', + choices=['egress', 'ingress']), + ethertype=dict(required=False, + type='str', + choices=['IPv4', 'IPv6']), + port_range_min=dict(required=False, type='int', min_ver="0.32.0"), + port_range_max=dict(required=False, type='int', min_ver="0.32.0"), + project=dict(required=False, type='str'), + protocol=dict(required=False, + type='str', + choices=['any', 'tcp', 'udp', 'icmp', '112', '132']), + remote_group=dict(required=False, type='str'), + remote_ip_prefix=dict(required=False, type='str', min_ver="0.32.0"), + revision_number=dict(required=False, type='int'), + rule=dict(required=False, type='str'), + security_group=dict(required=False, type='str') + ) + + module_kwargs = dict( + mutually_exclusive=[ + ['remote_ip_prefix', 'remote_group'], + ], + supports_check_mode=True + ) + + def run(self): + description = self.params['description'] + direction = self.params['direction'] + ethertype = self.params['ethertype'] + project = self.params['project'] + protocol = self.params['protocol'] + remote_group = self.params['remote_group'] + revision_number = self.params['revision_number'] + rule = self.params['rule'] + security_group = self.params['security_group'] + + changed = False + filters = self.check_versioned( + port_range_min=self.params['port_range_min'], + port_range_max=self.params['port_range_max'], + remote_ip_prefix=self.params['remote_ip_prefix'] + ) + data = [] + + if rule: + sec_rule = self.conn.network.get_security_group_rule(rule) + if sec_rule is None: + self.exit(changed=changed, security_group_rules=[]) + self.exit(changed=changed, + security_group_rules=sec_rule.to_dict()) + # query parameter id is currently not supported + # PR is open for that. + # filters['id] = sec_rule.id + if description: + filters['description'] = description + if direction: + filters['direction'] = direction + if ethertype: + filters['ethertype'] = ethertype + if project: + proj = self.conn.get_project(project) + if proj is None: + self.fail_json(msg='Project %s could not be found' % project) + filters['project_id'] = proj.id + if protocol: + filters['protocol'] = protocol + if remote_group: + filters['remote_group_id'] = remote_group + if revision_number: + filters['revision_number'] = revision_number + if security_group: + sec_grp = self.conn.network.find_security_group( + name_or_id=security_group, + ignore_missing=True) + if sec_grp is None: + self.fail_json(msg='Security group %s could not be found' % sec_grp) + filters['security_group_id'] = sec_grp.id + + for item in self.conn.network.security_group_rules(**filters): + item = item.to_dict() + data.append(item) + + self.exit_json(changed=changed, + security_group_rules=data) + + +def main(): + module = SecurityGroupRuleInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/server.py b/ansible_collections/openstack/cloud/plugins/modules/server.py new file mode 100644 index 00000000..a3ca7d05 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/server.py @@ -0,0 +1,805 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright 2019 Red Hat, Inc. +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# Copyright (c) 2013, John Dewey <john@dewey.ws> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server +short_description: Create/Delete Compute Instances from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Remove compute instances from OpenStack. +options: + name: + description: + - Name that has to be given to the instance. It is also possible to + specify the ID of the instance instead of its name if I(state) is I(absent). + required: true + type: str + image: + description: + - The name or id of the base image to boot. + - Required when I(boot_from_volume=true) + type: str + image_exclude: + description: + - Text to use to filter image names, for the case, such as HP, where + there are multiple image names matching the common identifying + portions. image_exclude is a negative match filter - it is text that + may not exist in the image name. + type: str + default: "(deprecated)" + flavor: + description: + - The name or id of the flavor in which the new instance has to be + created. + - Exactly one of I(flavor) and I(flavor_ram) must be defined when + I(state=present). + type: str + flavor_ram: + description: + - The minimum amount of ram in MB that the flavor in which the new + instance has to be created must have. + - Exactly one of I(flavor) and I(flavor_ram) must be defined when + I(state=present). + type: int + flavor_include: + description: + - Text to use to filter flavor names, for the case, such as Rackspace, + where there are multiple flavors that have the same ram count. + flavor_include is a positive match filter - it must exist in the + flavor name. + type: str + key_name: + description: + - The key pair name to be used when creating a instance + type: str + security_groups: + description: + - Names of the security groups to which the instance should be + added. This may be a YAML list or a comma separated string. + type: list + default: ['default'] + elements: str + network: + description: + - Name or ID of a network to attach this instance to. A simpler + version of the nics parameter, only one of network or nics should + be supplied. + type: str + nics: + description: + - A list of networks to which the instance's interface should + be attached. Networks may be referenced by net-id/net-name/port-id + or port-name. + - 'Also this accepts a string containing a list of (net/port)-(id/name) + Eg: nics: "net-id=uuid-1,port-name=myport" + Only one of network or nics should be supplied.' + type: list + elements: raw + suboptions: + tag: + description: + - 'A "tag" for the specific port to be passed via metadata. + Eg: tag: test_tag' + auto_ip: + description: + - Ensure instance has public ip however the cloud wants to do that + type: bool + default: 'yes' + aliases: ['auto_floating_ip', 'public_ip'] + floating_ips: + description: + - list of valid floating IPs that pre-exist to assign to this node + type: list + elements: str + floating_ip_pools: + description: + - Name of floating IP pool from which to choose a floating IP + type: list + elements: str + meta: + description: + - 'A list of key value pairs that should be provided as a metadata to + the new instance or a string containing a list of key-value pairs. + Eg: meta: "key1=value1,key2=value2"' + type: raw + wait: + description: + - If the module should wait for the instance to be created. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the instance to get + into active state. + default: 180 + type: int + config_drive: + description: + - Whether to boot the server with config drive enabled + type: bool + default: 'no' + userdata: + description: + - Opaque blob of data which is made available to the instance + type: str + aliases: ['user_data'] + boot_from_volume: + description: + - Should the instance boot from a persistent volume created based on + the image given. Mutually exclusive with boot_volume. + type: bool + default: 'no' + volume_size: + description: + - The size of the volume to create in GB if booting from volume based + on an image. + type: int + boot_volume: + description: + - Volume name or id to use as the volume to boot from. Implies + boot_from_volume. Mutually exclusive with image and boot_from_volume. + aliases: ['root_volume'] + type: str + terminate_volume: + description: + - If C(yes), delete volume when deleting instance (if booted from volume) + type: bool + default: 'no' + volumes: + description: + - A list of preexisting volumes names or ids to attach to the instance + default: [] + type: list + elements: str + scheduler_hints: + description: + - Arbitrary key/value pairs to the scheduler for custom use + type: dict + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + delete_fip: + description: + - When I(state) is absent and this option is true, any floating IP + associated with the instance will be deleted along with the instance. + type: bool + default: 'no' + reuse_ips: + description: + - When I(auto_ip) is true and this option is true, the I(auto_ip) code + will attempt to re-use unassigned floating ips in the project before + creating a new one. It is important to note that it is impossible + to safely do this concurrently, so if your use case involves + concurrent server creation, it is highly recommended to set this to + false and to delete the floating ip associated with a server when + the server is deleted using I(delete_fip). + type: bool + default: 'yes' + availability_zone: + description: + - Availability zone in which to create the server. + type: str + description: + description: + - Description of the server. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a new instance and attaches to a network and passes metadata to the instance + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: + - net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723 + - net-name: another_network + meta: + hostname: test1 + group: uge_master + +# Create a new instance in HP Cloud AE1 region availability zone az2 and +# automatically assigns a floating IP +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: username + password: Equality7-2521 + project_name: username-project1 + name: vm1 + region_name: region-b.geo-1 + availability_zone: az2 + image: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + timeout: 200 + flavor: 101 + security_groups: default + auto_ip: yes + +# Create a new instance in named cloud mordred availability zone az2 +# and assigns a pre-known floating IP +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + state: present + cloud: mordred + name: vm1 + availability_zone: az2 + image: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + timeout: 200 + flavor: 101 + floating_ips: + - 12.34.56.79 + +# Create a new instance with 4G of RAM on Ubuntu Trusty, ignoring +# deprecated images +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + cloud: mordred + region_name: region-b.geo-1 + image: Ubuntu Server 14.04 + image_exclude: deprecated + flavor_ram: 4096 + +# Create a new instance with 4G of RAM on Ubuntu Trusty on a Performance node +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + cloud: rax-dfw + state: present + image: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM) + flavor_ram: 4096 + flavor_include: Performance + +# Creates a new instance and attaches to multiple network +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance with a string + openstack.cloud.server: + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: "net-id=4cb08b20-62fe-11e5-9d70-feff819cdc9f,net-id=542f0430-62fe-11e5-9d70-feff819cdc9f..." + +- name: Creates a new instance and attaches to a network and passes metadata to the instance + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: + - net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723 + - net-name: another_network + meta: "hostname=test1,group=uge_master" + +- name: Creates a new instance and attaches to a specific network + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + network: another_network + +# Create a new instance with 4G of RAM on a 75G Ubuntu Trusty volume +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + boot_from_volume: True + volume_size: 75 + +# Creates a new instance with 2 volumes attached +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + volumes: + - photos + - music + +# Creates a new instance with provisioning userdata using Cloud-Init +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + image: "Ubuntu Server 14.04" + flavor: "P-1" + network: "Production" + userdata: | + #cloud-config + chpasswd: + list: | + ubuntu:{{ default_password }} + expire: False + packages: + - ansible + package_upgrade: true + +# Creates a new instance with provisioning userdata using Bash Scripts +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + image: "Ubuntu Server 14.04" + flavor: "P-1" + network: "Production" + userdata: | + {%- raw -%}#!/bin/bash + echo " up ip route add 10.0.0.0/8 via {% endraw -%}{{ intra_router }}{%- raw -%}" >> /etc/network/interfaces.d/eth0.conf + echo " down ip route del 10.0.0.0/8" >> /etc/network/interfaces.d/eth0.conf + ifdown eth0 && ifup eth0 + {% endraw %} + +# Create a new instance with server group for (anti-)affinity +# server group ID is returned from openstack.cloud.server_group module. +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + state: present + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + flavor: 4 + scheduler_hints: + group: f5c8c61a-9230-400a-8ed2-3b023c190a7f + +# Create an instance with "tags" for the nic +- name: Create instance with nics "tags" + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + flavor: 4 + nics: + - port-name: net1_port1 + tag: test_tag + - net-name: another_network + +# Deletes an instance via its ID +- name: remove an instance + hosts: localhost + tasks: + - name: remove an instance + openstack.cloud.server: + name: abcdef01-2345-6789-0abc-def0123456789 + state: absent + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_find_nova_addresses, OpenStackModule) + + +def _parse_nics(nics): + for net in nics: + if isinstance(net, str): + for nic in net.split(','): + yield dict((nic.split('='),)) + else: + yield net + + +def _parse_meta(meta): + if isinstance(meta, str): + metas = {} + for kv_str in meta.split(","): + k, v = kv_str.split("=") + metas[k] = v + return metas + if not meta: + return {} + return meta + + +class ServerModule(OpenStackModule): + deprecated_names = ('os_server', 'openstack.cloud.os_server') + + argument_spec = dict( + name=dict(required=True), + image=dict(default=None), + image_exclude=dict(default='(deprecated)'), + flavor=dict(default=None), + flavor_ram=dict(default=None, type='int'), + flavor_include=dict(default=None), + key_name=dict(default=None), + security_groups=dict(default=['default'], type='list', elements='str'), + network=dict(default=None), + nics=dict(default=[], type='list', elements='raw'), + meta=dict(default=None, type='raw'), + userdata=dict(default=None, aliases=['user_data']), + config_drive=dict(default=False, type='bool'), + auto_ip=dict(default=True, type='bool', aliases=['auto_floating_ip', 'public_ip']), + floating_ips=dict(default=None, type='list', elements='str'), + floating_ip_pools=dict(default=None, type='list', elements='str'), + volume_size=dict(default=None, type='int'), + boot_from_volume=dict(default=False, type='bool'), + boot_volume=dict(default=None, aliases=['root_volume']), + terminate_volume=dict(default=False, type='bool'), + volumes=dict(default=[], type='list', elements='str'), + scheduler_hints=dict(default=None, type='dict'), + state=dict(default='present', choices=['absent', 'present']), + delete_fip=dict(default=False, type='bool'), + reuse_ips=dict(default=True, type='bool'), + description=dict(default=None, type='str'), + ) + module_kwargs = dict( + mutually_exclusive=[ + ['auto_ip', 'floating_ips'], + ['auto_ip', 'floating_ip_pools'], + ['floating_ips', 'floating_ip_pools'], + ['flavor', 'flavor_ram'], + ['image', 'boot_volume'], + ['boot_from_volume', 'boot_volume'], + ['nics', 'network'], + ], + required_if=[ + ('boot_from_volume', True, ['volume_size', 'image']), + ], + ) + + def run(self): + + state = self.params['state'] + image = self.params['image'] + boot_volume = self.params['boot_volume'] + flavor = self.params['flavor'] + flavor_ram = self.params['flavor_ram'] + + if state == 'present': + if not (image or boot_volume): + self.fail( + msg="Parameter 'image' or 'boot_volume' is required " + "if state == 'present'" + ) + if not flavor and not flavor_ram: + self.fail( + msg="Parameter 'flavor' or 'flavor_ram' is required " + "if state == 'present'" + ) + + if state == 'present': + self._get_server_state() + self._create_server() + elif state == 'absent': + self._get_server_state() + self._delete_server() + + def _exit_hostvars(self, server, changed=True): + hostvars = self.conn.get_openstack_vars(server) + self.exit( + changed=changed, server=server, id=server.id, openstack=hostvars) + + def _get_server_state(self): + state = self.params['state'] + server = self.conn.get_server(self.params['name']) + if server and state == 'present': + if server.status not in ('ACTIVE', 'SHUTOFF', 'PAUSED', 'SUSPENDED'): + self.fail( + msg="The instance is available but not Active state: " + server.status) + (ip_changed, server) = self._check_ips(server) + (sg_changed, server) = self._check_security_groups(server) + (server_changed, server) = self._update_server(server) + self._exit_hostvars(server, ip_changed or sg_changed or server_changed) + if server and state == 'absent': + return True + if state == 'absent': + self.exit(changed=False, result="not present") + return True + + def _create_server(self): + flavor = self.params['flavor'] + flavor_ram = self.params['flavor_ram'] + flavor_include = self.params['flavor_include'] + + image_id = None + if not self.params['boot_volume']: + image_id = self.conn.get_image_id( + self.params['image'], self.params['image_exclude']) + if not image_id: + self.fail( + msg="Could not find image %s" % self.params['image']) + + if flavor: + flavor_dict = self.conn.get_flavor(flavor) + if not flavor_dict: + self.fail(msg="Could not find flavor %s" % flavor) + else: + flavor_dict = self.conn.get_flavor_by_ram(flavor_ram, flavor_include) + if not flavor_dict: + self.fail(msg="Could not find any matching flavor") + + nics = self._network_args() + + self.params['meta'] = _parse_meta(self.params['meta']) + + bootkwargs = self.check_versioned( + name=self.params['name'], + image=image_id, + flavor=flavor_dict['id'], + nics=nics, + meta=self.params['meta'], + security_groups=self.params['security_groups'], + userdata=self.params['userdata'], + config_drive=self.params['config_drive'], + ) + for optional_param in ( + 'key_name', 'availability_zone', 'network', + 'scheduler_hints', 'volume_size', 'volumes', + 'description'): + if self.params[optional_param]: + bootkwargs[optional_param] = self.params[optional_param] + + server = self.conn.create_server( + ip_pool=self.params['floating_ip_pools'], + ips=self.params['floating_ips'], + auto_ip=self.params['auto_ip'], + boot_volume=self.params['boot_volume'], + boot_from_volume=self.params['boot_from_volume'], + terminate_volume=self.params['terminate_volume'], + reuse_ips=self.params['reuse_ips'], + wait=self.params['wait'], timeout=self.params['timeout'], + **bootkwargs + ) + + self._exit_hostvars(server) + + def _update_server(self, server): + changed = False + + self.params['meta'] = _parse_meta(self.params['meta']) + + # self.conn.set_server_metadata only updates the key=value pairs, it doesn't + # touch existing ones + update_meta = {} + for (k, v) in self.params['meta'].items(): + if k not in server.metadata or server.metadata[k] != v: + update_meta[k] = v + + if update_meta: + self.conn.set_server_metadata(server, update_meta) + changed = True + # Refresh server vars + server = self.conn.get_server(self.params['name']) + + return (changed, server) + + def _delete_server(self): + try: + self.conn.delete_server( + self.params['name'], wait=self.params['wait'], + timeout=self.params['timeout'], + delete_ips=self.params['delete_fip']) + except Exception as e: + self.fail(msg="Error in deleting vm: %s" % e) + self.exit(changed=True, result='deleted') + + def _network_args(self): + args = [] + nics = self.params['nics'] + + if not isinstance(nics, list): + self.fail(msg='The \'nics\' parameter must be a list.') + + for num, net in enumerate(_parse_nics(nics)): + if not isinstance(net, dict): + self.fail( + msg='Each entry in the \'nics\' parameter must be a dict.') + + if net.get('net-id'): + args.append(net) + elif net.get('net-name'): + by_name = self.conn.get_network(net['net-name']) + if not by_name: + self.fail( + msg='Could not find network by net-name: %s' % + net['net-name']) + resolved_net = net.copy() + del resolved_net['net-name'] + resolved_net['net-id'] = by_name['id'] + args.append(resolved_net) + elif net.get('port-id'): + args.append(net) + elif net.get('port-name'): + by_name = self.conn.get_port(net['port-name']) + if not by_name: + self.fail( + msg='Could not find port by port-name: %s' % + net['port-name']) + resolved_net = net.copy() + del resolved_net['port-name'] + resolved_net['port-id'] = by_name['id'] + args.append(resolved_net) + + if 'tag' in net: + args[num]['tag'] = net['tag'] + return args + + def _detach_ip_list(self, server, extra_ips): + for ip in extra_ips: + ip_id = self.conn.get_floating_ip( + id=None, filters={'floating_ip_address': ip}) + self.conn.detach_ip_from_server( + server_id=server.id, floating_ip_id=ip_id) + + def _check_ips(self, server): + changed = False + + auto_ip = self.params['auto_ip'] + floating_ips = self.params['floating_ips'] + floating_ip_pools = self.params['floating_ip_pools'] + + if floating_ip_pools or floating_ips: + ips = openstack_find_nova_addresses(server.addresses, 'floating') + if not ips: + # If we're configured to have a floating but we don't have one, + # let's add one + server = self.conn.add_ips_to_server( + server, + auto_ip=auto_ip, + ips=floating_ips, + ip_pool=floating_ip_pools, + wait=self.params['wait'], + timeout=self.params['timeout'], + ) + changed = True + elif floating_ips: + # we were configured to have specific ips, let's make sure we have + # those + missing_ips = [] + for ip in floating_ips: + if ip not in ips: + missing_ips.append(ip) + if missing_ips: + server = self.conn.add_ip_list(server, missing_ips, + wait=self.params['wait'], + timeout=self.params['timeout']) + changed = True + extra_ips = [] + for ip in ips: + if ip not in floating_ips: + extra_ips.append(ip) + if extra_ips: + self._detach_ip_list(server, extra_ips) + changed = True + elif auto_ip: + if server['interface_ip']: + changed = False + else: + # We're configured for auto_ip but we're not showing an + # interface_ip. Maybe someone deleted an IP out from under us. + server = self.conn.add_ips_to_server( + server, + auto_ip=auto_ip, + ips=floating_ips, + ip_pool=floating_ip_pools, + wait=self.params['wait'], + timeout=self.params['timeout'], + ) + changed = True + return (changed, server) + + def _check_security_groups(self, server): + changed = False + + # server security groups were added to shade in 1.19. Until then this + # module simply ignored trying to update security groups and only set them + # on newly created hosts. + if not ( + hasattr(self.conn, 'add_server_security_groups') + and hasattr(self.conn, 'remove_server_security_groups') + ): + return changed, server + + module_security_groups = set(self.params['security_groups']) + server_security_groups = set(sg['name'] for sg in server.security_groups) + + add_sgs = module_security_groups - server_security_groups + remove_sgs = server_security_groups - module_security_groups + + if add_sgs: + self.conn.add_server_security_groups(server, list(add_sgs)) + changed = True + + if remove_sgs: + self.conn.remove_server_security_groups(server, list(remove_sgs)) + changed = True + + return (changed, server) + + +def main(): + module = ServerModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/server_action.py b/ansible_collections/openstack/cloud/plugins/modules/server_action.py new file mode 100644 index 00000000..341ff374 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/server_action.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2015, Jesse Keating <jlk@derpops.bike> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_action +short_description: Perform actions on Compute Instances from OpenStack +author: OpenStack Ansible SIG +description: + - Perform server actions on an existing compute instance from OpenStack. + This module does not return any data other than changed true/false. + When I(action) is 'rebuild', then I(image) parameter is required. +options: + server: + description: + - Name or ID of the instance + required: true + type: str + wait: + description: + - If the module should wait for the instance action to be performed. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the instance to perform + the requested action. + default: 180 + type: int + action: + description: + - Perform the given action. The lock and unlock actions always return + changed as the servers API does not provide lock status. + choices: [stop, start, pause, unpause, lock, unlock, suspend, resume, + rebuild, shelve, shelve_offload, unshelve] + type: str + required: true + image: + description: + - Image the server should be rebuilt with + type: str + admin_password: + description: + - Admin password for server to rebuild + type: str + all_projects: + description: + - Whether to search for server in all projects or just the current + auth scoped project. + type: bool + default: 'no' + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Pauses a compute instance +- openstack.cloud.server_action: + action: pause + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + server: vm1 + timeout: 200 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + +# If I(action) is set to C(shelve) then according to OpenStack's Compute API, the shelved +# server is in one of two possible states: +# +# SHELVED: The server is in shelved state. Depends on the shelve offload time, +# the server will be automatically shelved off loaded. +# SHELVED_OFFLOADED: The shelved server is offloaded (removed from the compute host) and +# it needs unshelved action to be used again. +# +# But wait_for_server can only wait for a single server state. If a shelved server is offloaded +# immediately, then a exceptions.ResourceTimeout will be raised if I(action) is set to C(shelve). +# This is likely to happen because shelved_offload_time in Nova's config is set to 0 by default. +# This also applies if you boot the server from volumes. +# +# Calling C(shelve_offload) instead of C(shelve) will also fail most likely because the default +# policy does not allow C(shelve_offload) for non-admin users while C(shelve) is allowed for +# admin users and server owners. +# +# As we cannot retrieve shelved_offload_time from Nova's config, we fall back to waiting for +# one state and if that fails then we fetch the server's state and match it against the other +# valid states from _action_map. +# +# Ref.: https://docs.openstack.org/api-guide/compute/server_concepts.html + +_action_map = {'stop': ['SHUTOFF'], + 'start': ['ACTIVE'], + 'pause': ['PAUSED'], + 'unpause': ['ACTIVE'], + 'lock': ['ACTIVE'], # API doesn't show lock/unlock status + 'unlock': ['ACTIVE'], + 'suspend': ['SUSPENDED'], + 'resume': ['ACTIVE'], + 'rebuild': ['ACTIVE'], + 'shelve': ['SHELVED_OFFLOADED', 'SHELVED'], + 'shelve_offload': ['SHELVED_OFFLOADED'], + 'unshelve': ['ACTIVE']} + +_admin_actions = ['pause', 'unpause', 'suspend', 'resume', 'lock', 'unlock', 'shelve_offload'] + + +class ServerActionModule(OpenStackModule): + deprecated_names = ('os_server_action', 'openstack.cloud.os_server_action') + + argument_spec = dict( + server=dict(required=True, type='str'), + action=dict(required=True, type='str', + choices=['stop', 'start', 'pause', 'unpause', + 'lock', 'unlock', 'suspend', 'resume', + 'rebuild', 'shelve', 'shelve_offload', 'unshelve']), + image=dict(required=False, type='str'), + admin_password=dict(required=False, type='str', no_log=True), + all_projects=dict(required=False, type='bool', default=False), + ) + module_kwargs = dict( + required_if=[('action', 'rebuild', ['image'])], + supports_check_mode=True, + ) + + def run(self): + os_server = self._preliminary_checks() + self._execute_server_action(os_server) + # for some reason we don't wait for lock and unlock before exit + if self.params['action'] not in ('lock', 'unlock'): + if self.params['wait']: + self._wait(os_server) + self.exit_json(changed=True) + + def _preliminary_checks(self): + # Using Munch object for getting information about a server + os_server = self.conn.get_server( + self.params['server'], + all_projects=self.params['all_projects'], + ) + if not os_server: + self.fail_json(msg='Could not find server %s' % self.params['server']) + # check mode + if self.ansible.check_mode: + self.exit_json(changed=self.__system_state_change(os_server)) + # examine special cases + # lock, unlock and rebuild don't depend on state, just do it + if self.params['action'] not in ('lock', 'unlock', 'rebuild'): + if not self.__system_state_change(os_server): + self.exit_json(changed=False) + return os_server + + def _execute_server_action(self, os_server): + if self.params['action'] == 'rebuild': + return self._rebuild_server(os_server) + if self.params['action'] == 'shelve_offload': + # shelve_offload is not supported in OpenstackSDK + return self._action(os_server, json={'shelveOffload': None}) + action_name = self.params['action'] + "_server" + try: + func_name = getattr(self.conn.compute, action_name) + except AttributeError: + self.fail_json( + msg="Method %s wasn't found in OpenstackSDK compute" % action_name) + func_name(os_server) + + def _rebuild_server(self, os_server): + # rebuild should ensure images exists + try: + image = self.conn.get_image(self.params['image']) + except Exception as e: + self.fail_json( + msg="Can't find the image %s: %s" % (self.params['image'], e)) + if not image: + self.fail_json(msg="Image %s was not found!" % self.params['image']) + # admin_password is required by SDK, but not required by Nova API + if self.params['admin_password']: + self.conn.compute.rebuild_server( + server=os_server, + name=os_server['name'], + image=image['id'], + admin_password=self.params['admin_password'] + ) + else: + self._action(os_server, json={'rebuild': {'imageRef': image['id']}}) + + def _action(self, os_server, json): + response = self.conn.compute.post( + '/servers/{server_id}/action'.format(server_id=os_server['id']), + json=json) + self.sdk.exceptions.raise_from_response(response) + return response + + def _wait(self, os_server): + """Wait for the server to reach the desired state for the given action.""" + # The wait_for_server function needs a Server object instead of the + # Munch object returned by self.conn.get_server + server = self.conn.compute.get_server(os_server['id']) + states = _action_map[self.params['action']] + + try: + self.conn.compute.wait_for_server( + server, + status=states[0], + wait=self.params['timeout']) + except self.sdk.exceptions.ResourceTimeout: + # raise if there is only one valid state + if len(states) < 2: + raise + # fetch current server status and compare to other valid states + server = self.conn.compute.get_server(os_server['id']) + if server.status not in states: + raise + + def __system_state_change(self, os_server): + """Check if system state would change.""" + return os_server.status not in _action_map[self.params['action']] + + +def main(): + module = ServerActionModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/server_group.py b/ansible_collections/openstack/cloud/plugins/modules/server_group.py new file mode 100644 index 00000000..84f59e6c --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/server_group.py @@ -0,0 +1,162 @@ +#!/usr/bin/python + +# Copyright (c) 2016 Catalyst IT Limited +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_group +short_description: Manage OpenStack server groups +author: OpenStack Ansible SIG +description: + - Add or remove server groups from OpenStack. +options: + state: + description: + - Indicate desired state of the resource. When I(state) is 'present', + then I(policies) is required. + choices: ['present', 'absent'] + required: false + default: present + type: str + name: + description: + - Server group name. + required: true + type: str + policies: + description: + - A list of one or more policy names to associate with the server + group. The list must contain at least one policy name. The current + valid policy names are anti-affinity, affinity, soft-anti-affinity + and soft-affinity. + required: false + type: list + elements: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a server group with 'affinity' policy. +- openstack.cloud.server_group: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: my_server_group + policies: + - affinity + +# Delete 'my_server_group' server group. +- openstack.cloud.server_group: + state: absent + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: my_server_group +''' + +RETURN = ''' +id: + description: Unique UUID. + returned: success + type: str +name: + description: The name of the server group. + returned: success + type: str +policies: + description: A list of one or more policy names of the server group. + returned: success + type: list +members: + description: A list of members in the server group. + returned: success + type: list +metadata: + description: Metadata key and value pairs. + returned: success + type: dict +project_id: + description: The project ID who owns the server group. + returned: success + type: str +user_id: + description: The user ID who owns the server group. + returned: success + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ServerGroupModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + policies=dict(required=False, type='list', elements='str'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + def _system_state_change(self, state, server_group): + if state == 'present' and not server_group: + return True + if state == 'absent' and server_group: + return True + + return False + + def run(self): + name = self.params['name'] + policies = self.params['policies'] + state = self.params['state'] + + server_group = self.conn.get_server_group(name) + + if self.ansible.check_mode: + self.exit_json( + changed=self._system_state_change(state, server_group) + ) + + changed = False + if state == 'present': + if not server_group: + if not policies: + self.fail_json( + msg="Parameter 'policies' is required in Server Group " + "Create" + ) + server_group = self.conn.create_server_group(name, policies) + changed = True + + self.exit_json( + changed=changed, + id=server_group['id'], + server_group=server_group + ) + if state == 'absent': + if server_group: + self.conn.delete_server_group(server_group['id']) + changed = True + self.exit_json(changed=changed) + + +def main(): + module = ServerGroupModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/server_info.py b/ansible_collections/openstack/cloud/plugins/modules/server_info.py new file mode 100644 index 00000000..bac1d211 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/server_info.py @@ -0,0 +1,96 @@ +#!/usr/bin/python + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_info +short_description: Retrieve information about one or more compute instances +author: OpenStack Ansible SIG +description: + - Retrieve information about server instances from OpenStack. + - This module was called C(os_server_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.server_info) module no longer returns C(ansible_facts)! +notes: + - The result contains a list of servers. +options: + server: + description: + - restrict results to servers with names or UUID matching + this glob expression (e.g., <web*>). + type: str + detailed: + description: + - when true, return additional detail about servers at the expense + of additional API calls. + type: bool + default: 'no' + filters: + description: + - restrict results to servers matching a dictionary of + filters + type: dict + all_projects: + description: + - Whether to list servers from all projects or just the current auth + scoped project. + type: bool + default: 'no' +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Gather information about all servers named <web*> that are in an active state: +- openstack.cloud.server_info: + cloud: rax-dfw + server: web* + filters: + vm_state: active + register: result +- debug: + msg: "{{ result.openstack_servers }}" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ServerInfoModule(OpenStackModule): + + deprecated_names = ('os_server_info', 'openstack.cloud.os_server_info') + + argument_spec = dict( + server=dict(required=False), + detailed=dict(required=False, type='bool', default=False), + filters=dict(required=False, type='dict', default=None), + all_projects=dict(required=False, type='bool', default=False), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + + kwargs = self.check_versioned( + detailed=self.params['detailed'], + filters=self.params['filters'], + all_projects=self.params['all_projects'] + ) + if self.params['server']: + kwargs['name_or_id'] = self.params['server'] + openstack_servers = self.conn.search_servers(**kwargs) + self.exit(changed=False, openstack_servers=openstack_servers) + + +def main(): + module = ServerInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/server_metadata.py b/ansible_collections/openstack/cloud/plugins/modules/server_metadata.py new file mode 100644 index 00000000..a1207e3b --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/server_metadata.py @@ -0,0 +1,165 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2016, Mario Santos <mario.rf.santos@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_metadata +short_description: Add/Update/Delete Metadata in Compute Instances from OpenStack +author: OpenStack Ansible SIG +description: + - Add, Update or Remove metadata in compute instances from OpenStack. +options: + server: + description: + - Name of the instance to update the metadata + required: true + aliases: ['name'] + type: str + meta: + description: + - 'A list of key value pairs that should be provided as a metadata to + the instance or a string containing a list of key-value pairs. + Eg: meta: "key1=value1,key2=value2"' + required: true + type: dict + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + availability_zone: + description: + - Availability zone in which to create the snapshot. + required: false + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Creates or updates hostname=test1 as metadata of the server instance vm1 +- name: add metadata to compute instance + hosts: localhost + tasks: + - name: add metadata to instance + openstack.cloud.server_metadata: + state: present + auth: + auth_url: https://openstack-api.example.com:35357/v2.0/ + username: admin + password: admin + project_name: admin + name: vm1 + meta: + hostname: test1 + group: group1 + +# Removes the keys under meta from the instance named vm1 +- name: delete metadata from compute instance + hosts: localhost + tasks: + - name: delete metadata from instance + openstack.cloud.server_metadata: + state: absent + auth: + auth_url: https://openstack-api.example.com:35357/v2.0/ + username: admin + password: admin + project_name: admin + name: vm1 + meta: + hostname: + group: +''' + +RETURN = ''' +server_id: + description: The compute instance id where the change was made + returned: success + type: str + sample: "324c4e91-3e03-4f62-9a4d-06119a8a8d16" +metadata: + description: The metadata of compute instance after the change + returned: success + type: dict + sample: {'key1': 'value1', 'key2': 'value2'} +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ServerMetadataModule(OpenStackModule): + argument_spec = dict( + server=dict(required=True, aliases=['name']), + meta=dict(required=True, type='dict'), + state=dict(default='present', choices=['absent', 'present']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def _needs_update(self, server_metadata=None, metadata=None): + if server_metadata is None: + server_metadata = {} + if metadata is None: + metadata = {} + return len(set(metadata.items()) - set(server_metadata.items())) != 0 + + def _get_keys_to_delete(self, server_metadata_keys=None, metadata_keys=None): + if server_metadata_keys is None: + server_metadata_keys = [] + if metadata_keys is None: + metadata_keys = [] + return set(server_metadata_keys) & set(metadata_keys) + + def run(self): + state = self.params['state'] + server_param = self.params['server'] + meta_param = self.params['meta'] + changed = False + + server = self.conn.get_server(server_param) + if not server: + self.fail_json( + msg='Could not find server {0}'.format(server_param)) + + if state == 'present': + # check if it needs update + if self._needs_update( + server_metadata=server.metadata, metadata=meta_param + ): + if not self.ansible.check_mode: + self.conn.set_server_metadata(server_param, meta_param) + changed = True + elif state == 'absent': + # remove from params the keys that do not exist in the server + keys_to_delete = self._get_keys_to_delete( + server.metadata.keys(), meta_param.keys()) + if len(keys_to_delete) > 0: + if not self.ansible.check_mode: + self.conn.delete_server_metadata( + server_param, keys_to_delete) + changed = True + + if changed: + server = self.conn.get_server(server_param) + + self.exit_json( + changed=changed, server_id=server.id, metadata=server.metadata) + + +def main(): + module = ServerMetadataModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/server_volume.py b/ansible_collections/openstack/cloud/plugins/modules/server_volume.py new file mode 100644 index 00000000..1deb8fa6 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/server_volume.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server_volume +short_description: Attach/Detach Volumes from OpenStack VM's +author: OpenStack Ansible SIG +description: + - Attach or Detach volumes from OpenStack VM's +options: + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + required: false + type: str + server: + description: + - Name or ID of server you want to attach a volume to + required: true + type: str + volume: + description: + - Name or id of volume you want to attach to a server + required: true + type: str + device: + description: + - Device you want to attach. Defaults to auto finding a device name. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Attaches a volume to a compute host +- name: attach a volume + hosts: localhost + tasks: + - name: attach volume to host + openstack.cloud.server_volume: + state: present + cloud: mordred + server: Mysql-server + volume: mysql-data + device: /dev/vdb +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +def _system_state_change(state, device): + """Check if system state would change.""" + if state == 'present': + if device: + return False + return True + if state == 'absent': + if device: + return True + return False + return False + + +class ServerVolumeModule(OpenStackModule): + + argument_spec = dict( + server=dict(required=True), + volume=dict(required=True), + device=dict(default=None), # None == auto choose device name + state=dict(default='present', choices=['absent', 'present']), + ) + + def run(self): + + state = self.params['state'] + wait = self.params['wait'] + timeout = self.params['timeout'] + + server = self.conn.get_server(self.params['server']) + volume = self.conn.get_volume(self.params['volume']) + + if not server: + self.fail(msg='server %s is not found' % self.params['server']) + + if not volume: + self.fail(msg='volume %s is not found' % self.params['volume']) + + dev = self.conn.get_volume_attach_device(volume, server.id) + + if self.ansible.check_mode: + self.exit(changed=_system_state_change(state, dev)) + + if state == 'present': + changed = False + if not dev: + changed = True + self.conn.attach_volume(server, volume, self.params['device'], + wait=wait, timeout=timeout) + + server = self.conn.get_server(self.params['server']) # refresh + volume = self.conn.get_volume(self.params['volume']) # refresh + hostvars = self.conn.get_openstack_vars(server) + + self.exit( + changed=changed, + id=volume['id'], + attachments=volume['attachments'], + openstack=hostvars + ) + + elif state == 'absent': + if not dev: + # Volume is not attached to this server + self.exit(changed=False) + + self.conn.detach_volume(server, volume, wait=wait, timeout=timeout) + self.exit( + changed=True, + result='Detached volume from server' + ) + + +def main(): + module = ServerVolumeModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/stack.py b/ansible_collections/openstack/cloud/plugins/modules/stack.py new file mode 100644 index 00000000..95b7bef5 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/stack.py @@ -0,0 +1,248 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2016, Mathieu Bultel <mbultel@redhat.com> +# (c) 2016, Steve Baker <sbaker@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: stack +short_description: Add/Remove Heat Stack +author: OpenStack Ansible SIG +description: + - Add or Remove a Stack to an OpenStack Heat +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Name of the stack that should be created, name could be char and digit, no space + required: true + type: str + tag: + description: + - Tag for the stack that should be created, name could be char and digit, no space + type: str + template: + description: + - Path of the template file to use for the stack creation + type: str + environment: + description: + - List of environment files that should be used for the stack creation + type: list + elements: str + parameters: + description: + - Dictionary of parameters for the stack creation + type: dict + rollback: + description: + - Rollback stack creation + type: bool + default: false + timeout: + description: + - Maximum number of seconds to wait for the stack creation + default: 3600 + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' +EXAMPLES = ''' +--- +- name: create stack + ignore_errors: True + register: stack_create + openstack.cloud.stack: + name: "{{ stack_name }}" + tag: "{{ tag_name }}" + state: present + template: "/path/to/my_stack.yaml" + environment: + - /path/to/resource-registry.yaml + - /path/to/environment.yaml + parameters: + bmc_flavor: m1.medium + bmc_image: CentOS + key_name: default + private_net: "{{ private_net_param }}" + node_count: 2 + name: undercloud + image: CentOS + my_flavor: m1.large + external_net: "{{ external_net_param }}" +''' + +RETURN = ''' +id: + description: Stack ID. + type: str + sample: "97a3f543-8136-4570-920e-fd7605c989d6" + returned: always + +stack: + description: stack info + type: complex + returned: always + contains: + action: + description: Action, could be Create or Update. + type: str + sample: "CREATE" + creation_time: + description: Time when the action has been made. + type: str + sample: "2016-07-05T17:38:12Z" + description: + description: Description of the Stack provided in the heat template. + type: str + sample: "HOT template to create a new instance and networks" + id: + description: Stack ID. + type: str + sample: "97a3f543-8136-4570-920e-fd7605c989d6" + name: + description: Name of the Stack + type: str + sample: "test-stack" + identifier: + description: Identifier of the current Stack action. + type: str + sample: "test-stack/97a3f543-8136-4570-920e-fd7605c989d6" + links: + description: Links to the current Stack. + type: list + elements: dict + sample: "[{'href': 'http://foo:8004/v1/7f6a/stacks/test-stack/97a3f543-8136-4570-920e-fd7605c989d6']" + outputs: + description: Output returned by the Stack. + type: list + elements: dict + sample: "{'description': 'IP address of server1 in private network', + 'output_key': 'server1_private_ip', + 'output_value': '10.1.10.103'}" + parameters: + description: Parameters of the current Stack + type: dict + sample: "{'OS::project_id': '7f6a3a3e01164a4eb4eecb2ab7742101', + 'OS::stack_id': '97a3f543-8136-4570-920e-fd7605c989d6', + 'OS::stack_name': 'test-stack', + 'stack_status': 'CREATE_COMPLETE', + 'stack_status_reason': 'Stack CREATE completed successfully', + 'status': 'COMPLETE', + 'template_description': 'HOT template to create a new instance and networks', + 'timeout_mins': 60, + 'updated_time': null}" +''' + + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class StackModule(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + tag=dict(required=False, default=None, min_ver='0.28.0'), + template=dict(default=None), + environment=dict(default=None, type='list', elements='str'), + parameters=dict(default={}, type='dict'), + rollback=dict(default=False, type='bool'), + timeout=dict(default=3600, type='int'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _create_stack(self, stack, parameters): + stack = self.conn.create_stack( + self.params['name'], + template_file=self.params['template'], + environment_files=self.params['environment'], + timeout=self.params['timeout'], + wait=True, + rollback=self.params['rollback'], + **parameters) + + stack = self.conn.get_stack(stack.id, None) + if stack.stack_status == 'CREATE_COMPLETE': + return stack + else: + self.fail_json(msg="Failure in creating stack: {0}".format(stack)) + + def _update_stack(self, stack, parameters): + stack = self.conn.update_stack( + self.params['name'], + template_file=self.params['template'], + environment_files=self.params['environment'], + timeout=self.params['timeout'], + rollback=self.params['rollback'], + wait=self.params['wait'], + **parameters) + + if stack['stack_status'] == 'UPDATE_COMPLETE': + return stack + else: + self.fail_json(msg="Failure in updating stack: %s" % + stack['stack_status_reason']) + + def _system_state_change(self, stack): + state = self.params['state'] + if state == 'present': + if not stack: + return True + if state == 'absent' and stack: + return True + return False + + def run(self): + state = self.params['state'] + name = self.params['name'] + # Check for required parameters when state == 'present' + if state == 'present': + for p in ['template']: + if not self.params[p]: + self.fail_json(msg='%s required with present state' % p) + + stack = self.conn.get_stack(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(stack)) + + if state == 'present': + parameters = self.params['parameters'] + if not stack: + stack = self._create_stack(stack, parameters) + else: + stack = self._update_stack(stack, parameters) + self.exit_json(changed=True, + stack=stack, + id=stack.id) + elif state == 'absent': + if not stack: + changed = False + else: + changed = True + if not self.conn.delete_stack(name, wait=self.params['wait']): + self.fail_json(msg='delete stack failed for stack: %s' % name) + self.exit_json(changed=changed) + + +def main(): + module = StackModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/stack_info.py b/ansible_collections/openstack/cloud/plugins/modules/stack_info.py new file mode 100644 index 00000000..ce56995a --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/stack_info.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2020, Sagi Shnaidman <sshnaidm@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: stack_info +short_description: Retrive information about Heat stacks +author: OpenStack Ansible SIG +description: + - Get information about Heat stack in openstack +options: + name: + description: + - Name of the stack as a string. + type: str + required: false + status: + description: + - Value of the status of the stack so that you can filter on "available" for example + type: str + required: false + project_id: + description: + - Project ID to be used as filter + type: str + required: false + owner_id: + description: + - Owner (parent) of the stack to be used as a filter + type: str + required: false + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +RETURN = ''' +stacks: + description: List of dictionaries describing stacks. + type: list + elements: dict + returned: always. + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + status: + description: Stack status. + type: str + +''' + +EXAMPLES = ''' +# Get backups. +- openstack.cloud.stack_info: + register: stack + +- openstack.cloud.stack_info: + name: my_stack + register: stack +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class StackInfoModule(OpenStackModule): + module_min_sdk_version = '0.53.0' + + argument_spec = dict( + name=dict(required=False, type='str'), + status=dict(required=False, type='str'), + project_id=dict(required=False, type='str'), + owner_id=dict(required=False, type='str') + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + data = [] + attrs = {} + + for param in ['name', 'status', 'project_id', 'owner_id']: + if self.params[param]: + attrs[param] = self.params[param] + + for raw in self.conn.orchestration.stacks(**attrs): + dt = raw.to_dict() + dt.pop('location') + data.append(dt) + + self.exit_json( + changed=False, + stacks=data + ) + + +def main(): + module = StackInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/subnet.py b/ansible_collections/openstack/cloud/plugins/modules/subnet.py new file mode 100644 index 00000000..dfe1eaca --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/subnet.py @@ -0,0 +1,364 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2013, Benno Joy <benno@ansible.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: subnet +short_description: Add/Remove subnet to an OpenStack network +author: OpenStack Ansible SIG +description: + - Add or Remove a subnet to an OpenStack network +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + network_name: + description: + - Name of the network to which the subnet should be attached + - Required when I(state) is 'present' + type: str + name: + description: + - The name of the subnet that should be created. Although Neutron + allows for non-unique subnet names, this module enforces subnet + name uniqueness. + required: true + type: str + cidr: + description: + - The CIDR representation of the subnet that should be assigned to + the subnet. Required when I(state) is 'present' and a subnetpool + is not specified. + type: str + ip_version: + description: + - The IP version of the subnet 4 or 6 + default: '4' + type: str + choices: ['4', '6'] + enable_dhcp: + description: + - Whether DHCP should be enabled for this subnet. + type: bool + default: 'yes' + gateway_ip: + description: + - The ip that would be assigned to the gateway for this subnet + type: str + no_gateway_ip: + description: + - The gateway IP would not be assigned for this subnet + type: bool + default: 'no' + dns_nameservers: + description: + - List of DNS nameservers for this subnet. + type: list + elements: str + allocation_pool_start: + description: + - From the subnet pool the starting address from which the IP should + be allocated. + type: str + allocation_pool_end: + description: + - From the subnet pool the last IP that should be assigned to the + virtual machines. + type: str + host_routes: + description: + - A list of host route dictionaries for the subnet. + type: list + elements: dict + suboptions: + destination: + description: The destination network (CIDR). + type: str + required: true + nexthop: + description: The next hop (aka gateway) for the I(destination). + type: str + required: true + ipv6_ra_mode: + description: + - IPv6 router advertisement mode + choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] + type: str + ipv6_address_mode: + description: + - IPv6 address mode + choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] + type: str + use_default_subnetpool: + description: + - Use the default subnetpool for I(ip_version) to obtain a CIDR. + type: bool + default: 'no' + project: + description: + - Project name or ID containing the subnet (name admin-only) + type: str + extra_specs: + description: + - Dictionary with extra key/value pairs passed to the API + required: false + default: {} + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a new (or update an existing) subnet on the specified network +- openstack.cloud.subnet: + state: present + network_name: network1 + name: net1subnet + cidr: 192.168.0.0/24 + dns_nameservers: + - 8.8.8.7 + - 8.8.8.8 + host_routes: + - destination: 0.0.0.0/0 + nexthop: 12.34.56.78 + - destination: 192.168.0.0/24 + nexthop: 192.168.0.1 + +# Delete a subnet +- openstack.cloud.subnet: + state: absent + name: net1subnet + +# Create an ipv6 stateless subnet +- openstack.cloud.subnet: + state: present + name: intv6 + network_name: internal + ip_version: 6 + cidr: 2db8:1::/64 + dns_nameservers: + - 2001:4860:4860::8888 + - 2001:4860:4860::8844 + ipv6_ra_mode: dhcpv6-stateless + ipv6_address_mode: dhcpv6-stateless +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class SubnetModule(OpenStackModule): + ipv6_mode_choices = ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] + argument_spec = dict( + name=dict(type='str', required=True), + network_name=dict(type='str'), + cidr=dict(type='str'), + ip_version=dict(type='str', default='4', choices=['4', '6']), + enable_dhcp=dict(type='bool', default=True), + gateway_ip=dict(type='str'), + no_gateway_ip=dict(type='bool', default=False), + dns_nameservers=dict(type='list', default=None, elements='str'), + allocation_pool_start=dict(type='str'), + allocation_pool_end=dict(type='str'), + host_routes=dict(type='list', default=None, elements='dict'), + ipv6_ra_mode=dict(type='str', choices=ipv6_mode_choices), + ipv6_address_mode=dict(type='str', choices=ipv6_mode_choices), + use_default_subnetpool=dict(type='bool', default=False), + extra_specs=dict(type='dict', default=dict()), + state=dict(type='str', default='present', choices=['absent', 'present']), + project=dict(type='str'), + ) + + module_kwargs = dict( + supports_check_mode=True, + required_together=[['allocation_pool_end', 'allocation_pool_start']] + ) + + def _can_update(self, subnet, filters=None): + """Check for differences in non-updatable values""" + network_name = self.params['network_name'] + ip_version = int(self.params['ip_version']) + ipv6_ra_mode = self.params['ipv6_ra_mode'] + ipv6_a_mode = self.params['ipv6_address_mode'] + + if network_name: + network = self.conn.get_network(network_name, filters) + if network: + netid = network['id'] + if netid != subnet['network_id']: + self.fail_json(msg='Cannot update network_name in existing subnet') + else: + self.fail_json(msg='No network found for %s' % network_name) + + if ip_version and subnet['ip_version'] != ip_version: + self.fail_json(msg='Cannot update ip_version in existing subnet') + if ipv6_ra_mode and subnet.get('ipv6_ra_mode', None) != ipv6_ra_mode: + self.fail_json(msg='Cannot update ipv6_ra_mode in existing subnet') + if ipv6_a_mode and subnet.get('ipv6_address_mode', None) != ipv6_a_mode: + self.fail_json(msg='Cannot update ipv6_address_mode in existing subnet') + + def _needs_update(self, subnet, filters=None): + """Check for differences in the updatable values.""" + + # First check if we are trying to update something we're not allowed to + self._can_update(subnet, filters) + + # now check for the things we are allowed to update + enable_dhcp = self.params['enable_dhcp'] + subnet_name = self.params['name'] + pool_start = self.params['allocation_pool_start'] + pool_end = self.params['allocation_pool_end'] + gateway_ip = self.params['gateway_ip'] + no_gateway_ip = self.params['no_gateway_ip'] + dns = self.params['dns_nameservers'] + host_routes = self.params['host_routes'] + if pool_start and pool_end: + pool = dict(start=pool_start, end=pool_end) + else: + pool = None + + changes = dict() + if subnet['enable_dhcp'] != enable_dhcp: + changes['enable_dhcp'] = enable_dhcp + if subnet_name and subnet['name'] != subnet_name: + changes['subnet_name'] = subnet_name + if pool and (not subnet['allocation_pools'] or subnet['allocation_pools'] != [pool]): + changes['allocation_pools'] = [pool] + if gateway_ip and subnet['gateway_ip'] != gateway_ip: + changes['gateway_ip'] = gateway_ip + if dns and sorted(subnet['dns_nameservers']) != sorted(dns): + changes['dns_nameservers'] = dns + if host_routes: + curr_hr = sorted(subnet['host_routes'], key=lambda t: t.keys()) + new_hr = sorted(host_routes, key=lambda t: t.keys()) + if curr_hr != new_hr: + changes['host_routes'] = host_routes + if no_gateway_ip and subnet['gateway_ip']: + changes['disable_gateway_ip'] = no_gateway_ip + return changes + + def _system_state_change(self, subnet, filters=None): + state = self.params['state'] + if state == 'present': + if not subnet: + return True + return bool(self._needs_update(subnet, filters)) + if state == 'absent' and subnet: + return True + return False + + def run(self): + + state = self.params['state'] + network_name = self.params['network_name'] + cidr = self.params['cidr'] + ip_version = self.params['ip_version'] + enable_dhcp = self.params['enable_dhcp'] + subnet_name = self.params['name'] + gateway_ip = self.params['gateway_ip'] + no_gateway_ip = self.params['no_gateway_ip'] + dns = self.params['dns_nameservers'] + pool_start = self.params['allocation_pool_start'] + pool_end = self.params['allocation_pool_end'] + host_routes = self.params['host_routes'] + ipv6_ra_mode = self.params['ipv6_ra_mode'] + ipv6_a_mode = self.params['ipv6_address_mode'] + use_default_subnetpool = self.params['use_default_subnetpool'] + project = self.params.pop('project') + extra_specs = self.params['extra_specs'] + + # Check for required parameters when state == 'present' + if state == 'present': + if not self.params['network_name']: + self.fail(msg='network_name required with present state') + if ( + not self.params['cidr'] + and not use_default_subnetpool + and not extra_specs.get('subnetpool_id', False) + ): + self.fail(msg='cidr or use_default_subnetpool or ' + 'subnetpool_id required with present state') + + if pool_start and pool_end: + pool = [dict(start=pool_start, end=pool_end)] + else: + pool = None + + if no_gateway_ip and gateway_ip: + self.fail_json(msg='no_gateway_ip is not allowed with gateway_ip') + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail_json(msg='Project %s could not be found' % project) + project_id = proj['id'] + filters = {'tenant_id': project_id} + else: + project_id = None + filters = None + + subnet = self.conn.get_subnet(subnet_name, filters=filters) + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(subnet, filters)) + + if state == 'present': + if not subnet: + kwargs = dict( + cidr=cidr, + ip_version=ip_version, + enable_dhcp=enable_dhcp, + subnet_name=subnet_name, + gateway_ip=gateway_ip, + disable_gateway_ip=no_gateway_ip, + dns_nameservers=dns, + allocation_pools=pool, + host_routes=host_routes, + ipv6_ra_mode=ipv6_ra_mode, + ipv6_address_mode=ipv6_a_mode, + tenant_id=project_id) + dup_args = set(kwargs.keys()) & set(extra_specs.keys()) + if dup_args: + raise ValueError('Duplicate key(s) {0} in extra_specs' + .format(list(dup_args))) + if use_default_subnetpool: + kwargs['use_default_subnetpool'] = use_default_subnetpool + kwargs = dict(kwargs, **extra_specs) + subnet = self.conn.create_subnet(network_name, **kwargs) + changed = True + else: + changes = self._needs_update(subnet, filters) + if changes: + subnet = self.conn.update_subnet(subnet['id'], **changes) + changed = True + else: + changed = False + self.exit_json(changed=changed, + subnet=subnet, + id=subnet['id']) + + elif state == 'absent': + if not subnet: + changed = False + else: + changed = True + self.conn.delete_subnet(subnet_name) + self.exit_json(changed=changed) + + +def main(): + module = SubnetModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/subnet_pool.py b/ansible_collections/openstack/cloud/plugins/modules/subnet_pool.py new file mode 100644 index 00000000..4272438f --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/subnet_pool.py @@ -0,0 +1,345 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2021 by Uemit Seren <uemit.seren@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: subnet_pool +short_description: Create or delete subnet pools from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Delete subnet pools from OpenStack. +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Name to be give to the subnet pool + required: true + type: str + project: + description: + - Unique name or ID of the project. + type: str + prefixes: + description: + - Set subnet pool prefixes (in CIDR notation) + type: list + elements: str + minimum_prefix_length: + description: + - The minimum prefix length that can be allocated from the subnet pool. + required: False + type: int + maximum_prefix_length: + description: + - The maximum prefix length that can be allocated from the subnet pool. + required: False + type: int + default_prefix_length: + description: + - The length of the prefix to allocate when the cidr or prefixlen attributes + are omitted when creating a subnet + type: int + required: False + address_scope: + description: + - Set address scope (ID or name) associated with the subnet pool + type: str + required: False + is_default: + description: + - Whether this subnet pool is by default + type: bool + default: 'no' + description: + description: The subnet pool description + type: str + required: False + default_quota: + description: + - A per-project quota on the prefix space that can be allocated + from the subnet pool for project subnets + required: False + type: int + shared: + description: + - Whether this subnet pool is shared or not. + type: bool + default: 'no' + extra_specs: + description: + - Dictionary with extra key/value pairs passed to the API + required: false + default: {} + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create an subnet pool. +- openstack.cloud.subnet_pool: + cloud: mycloud + state: present + name: my_subnet_pool + prefixes: + - 10.10.10.0/24 + +# Create a subnet pool for a given project. +- openstack.cloud.subnet_pool: + cloud: mycloud + state: present + name: my_subnet_pool + project: myproj + prefixes: + - 10.10.10.0/24 + +# Create a shared and default subnet pool in existing address scope +- openstack.cloud.subnet_pool: + cloud: mycloud + state: present + name: my_subnet_pool + address_scope: my_adress_scope + is_default: True + default_quota: 10 + maximum_prefix_length: 32 + minimum_prefix_length: 8 + default_prefix_length: 24 + shared: True + prefixes: + - 10.10.10.0/8 + +# Delete subnet poool. +- openstack.cloud.subnet_pool: + cloud: mycloud + state: absent + name: my_subnet_pool +''' + +RETURN = ''' +subnet_pool: + description: Dictionary describing the subnet pool. + returned: On success when I(state) is 'present' + type: complex + contains: + id: + description: Subnet Pool ID. + type: str + sample: "474acfe5-be34-494c-b339-50f06aa143e4" + name: + description: Subnet Pool name. + type: str + sample: "my_subnet_pool" + project_id: + description: The ID of the project. + type: str + sample: "861174b82b43463c9edc5202aadc60ef" + ip_version: + description: The IP version of the subnet pool 4 or 6. + type: int + sample: 4 + is_shared: + description: Indicates whether this subnet pool is shared across all projects. + type: bool + sample: false + is_default: + description: Indicates whether this is the default subnet pool. + type: bool + sample: false + address_scope_id: + description: The address scope ID. + type: str + sample: "861174b82b43463c9edc5202aadc60ef" + created_at: + description: Timestamp when the subnet pool was created. + type: str + sample: "" + default_prefix_length: + description: + - The length of the prefix to allocate when the cidr or prefixlen + attributes are omitted when creating a subnet + type: int + sample: 32 + default_quota: + description: + - The per-project quota on the prefix space that can be allocated + from the subnet pool for project subnets. + type: int + sample: 22 + description: + description: The subnet pool description. + type: str + sample: "My test subnet pool." + maximum_prefix_length: + description: The maximum prefix length that can be allocated from the subnet pool. + type: int + sample: 22 + minimum_prefix_length: + description: The minimum prefix length that can be allocated from the subnet pool. + type: int + sample: 8 + prefixes: + description: A list of subnet prefixes that are assigned to the subnet pool. + type: list + sample: ['10.10.20.0/24', '10.20.10.0/24'] + revision_number: + description: Revision number of the subnet pool. + type: int + sample: 5 + updated_at: + description: Timestamp when the subnet pool was last updated. + type: str + sample: +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class SubnetPoolModule(OpenStackModule): + argument_spec = dict( + state=dict(default='present', choices=['absent', 'present']), + name=dict(required=True), + shared=dict(default=False, type='bool'), + minimum_prefix_length=dict(default=None, type='int'), + maximum_prefix_length=dict(default=None, type='int'), + default_prefix_length=dict(default=None, type='int'), + description=dict(default=None, type='str'), + default_quota=dict(default=None, type='int'), + prefixes=dict(type='list', elements='str'), + is_default=dict(default=False, type='bool'), + address_scope=dict(default=None), + project=dict(default=None), + extra_specs=dict(type='dict', default=dict()) + ) + + def _needs_update(self, subnet_pool): + """Check for differences in the updatable values. + + NOTE: We don't currently allow name updates. + """ + compare_simple = ['is_default', + 'minimum_prefix_length', + 'maximum_prefix_length', + 'default_prefix_length', + 'description', + 'default_quota'] + compare_list = ['prefixes'] + + for key in compare_simple: + if self.params[key] is not None and self.params[key] != subnet_pool[key]: + return True + for key in compare_list: + if ( + self.params[key] is not None + and set(self.params[key]) != set(subnet_pool[key]) + ): + return True + + return False + + def _system_state_change(self, subnet_pool, filters=None): + """Check if the system state would be changed.""" + state = self.params['state'] + if state == 'absent' and subnet_pool: + return True + if state == 'present': + if not subnet_pool: + return True + return self._needs_update(subnet_pool, filters) + return False + + def _compose_subnet_pool_args(self): + subnet_pool_kwargs = {} + optional_parameters = ['name', + 'minimum_prefix_length', + 'maximum_prefix_length', + 'default_prefix_length', + 'description', + 'is_default', + 'default_quota', + 'prefixes'] + + for optional_param in optional_parameters: + if self.params[optional_param] is not None: + subnet_pool_kwargs[optional_param] = self.params[optional_param] + + return subnet_pool_kwargs + + def run(self): + + state = self.params['state'] + name = self.params['name'] + project = self.params['project'] + address_scope = self.params['address_scope'] + extra_specs = self.params['extra_specs'] + + if project is not None: + proj = self.conn.get_project(project) + if proj is None: + self.fail(msg='Project %s could not be found' % project) + project_id = proj['id'] + else: + project_id = self.conn.current_project_id + + address_scope_id = None + if address_scope is not None: + address_scope = self.conn.network.find_address_scope(name_or_id=address_scope) + if address_scope is None: + self.fail(msg='AddressScope %s could not be found' % address_scope) + address_scope_id = address_scope['id'] + subnet_pool = self.conn.network.find_subnet_pool(name_or_id=name) + if self.ansible.check_mode: + self.exit_json( + changed=self._system_state_change(subnet_pool) + ) + + if state == 'present': + changed = False + + if not subnet_pool: + kwargs = self._compose_subnet_pool_args() + kwargs['address_scope_id'] = address_scope_id + kwargs['project_id'] = project_id + kwargs['is_shared'] = self.params['shared'] + dup_args = set(kwargs.keys()) & set(extra_specs.keys()) + if dup_args: + raise ValueError('Duplicate key(s) {0} in extra_specs' + .format(list(dup_args))) + kwargs = dict(kwargs, **extra_specs) + subnet_pool = self.conn.network.create_subnet_pool(**kwargs) + changed = True + else: + if self._needs_update(subnet_pool): + kwargs = self._compose_subnet_pool_args() + subnet_pool = self.conn.network.update_subnet_pool(subnet_pool['id'], **kwargs) + changed = True + else: + changed = False + self.exit_json(changed=changed, subnet_pool=subnet_pool, id=subnet_pool['id']) + + elif state == 'absent': + if not subnet_pool: + self.exit(changed=False) + else: + self.conn.network.delete_subnet_pool(subnet_pool['id']) + self.exit_json(changed=True) + + +def main(): + module = SubnetPoolModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/subnets_info.py b/ansible_collections/openstack/cloud/plugins/modules/subnets_info.py new file mode 100644 index 00000000..7a771b53 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/subnets_info.py @@ -0,0 +1,164 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: subnets_info +short_description: Retrieve information about one or more OpenStack subnets. +author: OpenStack Ansible SIG +description: + - Retrieve information about one or more subnets from OpenStack. + - This module was called C(openstack.cloud.subnets_facts) before Ansible 2.9, returning C(ansible_facts). + Note that the M(openstack.cloud.subnets_info) module no longer returns C(ansible_facts)! +options: + name: + description: + - Name or ID of the subnet. + - Alias 'subnet' added in version 2.8. + required: false + aliases: ['subnet'] + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Gather information about previously created subnets + openstack.cloud.subnets_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + register: result + +- name: Show openstack subnets + debug: + msg: "{{ result.openstack_subnets }}" + +- name: Gather information about a previously created subnet by name + openstack.cloud.subnets_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + name: subnet1 + register: result + +- name: Show openstack subnets + debug: + msg: "{{ result.openstack_subnets }}" + +- name: Gather information about a previously created subnet with filter + # Note: name and filters parameters are not mutually exclusive + openstack.cloud.subnets_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + filters: + tenant_id: 55e2ce24b2a245b09f181bf025724cbe + register: result + +- name: Show openstack subnets + debug: + msg: "{{ result.openstack_subnets }}" +''' + +RETURN = ''' +openstack_subnets: + description: has all the openstack information about the subnets + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the subnet. + returned: success + type: str + network_id: + description: Network ID this subnet belongs in. + returned: success + type: str + cidr: + description: Subnet's CIDR. + returned: success + type: str + gateway_ip: + description: Subnet's gateway ip. + returned: success + type: str + enable_dhcp: + description: DHCP enable flag for this subnet. + returned: success + type: bool + ip_version: + description: IP version for this subnet. + returned: success + type: int + tenant_id: + description: Tenant id associated with this subnet. + returned: success + type: str + dns_nameservers: + description: DNS name servers for this subnet. + returned: success + type: list + elements: str + allocation_pools: + description: Allocation pools associated with this subnet. + returned: success + type: list + elements: dict +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class SubnetInfoModule(OpenStackModule): + + deprecated_names = ('subnets_facts', 'openstack.cloud.subnets_facts') + + argument_spec = dict( + name=dict(required=False, default=None, aliases=['subnet']), + filters=dict(required=False, type='dict', default=None) + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + kwargs = self.check_versioned( + filters=self.params['filters'] + ) + if self.params['name']: + kwargs['name_or_id'] = self.params['name'] + subnets = self.conn.search_subnets(**kwargs) + + self.exit(changed=False, openstack_subnets=subnets) + + +def main(): + module = SubnetInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/volume.py b/ansible_collections/openstack/cloud/plugins/modules/volume.py new file mode 100644 index 00000000..3a50c05a --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/volume.py @@ -0,0 +1,263 @@ +#!/usr/bin/python + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: volume +short_description: Create/Delete Cinder Volumes +author: OpenStack Ansible SIG +description: + - Create or Remove cinder block storage volumes +options: + size: + description: + - Size of volume in GB. This parameter is required when the + I(state) parameter is 'present'. + type: int + display_name: + description: + - Name of volume + required: true + type: str + aliases: [name] + display_description: + description: + - String describing the volume + type: str + aliases: [description] + volume_type: + description: + - Volume type for volume + type: str + image: + description: + - Image name or id for boot from volume + type: str + snapshot_id: + description: + - Volume snapshot id to create from + type: str + volume: + description: + - Volume name or id to create from + type: str + bootable: + description: + - Bootable flag for volume. + type: bool + default: False + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + scheduler_hints: + description: + - Scheduler hints passed to volume API in form of dict + type: dict + metadata: + description: + - Metadata for the volume + type: dict +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Creates a new volume +- name: create a volume + hosts: localhost + tasks: + - name: create 40g test volume + openstack.cloud.volume: + state: present + cloud: mordred + availability_zone: az2 + size: 40 + display_name: test_volume + scheduler_hints: + same_host: 243e8d3c-8f47-4a61-93d6-7215c344b0c0 +''' + +RETURNS = ''' +id: + description: Cinder's unique ID for this volume + returned: always + type: str + sample: fcc4ac1c-e249-4fe7-b458-2138bfb44c06 + +volume: + description: Cinder's representation of the volume object + returned: always + type: dict + sample: {'...'} +''' +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class VolumeModule(OpenStackModule): + + argument_spec = dict( + size=dict(type='int'), + volume_type=dict(type='str'), + display_name=dict(required=True, aliases=['name'], type='str'), + display_description=dict(aliases=['description'], type='str'), + image=dict(type='str'), + snapshot_id=dict(type='str'), + volume=dict(type='str'), + state=dict(default='present', choices=['absent', 'present'], type='str'), + scheduler_hints=dict(type='dict'), + metadata=dict(type='dict'), + bootable=dict(type='bool', default=False) + ) + + module_kwargs = dict( + mutually_exclusive=[ + ['image', 'snapshot_id', 'volume'], + ], + required_if=[ + ['state', 'present', ['size']], + ], + ) + + def _needs_update(self, volume): + ''' + check for differences in updatable values, at the moment + openstacksdk only supports extending the volume size, this + may change in the future. + :returns: bool + ''' + compare_simple = ['size'] + + for k in compare_simple: + if self.params[k] is not None and self.params[k] != volume.get(k): + return True + + return False + + def _modify_volume(self, volume): + ''' + modify volume, the only modification to an existing volume + available at the moment is extending the size, this is + limited by the openstacksdk and may change whenever the + functionality is extended. + ''' + volume = self.conn.get_volume(self.params['display_name']) + diff = {'before': volume, 'after': ''} + size = self.params['size'] + + if size < volume.get('size'): + self.fail_json( + msg='Cannot shrink volumes, size: {0} < {1}'.format(size, volume.get('size')) + ) + + if not self._needs_update(volume): + diff['after'] = volume + self.exit_json(changed=False, id=volume['id'], volume=volume, diff=diff) + + if self.ansible.check_mode: + diff['after'] = volume + self.exit_json(changed=True, id=volume['id'], volume=volume, diff=diff) + + self.conn.volume.extend_volume( + volume.id, + size + ) + diff['after'] = self.conn.get_volume(self.params['display_name']) + self.exit_json(changed=True, id=volume['id'], volume=volume, diff=diff) + + def _present_volume(self): + + diff = {'before': '', 'after': ''} + + volume_args = dict( + size=self.params['size'], + volume_type=self.params['volume_type'], + display_name=self.params['display_name'], + display_description=self.params['display_description'], + snapshot_id=self.params['snapshot_id'], + bootable=self.params['bootable'], + availability_zone=self.params['availability_zone'], + ) + if self.params['image']: + image_id = self.conn.get_image_id(self.params['image']) + if not image_id: + self.fail_json(msg="Failed to find image '%s'" % self.params['image']) + volume_args['imageRef'] = image_id + + if self.params['volume']: + volume_id = self.conn.get_volume_id(self.params['volume']) + if not volume_id: + self.fail_json(msg="Failed to find volume '%s'" % self.params['volume']) + volume_args['source_volid'] = volume_id + + if self.params['scheduler_hints']: + volume_args['scheduler_hints'] = self.params['scheduler_hints'] + + if self.params['metadata']: + volume_args['metadata'] = self.params['metadata'] + + if self.ansible.check_mode: + diff['after'] = volume_args + self.exit_json(changed=True, id=None, volume=volume_args, diff=diff) + + volume = self.conn.create_volume( + wait=self.params['wait'], timeout=self.params['timeout'], + **volume_args) + diff['after'] = volume + self.exit_json(changed=True, id=volume['id'], volume=volume, diff=diff) + + def _absent_volume(self, volume): + changed = False + diff = {'before': '', 'after': ''} + + if self.conn.volume_exists(self.params['display_name']): + volume = self.conn.get_volume(self.params['display_name']) + diff['before'] = volume + + if self.ansible.check_mode: + self.exit_json(changed=True, diff=diff) + + try: + changed = self.conn.delete_volume(name_or_id=self.params['display_name'], + wait=self.params['wait'], + timeout=self.params['timeout']) + except self.sdk.exceptions.ResourceTimeout: + diff['after'] = volume + self.exit_json(changed=changed, diff=diff) + + self.exit_json(changed=changed, diff=diff) + + def run(self): + + state = self.params['state'] + if self.conn.volume_exists(self.params['display_name']): + volume = self.conn.get_volume(self.params['display_name']) + else: + volume = None + + if state == 'present': + if not volume: + self._present_volume() + elif self._needs_update(volume): + self._modify_volume(volume) + else: + self.exit_json(changed=False, id=volume['id'], volume=volume) + if state == 'absent': + self._absent_volume(volume) + + +def main(): + module = VolumeModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/volume_backup.py b/ansible_collections/openstack/cloud/plugins/modules/volume_backup.py new file mode 100644 index 00000000..43cacc72 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/volume_backup.py @@ -0,0 +1,221 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2020 by Open Telekom Cloud, operated by T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = ''' +--- +module: volume_backup +short_description: Add/Delete Volume backup +extends_documentation_fragment: openstack.cloud.openstack +author: OpenStack Ansible SIG +description: + - Add or Remove Volume Backup in OTC. +options: + display_name: + description: + - Name that has to be given to the backup + required: true + type: str + aliases: ['name'] + display_description: + description: + - String describing the backup + required: false + type: str + aliases: ['description'] + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + volume: + description: + - Name or ID of the volume. Required when state is True. + type: str + required: False + snapshot: + description: Name or ID of the Snapshot to take backup of + type: str + force: + description: + - Indicates whether to backup, even if the volume is attached. + type: bool + default: False + metadata: + description: Metadata for the backup + type: dict + incremental: + description: The backup mode + type: bool + default: False +requirements: ["openstacksdk"] +''' + +RETURN = ''' +id: + description: The Volume backup ID. + returned: On success when C(state=present) + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +backup: + description: Dictionary describing the Cluster. + returned: On success when C(state=present) + type: complex + contains: + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + name: + description: Name given to the load balancer. + type: str + sample: "elb_test" +''' + +EXAMPLES = ''' +- name: Create backup + openstack.cloud.volume_backup: + display_name: test_volume_backup + volume: "test_volume" + +- name: Create backup from snapshot + openstack.cloud.volume_backup: + display_name: test_volume_backup + volume: "test_volume" + snapshot: "test_snapshot" + +- name: Delete volume backup + openstack.cloud.volume_backup: + display_name: test_volume_backup + state: absent +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class VolumeBackupModule(OpenStackModule): + module_min_sdk_version = '0.49.0' + + argument_spec = dict( + display_name=dict(required=True, aliases=['name'], type='str'), + display_description=dict(required=False, aliases=['description'], + type='str'), + volume=dict(required=False, type='str'), + snapshot=dict(required=False, type='str'), + state=dict(default='present', type='str', choices=['absent', 'present']), + force=dict(default=False, type='bool'), + metadata=dict(required=False, type='dict'), + incremental=dict(required=False, default=False, type='bool') + ) + module_kwargs = dict( + required_if=[ + ('state', 'present', ['volume']) + ], + supports_check_mode=True + ) + + def _create_backup(self): + if self.ansible.check_mode: + self.exit_json(changed=True) + + name = self.params['display_name'] + description = self.params['display_description'] + volume = self.params['volume'] + snapshot = self.params['snapshot'] + force = self.params['force'] + is_incremental = self.params['incremental'] + metadata = self.params['metadata'] + + changed = False + + cloud_volume = self.conn.block_storage.find_volume(volume) + cloud_snapshot_id = None + + attrs = { + 'name': name, + 'volume_id': cloud_volume.id, + 'force': force, + 'is_incremental': is_incremental + } + + if snapshot: + cloud_snapshot_id = self.conn.block_storage.find_snapshot( + snapshot, ignore_missing=False).id + attrs['snapshot_id'] = cloud_snapshot_id + + if metadata: + attrs['metadata'] = metadata + + if description: + attrs['description'] = description + + backup = self.conn.block_storage.create_backup(**attrs) + changed = True + + if self.params['wait']: + try: + backup = self.conn.block_storage.wait_for_status( + backup, + status='available', + wait=self.params['timeout']) + self.exit_json( + changed=True, volume_backup=backup.to_dict(), id=backup.id + ) + except self.sdk.exceptions.ResourceTimeout: + self.fail_json( + msg='Timeout failure waiting for backup ' + 'to complete' + ) + + self.exit_json( + changed=changed, volume_backup=backup.to_dict(), id=backup.id + ) + + def _delete_backup(self, backup): + if self.ansible.check_mode: + self.exit_json(changed=True) + + if backup: + self.conn.block_storage.delete_backup(backup) + if self.params['wait']: + try: + self.conn.block_storage.wait_for_delete( + backup, + interval=2, + wait=self.params['timeout']) + except self.sdk.exceptions.ResourceTimeout: + self.fail_json( + msg='Timeout failure waiting for backup ' + 'to be deleted' + ) + + self.exit_json(changed=True) + + def run(self): + name = self.params['display_name'] + + backup = self.conn.block_storage.find_backup(name) + + if self.params['state'] == 'present': + if not backup: + self._create_backup() + else: + # For the moment we do not support backup update, since SDK + # doesn't support it either => do nothing + self.exit_json(changed=False) + + elif self.params['state'] == 'absent': + self._delete_backup(backup) + + +def main(): + module = VolumeBackupModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/volume_backup_info.py b/ansible_collections/openstack/cloud/plugins/modules/volume_backup_info.py new file mode 100644 index 00000000..fdb61834 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/volume_backup_info.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2020 by Open Telekom Cloud, operated by T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = ''' +--- +module: volume_backup_info +short_description: Get Backups +author: OpenStack Ansible SIG +description: + - Get Backup info from the Openstack cloud. +options: + name: + description: + - Name of the Backup. + type: str + volume: + description: + - Name of the volume. + type: str +requirements: ["openstacksdk"] +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +volume_backups: + description: List of dictionaries describing volume backups. + type: list + elements: dict + returned: always. + contains: + availability_zone: + description: Backup availability zone. + type: str + created_at: + description: Backup creation time. + type: str + description: + description: Backup desciption. + type: str + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + is_incremental: + description: Backup incremental property. + type: bool + metadata: + description: Backup metadata. + type: dict + name: + description: Backup Name. + type: str + snapshot_id: + description: Snapshot ID. + type: str + status: + description: Backup status. + type: str + updated_at: + description: Backup update time. + type: str + volume_id: + description: Volume ID. + type: str + +''' + +EXAMPLES = ''' +# Get backups. +- openstack.cloud.volume_backup_info: + register: backup + +- openstack.cloud.volume_backup_info: + name: my_fake_backup + register: backup +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class VolumeBackupInfoModule(OpenStackModule): + module_min_sdk_version = '0.49.0' + + argument_spec = dict( + name=dict(required=False, type='str'), + volume=dict(required=False, type='str') + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + name_filter = self.params['name'] + volume = self.params['volume'] + + data = [] + attrs = {} + + if name_filter: + attrs['name'] = name_filter + if volume: + attrs['volume_id'] = self.conn.block_storage.find_volume(volume) + + for raw in self.conn.block_storage.backups(**attrs): + dt = raw.to_dict() + dt.pop('location') + data.append(dt) + + self.exit_json( + changed=False, + volume_backups=data + ) + + +def main(): + module = VolumeBackupInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/volume_info.py b/ansible_collections/openstack/cloud/plugins/modules/volume_info.py new file mode 100644 index 00000000..bcce4994 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/volume_info.py @@ -0,0 +1,149 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2020, Sagi Shnaidman <sshnaidm@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: volume_info +short_description: Retrive information about volumes +author: Sagi Shnaidman (@sshnaidm) +description: + - Get information about block storage in openstack +options: + details: + description: + - Whether to provide additional information about volumes + type: bool + all_projects: + description: + - Whether return the volumes in all projects + type: bool + name: + description: + - Name of the volume as a string. + type: str + required: false + status: + description: + - Value of the status of the volume so that you can filter on "available" for example + type: str + required: false + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +RETURN = ''' +volumes: + description: Volumes in project + returned: always + type: list + elements: dict + sample: + - attachments: [] + availability_zone: nova + consistency_group_id: null + created_at: '2017-11-15T10:51:19.000000' + description: '' + extended_replication_status: null + host: null + id: 103ac6ed-527f-4781-8484-7ff4467e34f5 + image_id: null + is_bootable: true + is_encrypted: false + links: + - href: https://... + rel: self + - href: https://... + rel: bookmark + location: + cloud: cloud + project: + domain_id: null + domain_name: Default + id: cfe04702154742fc964d9403c691c76e + name: username + region_name: regionOne + zone: nova + metadata: + readonly: 'False' + migration_id: null + migration_status: null + name: '' + project_id: cab34702154a42fc96ed9403c691c76e + replication_driver_data: null + replication_status: disabled + size: 9 + snapshot_id: null + source_volume_id: null + status: available + volume_image_metadata: + checksum: a14e113deeee3a3392462f167ed28cb5 + container_format: bare + disk_format: raw + family: centos-7 + image_id: afcf3320-1bf8-4a9a-a24d-5abd639a6e33 + image_name: CentOS-7-x86_64-GenericCloud-1708 + latest: centos-7-latest + min_disk: '0' + min_ram: '0' + official: 'True' + official-image: 'True' + size: '8589934592' + volume_type: null +''' + +EXAMPLES = ''' +- openstack.cloud.volume_info: + +- openstack.cloud.volume_info: + name: myvolume + +- openstack.cloud.volume_info: + all_projects: true + +- openstack.cloud.volume_info: + all_projects: true + details: true +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class VolumeInfoModule(OpenStackModule): + + argument_spec = dict( + details=dict(type='bool', required=False), + all_projects=dict(type='bool', required=False, min_ver='0.19'), + name=dict(type='str', required=False), + status=dict(type='str', required=False), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + kwargs = self.check_versioned( + details=self.params['details'], + name=self.params['name'], + all_projects=self.params['all_projects'], + status=self.params['status'], + ) + result = self.conn.block_storage.volumes(**kwargs) + result = [vol if isinstance(vol, dict) else vol.to_dict() for vol in result] + self.results.update({'volumes': result}) + + +def main(): + module = VolumeInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/volume_snapshot.py b/ansible_collections/openstack/cloud/plugins/modules/volume_snapshot.py new file mode 100644 index 00000000..8625984c --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/volume_snapshot.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2016, Mario Santos <mario.rf.santos@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: volume_snapshot +short_description: Create/Delete Cinder Volume Snapshots +author: OpenStack Ansible SIG +description: + - Create or Delete cinder block storage volume snapshots +options: + display_name: + description: + - Name of the snapshot + required: true + aliases: ['name'] + type: str + display_description: + description: + - String describing the snapshot + aliases: ['description'] + type: str + volume: + description: + - The volume name or id to create/delete the snapshot + required: True + type: str + force: + description: + - Allows or disallows snapshot of a volume to be created when the volume + is attached to an instance. + type: bool + default: 'no' + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Creates a snapshot on volume 'test_volume' +- name: create and delete snapshot + hosts: localhost + tasks: + - name: create snapshot + openstack.cloud.volume_snapshot: + state: present + cloud: mordred + availability_zone: az2 + display_name: test_snapshot + volume: test_volume + - name: delete snapshot + openstack.cloud.volume_snapshot: + state: absent + cloud: mordred + availability_zone: az2 + display_name: test_snapshot + volume: test_volume +''' + +RETURN = ''' +snapshot: + description: The snapshot instance after the change + returned: success + type: dict + sample: + id: 837aca54-c0ee-47a2-bf9a-35e1b4fdac0c + name: test_snapshot + volume_id: ec646a7c-6a35-4857-b38b-808105a24be6 + size: 2 + status: available + display_name: test_snapshot +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class VolumeSnapshotModule(OpenStackModule): + argument_spec = dict( + display_name=dict(required=True, aliases=['name']), + display_description=dict(default=None, aliases=['description']), + volume=dict(required=True), + force=dict(required=False, default=False, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def _present_volume_snapshot(self): + volume = self.conn.get_volume(self.params['volume']) + snapshot = self.conn.get_volume_snapshot( + self.params['display_name'], filters={'volume_id': volume.id}) + if not snapshot: + snapshot = self.conn.create_volume_snapshot( + volume.id, + force=self.params['force'], + wait=self.params['wait'], + timeout=self.params['timeout'], + name=self.params['display_name'], + description=self.params.get('display_description') + ) + self.exit_json(changed=True, snapshot=snapshot) + else: + self.exit_json(changed=False, snapshot=snapshot) + + def _absent_volume_snapshot(self): + volume = self.conn.get_volume(self.params['volume']) + snapshot = self.conn.get_volume_snapshot( + self.params['display_name'], filters={'volume_id': volume.id}) + if not snapshot: + self.exit_json(changed=False) + else: + self.conn.delete_volume_snapshot( + name_or_id=snapshot.id, + wait=self.params['wait'], + timeout=self.params['timeout'], + ) + self.exit_json(changed=True, snapshot_id=snapshot.id) + + def _system_state_change(self): + volume = self.conn.get_volume(self.params['volume']) + snapshot = self.conn.get_volume_snapshot( + self.params['display_name'], + filters={'volume_id': volume.id}) + state = self.params['state'] + + if state == 'present': + return snapshot is None + if state == 'absent': + return snapshot is not None + + def run(self): + state = self.params['state'] + + if self.conn.volume_exists(self.params['volume']): + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change()) + if state == 'present': + self._present_volume_snapshot() + if state == 'absent': + self._absent_volume_snapshot() + else: + self.fail_json( + msg="No volume with name or id '{0}' was found.".format( + self.params['volume'])) + + +def main(): + module = VolumeSnapshotModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/plugins/modules/volume_snapshot_info.py b/ansible_collections/openstack/cloud/plugins/modules/volume_snapshot_info.py new file mode 100644 index 00000000..fa50055d --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/volume_snapshot_info.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# Copyright (c) 2020 by Open Telekom Cloud, operated by T-Systems International GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = ''' +--- +module: volume_snapshot_info +short_description: Get volume snapshots +author: OpenStack Ansible SIG +description: + - Get Volume Snapshot info from the Openstack cloud. +options: + details: + description: More detailed output + type: bool + default: True + name: + description: + - Name of the Snapshot. + type: str + volume: + description: + - Name of the volume. + type: str + status: + description: + - Specifies the snapshot status. + choices: [creating, available, error, deleting, + error_deleting, rollbacking, backing-up] + type: str +requirements: ["openstacksdk"] +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +RETURN = ''' +volume_snapshots: + description: List of dictionaries describing volume snapshots. + type: list + elements: dict + returned: always. + contains: + created_at: + description: Snapshot creation time. + type: str + description: + description: Snapshot desciption. + type: str + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + metadata: + description: Snapshot metadata. + type: dict + name: + description: Snapshot Name. + type: str + status: + description: Snapshot status. + type: str + updated_at: + description: Snapshot update time. + type: str + volume_id: + description: Volume ID. + type: str + +''' + +EXAMPLES = ''' +# Get snapshots. +- openstack.cloud.volume_snapshot_info: + register: snapshots + +- openstack.cloud.volume_snapshotbackup_info: + name: my_fake_snapshot + register: snapshot +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class VolumeSnapshotInfoModule(OpenStackModule): + module_min_sdk_version = '0.49.0' + + argument_spec = dict( + details=dict(default=True, type='bool'), + name=dict(required=False, type='str'), + volume=dict(required=False, type='str'), + status=dict(required=False, type='str', + choices=['creating', 'available', 'error', + 'deleting', 'error_deleting', 'rollbacking', + 'backing-up']), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + + details_filter = self.params['details'] + name_filter = self.params['name'] + volume_filter = self.params['volume'] + status_filter = self.params['status'] + + data = [] + query = {} + if name_filter: + query['name'] = name_filter + if volume_filter: + query['volume_id'] = self.conn.block_storage.find_volume(volume_filter) + if status_filter: + query['status'] = status_filter.lower() + + for raw in self.conn.block_storage.snapshots(details_filter, **query): + dt = raw.to_dict() + dt.pop('location') + data.append(dt) + + self.exit_json( + changed=False, + volume_snapshots=data + ) + + +def main(): + module = VolumeSnapshotInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/requirements.txt b/ansible_collections/openstack/cloud/requirements.txt new file mode 100644 index 00000000..ff1e9021 --- /dev/null +++ b/ansible_collections/openstack/cloud/requirements.txt @@ -0,0 +1 @@ +openstacksdk>=0.36,<0.99.0 diff --git a/ansible_collections/openstack/cloud/scripts/inventory/openstack.yml b/ansible_collections/openstack/cloud/scripts/inventory/openstack.yml new file mode 100644 index 00000000..8053fb8f --- /dev/null +++ b/ansible_collections/openstack/cloud/scripts/inventory/openstack.yml @@ -0,0 +1,24 @@ +clouds: + vexxhost: + profile: vexxhost + auth: + project_name: 39e296b2-fc96-42bf-8091-cb742fa13da9 + username: fb886a9b-c37b-442a-9be3-964bed961e04 + password: fantastic-password1 + rax: + profile: rackspace + auth: + username: example + password: spectacular-password + project_id: 2352426 + region_name: DFW,ORD,IAD + devstack: + auth: + auth_url: https://devstack.example.com + username: stack + password: stack + project_name: stack +ansible: + use_hostnames: True + expand_hostvars: False + fail_on_errors: True diff --git a/ansible_collections/openstack/cloud/scripts/inventory/openstack_inventory.py b/ansible_collections/openstack/cloud/scripts/inventory/openstack_inventory.py new file mode 100644 index 00000000..f0b2ff89 --- /dev/null +++ b/ansible_collections/openstack/cloud/scripts/inventory/openstack_inventory.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python + +# Copyright (c) 2012, Marco Vito Moscaritolo <marco@agavee.com> +# Copyright (c) 2013, Jesse Keating <jesse.keating@rackspace.com> +# Copyright (c) 2015, Hewlett-Packard Development Company, L.P. +# Copyright (c) 2016, Rackspace Australia +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see <http://www.gnu.org/licenses/>. + +# The OpenStack Inventory module uses os-client-config for configuration. +# https://github.com/openstack/os-client-config +# This means it will either: +# - Respect normal OS_* environment variables like other OpenStack tools +# - Read values from a clouds.yaml file. +# If you want to configure via clouds.yaml, you can put the file in: +# - Current directory +# - ~/.config/openstack/clouds.yaml +# - /etc/openstack/clouds.yaml +# - /etc/ansible/openstack.yml +# The clouds.yaml file can contain entries for multiple clouds and multiple +# regions of those clouds. If it does, this inventory module will by default +# connect to all of them and present them as one contiguous inventory. You +# can limit to one cloud by passing the `--cloud` parameter, or use the +# OS_CLOUD environment variable. If caching is enabled, and a cloud is +# selected, then per-cloud cache folders will be used. +# +# See the adjacent openstack.yml file for an example config file +# There are two ansible inventory specific options that can be set in +# the inventory section. +# expand_hostvars controls whether or not the inventory will make extra API +# calls to fill out additional information about each server +# use_hostnames changes the behavior from registering every host with its UUID +# and making a group of its hostname to only doing this if the +# hostname in question has more than one server +# fail_on_errors causes the inventory to fail and return no hosts if one cloud +# has failed (for example, bad credentials or being offline). +# When set to False, the inventory will return hosts from +# whichever other clouds it can contact. (Default: True) +# +# Also it is possible to pass the correct user by setting an ansible_user: $myuser +# metadata attribute. + +import argparse +import collections +import os +import sys +import time +from ansible.module_utils.six import raise_from +try: + from ansible.module_utils.compat.version import StrictVersion +except ImportError: + try: + from distutils.version import StrictVersion + except ImportError as exc: + raise_from(ImportError('To use this plugin or module with ansible-core' + ' < 2.11, you need to use Python < 3.12 with ' + 'distutils.version present'), exc) +from io import StringIO + +import json + +import openstack as sdk +from openstack.cloud import inventory as sdk_inventory +from openstack.config import loader as cloud_config + +CONFIG_FILES = ['/etc/ansible/openstack.yaml', '/etc/ansible/openstack.yml'] + + +def get_groups_from_server(server_vars, namegroup=True): + groups = [] + + region = server_vars['region'] + cloud = server_vars['cloud'] + metadata = server_vars.get('metadata', {}) + + # Create a group for the cloud + groups.append(cloud) + + # Create a group on region + if region: + groups.append(region) + + # And one by cloud_region + groups.append("%s_%s" % (cloud, region)) + + # Check if group metadata key in servers' metadata + if 'group' in metadata: + groups.append(metadata['group']) + + for extra_group in metadata.get('groups', '').split(','): + if extra_group: + groups.append(extra_group.strip()) + + groups.append('instance-%s' % server_vars['id']) + if namegroup: + groups.append(server_vars['name']) + + for key in ('flavor', 'image'): + if 'name' in server_vars[key]: + groups.append('%s-%s' % (key, server_vars[key]['name'])) + + for key, value in iter(metadata.items()): + groups.append('meta-%s_%s' % (key, value)) + + az = server_vars.get('az', None) + if az: + # Make groups for az, region_az and cloud_region_az + groups.append(az) + groups.append('%s_%s' % (region, az)) + groups.append('%s_%s_%s' % (cloud, region, az)) + return groups + + +def get_host_groups(inventory, refresh=False, cloud=None): + (cache_file, cache_expiration_time) = get_cache_settings(cloud) + if is_cache_stale(cache_file, cache_expiration_time, refresh=refresh): + groups = to_json(get_host_groups_from_cloud(inventory)) + with open(cache_file, 'w') as f: + f.write(groups) + else: + with open(cache_file, 'r') as f: + groups = f.read() + return groups + + +def append_hostvars(hostvars, groups, key, server, namegroup=False): + hostvars[key] = dict( + ansible_ssh_host=server['interface_ip'], + ansible_host=server['interface_ip'], + openstack=server) + + metadata = server.get('metadata', {}) + if 'ansible_user' in metadata: + hostvars[key]['ansible_user'] = metadata['ansible_user'] + + for group in get_groups_from_server(server, namegroup=namegroup): + groups[group].append(key) + + +def get_host_groups_from_cloud(inventory): + groups = collections.defaultdict(list) + firstpass = collections.defaultdict(list) + hostvars = {} + list_args = {} + if hasattr(inventory, 'extra_config'): + use_hostnames = inventory.extra_config['use_hostnames'] + list_args['expand'] = inventory.extra_config['expand_hostvars'] + if StrictVersion(sdk.version.__version__) >= StrictVersion("0.13.0"): + list_args['fail_on_cloud_config'] = \ + inventory.extra_config['fail_on_errors'] + else: + use_hostnames = False + + for server in inventory.list_hosts(**list_args): + + if 'interface_ip' not in server: + continue + firstpass[server['name']].append(server) + for name, servers in firstpass.items(): + if len(servers) == 1 and use_hostnames: + append_hostvars(hostvars, groups, name, servers[0]) + else: + server_ids = set() + # Trap for duplicate results + for server in servers: + server_ids.add(server['id']) + if len(server_ids) == 1 and use_hostnames: + append_hostvars(hostvars, groups, name, servers[0]) + else: + for server in servers: + append_hostvars( + hostvars, groups, server['id'], server, + namegroup=True) + groups['_meta'] = {'hostvars': hostvars} + return groups + + +def is_cache_stale(cache_file, cache_expiration_time, refresh=False): + ''' Determines if cache file has expired, or if it is still valid ''' + if refresh: + return True + if os.path.isfile(cache_file) and os.path.getsize(cache_file) > 0: + mod_time = os.path.getmtime(cache_file) + current_time = time.time() + if (mod_time + cache_expiration_time) > current_time: + return False + return True + + +def get_cache_settings(cloud=None): + config_files = cloud_config.CONFIG_FILES + CONFIG_FILES + if cloud: + config = cloud_config.OpenStackConfig( + config_files=config_files).get_one(cloud=cloud) + else: + config = cloud_config.OpenStackConfig( + config_files=config_files).get_all()[0] + # For inventory-wide caching + cache_expiration_time = config.get_cache_expiration_time() + cache_path = config.get_cache_path() + if cloud: + cache_path = '{0}_{1}'.format(cache_path, cloud) + if not os.path.exists(cache_path): + os.makedirs(cache_path) + cache_file = os.path.join(cache_path, 'ansible-inventory.cache') + return (cache_file, cache_expiration_time) + + +def to_json(in_dict): + return json.dumps(in_dict, sort_keys=True, indent=2) + + +def parse_args(): + parser = argparse.ArgumentParser(description='OpenStack Inventory Module') + parser.add_argument('--cloud', default=os.environ.get('OS_CLOUD'), + help='Cloud name (default: None') + parser.add_argument('--private', + action='store_true', + help='Use private address for ansible host') + parser.add_argument('--refresh', action='store_true', + help='Refresh cached information') + parser.add_argument('--debug', action='store_true', default=False, + help='Enable debug output') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--list', action='store_true', + help='List active servers') + group.add_argument('--host', help='List details about the specific host') + + return parser.parse_args() + + +def main(): + args = parse_args() + try: + # openstacksdk library may write to stdout, so redirect this + sys.stdout = StringIO() + config_files = cloud_config.CONFIG_FILES + CONFIG_FILES + sdk.enable_logging(debug=args.debug) + inventory_args = dict( + refresh=args.refresh, + config_files=config_files, + private=args.private, + cloud=args.cloud, + ) + if hasattr(sdk_inventory.OpenStackInventory, 'extra_config'): + inventory_args.update(dict( + config_key='ansible', + config_defaults={ + 'use_hostnames': False, + 'expand_hostvars': True, + 'fail_on_errors': True, + } + )) + + inventory = sdk_inventory.OpenStackInventory(**inventory_args) + + sys.stdout = sys.__stdout__ + if args.list: + output = get_host_groups(inventory, refresh=args.refresh, cloud=args.cloud) + elif args.host: + output = to_json(inventory.get_host(args.host)) + print(output) + except sdk.exceptions.OpenStackCloudException as e: + sys.stderr.write('%s\n' % e.message) + sys.exit(1) + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/openstack/cloud/setup.py b/ansible_collections/openstack/cloud/setup.py new file mode 100644 index 00000000..d6d2ea1b --- /dev/null +++ b/ansible_collections/openstack/cloud/setup.py @@ -0,0 +1,8 @@ +# Copyright Red Hat, Inc. All Rights Reserved. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) |