diff --git a/ci/roles/loadbalancer/tasks/lb_modules.yml b/ci/roles/loadbalancer/tasks/lb_modules.yml new file mode 100644 index 00000000..49f52be4 --- /dev/null +++ b/ci/roles/loadbalancer/tasks/lb_modules.yml @@ -0,0 +1,322 @@ +--- +- name: Create external network + openstack.cloud.network: + cloud: "{{ cloud }}" + external: true + name: ansible_external_network + state: present + +- name: Create external subnet + openstack.cloud.subnet: + cidr: 10.6.6.0/24 + cloud: "{{ cloud }}" + name: ansible_external_subnet + network_name: ansible_external_network + state: present + +- name: Create internal network + openstack.cloud.network: + cloud: "{{ cloud }}" + name: ansible_internal_network + state: present + +- name: Create internal subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: present + network_name: ansible_internal_network + name: ansible_internal_subnet + cidr: 10.7.7.0/24 + +- name: Create router + openstack.cloud.router: + cloud: "{{ cloud }}" + external_fixed_ips: + - subnet: ansible_external_subnet + ip: 10.6.6.10 + interfaces: + - net: ansible_internal_network + subnet: ansible_internal_subnet + portip: 10.7.7.1 + name: ansible_router + network: ansible_external_network + state: present + +- name: Create load-balancer + openstack.cloud.loadbalancer: + assign_floating_ip: true + cloud: "{{ cloud }}" + floating_ip_network: ansible_external_network + name: ansible_lb + state: present + timeout: 450 + vip_subnet: ansible_internal_subnet + register: load_balancer + +- name: Create load-balancer listener + openstack.cloud.lb_listener: + cloud: "{{ cloud }}" + load_balancer: ansible_lb + name: ansible_listener + protocol: HTTP + protocol_port: 8080 + state: present + register: listener + +- name: Assert return values of lb_listener module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - "['allowed_cidrs', 'alpn_protocols', 'connection_limit', 'created_at', 'default_pool', 'default_pool_id', + 'default_tls_container_ref', 'description', 'id', 'insert_headers', 'is_admin_state_up', 'l7_policies', + 'load_balancer_id', 'load_balancers', 'name', 'operating_status', 'project_id', 'protocol', 'protocol_port', + 'provisioning_status', 'sni_container_refs', 'tags', 'timeout_client_data', 'timeout_member_connect', + 'timeout_member_data', 'timeout_tcp_inspect', 'tls_ciphers', 'tls_versions', 'updated_at' + ]|difference(listener.listener.keys())|length == 0" + +- name: Create load-balancer listener again + openstack.cloud.lb_listener: + cloud: "{{ cloud }}" + load_balancer: ansible_lb + name: ansible_listener + protocol: HTTP + protocol_port: 8080 + state: present + register: listener + +- name: Assert return values of lb_listener module + assert: + that: + - listener is not changed + +- name: Update load-balancer listener description + openstack.cloud.lb_listener: + cloud: "{{ cloud }}" + description: "Ansible load-balancer listener" + load_balancer: ansible_lb + name: ansible_listener + protocol: HTTP + protocol_port: 8080 + state: present + register: listener + +- name: Assert return values of lb_listener module + assert: + that: + - listener.listener.description == "Ansible load-balancer listener" + +- name: Create load-balancer pool + openstack.cloud.lb_pool: + cloud: "{{ cloud }}" + lb_algorithm: ROUND_ROBIN + listener: "{{ listener.listener.id }}" + name: ansible_pool + protocol: HTTP + state: present + register: pool + +- name: Assert return values of lb_pool module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - "['alpn_protocols', 'created_at', 'description', 'health_monitor_id', 'id', 'is_admin_state_up', 'lb_algorithm', + 'listener_id', 'listeners', 'loadbalancer_id', 'loadbalancers', 'members','name', 'operating_status', + 'project_id', 'protocol', 'provisioning_status', 'session_persistence', 'tags', 'tls_ciphers', 'tls_enabled', + 'tls_versions', 'updated_at' + ]|difference(pool.pool.keys())|length == 0" + +- name: Create load-balancer pool again + openstack.cloud.lb_pool: + cloud: "{{ cloud }}" + lb_algorithm: ROUND_ROBIN + listener: "{{ listener.listener.id }}" + name: ansible_pool + protocol: HTTP + state: present + register: pool + +- name: Assert return values of lb_pool module + assert: + that: + - pool is not changed + +- name: Update load-balancer pool description + openstack.cloud.lb_pool: + cloud: "{{ cloud }}" + description: "Ansible load-balancer pool" + lb_algorithm: ROUND_ROBIN + listener: "{{ listener.listener.id }}" + name: ansible_pool + protocol: HTTP + state: present + register: pool + +- name: Assert return values of lb_pool module + assert: + that: + - pool.pool.description == "Ansible load-balancer pool" + +- name: Create load-balancer pool member + openstack.cloud.lb_member: + address: 10.7.7.42 + cloud: "{{ cloud }}" + name: ansible_member + pool: ansible_pool + protocol_port: 8080 + state: present + register: member + +- name: Assert return values of lb_member module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - "['address', 'backup', 'created_at', 'id', 'is_admin_state_up', 'monitor_address', 'monitor_port', 'name', + 'operating_status', 'project_id', 'protocol_port', 'provisioning_status', 'subnet_id', 'tags', 'updated_at', + 'weight' + ]|difference(member.member.keys())|length == 0" + +- name: Create load-balancer pool member again + openstack.cloud.lb_member: + address: 10.7.7.42 + cloud: "{{ cloud }}" + name: ansible_member + pool: ansible_pool + protocol_port: 8080 + state: present + register: member + +- name: Assert return values of lb_member module + assert: + that: + - member is not changed + +- name: Update load-balancer pool member weight + openstack.cloud.lb_member: + address: 10.7.7.42 + cloud: "{{ cloud }}" + name: ansible_member + pool: ansible_pool + protocol_port: 8080 + state: present + weight: 42 + register: member + +- name: Assert return values of lb_member module + assert: + that: + - member.member.weight == 42 + +- name: Create load-balancer health monitor + openstack.cloud.lb_health_monitor: + cloud: "{{ cloud }}" + delay: 10 + health_monitor_timeout: 5 + max_retries: 3 + name: ansible_health_monitor + pool: ansible_pool + state: present + register: health_monitor + +- name: Assert return values of lb_health_monitor module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - "['created_at', 'delay', 'expected_codes', 'http_method', 'id', 'is_admin_state_up', 'max_retries', + 'max_retries_down', 'name', 'operating_status', 'pool_id', 'pools', 'project_id', 'provisioning_status', + 'tags', 'timeout', 'type', 'updated_at', 'url_path' + ]|difference(health_monitor.health_monitor.keys())|length == 0" + +- name: Create load-balancer health monitor again + openstack.cloud.lb_health_monitor: + cloud: "{{ cloud }}" + delay: 10 + health_monitor_timeout: 5 + max_retries: 3 + name: ansible_health_monitor + pool: ansible_pool + state: present + register: health_monitor + +- name: Assert return values of lb_health_monitor module + assert: + that: + - health_monitor is not changed + +- name: Update load-balancer health monitor delay + openstack.cloud.lb_health_monitor: + cloud: "{{ cloud }}" + delay: 1337 + health_monitor_timeout: 5 + max_retries: 3 + name: ansible_health_monitor + pool: ansible_pool + state: present + register: health_monitor + +- name: Assert return values of lb_health_monitor module + assert: + that: + - health_monitor.health_monitor.delay == 1337 + +- name: Delete load-balancer health monitor + openstack.cloud.lb_health_monitor: + cloud: "{{ cloud }}" + name: ansible_health_monitor + state: absent + +- name: Delete load-balancer pool member + openstack.cloud.lb_member: + cloud: "{{ cloud }}" + name: ansible_member + pool: ansible_pool + state: absent + +- name: Delete load-balancer pool + openstack.cloud.lb_pool: + cloud: "{{ cloud }}" + name: ansible_pool + state: absent + +- name: Delete load-balancer listener + openstack.cloud.lb_listener: + cloud: "{{ cloud }}" + name: ansible_listener + state: absent + +- name: Delete load-balancer + openstack.cloud.loadbalancer: + cloud: "{{ cloud }}" + delete_floating_ip: true + name: ansible_lb + state: absent + timeout: 150 + +- name: Delete router + openstack.cloud.router: + cloud: "{{ cloud }}" + name: ansible_router + state: absent + +- name: Delete internal subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + name: ansible_internal_subnet + state: absent + +- name: Delete internal network + openstack.cloud.network: + cloud: "{{ cloud }}" + name: ansible_internal_network + state: absent + +- name: Delete external subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + name: ansible_external_subnet + state: absent + +- name: Delete external network + openstack.cloud.network: + cloud: "{{ cloud }}" + name: ansible_external_network + state: absent diff --git a/ci/roles/loadbalancer/tasks/main.yml b/ci/roles/loadbalancer/tasks/main.yml index 7617ca1e..62ddbc9e 100644 --- a/ci/roles/loadbalancer/tasks/main.yml +++ b/ci/roles/loadbalancer/tasks/main.yml @@ -282,3 +282,5 @@ cloud: "{{ cloud }}" name: ansible_external_network state: absent + +- import_tasks: lb_modules.yml diff --git a/plugins/modules/lb_health_monitor.py b/plugins/modules/lb_health_monitor.py index ddfc0c68..5f009606 100644 --- a/plugins/modules/lb_health_monitor.py +++ b/plugins/modules/lb_health_monitor.py @@ -4,287 +4,356 @@ # 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 = ''' +DOCUMENTATION = r''' --- 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 +short_description: Manage health monitor in a OpenStack load-balancer pool description: - - Add or Remove a health monitor to/from a pool in the OpenStack load-balancer service. + - Add, update or remove health monitor from a load-balancer pool in OpenStack + cloud. 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"] + delay: + description: + - The interval, in seconds, between health checks. + - Required when I(state) is C(present). + type: int + expected_codes: + description: + - The list of HTTP status codes expected in response from the member to + declare it healthy. Specify one of the following values. + - For example, I(expected_codes) could be a single value, such as C(200), + a list, such as C(200, 202) or a range, such as C(200-204). + - "Octavia's default for I(expected_codes) is C(200)." + type: str + health_monitor_timeout: + description: + - The time, in seconds, after which a health check times out. + - Must be less than I(delay). + - Required when I(state) is C(present). + type: int + aliases: ['resp_timeout'] + http_method: + description: + - The HTTP method that the health monitor uses for requests. + - For example, I(http_method) could be C(CONNECT), C(DELETE), C(GET), + C(HEAD), C(OPTIONS), C(PATCH), C(POST), C(PUT), or C(TRACE). + - "Octavia's default for I(http_method) is C(GET)." + type: str + is_admin_state_up: + description: + - Whether the health monitor is up or down. + type: bool + aliases: ['admin_state_up'] + max_retries: + description: + - The number of successful checks before changing the operating status + of the member to ONLINE. + - Required when I(state) is C(present). + type: int + max_retries_down: + 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. + type: int + name: + description: + - Name that has to be given to the health monitor. + - This attribute cannot be updated. + type: str + required: true + pool: + description: + - The pool name or id to monitor by the health monitor. + - Required when I(state) is C(present). + - This attribute cannot be updated. + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + type: + default: HTTP + description: + - The type of health monitor. + - For example, I(type) could be C(HTTP), C(HTTPS), C(PING), C(SCTP), + C(TCP), C(TLS-HELLO) or C(UDP-CONNECT). + - This attribute cannot be updated. + type: str + url_path: + 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 (C(/)). + - "Octavia's default URL path is C(/)." + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" extends_documentation_fragment: -- openstack.cloud.openstack + - 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 +RETURN = r''' +health_monitor: + description: Dictionary describing the load-balancer health monitor. + returned: On success when I(state) is C(present). + type: dict + contains: + created_at: + description: The UTC date and timestamp when the resource was created. + type: str + delay: + description: The time, in seconds, between sending probes to members. + type: int + expected_codes: + description: The list of HTTP status codes expected in response from the + member to declare it healthy. + type: str + http_method: + description: The HTTP method that the health monitor uses for requests. + type: str + id: + description: The health monitor UUID. + type: str + is_admin_state_up: + description: The administrative state of the resource. + type: bool + max_retries: + description: The number of successful checks before changing the + operating status of the member to ONLINE. + type: int + max_retries_down: + description: The number of allowed check failures before changing the + operating status of the member to ERROR. + type: int + name: + description: Human-readable name of the resource. + type: str + operating_status: + description: The operating status of the resource. + type: str + pool_id: + description: The id of the pool. + type: str + pools: + description: List of associated pool ids. + type: list + project_id: + description: The ID of the project owning this resource. + type: str + provisioning_status: + description: The provisioning status of the resource. + type: str + tags: + description: A list of associated tags. + type: list + timeout: + description: The maximum time, in seconds, that a monitor waits to + connect before it times out. + type: int + type: + description: The type of health monitor. + type: str + updated_at: + description: The UTC date and timestamp when the resource was last + updated. + type: str + url_path: + description: The HTTP URL path of the request sent by the monitor to + test the health of a backend member. + type: str +''' + +EXAMPLES = r''' +- name: Create a load-balancer health monitor + openstack.cloud.lb_health_monitor: + cloud: devstack + delay: 10 + expected_codes: '200' + health_monitor_timeout: 5 + http_method: GET + is_admin_state_up: true + max_retries: 3 + max_retries_down: 4 + name: healthmonitor01 + pool: lb_pool + state: present + url_path: '/status' + +- name: Delete a load-balancer health monitor + openstack.cloud.lb_health_monitor: + cloud: devstack + name: healthmonitor01 + state: absent +''' 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) - ) +class LoadBalancerHealthMonitorModule(OpenStackModule): argument_spec = dict( + delay=dict(type='int'), + expected_codes=dict(), + health_monitor_timeout=dict(type='int', aliases=['resp_timeout']), + http_method=dict(), + is_admin_state_up=dict(type='bool', aliases=['admin_state_up']), + max_retries=dict(type='int'), + max_retries_down=dict(type='int'), name=dict(required=True), - delay=dict(required=True), - max_retries=dict(required=True), - max_retries_down=dict(default="3"), - resp_timeout=dict(required=True, type='int'), - pool=dict(required=True), - expected_codes=dict(default="200"), - admin_state_up=dict(default=True, type='bool'), + pool=dict(), state=dict(default='present', choices=['absent', 'present']), - http_method=dict(default="GET", choices=["GET", "CONNECT", "DELETE", - "HEAD", "OPTIONS", "PATCH", - "POST", "PUT", "TRACE"]), - url_path=dict(default="/"), - type=dict(default='HTTP', - choices=['HTTP', 'HTTPS', 'PING', 'SCTP', 'TCP', 'TLS-HELLO', 'UDP-CONNECT'])) + type=dict(default='HTTP'), + url_path=dict(), + ) - module_kwargs = dict(supports_check_mode=True) + module_kwargs = dict( + required_if=[ + ('state', 'present', ('delay', 'health_monitor_timeout', + 'max_retries', 'pool',)), + ], + supports_check_mode=True, + ) def run(self): + state = self.params['state'] - 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"] + health_monitor = self._find() - if self.ansible.check_mode: - self.exit_json(changed=True) + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, health_monitor)) - 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 + if state == 'present' and not health_monitor: + # Create health_monitor + health_monitor = self._create() + self.exit_json( + changed=True, + health_monitor=health_monitor.to_dict(computed=False)) - self.exit_json(changed=changed) - except Exception as e: - self.fail(msg=str(e)) + elif state == 'present' and health_monitor: + # Update health_monitor + update = self._build_update(health_monitor) + if update: + health_monitor = self._update(health_monitor, update) + + self.exit_json( + changed=bool(update), + health_monitor=health_monitor.to_dict(computed=False)) + + elif state == 'absent' and health_monitor: + # Delete health_monitor + self._delete(health_monitor) + self.exit_json(changed=True) + + elif state == 'absent' and not health_monitor: + # Do nothing + self.exit_json(changed=False) + + def _build_update(self, health_monitor): + update = {} + + non_updateable_keys = [k for k in ['type'] + if self.params[k] is not None + and self.params[k] != health_monitor[k]] + + pool_name_or_id = self.params['pool'] + pool = self.conn.load_balancer.find_pool(name_or_id=pool_name_or_id, + ignore_missing=False) + # Field pool_id is not returned from self.conn.load_balancer.\ + # find_pool() so use pools instead. + if health_monitor['pools'] != [dict(id=pool.id)]: + non_updateable_keys.append('pool') + + if non_updateable_keys: + self.fail_json(msg='Cannot update parameters {0}' + .format(non_updateable_keys)) + + attributes = dict((k, self.params[k]) + for k in ['delay', 'expected_codes', 'http_method', + 'is_admin_state_up', 'max_retries', + 'max_retries_down', 'type', 'url_path'] + if self.params[k] is not None + and self.params[k] != health_monitor[k]) + + health_monitor_timeout = self.params['health_monitor_timeout'] + if health_monitor_timeout is not None \ + and health_monitor_timeout != health_monitor['timeout']: + attributes['timeout'] = health_monitor_timeout + + if attributes: + update['attributes'] = attributes + + return update + + def _create(self): + kwargs = dict((k, self.params[k]) + for k in ['delay', 'expected_codes', 'http_method', + 'is_admin_state_up', 'max_retries', + 'max_retries_down', 'name', 'type', 'url_path'] + if self.params[k] is not None) + + health_monitor_timeout = self.params['health_monitor_timeout'] + if health_monitor_timeout is not None: + kwargs['timeout'] = health_monitor_timeout + + pool_name_or_id = self.params['pool'] + pool = self.conn.load_balancer.find_pool(name_or_id=pool_name_or_id, + ignore_missing=False) + kwargs['pool_id'] = pool.id + + health_monitor = \ + self.conn.load_balancer.create_health_monitor(**kwargs) + + if self.params['wait']: + health_monitor = self.sdk.resource.wait_for_status( + self.conn.load_balancer, health_monitor, + status='active', + failures=['error'], + wait=self.params['timeout'], + attribute='provisioning_status') + + return health_monitor + + def _delete(self, health_monitor): + self.conn.load_balancer.delete_health_monitor(health_monitor.id) + + def _find(self): + name = self.params['name'] + return self.conn.load_balancer.find_health_monitor(name_or_id=name) + + def _update(self, health_monitor, update): + attributes = update.get('attributes') + if attributes: + health_monitor = self.conn.load_balancer.update_health_monitor( + health_monitor.id, **attributes) + + if self.params['wait']: + health_monitor = self.sdk.resource.wait_for_status( + self.conn.load_balancer, health_monitor, + status='active', + failures=['error'], + wait=self.params['timeout'], + attribute='provisioning_status') + + return health_monitor + + def _will_change(self, state, health_monitor): + if state == 'present' and not health_monitor: + return True + elif state == 'present' and health_monitor: + return bool(self._build_update(health_monitor)) + elif state == 'absent' and health_monitor: + return True + else: + # state == 'absent' and not health_monitor: + return False def main(): - module = HealthMonitorModule() + module = LoadBalancerHealthMonitorModule() module() diff --git a/plugins/modules/lb_listener.py b/plugins/modules/lb_listener.py index a1e6f8fd..c2b10c7f 100644 --- a/plugins/modules/lb_listener.py +++ b/plugins/modules/lb_listener.py @@ -4,283 +4,383 @@ # 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 = ''' +DOCUMENTATION = r''' --- module: lb_listener -short_description: Add/Delete a listener for a load balancer from OpenStack Cloud +short_description: Manage load-balancer listener in a OpenStack cloud author: OpenStack Ansible SIG description: - - Add or Remove a listener for a load balancer from the OpenStack load-balancer service. + - Add, update or remove listener from OpenStack load-balancer. 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' + default_tls_container_ref: + description: + - A URI to a key manager service secrets container with TLS secrets. 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 + description: + description: + - A human-readable description for the load-balancer listener. + type: str + is_admin_state_up: + description: + - The administrative state of the listener, which is up or down. + type: bool + load_balancer: + description: + - The name or id of the load-balancer that this listener belongs to. + - Required when I(state) is C(present). + - This attribute cannot be updated. + type: str + aliases: ['loadbalancer'] + name: + description: + - Name that has to be given to the listener. + - This attribute cannot be updated. + required: true + type: str + protocol: + description: + - The protocol for the listener. + - For example, I(protocol) could be C(HTTP), C(HTTPS), C(TCP), + C(TERMINATED_HTTPS), C(UDP), C(SCTP) or C(PROMETHEUS). + - This attribute cannot be updated. + default: HTTP + type: str + protocol_port: + description: + - The protocol port number for the listener. + - This attribute cannot be updated. + type: int + sni_container_refs: + description: + - A list of URIs to the key manager service secrets containers with TLS + secrets. + type: list + elements: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + timeout_client_data: + description: + - Client inactivity timeout in milliseconds. + type: int + timeout_member_data: + description: + - Member inactivity timeout in milliseconds. + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: + - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Create a listener, wait for the loadbalancer to be active. -- openstack.cloud.lb_listener: +RETURN = r''' +listener: + description: Dictionary describing the listener. + returned: On success when I(state) is C(present). + type: dict + contains: + allowed_cidrs: + description: List of IPv4 or IPv6 CIDRs. + type: list + alpn_protocols: + description: List of ALPN protocols. + type: list + connection_limit: + description: The maximum number of connections permitted for this load + balancer. + type: str + created_at: + description: Timestamp when the listener was created. + type: str + default_pool: + description: Default pool to which the requests will be routed. + type: str + default_pool_id: + description: ID of default pool. Must have compatible protocol with + listener. + type: str + default_tls_container_ref: + description: A reference to a container of TLS secrets. + type: str + description: + description: The listener description. + type: str + sample: "description" + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + insert_headers: + description: Dictionary of additional headers insertion into HTTP header. + type: dict + is_admin_state_up: + description: The administrative state of the listener. + type: bool + sample: true + l7_policies: + description: A list of L7 policy objects. + type: list + load_balancer_id: + description: The load balancer UUID this listener belongs to. + type: str + sample: "b32eef7e-d2a6-4ea4-a301-60a873f89b3b" + load_balancers: + description: A list of load balancer IDs. + type: list + sample: [{"id": "b32eef7e-d2a6-4ea4-a301-60a873f89b3b"}] + name: + description: Name given to the listener. + type: str + sample: "test" + operating_status: + description: The operating status of the listener. + type: str + sample: "ONLINE" + project_id: + description: The ID of the project owning this resource. + type: str + 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 + provisioning_status: + description: The provisioning status of the listener. + type: str + sample: "ACTIVE" + sni_container_refs: + description: A list of references to TLS secrets. + type: list + tags: + description: A list of associated tags. + type: list + timeout_client_data: + description: Client inactivity timeout in milliseconds. + type: int + sample: 50000 + timeout_member_connect: + description: Backend member connection timeout in milliseconds. + type: int + timeout_member_data: + description: Member inactivity timeout in milliseconds. + type: int + sample: 50000 + timeout_tcp_inspect: + description: Time, in milliseconds, to wait for additional TCP packets + for content inspection. + type: int + tls_ciphers: + description: Stores a cipher string in OpenSSL format. + type: str + tls_versions: + description: A list of TLS protocols to be used by the listener. + type: list + updated_at: + description: Timestamp when the listener was last updated. + type: str +''' + +EXAMPLES = r''' +- name: Create a listener, wait for the loadbalancer to be active + openstack.cloud.lb_listener: cloud: mycloud - endpoint_type: admin - state: present + load_balancer: test-loadbalancer 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: +- name: Delete a listener + openstack.cloud.lb_listener: cloud: mycloud - endpoint_type: admin + load_balancer: test-loadbalancer + name: test-listener state: absent - name: test-listener - loadbalancer: test-loadbalancer -# Create a listener, increase timeouts for connection persistence (for SSH for example). -- openstack.cloud.lb_listener: +- name: Create a listener, increase timeouts for connection persistence + openstack.cloud.lb_listener: cloud: mycloud - endpoint_type: admin - state: present + load_balancer: test-loadbalancer name: test-listener - loadbalancer: test-loadbalancer protocol: TCP protocol_port: 22 + state: present timeout_client_data: 1800000 timeout_member_data: 1800000 ''' -import time - from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule -class LoadbalancerListenerModule(OpenStackModule): +class LoadBalancerListenerModule(OpenStackModule): argument_spec = dict( + default_tls_container_ref=dict(), + description=dict(), + is_admin_state_up=dict(type='bool'), + load_balancer=dict(aliases=['loadbalancer']), name=dict(required=True), + protocol=dict(default='HTTP'), + protocol_port=dict(type='int'), + sni_container_refs=dict(type='list', elements='str'), 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'), - timeout_client_data=dict(default=50000, type='int'), - timeout_member_data=dict(default=50000, type='int'), + timeout_client_data=dict(type='int'), + timeout_member_data=dict(type='int'), + ) + module_kwargs = dict( + required_if=[ + ('state', 'present', ('load_balancer',)), + ], + supports_check_mode=True, ) - 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 + state = self.params['state'] - changed = False - listener = self.conn.load_balancer.find_listener( - name_or_id=self.params['name']) + listener = self._find() - 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 + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, listener)) - 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 state == 'present' and not listener: + # Create listener + listener = self._create() + self.exit_json(changed=True, + rbac_listener=listener.to_dict(computed=False), + listener=listener.to_dict(computed=False)) - if not self.params['wait']: - self.exit_json( - changed=changed, listener=listener.to_dict(), - id=listener.id) + elif state == 'present' and listener: + # Update listener + update = self._build_update(listener) + if update: + listener = self._update(listener, update) - 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=bool(update), + rbac_listener=listener.to_dict(computed=False), + listener=listener.to_dict(computed=False)) - 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 + elif state == 'absent' and listener: + # Delete listener + self._delete(listener) + self.exit_json(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"]) + elif state == 'absent' and not listener: + # Do nothing + self.exit_json(changed=False) - self.exit_json(changed=changed) + def _build_update(self, listener): + update = {} + + non_updateable_keys = [k for k in ['protocol', 'protocol_port'] + if self.params[k] is not None + and self.params[k] != listener[k]] + + load_balancer_name_or_id = self.params['load_balancer'] + load_balancer = self.conn.load_balancer.find_load_balancer( + load_balancer_name_or_id, ignore_missing=False) + # Field load_balancer_id is not returned from self.conn.load_balancer.\ + # find_load_balancer() so use load_balancers instead. + if listener['load_balancers'] != [dict(id=load_balancer.id)]: + non_updateable_keys.append('load_balancer') + + if non_updateable_keys: + self.fail_json(msg='Cannot update parameters {0}' + .format(non_updateable_keys)) + + attributes = dict((k, self.params[k]) + for k in ['default_tls_container_ref', + 'description', + 'is_admin_state_up', + 'sni_container_refs', + 'timeout_client_data', + 'timeout_member_data'] + if self.params[k] is not None + and self.params[k] != listener[k]) + + if attributes: + update['attributes'] = attributes + + return update + + def _create(self): + kwargs = dict((k, self.params[k]) + for k in ['default_tls_container_ref', 'description', + 'is_admin_state_up', 'name', 'protocol', + 'protocol_port', 'sni_container_refs', + 'timeout_client_data', 'timeout_member_data'] + if self.params[k] is not None) + + load_balancer_name_or_id = self.params['load_balancer'] + load_balancer = self.conn.load_balancer.find_load_balancer( + load_balancer_name_or_id, ignore_missing=False) + kwargs['load_balancer_id'] = load_balancer.id + + listener = self.conn.load_balancer.create_listener(**kwargs) + + if self.params['wait']: + self.conn.load_balancer.wait_for_load_balancer( + listener.load_balancer_id, + wait=self.params['timeout']) + + return listener + + def _delete(self, listener): + self.conn.load_balancer.delete_listener(listener.id) + + if self.params['wait']: + # Field load_balancer_id is not returned from self.conn.\ + # load_balancer.find_listener() so use load_balancers instead. + if not listener.load_balancers \ + or len(listener.load_balancers) != 1: + raise AssertionError("A single load-balancer is expected") + + self.conn.load_balancer.wait_for_load_balancer( + listener.load_balancers[0]['id'], + wait=self.params['timeout']) + + def _find(self): + name = self.params['name'] + return self.conn.load_balancer.find_listener(name_or_id=name) + + def _update(self, listener, update): + attributes = update.get('attributes') + if attributes: + listener = self.conn.load_balancer.update_listener(listener.id, + **attributes) + + if self.params['wait']: + # Field load_balancer_id is not returned from self.conn.\ + # load_balancer.find_listener() so use load_balancers instead. + if not listener.load_balancers \ + or len(listener.load_balancers) != 1: + raise AssertionError("A single load-balancer is expected") + + self.conn.load_balancer.wait_for_load_balancer( + listener.load_balancers[0]['id'], + wait=self.params['timeout']) + + return listener + + def _will_change(self, state, listener): + if state == 'present' and not listener: + return True + elif state == 'present' and listener: + return bool(self._build_update(listener)) + elif state == 'absent' and listener: + return True + else: + # state == 'absent' and not listener: + return False def main(): - module = LoadbalancerListenerModule() + module = LoadBalancerListenerModule() module() diff --git a/plugins/modules/lb_member.py b/plugins/modules/lb_member.py index b7c9e31d..a170745d 100644 --- a/plugins/modules/lb_member.py +++ b/plugins/modules/lb_member.py @@ -4,231 +4,377 @@ # 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 = ''' +DOCUMENTATION = r''' --- module: lb_member -short_description: Add/Delete a member for a pool in load balancer from OpenStack Cloud +short_description: Manage members in a OpenStack load-balancer pool author: OpenStack Ansible SIG description: - - Add or Remove a member for a pool from the OpenStack load-balancer service. + - Add, update or remove member from OpenStack load-balancer pool. 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' + address: + description: + - The IP address of the member. + - Required when I(state) is C(present). + - This attribute cannot be updated. type: str - sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + monitor_address: + description: + - IP address used to monitor this member. + type: str + monitor_port: + description: + - Port used to monitor this member. + type: int + name: + description: + - Name that has to be given to the member. + required: true + type: str + pool: + description: + - The name or id of the pool that this member belongs to. + - This attribute cannot be updated. + required: true + type: str + protocol_port: + description: + - The protocol port number for the member. + - Required when I(state) is C(present). + - This attribute cannot be updated. + type: int + state: + description: + - Should the resource be C(present) or C(absent). + choices: [present, absent] + default: present + type: str + subnet_id: + description: + - The subnet ID the member service is accessible from. + - This attribute cannot be updated. + type: str + weight: + description: + - The weight of a member determines the portion of requests or + connections it services compared to the other members of the pool. + - For example, a member with a weight of 10 receives five times as many + requests as a member with a weight of 2. A value of 0 means the member + does not receive new connections but continues to service existing + connections. A valid value is from 0 to 256. + - "Octavia's default for I(weight) is C(1)." + type: int +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +RETURN = r''' 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" + description: Dictionary describing the load-balancer pool member. + returned: On success when I(state) is C(present). + type: dict + contains: + address: + description: The IP address of the backend member server. + type: str + backup: + description: A bool value that indicates whether the member is a backup + or not. Backup members only receive traffic when all + non-backup members are down. + type: bool + created_at: + description: Timestamp when the member was created. + type: str + id: + description: Unique UUID. + type: str + is_admin_state_up: + description: The administrative state of the member. + type: bool + monitor_address: + description: IP address used to monitor this member. + type: str + monitor_port: + description: Port used to monitor this member. + type: int + name: + description: Name given to the member. + type: str + operating_status: + description: Operating status of the member. + type: str + project_id: + description: The ID of the project this member is associated with. + type: str + protocol_port: + description: The protocol port number for the member. + type: int + provisioning_status: + description: The provisioning status of the member. + type: str + subnet_id: + description: The subnet ID the member service is accessible from. + type: str + tags: + description: A list of associated tags. + type: list + updated_at: + description: Timestamp when the member was last updated. + type: str + weight: + description: A positive integer value that indicates the relative portion + of traffic that this member should receive from the pool. + For example, a member with a weight of 10 receives five + times as much traffic as a member with weight of 2. + type: int +pool: + description: Dictionary describing the load-balancer pool. + returned: On success when I(state) is C(present). + type: dict + contains: + alpn_protocols: + description: List of ALPN protocols. + type: list + created_at: + description: Timestamp when the pool was created. + type: str + description: + description: The pool description. + type: str + health_monitor_id: + description: Health Monitor ID. + type: str + id: + description: Unique UUID. + type: str + is_admin_state_up: + description: The administrative state of the pool. + type: bool + lb_algorithm: + description: The load balancing algorithm for the pool. + type: str + listener_id: + description: The listener ID the pool belongs to. + type: str + listeners: + description: A list of listener IDs. + type: list + loadbalancer_id: + description: The load balancer ID the pool belongs to. This field is set + when the pool does not belong to any listener in the load + balancer. + type: str + loadbalancers: + description: A list of load balancer IDs. + type: list + members: + description: A list of member IDs. + type: list + name: + description: Name given to the pool. + type: str + operating_status: + description: The operating status of the pool. + type: str + project_id: + description: The ID of the project. + type: str + protocol: + description: The protocol for the pool. + type: str + provisioning_status: + description: The provisioning status of the pool. + type: str + session_persistence: + description: A JSON object specifying the session persistence for the + pool. + type: dict + tags: + description: A list of associated tags. + type: list + tls_ciphers: + description: Stores a string of cipher strings in OpenSSL format. + type: str + tls_enabled: + description: Use TLS for connections to backend member servers. + type: bool + tls_versions: + description: A list of TLS protocol versions to be used in by the pool. + type: list + updated_at: + description: Timestamp when the pool was updated. + type: str ''' -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 +EXAMPLES = r''' +- name: Create member in a load-balancer pool + openstack.cloud.lb_member: 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 -''' + protocol_port: 8080 + state: present -import time +- name: Delete member from a load-balancer pool + openstack.cloud.lb_member: + cloud: mycloud + name: test-member + pool: test-pool + state: absent +''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule -class LoadbalancerMemberModule(OpenStackModule): +class LoadBalancerMemberModule(OpenStackModule): argument_spec = dict( - name=dict(required=True), - state=dict(default='present', choices=['absent', 'present']), - pool=dict(required=True), address=dict(), - protocol_port=dict(default=80, type='int'), - subnet_id=dict(), monitor_address=dict(), - monitor_port=dict(type='int') + monitor_port=dict(type='int'), + name=dict(required=True), + pool=dict(required=True), + protocol_port=dict(type='int'), + state=dict(default='present', choices=['absent', 'present']), + subnet_id=dict(), + weight=dict(type='int'), + ) + module_kwargs = dict( + required_if=[ + ('state', 'present', ('address', 'protocol_port',)), + ], + supports_check_mode=True, ) - 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): + state = self.params['state'] + + member, pool = self._find() + + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, member, pool)) + + if state == 'present' and not member: + # Create member + member = self._create(pool) + self.exit_json(changed=True, + member=member.to_dict(computed=False), + pool=pool.to_dict(computed=False)) + + elif state == 'present' and member: + # Update member + update = self._build_update(member, pool) + if update: + member = self._update(member, pool, update) + + self.exit_json(changed=bool(update), + member=member.to_dict(computed=False), + pool=pool.to_dict(computed=False)) + + elif state == 'absent' and member: + # Delete member + self._delete(member, pool) + self.exit_json(changed=True) + + elif state == 'absent' and not member: + # Do nothing + self.exit_json(changed=False) + + def _build_update(self, member, pool): + update = {} + + non_updateable_keys = [k for k in ['address', 'name', 'protocol_port', + 'subnet_id'] + if self.params[k] is not None + and self.params[k] != member[k]] + + if non_updateable_keys: + self.fail_json(msg='Cannot update parameters {0}' + .format(non_updateable_keys)) + + attributes = dict((k, self.params[k]) + for k in ['monitor_address', 'monitor_port', + 'weight'] + if self.params[k] is not None + and self.params[k] != member[k]) + + if attributes: + update['attributes'] = attributes + + return update + + def _create(self, pool): + kwargs = dict((k, self.params[k]) + for k in ['address', 'monitor_address', 'monitor_port', + 'name', 'protocol_port', 'subnet_id', 'weight'] + if self.params[k] is not None) + + member = self.conn.load_balancer.create_member(pool.id, **kwargs) + + if self.params['wait']: + member = self.sdk.resource.wait_for_status( + self.conn.load_balancer, member, + status='active', + failures=['error'], + wait=self.params['timeout'], + attribute='provisioning_status') + + return member + + def _delete(self, member, pool): + self.conn.load_balancer.delete_member(member.id, pool.id) + + if self.params['wait']: + for count in self.sdk.utils.iterate_timeout( + timeout=self.params['timeout'], + message="Timeout waiting for load-balancer member to be absent" + ): + if self.conn.load_balancer.\ + find_member(member.id, pool.id) is None: + break + + def _find(self): name = self.params['name'] - pool = self.params['pool'] + pool_name_or_id = self.params['pool'] - changed = False + pool = self.conn.load_balancer.find_pool(name_or_id=pool_name_or_id, + ignore_missing=False) + member = self.conn.load_balancer.find_member(name, pool.id) - 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) + return member, pool - pool_id = pool_ret.id - member = self.conn.load_balancer.find_member(name, pool_id) + def _update(self, member, pool, update): + attributes = update.get('attributes') + if attributes: + member = self.conn.load_balancer.update_member(member.id, pool.id, + **attributes) + if self.params['wait']: + member = self.sdk.resource.wait_for_status( + self.conn.load_balancer, member, + status='active', + failures=['error'], + wait=self.params['timeout'], + attribute='provisioning_status') - 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 + return member - 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 _will_change(self, state, member, pool): + if state == 'present' and not member: + return True + elif state == 'present' and member: + return bool(self._build_update(member, pool)) + elif state == 'absent' and member: + return True + else: + # state == 'absent' and not member: + return False def main(): - module = LoadbalancerMemberModule() + module = LoadBalancerMemberModule() module() diff --git a/plugins/modules/lb_pool.py b/plugins/modules/lb_pool.py index fa7afbe3..558c20c3 100644 --- a/plugins/modules/lb_pool.py +++ b/plugins/modules/lb_pool.py @@ -4,259 +4,332 @@ # 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 = ''' +DOCUMENTATION = r''' --- module: lb_pool -short_description: Add/Delete a pool in the load balancing service from OpenStack Cloud +short_description: Manage load-balancer pool in a OpenStack cloud. author: OpenStack Ansible SIG description: - - Add or Remove a pool from the OpenStack load-balancer service. + - Add, update or remove load-balancer pool from OpenStack cloud. 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' + description: + description: + - A human-readable description for the load-balancer pool. 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" + lb_algorithm: + description: + - The load balancing algorithm for the pool. + - For example, I(lb_algorithm) could be C(LEAST_CONNECTIONS), + C(ROUND_ROBIN), C(SOURCE_IP) or C(SOURCE_IP_PORT). + default: ROUND_ROBIN + type: str + listener: + description: + - The name or id of the listener that this pool belongs to. + - Either I(listener) or I(loadbalancer) must be specified for pool + creation. + - This attribute cannot be updated. + type: str + loadbalancer: + description: + - The name or id of the load balancer that this pool belongs to. + - Either I(listener) or I(loadbalancer) must be specified for pool + creation. + - This attribute cannot be updated. + type: str + name: + description: + - Name that has to be given to the pool. + - This attribute cannot be updated. + required: true + type: str + protocol: + description: + - The protocol for the pool. + - For example, I(protocol) could be C(HTTP), C(HTTPS), C(PROXY), + C(PROXYV2), C(SCTP), C(TCP) and C(UDP). + - This attribute cannot be updated. + default: HTTP + 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 pool, wait for the pool to be active. -- openstack.cloud.lb_pool: +RETURN = r''' +pool: + description: Dictionary describing the load-balancer pool. + returned: On success when I(state) is C(present). + type: dict + contains: + alpn_protocols: + description: List of ALPN protocols. + type: list + created_at: + description: Timestamp when the pool was created. + type: str + description: + description: The pool description. + type: str + health_monitor_id: + description: Health Monitor ID. + type: str + id: + description: Unique UUID. + type: str + is_admin_state_up: + description: The administrative state of the pool. + type: bool + lb_algorithm: + description: The load balancing algorithm for the pool. + type: str + listener_id: + description: The listener ID the pool belongs to. + type: str + listeners: + description: A list of listener IDs. + type: list + loadbalancer_id: + description: The load balancer ID the pool belongs to. This field is set + when the pool does not belong to any listener in the load + balancer. + type: str + loadbalancers: + description: A list of load balancer IDs. + type: list + members: + description: A list of member IDs. + type: list + name: + description: Name given to the pool. + type: str + operating_status: + description: The operating status of the pool. + type: str + project_id: + description: The ID of the project. + type: str + protocol: + description: The protocol for the pool. + type: str + provisioning_status: + description: The provisioning status of the pool. + type: str + session_persistence: + description: A JSON object specifying the session persistence for the + pool. + type: dict + tags: + description: A list of associated tags. + type: list + tls_ciphers: + description: Stores a string of cipher strings in OpenSSL format. + type: str + tls_enabled: + description: Use TLS for connections to backend member servers. + type: bool + tls_versions: + description: A list of TLS protocol versions to be used in by the pool. + type: list + updated_at: + description: Timestamp when the pool was updated. + type: str +''' + +EXAMPLES = r''' +- name: Create a load-balander pool + 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 + loadbalancer: test-loadbalancer name: test-pool -''' + protocol: HTTP + state: present -import time +- name: Delete a load-balander pool + openstack.cloud.lb_pool: + cloud: mycloud + name: test-pool + state: absent +''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule -class LoadbalancerPoolModule(OpenStackModule): +class LoadBalancerPoolModule(OpenStackModule): argument_spec = dict( - name=dict(required=True), - state=dict(default='present', choices=['absent', 'present']), - loadbalancer=dict(), + description=dict(), + lb_algorithm=dict(default='ROUND_ROBIN'), listener=dict(), - protocol=dict(default='HTTP', - choices=['HTTP', 'HTTPS', 'TCP', 'UDP', 'PROXY']), - lb_algorithm=dict( - default='ROUND_ROBIN', - choices=['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'] - ) + loadbalancer=dict(), + name=dict(required=True), + protocol=dict(default='HTTP'), + state=dict(default='present', choices=['absent', 'present']), ) module_kwargs = dict( - mutually_exclusive=[['loadbalancer', 'listener']] + required_if=[ + ('state', 'present', ('listener', 'loadbalancer'), True), + ], + mutually_exclusive=[ + ('listener', 'loadbalancer') + ], + supports_check_mode=True, ) - 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'] + state = self.params['state'] - changed = False - pool = self.conn.load_balancer.find_pool(name_or_id=self.params['name']) + pool = self._find() - 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 self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, pool)) - 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 + if state == 'present' and not pool: + # Create pool + pool = self._create() + self.exit_json(changed=True, + pool=pool.to_dict(computed=False)) - 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 + elif state == 'present' and pool: + # Update pool + update = self._build_update(pool) + if update: + pool = self._update(pool, update) - 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 + self.exit_json(changed=bool(update), + pool=pool.to_dict(computed=False)) - if not self.params['wait']: - self.exit_json( - changed=changed, pool=pool.to_dict(), id=pool.id) + elif state == 'absent' and pool: + # Delete pool + self._delete(pool) + self.exit_json(changed=True) - if self.params['wait']: - pool = self._wait_for_pool_status( - pool.id, "ACTIVE", ["ERROR"]) + elif state == 'absent' and not pool: + # Do nothing + self.exit_json(changed=False) - self.exit_json( - changed=changed, pool=pool.to_dict(), id=pool.id) + def _build_update(self, pool): + update = {} - elif self.params['state'] == 'absent': - if pool: - self.conn.load_balancer.delete_pool(pool) - changed = True + non_updateable_keys = [k for k in ['protocol'] + if self.params[k] is not None + and self.params[k] != pool[k]] - self.exit_json(changed=changed) + listener_name_or_id = self.params['listener'] + if listener_name_or_id: + listener = self.conn.load_balancer.find_listener( + listener_name_or_id, ignore_missing=False) + # Field listener_id is not returned from self.conn.load_balancer.\ + # find_listener() so use listeners instead. + if pool['listeners'] != [dict(id=listener.id)]: + non_updateable_keys.append('listener_id') + + loadbalancer_name_or_id = self.params['loadbalancer'] + if loadbalancer_name_or_id: + loadbalancer = self.conn.load_balancer.find_load_balancer( + loadbalancer_name_or_id, ignore_missing=False) + # Field load_balancer_id is not returned from self.conn.\ + # load_balancer.find_load_balancer() so use load_balancers instead. + if listener['load_balancers'] != [dict(id=loadbalancer.id)]: + non_updateable_keys.append('loadbalancer_id') + + if non_updateable_keys: + self.fail_json(msg='Cannot update parameters {0}' + .format(non_updateable_keys)) + + attributes = dict((k, self.params[k]) + for k in ['description', 'lb_algorithm'] + if self.params[k] is not None + and self.params[k] != pool[k]) + + if attributes: + update['attributes'] = attributes + + return update + + def _create(self): + kwargs = dict((k, self.params[k]) + for k in ['description', 'name', 'protocol', + 'lb_algorithm'] + if self.params[k] is not None) + + listener_name_or_id = self.params['listener'] + if listener_name_or_id: + listener = self.conn.load_balancer.find_listener( + listener_name_or_id, ignore_missing=False) + kwargs['listener_id'] = listener.id + + loadbalancer_name_or_id = self.params['loadbalancer'] + if loadbalancer_name_or_id: + loadbalancer = self.conn.load_balancer.find_load_balancer( + loadbalancer_name_or_id, ignore_missing=False) + kwargs['loadbalancer_id'] = loadbalancer.id + + pool = self.conn.load_balancer.create_pool(**kwargs) + + if self.params['wait']: + pool = self.sdk.resource.wait_for_status( + self.conn.load_balancer, pool, + status='active', + failures=['error'], + wait=self.params['timeout'], + attribute='provisioning_status') + + return pool + + def _delete(self, pool): + self.conn.load_balancer.delete_pool(pool.id) + + if self.params['wait']: + for count in self.sdk.utils.iterate_timeout( + timeout=self.params['timeout'], + message="Timeout waiting for load-balancer pool to be absent" + ): + if self.conn.load_balancer.\ + find_pool(pool.id) is None: + break + + def _find(self): + name = self.params['name'] + return self.conn.load_balancer.find_pool(name_or_id=name) + + def _update(self, pool, update): + attributes = update.get('attributes') + if attributes: + pool = self.conn.load_balancer.update_pool(pool.id, **attributes) + + if self.params['wait']: + pool = self.sdk.resource.wait_for_status( + self.conn.load_balancer, pool, + status='active', + failures=['error'], + wait=self.params['timeout'], + attribute='provisioning_status') + + return pool + + def _will_change(self, state, pool): + if state == 'present' and not pool: + return True + elif state == 'present' and pool: + return bool(self._build_update(pool)) + elif state == 'absent' and pool: + return True + else: + # state == 'absent' and not pool: + return False def main(): - module = LoadbalancerPoolModule() + module = LoadBalancerPoolModule() module()