Skip to content

Commit 753f301

Browse files
committed
Use Nautobot tenant names composed like "{domain_name}:{project_name}"
Given that: - openstack "domain" acts a namespace for projects - nautobot tenants are all in a flat namespace - nautobot requires tenant names to be unique A better idea seems to be to create tenants in nautobot with names in the form above.
1 parent 214d315 commit 753f301

File tree

4 files changed

+52
-112
lines changed

4 files changed

+52
-112
lines changed

python/understack-workflows/tests/conftest.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,15 @@ def _get_project(project_id):
6060
"id": project_id,
6161
"domain_id": project_data["domain_id"].hex,
6262
}
63-
return openstack.identity.v3.project.Project(**data)
64-
raise openstack.exceptions.NotFoundException
63+
elif project_id == project_data["domain_id"].hex:
64+
data = {
65+
**project_data,
66+
"id": project_data["domain_id"].hex,
67+
"domain_id": "default",
68+
}
69+
else:
70+
raise openstack.exceptions.NotFoundException
71+
return openstack.identity.v3.project.Project(**data)
6572

6673
conn = MagicMock(spec_set=openstack.connection.Connection)
6774
conn.identity.get_project.side_effect = _get_project
@@ -111,4 +118,6 @@ def nautobot(requests_mock, nautobot_url: str, tenant_data: dict) -> Nautobot:
111118
requests_mock.get(tenant_data["url"], json=tenant_data)
112119
requests_mock.delete(tenant_data["url"])
113120
requests_mock.post(f"{nautobot_url}/api/tenancy/tenants/", json=tenant_data)
121+
requests_mock.patch(tenant_data["url"], json=tenant_data)
122+
114123
return Nautobot(nautobot_url, "blah")

python/understack-workflows/tests/test_sync_keystone.py

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
import pytest
55
from pytest_lazy_fixtures import lf
66

7-
from understack_workflows.domain import DefaultDomain
87
from understack_workflows.main.sync_keystone import Event
98
from understack_workflows.main.sync_keystone import argument_parser
109
from understack_workflows.main.sync_keystone import do_action
11-
from understack_workflows.main.sync_keystone import is_valid_domain
1210

1311

1412
@pytest.mark.parametrize(
@@ -23,16 +21,14 @@
2321
),
2422
(
2523
[
26-
"--only-domain",
27-
lf("domain_id"),
2824
"identity.project.created",
2925
lf("project_id"),
3026
],
3127
nullcontext(),
3228
lf("project_id"),
3329
),
3430
(
35-
["--only-domain", "default", "identity.project.created", lf("project_id")],
31+
["identity.project.created", lf("project_id")],
3632
nullcontext(),
3733
lf("project_id"),
3834
),
@@ -46,69 +42,35 @@ def test_parse_object_id(arg_list, context, expected_id):
4642
assert args.object == expected_id
4743

4844

49-
@pytest.mark.parametrize(
50-
"only_domain,expected",
51-
[
52-
(None, True),
53-
(DefaultDomain(), False),
54-
(lf("domain_id"), True),
55-
],
56-
)
57-
def test_is_valid_domain(os_conn, project_id, only_domain, expected):
58-
assert is_valid_domain(os_conn, project_id, only_domain) == expected
5945

60-
61-
@pytest.mark.parametrize(
62-
"only_domain",
63-
[
64-
None,
65-
lf("domain_id"),
66-
uuid.uuid4(),
67-
],
68-
)
6946
def test_create_project(
7047
os_conn,
7148
nautobot,
7249
project_id: uuid.UUID,
73-
only_domain: uuid.UUID | DefaultDomain | None,
50+
domain_id: uuid.UUID,
7451
):
75-
ret = do_action(os_conn, nautobot, Event.ProjectCreate, project_id, only_domain)
76-
os_conn.identity.get_project.assert_called_with(project_id.hex)
52+
ret = do_action(os_conn, nautobot, Event.ProjectCreate, project_id)
53+
os_conn.identity.get_project.assert_any_call(domain_id.hex)
54+
os_conn.identity.get_project.assert_any_call(project_id.hex)
7755
assert ret == 0
7856

