inmanta-module-openstack 5.0.2__tar.gz → 5.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/PKG-INFO +1 -1
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_module_openstack.egg-info/PKG-INFO +1 -1
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_module_openstack.egg-info/SOURCES.txt +3 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/common/deps.py +37 -15
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/compute/flavor.py +48 -38
- inmanta_module_openstack-5.1.0/inmanta_plugins/openstack/compute/host_aggregate.py +117 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/compute/host_port.py +33 -3
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/compute/virtual_machine.py +15 -2
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/model/_init.cf +43 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/network_port.py +1 -1
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/setup.cfg +1 -1
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/setup.cfg +1 -1
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_flavor.py +68 -3
- inmanta_module_openstack-5.1.0/tests/test_host_aggregate.py +138 -0
- inmanta_module_openstack-5.1.0/tests/test_host_port_security_groups.py +441 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_neutron.py +81 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_nova.py +67 -2
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/MANIFEST.in +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/README.md +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_module_openstack.egg-info/dependency_links.txt +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_module_openstack.egg-info/not-zip-safe +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_module_openstack.egg-info/requires.txt +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_module_openstack.egg-info/top_level.txt +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/__init__.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/common/__init__.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/common/base.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/compute/__init__.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/identity/__init__.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/identity/group.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/identity/group_role.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/identity/keystone_base.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/identity/project.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/identity/role.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/identity/user.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/image/__init__.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/image/image.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/__init__.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/floating_ip.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/network.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/qos_policy.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/router.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/router_port.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/security_group.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/subnet.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/subnet_v6.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/trunk_network.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/network/trunk_port.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/quota/__init__.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/inmanta_plugins/openstack/quota/quota.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/pyproject.toml +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_block_devices.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_dependency_handling.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_examples.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_image.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_keystone.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_quota.py +0 -0
- {inmanta_module_openstack-5.0.2 → inmanta_module_openstack-5.1.0}/tests/test_trunk.py +0 -0
|
@@ -15,6 +15,7 @@ inmanta_plugins/openstack/common/base.py
|
|
|
15
15
|
inmanta_plugins/openstack/common/deps.py
|
|
16
16
|
inmanta_plugins/openstack/compute/__init__.py
|
|
17
17
|
inmanta_plugins/openstack/compute/flavor.py
|
|
18
|
+
inmanta_plugins/openstack/compute/host_aggregate.py
|
|
18
19
|
inmanta_plugins/openstack/compute/host_port.py
|
|
19
20
|
inmanta_plugins/openstack/compute/virtual_machine.py
|
|
20
21
|
inmanta_plugins/openstack/identity/__init__.py
|
|
@@ -45,6 +46,8 @@ tests/test_block_devices.py
|
|
|
45
46
|
tests/test_dependency_handling.py
|
|
46
47
|
tests/test_examples.py
|
|
47
48
|
tests/test_flavor.py
|
|
49
|
+
tests/test_host_aggregate.py
|
|
50
|
+
tests/test_host_port_security_groups.py
|
|
48
51
|
tests/test_image.py
|
|
49
52
|
tests/test_keystone.py
|
|
50
53
|
tests/test_neutron.py
|
|
@@ -17,11 +17,15 @@ Contact: code@inmanta.com
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
from collections import defaultdict
|
|
20
|
+
from collections.abc import Mapping
|
|
20
21
|
|
|
21
22
|
from inmanta.export import dependency_manager
|
|
22
23
|
|
|
23
24
|
from inmanta_plugins.openstack.common.base import OpenstackResource
|
|
24
25
|
|
|
26
|
+
# Per-provider view of the collected resources: resource type -> resource name -> resource.
|
|
27
|
+
ResourceCollector = Mapping[str, Mapping[str, OpenstackResource]]
|
|
28
|
+
|
|
25
29
|
MANAGED_DEPENDENCIES = {
|
|
26
30
|
"openstack::Project",
|
|
27
31
|
"openstack::Network",
|
|
@@ -34,9 +38,15 @@ MANAGED_DEPENDENCIES = {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
|
|
37
|
-
def resource_collector(
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
def resource_collector(
|
|
42
|
+
resource_model: Mapping[object, OpenstackResource],
|
|
43
|
+
) -> tuple[Mapping[object, ResourceCollector], Mapping[object, ResourceCollector]]:
|
|
44
|
+
purged_resources: dict[object, dict[str, dict[str, OpenstackResource]]] = (
|
|
45
|
+
defaultdict(lambda: defaultdict(dict))
|
|
46
|
+
)
|
|
47
|
+
non_purged_resources: dict[object, dict[str, dict[str, OpenstackResource]]] = (
|
|
48
|
+
defaultdict(lambda: defaultdict(dict))
|
|
49
|
+
)
|
|
40
50
|
|
|
41
51
|
for current_resource in resource_model.values():
|
|
42
52
|
rtype = current_resource.id.entity_type
|
|
@@ -51,14 +61,13 @@ def resource_collector(resource_model):
|
|
|
51
61
|
return purged_resources, non_purged_resources
|
|
52
62
|
|
|
53
63
|
|
|
54
|
-
def set_dependencies_on_purged_resources(
|
|
55
|
-
collector: dict[str, OpenstackResource],
|
|
56
|
-
) -> None:
|
|
64
|
+
def set_dependencies_on_purged_resources(collector: ResourceCollector) -> None:
|
|
57
65
|
"""
|
|
58
|
-
|
|
66
|
+
Set the dependencies between purged VMs/ports and the security groups they reference.
|
|
59
67
|
"""
|
|
60
68
|
vms = collector.get("openstack::VirtualMachine", {})
|
|
61
69
|
ports = collector.get("openstack::HostPort", {})
|
|
70
|
+
sgs = collector.get("openstack::SecurityGroup", {})
|
|
62
71
|
|
|
63
72
|
# When the VM is purged, all its purged ports should be deployed first
|
|
64
73
|
for vm in vms.values():
|
|
@@ -67,10 +76,15 @@ def set_dependencies_on_purged_resources(
|
|
|
67
76
|
port_id = ports[vm_port["name"]].id
|
|
68
77
|
vm.requires.add(port_id)
|
|
69
78
|
|
|
79
|
+
# When a security group is purged, every port referencing it must be removed (or have the
|
|
80
|
+
# reference dropped) before the group itself can be deleted.
|
|
81
|
+
for port in ports.values():
|
|
82
|
+
for sg_name in port.security_groups:
|
|
83
|
+
if sg_name in sgs:
|
|
84
|
+
sgs[sg_name].requires.add(port.id)
|
|
70
85
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
) -> None:
|
|
86
|
+
|
|
87
|
+
def set_dependencies_on_non_purged_resources(collector: ResourceCollector) -> None:
|
|
74
88
|
projects = collector.get("openstack::Project", {})
|
|
75
89
|
networks = collector.get("openstack::Network", {})
|
|
76
90
|
routers = collector.get("openstack::Router", {})
|
|
@@ -131,6 +145,10 @@ def set_dependencies_on_non_purged_resources(
|
|
|
131
145
|
if port.host in vms:
|
|
132
146
|
port.requires.add(vms[port.host].id)
|
|
133
147
|
|
|
148
|
+
for sg_name in port.security_groups:
|
|
149
|
+
if sg_name in sgs:
|
|
150
|
+
port.requires.add(sgs[sg_name].id)
|
|
151
|
+
|
|
134
152
|
for fip in fips.values():
|
|
135
153
|
if fip.external_network in networks:
|
|
136
154
|
fip.requires.add(networks[fip.external_network].id)
|
|
@@ -146,7 +164,9 @@ def set_dependencies_on_non_purged_resources(
|
|
|
146
164
|
|
|
147
165
|
|
|
148
166
|
@dependency_manager
|
|
149
|
-
def openstack_dependencies(
|
|
167
|
+
def openstack_dependencies(
|
|
168
|
+
config_model: object, resource_model: Mapping[object, OpenstackResource]
|
|
169
|
+
) -> None:
|
|
150
170
|
purged_resources, non_purged_resources = resource_collector(resource_model)
|
|
151
171
|
for project, collector in non_purged_resources.items():
|
|
152
172
|
set_dependencies_on_non_purged_resources(collector)
|
|
@@ -155,10 +175,12 @@ def openstack_dependencies(config_model, resource_model):
|
|
|
155
175
|
|
|
156
176
|
|
|
157
177
|
@dependency_manager
|
|
158
|
-
def keystone_dependencies(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
178
|
+
def keystone_dependencies(
|
|
179
|
+
config_model: object, resource_model: Mapping[object, OpenstackResource]
|
|
180
|
+
) -> None:
|
|
181
|
+
projects: dict[str, OpenstackResource] = {}
|
|
182
|
+
users: dict[str, OpenstackResource] = {}
|
|
183
|
+
roles: list[OpenstackResource] = []
|
|
162
184
|
for _, res in resource_model.items():
|
|
163
185
|
if res.id.entity_type == "openstack::Project":
|
|
164
186
|
if not res.purged:
|
|
@@ -79,6 +79,29 @@ class FlavorHandler(OpenStackHandler):
|
|
|
79
79
|
)
|
|
80
80
|
return self.require_single_item(flavors)
|
|
81
81
|
|
|
82
|
+
def _create_flavor(self, resource: Flavor):
|
|
83
|
+
"""Create a flavor in OpenStack."""
|
|
84
|
+
flavor = self._connection.compute.create_flavor(
|
|
85
|
+
name=resource.name,
|
|
86
|
+
ram=resource.ram,
|
|
87
|
+
vcpus=resource.vcpus,
|
|
88
|
+
disk=resource.disk,
|
|
89
|
+
id=resource.flavor_id,
|
|
90
|
+
ephemeral=resource.ephemeral,
|
|
91
|
+
swap=resource.swap,
|
|
92
|
+
rxtx_factor=resource.rxtx_factor,
|
|
93
|
+
is_public=resource.is_public,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if resource.extra_specs:
|
|
97
|
+
self._connection.compute.create_flavor_extra_specs(
|
|
98
|
+
flavor, resource.extra_specs
|
|
99
|
+
)
|
|
100
|
+
return flavor
|
|
101
|
+
|
|
102
|
+
def _delete_flavor(self) -> None:
|
|
103
|
+
self._connection.compute.delete_flavor(self.flavor.id, ignore_missing=True)
|
|
104
|
+
|
|
82
105
|
def read_resource(self, ctx: handler.HandlerContext, resource: Flavor) -> None:
|
|
83
106
|
self.flavor = self._get_flavor(resource)
|
|
84
107
|
if self.flavor is None:
|
|
@@ -101,27 +124,12 @@ class FlavorHandler(OpenStackHandler):
|
|
|
101
124
|
self._set_facts(ctx)
|
|
102
125
|
|
|
103
126
|
def create_resource(self, ctx: handler.HandlerContext, resource: Flavor) -> None:
|
|
104
|
-
self.flavor = self.
|
|
105
|
-
name=resource.name,
|
|
106
|
-
ram=resource.ram,
|
|
107
|
-
vcpus=resource.vcpus,
|
|
108
|
-
disk=resource.disk,
|
|
109
|
-
id=resource.flavor_id,
|
|
110
|
-
ephemeral=resource.ephemeral,
|
|
111
|
-
swap=resource.swap,
|
|
112
|
-
rxtx_factor=resource.rxtx_factor,
|
|
113
|
-
is_public=resource.is_public,
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
if resource.extra_specs:
|
|
117
|
-
self._connection.compute.create_flavor_extra_specs(
|
|
118
|
-
self.flavor, resource.extra_specs
|
|
119
|
-
)
|
|
127
|
+
self.flavor = self._create_flavor(resource)
|
|
120
128
|
self._set_facts(ctx)
|
|
121
129
|
ctx.set_created()
|
|
122
130
|
|
|
123
131
|
def delete_resource(self, ctx: handler.HandlerContext, resource: Flavor) -> None:
|
|
124
|
-
self.
|
|
132
|
+
self._delete_flavor()
|
|
125
133
|
ctx.set_purged()
|
|
126
134
|
|
|
127
135
|
def update_resource(
|
|
@@ -130,31 +138,33 @@ class FlavorHandler(OpenStackHandler):
|
|
|
130
138
|
changes: dict,
|
|
131
139
|
resource: Flavor,
|
|
132
140
|
) -> None:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
self._connection.compute.delete_flavor_extra_specs_property(
|
|
147
|
-
self.flavor, key
|
|
148
|
-
)
|
|
149
|
-
self._connection.compute.create_flavor_extra_specs(
|
|
150
|
-
self.flavor, new_extra_specs
|
|
141
|
+
"""Update flavor parameters.
|
|
142
|
+
|
|
143
|
+
OpenStack flavors must be immutable, so *every* change - including
|
|
144
|
+
extra_specs - is applied by deleting the existing flavor and
|
|
145
|
+
recreating it. This is only safe when OpenStack assigns the id: an
|
|
146
|
+
explicit flavor_id would collide with the soft-deleted flavor, so we
|
|
147
|
+
refuse such changes instead.
|
|
148
|
+
"""
|
|
149
|
+
if self._resource_has_explicit_flavor_id(resource):
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Cannot change {sorted(changes)} on a flavor with an explicit "
|
|
152
|
+
f"flavor_id: recreating it would reuse the soft-deleted id. "
|
|
153
|
+
f"Remove the flavor and create a new one instead."
|
|
151
154
|
)
|
|
152
|
-
|
|
155
|
+
|
|
156
|
+
ctx.info(
|
|
157
|
+
"Recreating flavor; changed field(s): %(fields)s",
|
|
158
|
+
fields=sorted(changes),
|
|
159
|
+
)
|
|
160
|
+
self._delete_flavor()
|
|
161
|
+
self.flavor = self._create_flavor(resource)
|
|
162
|
+
self._set_facts(ctx)
|
|
153
163
|
ctx.set_updated()
|
|
154
164
|
|
|
155
165
|
def _set_facts(self, ctx):
|
|
156
|
-
ctx.set_fact("flavor_id", self.flavor.id
|
|
157
|
-
ctx.set_fact("is_disabled", str(self.flavor.is_disabled)
|
|
166
|
+
ctx.set_fact("flavor_id", self.flavor.id)
|
|
167
|
+
ctx.set_fact("is_disabled", str(self.flavor.is_disabled))
|
|
158
168
|
|
|
159
169
|
@cache(timeout=5)
|
|
160
170
|
def facts(self, ctx, resource: Flavor) -> dict:
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2026 Inmanta
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: code@inmanta.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from inmanta.agent import handler
|
|
20
|
+
from inmanta.agent.handler import ResourcePurged, SkipResource, provider
|
|
21
|
+
from inmanta.resources import resource
|
|
22
|
+
|
|
23
|
+
from inmanta_plugins.openstack.common.base import (
|
|
24
|
+
OpenstackAdminResource,
|
|
25
|
+
OpenStackHandler,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@resource(
|
|
30
|
+
"openstack::HostAggregateMetadata",
|
|
31
|
+
agent="provider.name",
|
|
32
|
+
id_attribute="metadata_id",
|
|
33
|
+
)
|
|
34
|
+
class HostAggregateMetadata(OpenstackAdminResource):
|
|
35
|
+
"""A single key/value metadata item on a Nova host aggregate."""
|
|
36
|
+
|
|
37
|
+
fields = ("aggregate", "key", "value")
|
|
38
|
+
aggregate: str
|
|
39
|
+
key: str
|
|
40
|
+
value: str
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def get_metadata_id(_, resource) -> str:
|
|
44
|
+
# Unique per (aggregate, key) within the provider's agent.
|
|
45
|
+
return f"{resource.aggregate}::{resource.key}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@provider("openstack::HostAggregateMetadata", name="openstack")
|
|
49
|
+
class HostAggregateMetadataHandler(OpenStackHandler):
|
|
50
|
+
"""Manage a single metadata key on a host aggregate.
|
|
51
|
+
|
|
52
|
+
create/update -> set the key to the desired value
|
|
53
|
+
delete -> remove the key (Nova removes a key when its value is None),
|
|
54
|
+
leaving any other keys on the aggregate untouched
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def _get_aggregate(self, resource: HostAggregateMetadata, *, required: bool):
|
|
58
|
+
aggregate = self._connection.compute.find_aggregate(
|
|
59
|
+
resource.aggregate, ignore_missing=True
|
|
60
|
+
)
|
|
61
|
+
if aggregate is None and required:
|
|
62
|
+
raise SkipResource(
|
|
63
|
+
f"Host aggregate '{resource.aggregate}' does not exist; cannot manage "
|
|
64
|
+
f"metadata key '{resource.key}'. The aggregate must be created first."
|
|
65
|
+
)
|
|
66
|
+
return aggregate
|
|
67
|
+
|
|
68
|
+
def read_resource(
|
|
69
|
+
self, ctx: handler.HandlerContext, resource: HostAggregateMetadata
|
|
70
|
+
) -> None:
|
|
71
|
+
aggregate = self._get_aggregate(resource, required=True)
|
|
72
|
+
metadata = aggregate.metadata or {}
|
|
73
|
+
|
|
74
|
+
if resource.key not in metadata:
|
|
75
|
+
raise ResourcePurged()
|
|
76
|
+
|
|
77
|
+
resource.purged = False
|
|
78
|
+
# Surface the current value so a changed value is detected as a diff.
|
|
79
|
+
resource.value = metadata[resource.key]
|
|
80
|
+
|
|
81
|
+
def create_resource(
|
|
82
|
+
self, ctx: handler.HandlerContext, resource: HostAggregateMetadata
|
|
83
|
+
) -> None:
|
|
84
|
+
aggregate = self._get_aggregate(resource, required=True)
|
|
85
|
+
self._connection.compute.set_aggregate_metadata(
|
|
86
|
+
aggregate, metadata={resource.key: resource.value}
|
|
87
|
+
)
|
|
88
|
+
ctx.set_created()
|
|
89
|
+
|
|
90
|
+
def update_resource(
|
|
91
|
+
self,
|
|
92
|
+
ctx: handler.HandlerContext,
|
|
93
|
+
changes: dict,
|
|
94
|
+
resource: HostAggregateMetadata,
|
|
95
|
+
) -> None:
|
|
96
|
+
# Only the value can change; aggregate and key are part of the resource id.
|
|
97
|
+
aggregate = self._get_aggregate(resource, required=True)
|
|
98
|
+
self._connection.compute.set_aggregate_metadata(
|
|
99
|
+
aggregate, metadata={resource.key: resource.value}
|
|
100
|
+
)
|
|
101
|
+
ctx.set_updated()
|
|
102
|
+
|
|
103
|
+
def delete_resource(
|
|
104
|
+
self, ctx: handler.HandlerContext, resource: HostAggregateMetadata
|
|
105
|
+
) -> None:
|
|
106
|
+
aggregate = self._get_aggregate(resource, required=False)
|
|
107
|
+
if aggregate is None:
|
|
108
|
+
# Aggregate gone -> the key is gone with it; nothing to unset.
|
|
109
|
+
ctx.set_purged()
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# Nova removes a metadata key when it is set to None, leaving any other keys
|
|
113
|
+
# on the aggregate untouched.
|
|
114
|
+
self._connection.compute.set_aggregate_metadata(
|
|
115
|
+
aggregate, metadata={resource.key: None}
|
|
116
|
+
)
|
|
117
|
+
ctx.set_purged()
|
|
@@ -47,6 +47,7 @@ class HostPort(Port):
|
|
|
47
47
|
"wait",
|
|
48
48
|
"allowed_address_pairs",
|
|
49
49
|
"wait_for_vm",
|
|
50
|
+
"security_groups",
|
|
50
51
|
)
|
|
51
52
|
host: str
|
|
52
53
|
portsecurity: bool
|
|
@@ -56,6 +57,7 @@ class HostPort(Port):
|
|
|
56
57
|
wait: bool
|
|
57
58
|
allowed_address_pairs: dict[str, Optional[str]]
|
|
58
59
|
wait_for_vm: bool
|
|
60
|
+
security_groups: list[str]
|
|
59
61
|
|
|
60
62
|
@staticmethod
|
|
61
63
|
def get_host(_, port):
|
|
@@ -77,6 +79,10 @@ class HostPort(Port):
|
|
|
77
79
|
"""field used to determine if we expect the VM to be present at all"""
|
|
78
80
|
return not (port.vm.purged)
|
|
79
81
|
|
|
82
|
+
@staticmethod
|
|
83
|
+
def get_security_groups(_, port):
|
|
84
|
+
return sorted([v.name for v in port.security_groups])
|
|
85
|
+
|
|
80
86
|
|
|
81
87
|
@provider("openstack::HostPort", name="openstack")
|
|
82
88
|
class HostPortHandler(OpenStackHandler):
|
|
@@ -182,6 +188,13 @@ class HostPortHandler(OpenStackHandler):
|
|
|
182
188
|
pair["ip_address"]: pair.get("mac_address")
|
|
183
189
|
for pair in self.port.allowed_address_pairs
|
|
184
190
|
}
|
|
191
|
+
|
|
192
|
+
if resource.security_groups:
|
|
193
|
+
resource.security_groups = sorted(
|
|
194
|
+
self.get_security_group(project_id=self.project_id, group_id=sg_id).name
|
|
195
|
+
for sg_id in self.port.security_group_ids
|
|
196
|
+
)
|
|
197
|
+
|
|
185
198
|
resource.name = self.port.name
|
|
186
199
|
self._set_facts(ctx)
|
|
187
200
|
|
|
@@ -209,7 +222,13 @@ class HostPortHandler(OpenStackHandler):
|
|
|
209
222
|
]
|
|
210
223
|
|
|
211
224
|
port_attrs["port_security_enabled"] = resource.portsecurity
|
|
212
|
-
|
|
225
|
+
if resource.security_groups:
|
|
226
|
+
port_attrs["security_group_ids"] = [
|
|
227
|
+
self.get_security_group(project_id=self.project_id, name=sg).id
|
|
228
|
+
for sg in resource.security_groups
|
|
229
|
+
]
|
|
230
|
+
else:
|
|
231
|
+
port_attrs["security_group_ids"] = None
|
|
213
232
|
|
|
214
233
|
# Allowed address pairs
|
|
215
234
|
if resource.allowed_address_pairs:
|
|
@@ -248,16 +267,27 @@ class HostPortHandler(OpenStackHandler):
|
|
|
248
267
|
resource: HostPort,
|
|
249
268
|
) -> None:
|
|
250
269
|
self.validate_allowed_fields(
|
|
251
|
-
changes,
|
|
270
|
+
changes,
|
|
271
|
+
{"portsecurity", "name", "allowed_address_pairs", "security_groups"},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
disabling_portsecurity = (
|
|
275
|
+
"portsecurity" in changes and not changes["portsecurity"]["desired"]
|
|
252
276
|
)
|
|
253
277
|
|
|
254
278
|
if self.port.is_port_security_enabled and "portsecurity" in changes:
|
|
255
279
|
if not changes["portsecurity"]["desired"]:
|
|
256
280
|
self.port.is_port_security_enabled = False
|
|
257
|
-
self.port.
|
|
281
|
+
self.port.security_group_ids = []
|
|
258
282
|
else:
|
|
259
283
|
raise SkipResource("Turning port security on again is not supported.")
|
|
260
284
|
|
|
285
|
+
if "security_groups" in changes and not disabling_portsecurity:
|
|
286
|
+
self.port.security_group_ids = [
|
|
287
|
+
self.get_security_group(project_id=self.project_id, name=sg).id
|
|
288
|
+
for sg in resource.security_groups
|
|
289
|
+
]
|
|
290
|
+
|
|
261
291
|
# Handle name change
|
|
262
292
|
if "name" in changes:
|
|
263
293
|
self.port.name = resource.name
|
|
@@ -57,6 +57,7 @@ class VirtualMachine(OpenstackResource):
|
|
|
57
57
|
"metadata",
|
|
58
58
|
"personality",
|
|
59
59
|
"block_devices",
|
|
60
|
+
"availability_zone",
|
|
60
61
|
)
|
|
61
62
|
name: str
|
|
62
63
|
flavor: str
|
|
@@ -69,6 +70,7 @@ class VirtualMachine(OpenstackResource):
|
|
|
69
70
|
config_drive: bool
|
|
70
71
|
metadata: dict[str, str]
|
|
71
72
|
personality: dict[str, str]
|
|
73
|
+
availability_zone: str | None
|
|
72
74
|
block_devices: list[dict]
|
|
73
75
|
|
|
74
76
|
@staticmethod
|
|
@@ -282,8 +284,17 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
282
284
|
)
|
|
283
285
|
|
|
284
286
|
resource.purged = False
|
|
285
|
-
|
|
286
|
-
|
|
287
|
+
if resource.security_groups:
|
|
288
|
+
# An empty relation leaves the server-level security groups unmanaged, so port
|
|
289
|
+
# security groups (managed through neutron) can be used for fine-grained control
|
|
290
|
+
# without the VM handler fighting over the nova server-level groups.
|
|
291
|
+
self._connection.compute.fetch_server_security_groups(self.server)
|
|
292
|
+
resource.security_groups = [
|
|
293
|
+
sg["name"] for sg in self.server.security_groups
|
|
294
|
+
]
|
|
295
|
+
# AZ is create-only; reflect the live value only when pinned to avoid spurious drift.
|
|
296
|
+
if resource.availability_zone is not None:
|
|
297
|
+
resource.availability_zone = self.server.availability_zone
|
|
287
298
|
# The port handler has to handle all network/port related changes
|
|
288
299
|
self._set_facts(ctx)
|
|
289
300
|
|
|
@@ -316,6 +327,8 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
316
327
|
args["block_device_mapping_v2"] = list(resource.block_devices)
|
|
317
328
|
else:
|
|
318
329
|
args["image_id"] = resource.image
|
|
330
|
+
if resource.availability_zone is not None:
|
|
331
|
+
args["availability_zone"] = resource.availability_zone
|
|
319
332
|
ctx.info(
|
|
320
333
|
f"Creating server with name {resource.name} and options",
|
|
321
334
|
options=args,
|
|
@@ -365,7 +365,16 @@ entity HostPort extends Port:
|
|
|
365
365
|
int wait=5
|
|
366
366
|
end
|
|
367
367
|
|
|
368
|
+
implementation noSecurityGroupsWhenPortSecurityDisabled for HostPort:
|
|
369
|
+
# Security groups can only be enforced on a port when port security is enabled.
|
|
370
|
+
std::assert(
|
|
371
|
+
std::count(self.security_groups) == 0,
|
|
372
|
+
"HostPort '{{name}}' cannot have security_groups assigned while portsecurity is disabled."
|
|
373
|
+
)
|
|
374
|
+
end
|
|
375
|
+
|
|
368
376
|
implement HostPort using providerRequire
|
|
377
|
+
implement HostPort using noSecurityGroupsWhenPortSecurityDisabled when not self.portsecurity
|
|
369
378
|
|
|
370
379
|
HostPort.subnet [1] -- Subnet.host_ports [0:]
|
|
371
380
|
HostPort.vm [1] -- VirtualMachine.ports [0:]
|
|
@@ -618,6 +627,12 @@ end
|
|
|
618
627
|
SecurityGroup.provider [1] -- Provider.security_groups [0:]
|
|
619
628
|
SecurityGroup.project [1] -- Project.security_groups [0:]
|
|
620
629
|
SecurityGroup.virtual_machines [0:] -- VirtualMachine.security_groups [0:]
|
|
630
|
+
HostPort.security_groups [0:] -- SecurityGroup
|
|
631
|
+
"""
|
|
632
|
+
:rel security_groups: The security groups to enforce on this port. When left empty (the default), the
|
|
633
|
+
port's security groups are left unmanaged for backward compatibility: existing groups (including the
|
|
634
|
+
Neutron ``default`` group) are kept untouched and never cleared. Requires portsecurity to be enabled.
|
|
635
|
+
"""
|
|
621
636
|
|
|
622
637
|
typedef protocol as string matching self in ["tcp", "udp", "icmp", "sctp", "all"]
|
|
623
638
|
|
|
@@ -683,6 +698,7 @@ entity VMAttributes:
|
|
|
683
698
|
:param metadata: A dict of metadata items
|
|
684
699
|
:param personality: A dict of files (personality)
|
|
685
700
|
:param config_drive: Attach a configuration drive to the vm
|
|
701
|
+
:param availability_zone: Nova availability zone to schedule the VM in. When null, Nova chooses.
|
|
686
702
|
"""
|
|
687
703
|
string flavor
|
|
688
704
|
string? image = null
|
|
@@ -690,6 +706,7 @@ entity VMAttributes:
|
|
|
690
706
|
dict metadata={}
|
|
691
707
|
dict personality={}
|
|
692
708
|
bool config_drive=false
|
|
709
|
+
string? availability_zone = null
|
|
693
710
|
end
|
|
694
711
|
|
|
695
712
|
entity VirtualMachine extends OpenStackResource, VMAttributes:
|
|
@@ -770,6 +787,32 @@ end
|
|
|
770
787
|
|
|
771
788
|
implement Flavor using providerRequire
|
|
772
789
|
|
|
790
|
+
index HostAggregateMetadata(provider, aggregate, key)
|
|
791
|
+
HostAggregateMetadata.provider [1] -- Provider.host_aggregate_metadata [0:]
|
|
792
|
+
entity HostAggregateMetadata extends OpenStackResource:
|
|
793
|
+
"""
|
|
794
|
+
A single metadata item (key/value) on a Nova host aggregate.
|
|
795
|
+
|
|
796
|
+
Each instance manages exactly one key on the named aggregate and leaves all
|
|
797
|
+
other keys untouched, so multiple instances - and multiple consumers - can
|
|
798
|
+
safely manage different keys on the same aggregate. Purging the resource removes
|
|
799
|
+
the key from the aggregate again.
|
|
800
|
+
|
|
801
|
+
A typical use case is the ``AggregateMultiTenancyIsolation`` scheduler filter,
|
|
802
|
+
where keys of the form ``filter_tenant_id_<suffix>`` pin projects to an aggregate.
|
|
803
|
+
|
|
804
|
+
:attr aggregate: Name of the host aggregate to manage the metadata on. The
|
|
805
|
+
aggregate must already exist; this resource does not create or delete it.
|
|
806
|
+
:attr key: The metadata key to manage.
|
|
807
|
+
:attr value: The value to set for ``key``.
|
|
808
|
+
"""
|
|
809
|
+
string aggregate
|
|
810
|
+
string key
|
|
811
|
+
string value
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
implement HostAggregateMetadata using providerRequire
|
|
815
|
+
|
|
773
816
|
index Image(provider, name)
|
|
774
817
|
Image.provider [1] -- Provider.images [0:]
|
|
775
818
|
|
|
@@ -70,7 +70,7 @@ class NetworkPortHandler(OpenStackHandler):
|
|
|
70
70
|
"Cannot read a port when the project id is not yet known."
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
-
self.network = self.get_network(
|
|
73
|
+
self.network = self.get_network(self.project_id, resource.network)
|
|
74
74
|
if self.network is None:
|
|
75
75
|
raise SkipResource(
|
|
76
76
|
"Network {} for port {} not found.".format(
|