summaryrefslogtreecommitdiffstats
path: root/ansible_collections/openstack/cloud/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/openstack/cloud/plugins
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/openstack/cloud/plugins')
-rw-r--r--ansible_collections/openstack/cloud/plugins/doc_fragments/__init__.py0
-rw-r--r--ansible_collections/openstack/cloud/plugins/doc_fragments/openstack.py113
-rw-r--r--ansible_collections/openstack/cloud/plugins/inventory/__init__.py0
-rw-r--r--ansible_collections/openstack/cloud/plugins/inventory/openstack.py415
-rw-r--r--ansible_collections/openstack/cloud/plugins/module_utils/__init__.py0
-rw-r--r--ansible_collections/openstack/cloud/plugins/module_utils/ironic.py68
-rw-r--r--ansible_collections/openstack/cloud/plugins/module_utils/openstack.py470
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/__init__.py0
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/address_scope.py201
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/auth.py62
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/baremetal_inspect.py133
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/baremetal_node.py441
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/baremetal_node_action.py362
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/baremetal_node_info.py555
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/baremetal_port.py373
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/baremetal_port_info.py208
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/catalog_service.py190
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/coe_cluster.py292
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/coe_cluster_template.py388
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/compute_flavor.py274
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/compute_flavor_info.py247
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/compute_service_info.py122
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/config.py76
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/container.py207
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/dns_zone.py244
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/dns_zone_info.py176
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/endpoint.py218
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/federation_idp.py220
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/federation_idp_info.py89
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/federation_mapping.py197
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/federation_mapping_info.py88
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/floating_ip.py307
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/floating_ip_info.py206
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/group_assignment.py96
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/host_aggregate.py236
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/identity_domain.py175
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/identity_domain_info.py119
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/identity_group.py157
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/identity_group_info.py150
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/identity_role.py113
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/identity_role_info.py102
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/identity_user.py263
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/identity_user_info.py153
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/image.py270
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/image_info.py204
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/keypair.py156
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/keypair_info.py141
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/keystone_federation_protocol.py187
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/keystone_federation_protocol_info.py98
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/lb_health_monitor.py291
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/lb_listener.py287
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/lb_member.py235
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/lb_pool.py263
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/loadbalancer.py691
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/network.py245
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/networks_info.py149
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/neutron_rbac_policies_info.py237
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/neutron_rbac_policy.py308
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/object.py120
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/object_container.py207
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_auth.py62
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_client_config.py76
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_coe_cluster.py292
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_coe_cluster_template.py388
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_flavor_info.py247
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_floating_ip.py307
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_group.py157
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_group_info.py150
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_image.py270
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_image_info.py204
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_ironic.py441
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_ironic_inspect.py133
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_ironic_node.py362
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keypair.py156
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_domain.py175
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_domain_info.py119
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_endpoint.py218
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_federation_protocol.py187
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_federation_protocol_info.py98
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_identity_provider.py220
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_identity_provider_info.py89
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_mapping.py197
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_mapping_info.py88
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_role.py113
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_keystone_service.py190
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_listener.py287
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_loadbalancer.py691
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_member.py235
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_network.py245
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_networks_info.py149
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_nova_flavor.py274
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_nova_host_aggregate.py236
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_object.py120
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_pool.py263
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_port.py530
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_port_info.py210
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_project.py220
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_project_access.py193
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_project_info.py156
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_quota.py466
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_recordset.py260
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_router.py571
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_routers_info.py194
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_security_group.py153
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_security_group_rule.py389
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_server.py805
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_server_action.py236
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_server_group.py162
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_server_info.py96
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_server_metadata.py165
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_server_volume.py139
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_stack.py248
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_subnet.py364
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_subnets_info.py164
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_user.py263
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_user_group.py96
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_user_info.py153
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_user_role.py190
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_volume.py263
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_volume_snapshot.py167
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/os_zone.py244
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/port.py530
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/port_info.py210
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/project.py220
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/project_access.py193
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/project_info.py156
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/quota.py466
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/recordset.py260
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/role_assignment.py190
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/router.py571
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/routers_info.py194
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/security_group.py153
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/security_group_info.py196
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/security_group_rule.py389
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/security_group_rule_info.py251
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/server.py805
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/server_action.py236
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/server_group.py162
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/server_info.py96
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/server_metadata.py165
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/server_volume.py139
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/stack.py248
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/stack_info.py112
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/subnet.py364
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/subnet_pool.py345
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/subnets_info.py164
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/volume.py263
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/volume_backup.py221
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/volume_backup_info.py126
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/volume_info.py149
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/volume_snapshot.py167
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/volume_snapshot_info.py136
152 files changed, 35008 insertions, 0 deletions
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 000000000..e69de29bb
--- /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 000000000..37d51bb2e
--- /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 000000000..e69de29bb
--- /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 000000000..def6c04ba
--- /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 000000000..e69de29bb
--- /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 000000000..a7ab19ef2
--- /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 000000000..8663d2fca
--- /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 000000000..e69de29bb
--- /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 000000000..eb5b187af
--- /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 000000000..1f2c516e4
--- /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 000000000..f7d90d1c5
--- /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 000000000..1adb560db
--- /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 000000000..267e43088
--- /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 000000000..8141fcdfa
--- /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 000000000..a72c1da6c
--- /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 000000000..d70c284dd
--- /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 000000000..6d1962f3e
--- /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 000000000..feb202a3b
--- /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 000000000..0596f39b7
--- /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 000000000..8a993ca51
--- /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 000000000..61ee7a5b7
--- /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 000000000..6665dd63e
--- /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 000000000..94036e499
--- /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 000000000..23ed38e54
--- /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 000000000..98cf655e3
--- /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 000000000..22a3da5c7
--- /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 000000000..e7864ecf1
--- /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 000000000..35606cca7
--- /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 000000000..4fe719494
--- /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 000000000..6c07a41da
--- /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 000000000..2ba317c98
--- /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 000000000..6b5fb0d66
--- /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 000000000..50e7c879f
--- /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 000000000..ce8f28e12
--- /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 000000000..4c95fd299
--- /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 000000000..660748c49
--- /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 000000000..e0e33cde5
--- /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 000000000..5b45efa4b
--- /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 000000000..68f00d73a
--- /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 000000000..272d98216
--- /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 000000000..42de17bd1
--- /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 000000000..047b3ed8b
--- /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 000000000..c0e0d9499
--- /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 000000000..fae13a2e5
--- /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 000000000..f02079c00
--- /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 000000000..232d4985e
--- /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 000000000..1fffe2c89
--- /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 000000000..5a33d8a32
--- /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 000000000..b281b13e3
--- /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 000000000..94de4485e
--- /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 000000000..f4cdad48a
--- /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 000000000..264f2b8e6
--- /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 000000000..6f73ea1ce
--- /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 000000000..336da966c
--- /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 000000000..780d49ba7
--- /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 000000000..251af3e72
--- /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 000000000..b451bc264
--- /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 000000000..f5162e08d
--- /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 000000000..4a22604ed
--- /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 000000000..23ed38e54
--- /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 000000000..1f2c516e4
--- /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 000000000..94036e499
--- /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 000000000..feb202a3b
--- /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 000000000..0596f39b7
--- /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 000000000..61ee7a5b7
--- /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 000000000..6b5fb0d66
--- /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 000000000..5b45efa4b
--- /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 000000000..68f00d73a
--- /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 000000000..fae13a2e5
--- /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 000000000..f02079c00
--- /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 000000000..1adb560db
--- /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 000000000..f7d90d1c5
--- /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 000000000..267e43088
--- /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 000000000..232d4985e
--- /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 000000000..660748c49
--- /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 000000000..e0e33cde5
--- /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 000000000..e7864ecf1
--- /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 000000000..5a33d8a32
--- /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 000000000..b281b13e3
--- /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 000000000..35606cca7
--- /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 000000000..4fe719494
--- /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 000000000..6c07a41da
--- /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 000000000..2ba317c98
--- /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 000000000..272d98216
--- /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 000000000..6d1962f3e
--- /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 000000000..f4cdad48a
--- /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 000000000..336da966c
--- /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 000000000..264f2b8e6
--- /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 000000000..780d49ba7
--- /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 000000000..251af3e72
--- /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 000000000..8a993ca51
--- /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 000000000..4c95fd299
--- /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 000000000..4a22604ed
--- /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 000000000..6f73ea1ce
--- /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 000000000..accef4fcc
--- /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 000000000..0ed3f0599
--- /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 000000000..9719452dc
--- /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 000000000..c49a84495
--- /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 000000000..fb1e2767d
--- /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 000000000..0d6a4f04c
--- /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 000000000..921d6efaa
--- /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 000000000..58c5c124e
--- /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 000000000..990eef8dc
--- /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 000000000..8208a1c22
--- /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 000000000..53fe6f590
--- /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 000000000..a3ca7d051
--- /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 000000000..341ff3742
--- /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 000000000..84f59e6cb
--- /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 000000000..bac1d2114
--- /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 000000000..a1207e3b3
--- /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 000000000..1deb8fa6e
--- /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 000000000..95b7bef5e
--- /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 000000000..dfe1eaca3
--- /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 000000000..7a771b53a
--- /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 000000000..047b3ed8b
--- /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 000000000..ce8f28e12
--- /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 000000000..c0e0d9499
--- /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 000000000..5ad7dce42
--- /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 000000000..3a50c05a8
--- /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 000000000..8625984c6
--- /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 000000000..98cf655e3
--- /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 000000000..accef4fcc
--- /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 000000000..0ed3f0599
--- /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 000000000..9719452dc
--- /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 000000000..c49a84495
--- /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 000000000..fb1e2767d
--- /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 000000000..0d6a4f04c
--- /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 000000000..921d6efaa
--- /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 000000000..5ad7dce42
--- /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 000000000..58c5c124e
--- /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 000000000..990eef8dc
--- /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 000000000..8208a1c22
--- /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 000000000..bc05356af
--- /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 000000000..53fe6f590
--- /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 000000000..b00f71927
--- /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 000000000..a3ca7d051
--- /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 000000000..341ff3742
--- /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 000000000..84f59e6cb
--- /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 000000000..bac1d2114
--- /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 000000000..a1207e3b3
--- /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 000000000..1deb8fa6e
--- /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 000000000..95b7bef5e
--- /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 000000000..ce56995a4
--- /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 000000000..dfe1eaca3
--- /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 000000000..4272438fd
--- /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 000000000..7a771b53a
--- /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 000000000..3a50c05a8
--- /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 000000000..43cacc72b
--- /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 000000000..fdb61834c
--- /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 000000000..bcce49940
--- /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 000000000..8625984c6
--- /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 000000000..fa50055da
--- /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()