7957

80-
@pytest.mark.parametrize(
81-
"only_domain",
82-
[
83-
None,
84-
lf("domain_id"),
85-
uuid.uuid4(),
86-
],
87-
)
8858
def test_update_project(
8959
os_conn,
9060
nautobot,
9161
project_id: uuid.UUID,
92-
only_domain: uuid.UUID | DefaultDomain | None,
62+
domain_id: uuid.UUID,
9363
):
94-
ret = do_action(os_conn, nautobot, Event.ProjectUpdate, project_id, only_domain)
95-
os_conn.identity.get_project.assert_called_with(project_id.hex)
64+
ret = do_action(os_conn, nautobot, Event.ProjectUpdate, project_id)
65+
os_conn.identity.get_project.assert_any_call(domain_id.hex)
66+
os_conn.identity.get_project.assert_any_call(project_id.hex)
9667
assert ret == 0
9768

9869

99-
@pytest.mark.parametrize(
100-
"only_domain",
101-
[
102-
None,
103-
lf("domain_id"),
104-
uuid.uuid4(),
105-
],
106-
)
10770
def test_delete_project(
10871
os_conn,
10972
nautobot,
11073
project_id: uuid.UUID,
111-
only_domain: uuid.UUID | DefaultDomain | None,
11274
):
113-
ret = do_action(os_conn, nautobot, Event.ProjectDelete, project_id, only_domain)
75+
ret = do_action(os_conn, nautobot, Event.ProjectDelete, project_id)
11476
assert ret == 0

python/understack-workflows/understack_workflows/domain.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

python/understack-workflows/understack_workflows/main/sync_keystone.py

Lines changed: 31 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import argparse
22
import logging
3+
from typing import Any
34
import uuid
45
from enum import StrEnum
56

6-
from understack_workflows.domain import DefaultDomain
7-
from understack_workflows.domain import domain_id
87
from understack_workflows.helpers import credential
98
from understack_workflows.helpers import parser_nautobot_args
109
from understack_workflows.helpers import setup_logger
@@ -39,11 +38,6 @@ def argument_parser():
3938
help="Cloud to load. default: %(default)s",
4039
)
4140

42-
parser.add_argument(
43-
"--only-domain",
44-
type=domain_id,
45-
help="Only operate on projects from specified domain",
46-
)
4741
parser.add_argument("event", type=Event, choices=[item.value for item in Event])
4842
parser.add_argument(
4943
"object", type=uuid.UUID, help="Keystone ID of object the event happened on"
@@ -53,25 +47,6 @@ def argument_parser():
5347
return parser
5448

5549

56-
def is_valid_domain(
57-
conn: Connection,
58-
project_id: uuid.UUID,
59-
only_domain: uuid.UUID | DefaultDomain | None,
60-
) -> bool:
61-
if only_domain is None:
62-
return True
63-
project = conn.identity.get_project(project_id.hex) # type: ignore
64-
ret = project.domain_id == only_domain.hex
65-
if not ret:
66-
logger.info(
67-
"keystone project %s part of domain %s and not %s",
68-
project_id,
69-
project.domain_id,
70-
only_domain,
71-
)
72-
return ret
73-
74-
7550
def _create_outside_network(conn: Connection, project_id: uuid.UUID):
7651
network = _find_outside_network(conn, project_id.hex)
7752
if network:
@@ -119,45 +94,61 @@ def _find_outside_network(conn: Connection, project_id: str):
11994
name_or_id=OUTSIDE_NETWORK_NAME,
12095
)
12196

97+
def _tenant_attrs(conn: Connection, project_id: uuid.UUID) -> tuple[str, str]:
98+
project = conn.identity.get_project(project_id.hex) # type: ignore
99+
domain_id = project.domain_id
100+
101+
if domain_id == "default":
102+
domain_name = "default"
103+
else:
104+
domain = conn.identity.get_project(domain_id) # type: ignore
105+
domain_name = domain.name
106+
107+
tenant_name = f"{domain_name}:{project.name}"
108+
return tenant_name, str(project.description)
122109

