16 Commits

Author SHA1 Message Date
Zuul
ccec4d07b3 Merge "Add support for managing network segments" 2026-03-23 15:16:45 +00:00
Zuul
04b70b99da Merge "Support updating extra_specs in project module" 2026-03-19 21:41:00 +00:00
Zuul
ed4c4036af Merge "feat(images): Adds support for image ID reservation and queued images" 2026-03-19 07:37:34 +00:00
Zuul
e2ebc1c8d0 Merge "tox: Drop basepython" 2026-03-19 07:37:33 +00:00
Zuul
99eb60f7dc Merge "Add baremetal_port_group module" 2026-03-19 06:56:08 +00:00
Nicholas Kuechler
e90fd7a915 feat(images): Adds support for image ID reservation and queued images
Change-Id: I3aa319deb711eaa1ccad4f48eedb079afd801872
Signed-off-by: Nicholas Kuechler <nicholas.kuechler@rackspace.com>
2026-03-12 14:52:48 -05:00
Artem Goncharov
1bc4f648fb Rotate the galaxy secret
Change-Id: I4d25f3b7831c80c615e6dd967a5e5065452cdce8
Signed-off-by: Artem Goncharov <artem.goncharov@gmail.com>
2026-02-17 17:15:54 +01:00
Grzegorz Koper
1a654a9c38 Add baremetal_port_group module
Add support for managing Ironic baremetal port groups.

Include CI role coverage and unit tests for create, update, delete, and check mode behavior.

Add a reno fragment describing the new module.

Tests-Run: python -m pytest tests/unit/modules/cloud/openstack/test_baremetal_port_group.py
Change-Id: I98564fcb5b81a1dd7be1fbf5ffca364483296655
2026-02-10 14:45:35 +01:00
Ivan Anfimov
67b7ec5e58 tox: Drop basepython
Python 2 reached its EOL long time ago and we no longer expect any
user may attempt to run tox in Python 2.

Removing the option allows us to remove ignore_basepython_conflict.

Change-Id: I54c0581e77c924f9d98975964e4470e89fa3d954
Co-authored-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
Signed-off-by: Ivan Anfimov <lazekteam@gmail.com>
2026-01-24 12:53:27 +00:00
naosuke
70128d6230 Support updating extra_specs in project module
It supports for adding extra_specs in updating project.

Change-Id: I98a73ed9367d52df82213b3b7c484ceac10acf3d
Signed-off-by: Naoki Hanakawa <naoki.hanakawa@lycorp.co.jp>
2025-12-17 10:13:52 +09:00
Riccardo Pittau
1dc367b566 Use new bifrost CI jobs names
Ironic do not support tinyipa anymore, all jobs run with DIB
based ipa ramdisks, so we updated their config and names.

Change-Id: Id7b260a0965d941d3a34eb181a068a9a5e7189ef
Signed-off-by: Riccardo Pittau <elfosardo@gmail.com>
2025-12-16 09:28:32 +01:00
Maksim Malchuk
a178493281 Drop duplicate lines in the Changelog
Change-Id: I0475545d8114d8731a2131eed372c7557b579e3f
Signed-off-by: Maksim Malchuk <maksim.malchuk@gmail.com>
2025-11-17 16:19:28 +03:00
Austin Jamias
b2aac80b41 Fix tox.ini linters_2_18 config
Correct a typo where linters_2_18's requirements were not being
referenced correctly.

Change-Id: I1c7a7555c9effd4f0ceb417be709059aa10d0a5e
Signed-off-by: Austin Jamias <ajamias@redhat.com>
2025-11-10 19:45:36 -05:00
Zuul
ae6e48be00 Merge "Fix Ansible errors" 2025-10-31 18:03:37 +00:00
Michal Nasiadka
e06a61f97a Fix Ansible errors
Change-Id: I826ec0b01f8cfdf78235d146c90d790c8e891cc9
Signed-off-by: Michal Nasiadka <mnasiadka@gmail.com>
2025-10-27 07:40:26 +00:00
Andrew Bonney
eef8368e6f Add support for managing network segments
Adds a module to manage Neutron network segments where the
segmentation plugin is enabled.

Segments are relatively simple and do not support modification
beyond the name/description, so most attributes are used for
initial segment creation, or filtering results in order to
perform updates.

Depends-On: https://review.opendev.org/c/openstack/ansible-collections-openstack/+/955752
Change-Id: I4647fd96aaa15460d82765365f98a18ddf2693db
2025-07-24 09:04:30 +00:00
21 changed files with 1133 additions and 21 deletions

View File

