diff --git a/.zuul.yaml b/.zuul.yaml index 5642e2aa..0205bc21 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -47,6 +47,7 @@ devstack_services: designate: true neutron-dns: true + neutron-trunk: true zuul_copy_output: '{{ devstack_log_dir }}/test_output.log': 'logs' extensions_to_txt: diff --git a/ci/roles/trunk/defaults/main.yml b/ci/roles/trunk/defaults/main.yml new file mode 100644 index 00000000..0de73cb1 --- /dev/null +++ b/ci/roles/trunk/defaults/main.yml @@ -0,0 +1,21 @@ +expected_fields: + - created_at + - description + - id + - is_admin_state_up + - name + - port_id + - project_id + - revision_number + - status + - sub_ports + - tags + - tenant_id + - updated_at +trunk_name: ansible_trunk +parent_network_name: ansible_parent_port_network +parent_subnet_name: ansible_parent_port_subnet +parent_port_name: ansible_parent_port +subport_network_name: ansible_subport_network +subport_subnet_name: ansible_subport_subnet +subport_name: ansible_subport diff --git a/ci/roles/trunk/tasks/main.yml b/ci/roles/trunk/tasks/main.yml new file mode 100644 index 00000000..c48a1734 --- /dev/null +++ b/ci/roles/trunk/tasks/main.yml @@ -0,0 +1,131 @@ +--- +- name: Create parent network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: present + name: "{{ parent_network_name }}" + external: true + register: parent_network + +- name: Create parent subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: present + name: "{{ parent_subnet_name }}" + network_name: "{{ parent_network_name }}" + cidr: 10.5.5.0/24 + register: parent_subnet + +- name: Create parent port + openstack.cloud.port: + cloud: "{{ cloud }}" + state: present + name: "{{ parent_port_name }}" + network: "{{ parent_network_name }}" + fixed_ips: + - ip_address: 10.5.5.69 + register: parent_port + +- name: Create subport network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: present + name: "{{ subport_network_name }}" + external: true + register: subport_network + +- name: Create subport subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: present + name: "{{ subport_subnet_name }}" + network_name: "{{ subport_network_name }}" + cidr: 10.5.6.0/24 + register: subport_subnet + +- name: Create subport + openstack.cloud.port: + cloud: "{{ cloud }}" + state: present + name: "{{ subport_name }}" + network: "{{ subport_network_name }}" + fixed_ips: + - ip_address: 10.5.6.55 + register: subport + +- name: Create trunk + openstack.cloud.trunk: + cloud: "{{ cloud }}" + state: present + name: "{{ trunk_name }}" + port: "{{ parent_port_name }}" + register: trunk + +- debug: var=trunk + +- name: assert return values of trunk module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(trunk.trunk.keys())|length == 0 + +- name: Add subport to trunk + openstack.cloud.trunk: + cloud: "{{ cloud }}" + state: present + name: "{{ trunk_name }}" + port: "{{ parent_port_name }}" + sub_ports: + - port: "{{ subport_name }}" + segmentation_type: vlan + segmentation_id: 123 + +- name: Update subport from trunk + openstack.cloud.trunk: + cloud: "{{ cloud }}" + state: present + name: "{{ trunk_name }}" + port: "{{ parent_port_name }}" + sub_ports: [] + +- name: Delete trunk + openstack.cloud.trunk: + cloud: "{{ cloud }}" + state: absent + name: "{{ trunk_name }}" + +- name: Delete subport + openstack.cloud.port: + cloud: "{{ cloud }}" + state: absent + name: "{{ subport_name }}" + +- name: Delete subport subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: "{{ subport_subnet_name }}" + +- name: Delete subport network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: "{{ subport_network_name }}" + +- name: Delete parent port + openstack.cloud.port: + cloud: "{{ cloud }}" + state: absent + name: "{{ parent_port_name }}" + +- name: Delete parent subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: "{{ parent_subnet_name }}" + +- name: Delete parent network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: "{{ parent_network_name }}" diff --git a/ci/run-collection.yml b/ci/run-collection.yml index e0cfa9a3..1d82b178 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -36,6 +36,7 @@ - { role: object, tags: object } - { role: object_container, tags: object_container } - { role: port, tags: port } + - { role: trunk, tags: trunk } - { role: project, tags: project } - { role: quota, tags: quota } - { role: recordset, tags: recordset } diff --git a/plugins/modules/trunk.py b/plugins/modules/trunk.py new file mode 100644 index 00000000..4d918e24 --- /dev/null +++ b/plugins/modules/trunk.py @@ -0,0 +1,306 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2024 Binero AB +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: trunk +short_description: Add or delete trunks from an OpenStack cloud. +author: OpenStack Ansible SIG +description: + - Add or delete trunk from an OpenStack cloud. +options: + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + name: + description: + - Name that has to be given to the trunk. + - This port attribute cannot be updated. + type: str + required: true + port: + description: + - The name or ID of the port for the trunk. + type: str + required: false + sub_ports: + description: + - The sub ports on the trunk. + type: list + required: false + elements: dict + suboptions: + port: + description: The ID or name of the port. + type: str + segmentation_type: + description: The segmentation type to use. + type: str + segmentation_id: + description: The segmentation ID to use. + type: int +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +# Create a trunk +- openstack.cloud.trunk: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: trunk1 + port: port1 + +# Create a trunk with a subport +- openstack.cloud.trunk: + state: present + cloud: my-cloud + name: trunk1 + port: port1 + sub_ports: + - name: subport1 + segmentation_type: vlan + segmentation_id: 123 + +# Remove a trunk +- openstack.cloud.trunk: + state: absent + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: trunk1 +''' + +RETURN = ''' +trunk: + description: Dictionary describing the trunk. + type: dict + returned: On success when I(state) is C(present). + contains: + created_at: + description: Timestamp when the trunk was created. + returned: success + type: str + sample: "2022-02-03T13:28:25Z" + description: + description: The trunk description. + returned: success + type: str + id: + description: The trunk ID. + returned: success + type: str + sample: "3ec25c97-7052-4ab8-a8ba-92faf84148de" + is_admin_state_up: + description: | + The administrative state of the trunk, which is up C(True) or + down C(False). + returned: success + type: bool + sample: true + name: + description: The trunk name. + returned: success + type: str + sample: "trunk_name" + port_id: + description: The ID of the port for the trunk + returned: success + type: str + sample: "5ec25c97-7052-4ab8-a8ba-92faf84148df" + project_id: + description: The ID of the project who owns the trunk. + returned: success + type: str + sample: "aa1ede4f-3952-4131-aab6-3b8902268c7d" + revision_number: + description: The revision number of the resource. + returned: success + type: int + sample: 0 + status: + description: The trunk status. Value is C(ACTIVE) or C(DOWN). + returned: success + type: str + sample: "ACTIVE" + sub_ports: + description: List of sub ports on the trunk. + returned: success + type: list + sample: [] + tags: + description: The list of tags on the resource. + returned: success + type: list + sample: [] + tenant_id: + description: Same as I(project_id). Deprecated. + returned: success + type: str + sample: "51fce036d7984ba6af4f6c849f65ef00" + updated_at: + description: Timestamp when the trunk was last updated. + returned: success + type: str + sample: "2022-02-03T13:28:25Z" +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class TrunkModule(OpenStackModule): + argument_spec = dict( + state=dict(default='present', choices=['absent', 'present']), + name=dict(required=True), + port=dict(), + sub_ports=dict(type='list', elements='dict'), + ) + + module_kwargs = dict( + required_if=[ + ('state', 'present', ('port',)), + ], + supports_check_mode=True + ) + + def run(self): + port_name_or_id = self.params['port'] + name_or_id = self.params['name'] + state = self.params['state'] + + port = None + if port_name_or_id: + port = self.conn.network.find_port( + port_name_or_id, ignore_missing=False) + + trunk = self.conn.network.find_trunk(name_or_id) + + sub_ports = [] + psp = self.params['sub_ports'] or [] + for sp in psp: + subport = self.conn.network.find_port( + sp['port'], ignore_missing=False) + sub_ports.append(subport) + + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, trunk, sub_ports)) + + if state == 'present' and not trunk: + # create trunk + trunk = self._create(name_or_id, port) + self.exit_json(changed=True, + trunk=trunk.to_dict(computed=False)) + elif state == 'present' and trunk: + # update trunk + update = self._build_update(trunk, sub_ports) + if update: + trunk = self._update(trunk, update) + + self.exit_json(changed=bool(update), + trunk=trunk.to_dict(computed=False)) + elif state == 'absent' and trunk: + # delete trunk + self._delete(trunk) + self.exit_json(changed=True) + elif state == 'absent' and not trunk: + # do nothing + self.exit_json(changed=False) + + def _build_update(self, trunk, sub_ports): + add_sub_ports = [] + del_sub_ports = [] + + for sp in sub_ports: + found = False + for tsp in trunk['sub_ports']: + if tsp['port_id'] == sp['id']: + found = True + break + if found is False: + psp = self.params['sub_ports'] or [] + for k in psp: + if sp['name'] == k['port']: + spobj = { + 'port_id': sp['id'], + 'segmentation_type': k['segmentation_type'], + 'segmentation_id': k['segmentation_id'], + } + add_sub_ports.append(spobj) + break + + for tsp in trunk['sub_ports']: + found = False + for sp in sub_ports: + if sp['id'] == tsp['port_id']: + found = True + break + if found is False: + del_sub_ports.append({'port_id': tsp['port_id']}) + + update = {} + + if len(add_sub_ports) > 0: + update['add_sub_ports'] = add_sub_ports + + if len(del_sub_ports) > 0: + update['del_sub_ports'] = del_sub_ports + + return update + + def _create(self, name, port): + args = {} + args['name'] = name + args['port_id'] = port.id + + return self.conn.network.create_trunk(**args) + + def _delete(self, trunk): + sub_ports = [] + for sp in trunk['sub_ports']: + sub_ports.append({'port_id': sp['port_id']}) + + self.conn.network.delete_trunk_subports(trunk.id, sub_ports) + self.conn.network.delete_trunk(trunk.id) + + def _update(self, trunk, update): + if update.get('add_sub_ports', None): + self.conn.network.add_trunk_subports( + trunk, update['add_sub_ports']) + + if update.get('del_sub_ports', None): + self.conn.network.delete_trunk_subports( + trunk, update['del_sub_ports']) + + return self.conn.network.find_trunk(trunk.id) + + def _will_change(self, state, trunk, sub_ports): + if state == 'present' and not trunk: + return True + elif state == 'present' and trunk: + return bool(self._build_update(trunk, sub_ports)) + elif state == 'absent' and trunk: + return True + else: + return False + + +def main(): + module = TrunkModule() + module() + + +if __name__ == '__main__': + main()