123110
def handle_project_create(
124111
conn: Connection, nautobot: Nautobot, project_id: uuid.UUID
125112
) -> int:
126-
logger.info("got request to create tenant %s", project_id)
127-
project = conn.identity.get_project(project_id.hex) # type: ignore
128-
ten_api = nautobot.session.tenancy.tenants
113+
logger.info("got request to create tenant %s", project_id.hex)
114+
tenant_name, tenant_description = _tenant_attrs(conn, project_id)
115+
116+
nautobot_tenant_api = nautobot.session.tenancy.tenants
129117
try:
130-
ten = ten_api.create(
131-
id=str(project_id), name=project.name, description=project.description
118+
tenant = nautobot_tenant_api.create(
119+
id=str(project_id),
120+
name=tenant_name,
121+
description=tenant_description
132122
)
133123
_create_outside_network(conn, project_id)
134124
except Exception:
135125
logger.exception(
136-
"Unable to create project %s / %s", str(project_id), project.name
126+
"Unable to create project %s / %s", str(project_id), tenant_name
137127
)
138128
return _EXIT_API_ERROR
139129

140-
logger.info("tenant %s created %s", project_id, ten.created) # type: ignore
130+
logger.info("tenant %s created %s", project_id, tenant.created) # type: ignore
141131
return _EXIT_SUCCESS
142132

143133

144134
def handle_project_update(
145135
conn: Connection, nautobot: Nautobot, project_id: uuid.UUID
146136
) -> int:
147-
logger.info("got request to update tenant %s", project_id)
148-
project = conn.identity.get_project(project_id.hex) # type: ignore
149-
tenant_api = nautobot.session.tenancy.tenants
137+
logger.info("got request to update tenant %s", project_id.hex)
138+
tenant_name, tenant_description = _tenant_attrs(conn, project_id)
150139

140+
tenant_api = nautobot.session.tenancy.tenants
151141
existing_tenant = tenant_api.get(project_id)
152142
logger.info("existing_tenant: %s", existing_tenant)
153143
try:
154144
if existing_tenant is None:
155145
new_tenant = tenant_api.create(
156-
id=str(project_id), name=project.name, description=project.description
146+
id=str(project_id), name=tenant_name, description=tenant_description
157147
)
158148
logger.info("tenant %s created %s", project_id, new_tenant.created) # type: ignore
159149
else:
160-
existing_tenant.description = project.description # type: ignore
150+
existing_tenant.name = tenant_name # type: ignore
151+
existing_tenant.description = tenant_description # type: ignore
161152
existing_tenant.save() # type: ignore
162153
logger.info(
163154
"tenant %s last updated %s",
@@ -168,7 +159,7 @@ def handle_project_update(
168159
_create_outside_network(conn, project_id)
169160
except Exception:
170161
logger.exception(
171-
"Unable to update project %s / %s", str(project_id), project.name
162+
"Unable to update project %s / %s", str(project_id), tenant_name
172163
)
173164
return _EXIT_API_ERROR
174165
return _EXIT_SUCCESS
@@ -194,16 +185,7 @@ def do_action(
194185
nautobot: Nautobot,
195186
event: Event,
196187
project_id: uuid.UUID,
197-
only_domain: uuid.UUID | DefaultDomain | None,
198188
) -> int:
199-
if event in [Event.ProjectCreate, Event.ProjectUpdate] and not is_valid_domain(
200-
conn, project_id, only_domain
201-
):
202-
logger.info(
203-
"keystone project %s not part of %s, skipping", project_id, only_domain
204-
)
205-
return _EXIT_SUCCESS
206-
207189
match event:
208190
case Event.ProjectCreate:
209191
return handle_project_create(conn, nautobot, project_id)
@@ -224,4 +206,4 @@ def main() -> int:
224206
nb_token = args.nautobot_token or credential("nb-token", "token")
225207
nautobot = Nautobot(args.nautobot_url, nb_token, logger=logger)
226208

227-
return do_action(conn, nautobot, args.event, args.object, args.only_domain)
209+
return do_action(conn, nautobot, args.event, args.object)

0 commit comments

Comments
 (0)