@@ -48,6 +48,7 @@
designate: true
neutron-dns: true
neutron-trunk: true
neutron-segments: true
zuul_copy_output:
'{{ devstack_log_dir }}/test_output.log': 'logs'
extensions_to_txt:
@@ -275,7 +276,7 @@
# Cross-checks with other projects
- job:
name: bifrost-collections-src
parent: bifrost-integration-tinyipa-ubuntu-jammy
parent: bifrost-integration-on-ubuntu-jammy
required-projects:
- openstack/ansible-collections-openstack
- # always use master branch when collecting parent job variants, refer to git blame for rationale.
@@ -286,7 +287,7 @@
override-checkout: master
- job:
name: bifrost-keystone-collections-src
parent: bifrost-integration-tinyipa-keystone-ubuntu-jammy
parent: bifrost-integration-keystone-on-ubuntu-jammy
required-projects:
- openstack/ansible-collections-openstack
- # always use master branch when collecting parent job variants, refer to git blame for rationale.
@@ -308,16 +309,16 @@
data:
url: https://galaxy.ansible.com
token: !encrypted/pkcs1-oaep
- QJ3c5LfmM4YmqwwLKv4wK5lroWDLGeMyPkmHXhvf0ry3vGjKZvZxVpbIhFXJHXevHov/r
nvlqwmG8D5msynQKZDFg2ZwSMIQWRKfSbsSLe7A6NWI2wC+QtZSPiRiBcBcHY1QbNNW21
84cssYa1oHOA0WXpomBz1qXuPV48aKLjMnWysgFhNSx3Oog+ZOSCczyyVVuXP1lIWIO26
AtRTrEcr37K3JY9usE2PCbZKFOq/+IDPz9fbS7PtBOv7iXOHOf3AfBiJiaJe3q/ecoaaq
ejk2WTKWfvq/3rY4pU1976kUcxgcd+jj9ReFyw8edCsc1ecL0qmZFbdHmC03jEcVo4p8I
WJQ0D5wk4/u2Fu9texNuBvb62Yu3Y028Zhm5rz8Zl/ISsdaA3losn5S7C7iAH/yKlGQEI
N/1X4M0tVPaMtsIhZyyz+JMbeNyVR9ZarqbtpzRtVhjxL7KOiAQbEzAmZcBbCJ2Z5iI+P
bTp03f9Y/tZNtkohARvx1TKhv8CvsmyGkMm+r5Y8aWz3SNy8LL6bSwtGun/ifbnadHmw/
TD5/UUXHHjBGkeAu9HTtwUZ5Qdkfg92PnPgruAAuOkF1Y4RyRS9qvwhtqyHO8TwU0INRY
5MHEzeOQWemoQb/qdENp+J/Q9oMEbpFYv9TkrWkxVoKop6Str8e3FF5sxmN/SE=
- K93hOZo1B5z248H04COB1N2HCkGbFPo2EUr+0W7qFzsrdvmbsAI86Hl9bUCfEENGrwvfV
0j9CE5iO0tyqal3r6ucMhGT44MgQWL3MBeRvK89yAJpSNMU7R7rEY/zbjZMoC9YElcHEv
GEDZSA/0gQHCHpZVDlx4JMGwrnd+Nz9ha3c12BYeZS8rS/dQl7EmZ867OsozmNdG9UkkC
0vP/dkenUQNvoZOSWgZztRBlbAyI1nc5iEEw9vvpLh19HcY9+S2iAZkgSq4jOOO4wn7gE
XAZPr0HRdwS2m4Hw0Pusrg7SdC3+2O0N/fvFGnvvKXHcSgQk3rPLn6HfKzOJoPWc4WlDX
MA79jYloNBXjOaeXOoiwYzzshWK53F6Ci+3leq1cYuFyHSi2ds2mYXat7YndZSsmsk5um
hj0+Ddy9Om1uYy3nhHyZLULE7UDUmduA9EPkvdyWlcW0yZL2kXcrDTHlSp4PaJg9iKVys
0aOOo9CNMwhyXAOGiFCYF/m7Efbnp50zUQhHN9+7LeVzXZuiH98C8kNvWfE0qrkrrgQ1n
78UMqGcGpdw4ZSlWrDTbrbd4v0bRnsJ+IAWISnT5OXaeJgGZwXRuBHtTXqbjoosBeX/8w
YKb0lx7E5ZtSw7+Y6LNDGihGTmVg1nkZUWo85CxyF/RiWHuNvpkzzqXmdGS1bg=
- project:
check:

View File

@@ -27,7 +27,6 @@ Minor Changes
- Allow role_assignment module to work cross domain
- Don't compare current state for `reboot_*` actions
- Fix disable_gateway_ip for subnet
- Fix disable_gateway_ip for subnet
- Fix example in the dns_zone_info module doc
- Fix router module external IPs when only subnet specified
- Fix the bug reporting url

View File

@@ -628,7 +628,6 @@ releases:
- Allow role_assignment module to work cross domain
- Don't compare current state for `reboot_*` actions
- Fix disable_gateway_ip for subnet
- Fix disable_gateway_ip for subnet
- Fix example in the dns_zone_info module doc
- Fix router module external IPs when only subnet specified
- Fix the bug reporting url

View File

@@ -0,0 +1,5 @@
---
minor_changes:
- Added the new ``openstack.cloud.baremetal_port_group`` module to manage
Bare Metal port groups (create, update, and delete), including CI role
coverage and unit tests.

View File

@@ -0,0 +1,12 @@
expected_fields:
- address
- created_at
- extra
- id
- links
- mode
- name
- node_id
- properties
- standalone_ports_supported
- updated_at

View File

@@ -0,0 +1,100 @@
---
# TODO: Actually run this role in CI. Atm we do not have DevStack's ironic plugin enabled.
- name: Create baremetal node
openstack.cloud.baremetal_node:
cloud: "{{ cloud }}"
driver_info:
ipmi_address: "1.2.3.4"
ipmi_username: "admin"
ipmi_password: "secret"
name: ansible_baremetal_node
nics:
- mac: "aa:bb:cc:aa:bb:cc"
state: present
register: node
- name: Create baremetal port group
openstack.cloud.baremetal_port_group:
cloud: "{{ cloud }}"
state: present
name: ansible_baremetal_port_group
node: ansible_baremetal_node
address: fa:16:3e:aa:aa:ab
mode: active-backup
standalone_ports_supported: true
extra:
test: created
properties:
miimon: '100'
register: port_group
- debug: var=port_group
- name: Assert return values of baremetal_port_group module
assert:
that:
# allow new fields to be introduced but prevent fields from being removed
- expected_fields|difference(port_group.port_group.keys())|length == 0
- port_group.port_group.name == "ansible_baremetal_port_group"
- port_group.port_group.node_id == node.node.id
- name: Update baremetal port group
openstack.cloud.baremetal_port_group:
cloud: "{{ cloud }}"
state: present
id: "{{ port_group.port_group.id }}"
mode: 802.3ad
standalone_ports_supported: false
extra:
test: updated
register: updated_port_group
- name: Assert return values of updated baremetal port group
assert:
that:
- updated_port_group is changed
- updated_port_group.port_group.id == port_group.port_group.id
- updated_port_group.port_group.mode == "802.3ad"
- not updated_port_group.port_group.standalone_ports_supported
- updated_port_group.port_group.extra.test == "updated"
- name: Update baremetal port group again
openstack.cloud.baremetal_port_group:
cloud: "{{ cloud }}"
state: present
id: "{{ port_group.port_group.id }}"
mode: 802.3ad
standalone_ports_supported: false
extra:
test: updated
register: updated_port_group
- name: Assert idempotency for baremetal port group module
assert:
that:
- updated_port_group is not changed
- updated_port_group.port_group.id == port_group.port_group.id
- name: Delete baremetal port group
openstack.cloud.baremetal_port_group:
cloud: "{{ cloud }}"
state: absent
id: "{{ port_group.port_group.id }}"
- name: Delete baremetal port group again
openstack.cloud.baremetal_port_group:
cloud: "{{ cloud }}"
state: absent
id: "{{ port_group.port_group.id }}"
register: deleted_port_group
- name: Assert idempotency for deleted baremetal port group
assert:
that:
- deleted_port_group is not changed
- name: Delete baremetal node
openstack.cloud.baremetal_node:
cloud: "{{ cloud }}"
name: ansible_baremetal_node
state: absent

View File

@@ -0,0 +1,17 @@
---
expected_fields:
- description
- id
- name
- network_id
- network_type
- physical_network
- segmentation_id
network_name: segment_network
segment_name: example_segment
network_type: vlan
segmentation_id: 999
physical_network: public
initial_description: "example segment description"
updated_description: "updated segment description"

View File

@@ -0,0 +1,72 @@
---
- name: Create network {{ network_name }}
openstack.cloud.network:
cloud: "{{ cloud }}"
name: "{{ network_name }}"
state: present
- name: Create segment {{ segment_name }}
openstack.cloud.network_segment:
cloud: "{{ cloud }}"
name: "{{ segment_name }}"
description: "{{ initial_description }}"
network: "{{ network_name }}"
network_type: "{{ network_type }}"
segmentation_id: "{{ segmentation_id }}"
physical_network: "{{ physical_network }}"
state: present
register: segment
- name: Assert changed
assert:
that: segment is changed
- name: Assert segment fields
assert:
that: item in segment.network_segment
loop: "{{ expected_fields }}"
- name: Update segment {{ segment_name }} by name - no changes
openstack.cloud.network_segment:
cloud: "{{ cloud }}"
name: "{{ segment_name }}"
description: "{{ initial_description }}"
state: present
register: segment
- name: Assert not changed
assert:
that: segment is not changed
- name: Update segment {{ segment_name }} by all fields - changes
openstack.cloud.network_segment:
cloud: "{{ cloud }}"
name: "{{ segment_name }}"
description: "{{ updated_description }}"
network: "{{ network_name }}"
network_type: "{{ network_type }}"
segmentation_id: "{{ segmentation_id }}"
physical_network: "{{ physical_network }}"
state: present
register: segment
- name: Assert changed
assert:
that: segment is changed
- name: Delete segment {{ segment_name }}
openstack.cloud.network_segment:
cloud: "{{ cloud }}"
name: "{{ segment_name }}"
state: absent
register: segment
- name: Assert changed
assert:
that: segment is changed
- name: Delete network {{ network_name }}
openstack.cloud.network:
cloud: "{{ cloud }}"
name: "{{ network_name }}"
state: absent

View File

@@ -174,6 +174,38 @@
that:
- project.project.is_enabled == True
- name: Update project to add new extra_specs
openstack.cloud.project:
cloud: "{{ cloud }}"
state: present
name: ansible_project
extra_specs:
is_enabled: True
another_tag: True
register: project
- name: Assert return values of project module
assert:
that:
- project.project.is_enabled == True
- project.project.another_tag == True
- name: Update project to change existing extra_specs
openstack.cloud.project:
cloud: "{{ cloud }}"
state: present
name: ansible_project
extra_specs:
is_enabled: True
another_tag: False
register: project
- name: Assert return values of project module
assert:
that:
- project.project.is_enabled == True
- project.project.another_tag == False
- name: Delete project
openstack.cloud.project:
cloud: "{{ cloud }}"

View File

@@ -25,3 +25,4 @@ expected_fields:
- updated_at
- use_default_subnet_pool
subnet_name: shade_subnet
segment_name: example_segment

View File

@@ -17,10 +17,20 @@
name: "{{ network_name }}"
state: present
- name: Create network segment {{ segment_name }}
openstack.cloud.network_segment:
cloud: "{{ cloud }}"
name: "{{ segment_name }}"
network: "{{ network_name }}"
network_type: "vxlan"
segmentation_id: 1000
state: present
- name: Create subnet {{ subnet_name }} on network {{ network_name }}
openstack.cloud.subnet:
cloud: "{{ cloud }}"
network_name: "{{ network_name }}"
network_segment: "{{ segment_name }}"
name: "{{ subnet_name }}"
state: present
enable_dhcp: "{{ enable_subnet_dhcp }}"
@@ -177,6 +187,13 @@
state: absent
register: subnet
- name: Delete network segment {{ segment_name }}
openstack.cloud.network_segment:
cloud: "{{ cloud }}"
name: "{{ segment_name }}"
network: "{{ network_name }}"
state: absent
- name: Delete network {{ network_name }}
openstack.cloud.network:
cloud: "{{ cloud }}"

View File

@@ -40,7 +40,7 @@
- assert:
that:
- new_vol.volume.name == "{{ managed_volume }}"
- new_vol.volume.name == managed_volume
- name: Manage volume again
openstack.cloud.volume_manage:

View File

@@ -32,6 +32,7 @@
- { role: loadbalancer, tags: loadbalancer }
- { role: logging, tags: logging }
- { role: network, tags: network }
- { role: network_segment, tags: network_segment }
- { role: neutron_rbac_policy, tags: neutron_rbac_policy }
- { role: object, tags: object }
- { role: object_container, tags: object_container }

View File

@@ -10,6 +10,7 @@ action_groups:
- baremetal_node_action
- baremetal_node_info
- baremetal_port
- baremetal_port_group
- baremetal_port_info
- catalog_service
- catalog_service_info
@@ -51,6 +52,7 @@ action_groups:
- lb_pool
- loadbalancer
- network
- network_segment
- networks_info
- neutron_rbac_policies_info
- neutron_rbac_policy

View File

@@ -0,0 +1,257 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2026 OpenStack Ansible SIG
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = r'''
module: baremetal_port_group
short_description: Create/Delete Bare Metal port group resources from OpenStack
author: OpenStack Ansible SIG
description:
- Create, update and remove Bare Metal port groups from OpenStack.
options:
id:
description:
- ID of the port group.
- Will be auto-generated if not specified.
type: str
aliases: ['uuid']
name:
description:
- Name of the port group.
type: str
node:
description:
- ID or Name of the node this resource belongs to.
- Required when creating a new port group.
type: str
address:
description:
- Physical hardware address of this port group, typically the hardware
MAC address.
type: str
extra:
description:
- A set of one or more arbitrary metadata key and value pairs.
type: dict
standalone_ports_supported:
description:
- Whether the port group supports ports that are not members of this
port group.
type: bool
mode:
description:
- The port group mode.
type: str
properties:
description:
- Key/value properties for the port group.
type: dict
state:
description:
- Indicates desired state of the resource.
choices: ['present', 'absent']
default: present
type: str
extends_documentation_fragment:
- openstack.cloud.openstack
'''
EXAMPLES = r'''
- name: Create Bare Metal port group
openstack.cloud.baremetal_port_group:
cloud: devstack
state: present
name: bond0
node: bm-0
address: fa:16:3e:aa:aa:aa
mode: '802.3ad'
standalone_ports_supported: true
register: result
- name: Update Bare Metal port group
openstack.cloud.baremetal_port_group:
cloud: devstack
state: present
id: 1a85ebca-22bf-42eb-ad9e-f640789b8098
mode: 'active-backup'
properties:
miimon: '100'
register: result
- name: Delete Bare Metal port group
openstack.cloud.baremetal_port_group:
cloud: devstack
state: absent
id: 1a85ebca-22bf-42eb-ad9e-f640789b8098
register: result
'''
RETURN = r'''
port_group:
description: A port group dictionary, subset of the dictionary keys listed
below may be returned, depending on your cloud provider.
returned: success
type: dict
contains:
address:
description: Physical hardware address of the port group.
returned: success
type: str
created_at:
description: Bare Metal port group 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 Bare Metal port group resource.
returned: success
type: str
links:
description: A list of relative links, including the self and
bookmark links.
returned: success
type: list
mode:
description: The port group mode.
returned: success
type: str
name:
description: Bare Metal port group name.
returned: success
type: str
node_id:
description: UUID of the Bare Metal node this resource belongs to.
returned: success
type: str
properties:
description: Key/value properties for this port group.
returned: success
type: dict
standalone_ports_supported:
description: Whether standalone ports are supported.
returned: success
type: bool
updated_at:
description: Bare Metal port group updated at timestamp.
returned: success
type: str
'''
from ansible_collections.openstack.cloud.plugins.module_utils.openstack import (
OpenStackModule
)
class BaremetalPortGroupModule(OpenStackModule):
argument_spec = dict(
id=dict(aliases=['uuid']),
name=dict(),
node=dict(),
address=dict(),
extra=dict(type='dict'),
standalone_ports_supported=dict(type='bool'),
mode=dict(),
properties=dict(type='dict'),
state=dict(default='present', choices=['present', 'absent']),
)
module_kwargs = dict(
required_one_of=[
('id', 'name'),
],
supports_check_mode=True,
)
def _find_port_group(self):
id_or_name = self.params['id'] if self.params['id'] else self.params['name']
if not id_or_name:
return None
try:
return self.conn.baremetal.find_port_group(id_or_name)
except self.sdk.exceptions.ResourceNotFound:
return None
def _build_create_attrs(self):
attrs = {}
for key in ['id', 'name', 'address', 'extra',
'standalone_ports_supported', 'mode', 'properties']:
if self.params[key] is not None:
attrs[key] = self.params[key]
node_name_or_id = self.params['node']
if not node_name_or_id:
self.fail_json(msg="Parameter 'node' is required when creating a new port group")
node = self.conn.baremetal.find_node(node_name_or_id, ignore_missing=False)
attrs['node_id'] = node['id']
return attrs
def _build_update_attrs(self, port_group):
attrs = {}
for key in ['name', 'address', 'extra',
'standalone_ports_supported', 'mode', 'properties']:
if self.params[key] is not None and self.params[key] != port_group.get(key):
attrs[key] = self.params[key]
return attrs
def _will_change(self, port_group, state):
if state == 'absent':
return bool(port_group)
if not port_group:
return True
return bool(self._build_update_attrs(port_group))
def run(self):
state = self.params['state']
port_group = self._find_port_group()
if self.ansible.check_mode:
if state == 'present' and not port_group:
self._build_create_attrs()
self.exit_json(changed=self._will_change(port_group, state))
if state == 'present':
if not port_group:
port_group = self.conn.baremetal.create_port_group(
**self._build_create_attrs())
self.exit_json(
changed=True,
port_group=port_group.to_dict(computed=False))
update_attrs = self._build_update_attrs(port_group)
changed = bool(update_attrs)
if changed:
port_group = self.conn.baremetal.update_port_group(
port_group['id'], **update_attrs)
self.exit_json(
changed=changed,
port_group=port_group.to_dict(computed=False))
if not port_group:
self.exit_json(changed=False)
self.conn.baremetal.delete_port_group(port_group['id'])
self.exit_json(changed=True)
def main():
module = BaremetalPortGroupModule()
module()
if __name__ == "__main__":
main()

View File

@@ -529,6 +529,22 @@ class ImageModule(OpenStackModule):
if image['status'] == 'deactivated':
self.conn.image.reactivate_image(image)
changed = True
elif image['status'] == 'queued':
if (
self.params['filename']
and hasattr(self.conn.image, 'stage_image')):
self.conn.image.stage_image(
image, filename=self.params['filename'])
changed = True
elif self.params['filename']:
with open(self.params['filename'], 'rb') as image_data:
self.conn.image.upload_image(
container_format=self.params['container_format'],
disk_format=self.params['disk_format'],
data=image_data,
id=image.id,
name=image.name)
changed = True
update_payload = self._build_update(image)

View File

@@ -0,0 +1,183 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2025 British Broadcasting Corporation
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = '''
---
module: network_segment
short_description: Creates/removes network segments from OpenStack
author: OpenStack Ansible SIG
description:
- Add, update or remove network segments from OpenStack.
options:
name:
description:
- Name to be assigned to the segment. Although Neutron allows for
non-unique segment names, this module enforces segment name
uniqueness.
required: true
type: str
description:
description:
- Description of the segment
type: str
network:
description:
- Name or id of the network to which the segment should be attached
type: str
network_type:
description:
- The type of physical network that maps to this segment resource.
type: str
physical_network:
description:
- The physical network where this segment object is implemented.
type: str
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
state:
description:
- Indicate desired state of the resource.
choices: ['present', 'absent']
default: present
type: str
extends_documentation_fragment:
- openstack.cloud.openstack
'''
EXAMPLES = '''
# Create a VLAN type network segment named 'segment1'.
- openstack.cloud.network_segment:
cloud: mycloud
name: segment1
network: my_network
network_type: vlan
segmentation_id: 2000
physical_network: my_physnet
state: present
'''
RETURN = '''
id:
description: Id of segment
returned: On success when segment exists.
type: str
network_segment:
description: Dictionary describing the network segment.
returned: On success when network segment exists.
type: dict
contains:
description:
description: Description
type: str
id:
description: Id
type: str
name:
description: Name
type: str
network_id:
description: Network Id
type: str
network_type:
description: Network type
type: str
physical_network:
description: Physical network
type: str
segmentation_id:
description: Segmentation Id
type: int
'''
from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
class NetworkSegmentModule(OpenStackModule):
argument_spec = dict(
name=dict(required=True),
description=dict(),
network=dict(),
network_type=dict(),
physical_network=dict(),
segmentation_id=dict(type='int'),
state=dict(default='present', choices=['absent', 'present'])
)
def run(self):
state = self.params['state']
name = self.params['name']
network_name_or_id = self.params['network']
kwargs = {}
filters = {}
for arg in ('description', 'network_type', 'physical_network', 'segmentation_id'):
if self.params[arg] is not None:
kwargs[arg] = self.params[arg]
for arg in ('network_type', 'physical_network'):
if self.params[arg] is not None:
filters[arg] = self.params[arg]
if network_name_or_id:
network = self.conn.network.find_network(network_name_or_id,
ignore_missing=False,
**filters)
kwargs['network_id'] = network.id
filters['network_id'] = network.id
segment = self.conn.network.find_segment(name, **filters)
if state == 'present':
if not segment:
segment = self.conn.network.create_segment(name=name, **kwargs)
changed = True
else:
changed = False
update_kwargs = {}
# As the name is required and all other attributes cannot be
# changed (and appear in filters above), we only need to handle
# updates to the description here.
for arg in ["description"]:
if (
arg in kwargs
# ensure user wants something specific
and kwargs[arg] is not None
# and this is not what we have right now
and kwargs[arg] != segment[arg]
):
update_kwargs[arg] = kwargs[arg]
if update_kwargs:
segment = self.conn.network.update_segment(
segment.id, **update_kwargs
)
changed = True
segment = segment.to_dict(computed=False)
self.exit(changed=changed, network_segment=segment, id=segment['id'])
elif state == 'absent':
if not segment:
self.exit(changed=False)
else:
self.conn.network.delete_segment(segment['id'])
self.exit(changed=True)
def main():
module = NetworkSegmentModule()
module()
if __name__ == '__main__':
main()

View File

@@ -181,7 +181,7 @@ class IdentityProjectModule(OpenStackModule):
raise ValueError('Duplicate key(s) in extra_specs: {0}'
.format(', '.join(list(duplicate_keys))))
for k, v in extra_specs.items():
if v != project[k]:
if k not in project or v != project[k]:
attributes[k] = v
if attributes:

View File

@@ -115,6 +115,10 @@ options:
- Required when I(state) is 'present'
aliases: ['network_name']
type: str
network_segment:
description:
- Name or id of the network segment to which the subnet should be associated
type: str
project:
description:
- Project name or ID containing the subnet (name admin-only)
@@ -294,6 +298,7 @@ class SubnetModule(OpenStackModule):
argument_spec = dict(
name=dict(required=True),
network=dict(aliases=['network_name']),
network_segment=dict(),
cidr=dict(),
description=dict(),
ip_version=dict(type='int', default=4, choices=[4, 6]),
@@ -369,9 +374,11 @@ class SubnetModule(OpenStackModule):
return [dict(start=pool_start, end=pool_end)]
return None
def _build_params(self, network, project, subnet_pool):
def _build_params(self, network, segment, project, subnet_pool):
params = {attr: self.params[attr] for attr in self.attr_params}
params['network_id'] = network.id
if segment:
params['segment_id'] = segment.id
if project:
params['project_id'] = project.id
if subnet_pool:
@@ -416,6 +423,7 @@ class SubnetModule(OpenStackModule):
def run(self):
state = self.params['state']
network_name_or_id = self.params['network']
network_segment_name_or_id = self.params['network_segment']
project_name_or_id = self.params['project']
subnet_pool_name_or_id = self.params['subnet_pool']
subnet_name = self.params['name']
@@ -444,6 +452,13 @@ class SubnetModule(OpenStackModule):
**filters)
filters['network_id'] = network.id
segment = None
if network_segment_name_or_id:
segment = self.conn.network.find_segment(network_segment_name_or_id,
ignore_missing=False,
**filters)
filters['segment_id'] = segment.id
subnet_pool = None
if subnet_pool_name_or_id:
subnet_pool = self.conn.network.find_subnet_pool(
@@ -460,7 +475,7 @@ class SubnetModule(OpenStackModule):
changed = False
if state == 'present':
params = self._build_params(network, project, subnet_pool)
params = self._build_params(network, segment, project, subnet_pool)
if subnet is None:
subnet = self.conn.network.create_subnet(**params)
changed = True

View File

@@ -0,0 +1,385 @@
import importlib.util
import json
import unittest
from pathlib import Path
from unittest import mock
from unittest.mock import patch
from ansible.module_utils import basic
from ansible.module_utils._text import to_bytes
def _load_module_under_test():
module_path = Path(__file__).resolve().parents[5] / 'plugins/modules/baremetal_port_group.py'
spec = importlib.util.spec_from_file_location('baremetal_port_group', str(module_path))
if spec is None or spec.loader is None:
raise ImportError('Cannot load baremetal_port_group module for tests')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
baremetal_port_group = _load_module_under_test()
def set_module_args(args):
if '_ansible_remote_tmp' not in args:
args['_ansible_remote_tmp'] = '/tmp'
if '_ansible_keep_remote_files' not in args:
args['_ansible_keep_remote_files'] = False
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
class AnsibleExitJson(Exception):
pass
class AnsibleFailJson(Exception):
pass
def exit_json(*args, **kwargs):
if 'changed' not in kwargs:
kwargs['changed'] = False
raise AnsibleExitJson(kwargs)
def fail_json(*args, **kwargs):
kwargs['failed'] = True
raise AnsibleFailJson(kwargs)
class ModuleTestCase(unittest.TestCase):
mock_module = None
mock_sleep = None
def setUp(self):
self.mock_module = patch.multiple(
basic.AnsibleModule,
exit_json=exit_json,
fail_json=fail_json,
)
self.mock_module.start()
self.mock_sleep = patch('time.sleep')
self.mock_sleep.start()
set_module_args({})
self.addCleanup(self.mock_module.stop)
self.addCleanup(self.mock_sleep.stop)
class FakePortGroup(dict[str, object]):
def to_dict(self, computed=False):
return dict(self)
class FakeSDK(object):
class exceptions:
class OpenStackCloudException(Exception):
pass
class ResourceNotFound(Exception):
pass
class TestBaremetalPortGroup(ModuleTestCase):
module = baremetal_port_group
def setUp(self):
super(TestBaremetalPortGroup, self).setUp()
self.module = baremetal_port_group
def _run_module(self, module_args, baremetal):
set_module_args(module_args)
conn = mock.Mock()
conn.baremetal = baremetal
with mock.patch.object(
baremetal_port_group.BaremetalPortGroupModule,
'openstack_cloud_from_module',
return_value=(FakeSDK(), conn),
):
self.module.main()
def _new_baremetal(self):
baremetal = mock.Mock()
baremetal.find_port_group.return_value = None
baremetal.find_node.return_value = {'id': 'node-1'}
return baremetal
def test_create_port_group(self):
baremetal = self._new_baremetal()
baremetal.create_port_group.return_value = FakePortGroup(
id='pg-1',
name='bond0',
node_id='node-1',
address='fa:16:3e:aa:aa:aa',
mode='active-backup',
extra={},
properties={},
standalone_ports_supported=True,
links=[],
created_at='2026-01-01T00:00:00+00:00',
updated_at=None,
)
with self.assertRaises(AnsibleExitJson) as ex:
self._run_module(
{
'id': None,
'name': 'bond0',
'node': 'node-name',
'address': 'fa:16:3e:aa:aa:aa',
'extra': {},
'standalone_ports_supported': True,
'mode': 'active-backup',
'properties': {},
'state': 'present',
},
baremetal,
)
result = ex.exception.args[0]
self.assertTrue(result['changed'])
self.assertEqual('pg-1', result['port_group']['id'])
baremetal.find_node.assert_called_once_with('node-name', ignore_missing=False)
baremetal.create_port_group.assert_called_once_with(
name='bond0',
node_id='node-1',
address='fa:16:3e:aa:aa:aa',
extra={},
standalone_ports_supported=True,
mode='active-backup',
properties={},
)
def test_create_port_group_without_node_fails(self):
baremetal = self._new_baremetal()
with self.assertRaises(AnsibleFailJson) as ex:
self._run_module(
{
'id': None,
'name': 'bond0',
'node': None,
'address': None,
'extra': None,
'standalone_ports_supported': None,
'mode': None,
'properties': None,
'state': 'present',
},
baremetal,
)
self.assertIn("Parameter 'node' is required", ex.exception.args[0]['msg'])
baremetal.create_port_group.assert_not_called()
def test_update_port_group_when_values_changed(self):
baremetal = self._new_baremetal()
baremetal.find_port_group.return_value = FakePortGroup(
id='pg-1',
name='bond0',
node_id='node-1',
mode='active-backup',
address=None,
extra={},
properties={},
standalone_ports_supported=True,
links=[],
created_at='2026-01-01T00:00:00+00:00',
updated_at=None,
)
baremetal.update_port_group.return_value = FakePortGroup(
id='pg-1',
name='bond0',
node_id='node-1',
mode='802.3ad',
address=None,
extra={},
properties={},
standalone_ports_supported=True,
links=[],
created_at='2026-01-01T00:00:00+00:00',
updated_at='2026-01-02T00:00:00+00:00',
)
with self.assertRaises(AnsibleExitJson) as ex:
self._run_module(
{
'id': 'pg-1',
'name': None,
'node': None,
'address': None,
'extra': None,
'standalone_ports_supported': None,
'mode': '802.3ad',
'properties': None,
'state': 'present',
},
baremetal,
)
result = ex.exception.args[0]
self.assertTrue(result['changed'])
self.assertEqual('802.3ad', result['port_group']['mode'])
baremetal.update_port_group.assert_called_once_with('pg-1', mode='802.3ad')
def test_present_noop_when_already_matching(self):
baremetal = self._new_baremetal()
baremetal.find_port_group.return_value = FakePortGroup(
id='pg-1',
name='bond0',
node_id='node-1',
mode='active-backup',
address='fa:16:3e:aa:aa:aa',
extra={'a': 'b'},
properties={'miimon': '100'},
standalone_ports_supported=False,
links=[],
created_at='2026-01-01T00:00:00+00:00',
updated_at=None,
)
with self.assertRaises(AnsibleExitJson) as ex:
self._run_module(
{
'id': 'pg-1',
'name': 'bond0',
'node': None,
'address': 'fa:16:3e:aa:aa:aa',
'extra': {'a': 'b'},
'standalone_ports_supported': False,
'mode': 'active-backup',
'properties': {'miimon': '100'},
'state': 'present',
},
baremetal,
)
result = ex.exception.args[0]
self.assertFalse(result['changed'])
baremetal.update_port_group.assert_not_called()
def test_delete_existing_port_group(self):
baremetal = self._new_baremetal()
baremetal.find_port_group.return_value = FakePortGroup(id='pg-1', name='bond0')
with self.assertRaises(AnsibleExitJson) as ex:
self._run_module(
{
'id': 'pg-1',
'name': None,
'node': None,
'address': None,
'extra': None,
'standalone_ports_supported': None,
'mode': None,
'properties': None,
'state': 'absent',
},
baremetal,
)
result = ex.exception.args[0]
self.assertTrue(result['changed'])
baremetal.delete_port_group.assert_called_once_with('pg-1')
def test_delete_missing_port_group_is_noop(self):
baremetal = self._new_baremetal()
baremetal.find_port_group.return_value = None
with self.assertRaises(AnsibleExitJson) as ex:
self._run_module(
{
'id': 'pg-1',
'name': None,
'node': None,
'address': None,
'extra': None,
'standalone_ports_supported': None,
'mode': None,
'properties': None,
'state': 'absent',
},
baremetal,
)
result = ex.exception.args[0]
self.assertFalse(result['changed'])
baremetal.delete_port_group.assert_not_called()
def test_check_mode_create_marks_changed(self):
baremetal = self._new_baremetal()
baremetal.find_port_group.return_value = None
with self.assertRaises(AnsibleExitJson) as ex:
self._run_module(
{
'_ansible_check_mode': True,
'id': None,
'name': 'bond0',
'node': 'node-name',
'address': None,
'extra': None,
'standalone_ports_supported': None,
'mode': None,
'properties': None,
'state': 'present',
},
baremetal,
)
result = ex.exception.args[0]
self.assertTrue(result['changed'])
baremetal.create_port_group.assert_not_called()
baremetal.find_node.assert_called_once_with('node-name', ignore_missing=False)
def test_check_mode_create_without_node_fails(self):
baremetal = self._new_baremetal()
baremetal.find_port_group.return_value = None
with self.assertRaises(AnsibleFailJson) as ex:
self._run_module(
{
'_ansible_check_mode': True,
'id': None,
'name': 'bond0',
'node': None,
'address': None,
'extra': None,
'standalone_ports_supported': None,
'mode': None,
'properties': None,
'state': 'present',
},
baremetal,
)
self.assertIn("Parameter 'node' is required", ex.exception.args[0]['msg'])
baremetal.create_port_group.assert_not_called()
baremetal.find_node.assert_not_called()
def test_find_port_group_resource_not_found_returns_none(self):
baremetal = self._new_baremetal()
baremetal.find_port_group.side_effect = FakeSDK.exceptions.ResourceNotFound()
with self.assertRaises(AnsibleExitJson) as ex:
self._run_module(
{
'id': 'pg-1',
'name': None,
'node': None,
'address': None,
'extra': None,
'standalone_ports_supported': None,
'mode': None,
'properties': None,
'state': 'absent',
},
baremetal,
)
result = ex.exception.args[0]
self.assertFalse(result['changed'])

View File

@@ -2,12 +2,10 @@
minversion = 3.18.0
envlist = linters_latest,ansible_latest
skipsdist = True
ignore_basepython_conflict = True
[testenv]
skip_install = True
install_command = python3 -m pip install {opts} {packages}
basepython = python3
passenv =
OS_*
setenv =
@@ -58,7 +56,7 @@ deps =
linters_2_11: -r{toxinidir}/tests/requirements-ansible-2.11.txt
linters_2_12: -r{toxinidir}/tests/requirements-ansible-2.12.txt
linters_2_16: -r{toxinidir}/tests/requirements-ansible-2.16.txt
linters_2_16: -r{toxinidir}/tests/requirements-ansible-2.18.txt
linters_2_18: -r{toxinidir}/tests/requirements-ansible-2.18.txt
passenv = *
[flake8]