Skip to content

Commit d779c98

Browse files
authored
Merge pull request #6522 from opsmill/pog-disallow-deletion-of-numberpools
Forbid the deletion of NumberPools in use by NumberPool attributes
2 parents 8b97edb + 61a48bd commit d779c98

File tree

4 files changed

+193
-3
lines changed

4 files changed

+193
-3
lines changed

backend/infrahub/core/schema/manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,3 +764,6 @@ def purge_inactive_branches(self, active_branches: list[str]) -> list[str]:
764764
del self._cache[hash_key]
765765

766766
return removed_branches
767+
768+
def get_branches(self) -> list[str]:
769+
return list(self._branches.keys())

backend/infrahub/graphql/mutations/resource_manager.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
from graphene.types.generic import GenericScalar
77
from typing_extensions import Self
88

9-
from infrahub.core import registry
9+
from infrahub.core import protocols, registry
1010
from infrahub.core.constants import InfrahubKind
1111
from infrahub.core.ipam.constants import PrefixMemberType
12+
from infrahub.core.manager import NodeManager
1213
from infrahub.core.schema import NodeSchema
1314
from infrahub.database import retry_db_transaction
1415
from infrahub.exceptions import QueryValidationError, SchemaNotFoundError, ValidationError
1516

1617
from ..queries.resource_manager import PoolAllocatedNode
17-
from .main import InfrahubMutationMixin, InfrahubMutationOptions
18+
from .main import DeleteResult, InfrahubMutationMixin, InfrahubMutationOptions
1819

1920
if TYPE_CHECKING:
2021
from graphql import GraphQLResolveInfo
@@ -221,3 +222,43 @@ async def mutate_update(
221222
raise ValidationError(input_value="start_range can't be larger than end_range")
222223

223224
return number_pool, result
225+
226+
@classmethod
227+
@retry_db_transaction(name="resource_manager_update")
228+
async def mutate_delete(
229+
cls,
230+
info: GraphQLResolveInfo,
231+
data: InputObjectType,
232+
branch: Branch,
233+
) -> DeleteResult:
234+
graphql_context: GraphqlContext = info.context
235+
236+
number_pool = await NodeManager.find_object(
237+
db=graphql_context.db,
238+
kind=protocols.CoreNumberPool,
239+
id=data.get("id"),
240+
hfid=data.get("hfid"),
241+
branch=branch,
242+
)
243+
244+
active_branches = registry.schema.get_branches()
245+
violating_branches = []
246+
for active_branch in active_branches:
247+
try:
248+
schema = registry.schema.get(name=number_pool.node.value, branch=active_branch)
249+
except SchemaNotFoundError:
250+
continue
251+
252+
if number_pool.node_attribute.value in schema.attribute_names:
253+
attribute = schema.get_attribute(name=number_pool.node_attribute.value)
254+
if attribute.kind == "NumberPool":
255+
violating_branches.append(active_branch)
256+
257+
if violating_branches:
258+
raise ValidationError(
259+
input_value=f"Unable to delete number pool {number_pool.node.value}.{
260+
number_pool.node_attribute.value
261+
} is in use (branches: {','.join(violating_branches)})"
262+
)
263+
264+
return await super().mutate_delete(info=info, data=data, branch=branch)

backend/tests/helpers/schema/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .location import CONTINENT, COUNTRY, LOCATION, SITE
1414
from .manufacturer import MANUFACTURER
1515
from .person import PERSON
16+
from .snow import SNOW_INCIDENT, SNOW_REQUEST, SNOW_TASK
1617
from .thing import THING
1718
from .ticket import TICKET
1819
from .tshirt import TSHIRT
@@ -27,6 +28,7 @@
2728
generics=[INTERFACE, INTERFACE_HOLDER], nodes=[DEVICE, PHYSICAL_INTERFACE, VIRTUAL_INTERFACE, SFP]
2829
)
2930
LOCATION_SCHEMA = SchemaRoot(generics=[LOCATION], nodes=[CONTINENT, COUNTRY, SITE])
31+
SNOW_TICKET_SCHEMA = SchemaRoot(generics=[SNOW_TASK], nodes=[SNOW_INCIDENT, SNOW_REQUEST])
3032

3133

3234
async def load_schema(

backend/tests/unit/graphql/mutations/test_resource_manager.py

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
from infrahub.core.node import Node
77
from infrahub.core.node.resource_manager.ip_address_pool import CoreIPAddressPool
88
from infrahub.core.node.resource_manager.ip_prefix_pool import CoreIPPrefixPool
9+
from infrahub.core.node.resource_manager.number_pool import CoreNumberPool
910
from infrahub.core.schema import SchemaRoot
11+
from infrahub.core.schema.attribute_parameters import NumberPoolParameters
1012
from infrahub.core.schema.schema_branch import SchemaBranch
1113
from infrahub.database import InfrahubDatabase
1214
from infrahub.graphql.initialization import prepare_graphql_params
1315
from tests.helpers.graphql import graphql
14-
from tests.helpers.schema import TICKET, load_schema
16+
from tests.helpers.schema import SNOW_TICKET_SCHEMA, TICKET, load_schema
1517

1618

1719
@pytest.fixture
@@ -662,6 +664,34 @@ async def test_address_pool_get_resource_with_prefix_length(
662664
"""
663665

664666

667+
DELETE_NUMBER_POOL = """
668+
mutation DeleteNumberPool(
669+
$id: String!,
670+
) {
671+
CoreNumberPoolDelete(
672+
data: {
673+
id: $id,
674+
}
675+
) {
676+
ok
677+
}
678+
}
679+
"""
680+
681+
682+
QUERY_NUMBER_POOL = """
683+
query NumberPool(
684+
$id: ID!,
685+
) {
686+
CoreNumberPool(
687+
ids: [$id]
688+
) {
689+
count
690+
}
691+
}
692+
"""
693+
694+
665695
async def test_test_number_pool_creation_errors(
666696
db: InfrahubDatabase, default_branch: Branch, register_core_models_schema
667697
):
@@ -812,3 +842,117 @@ async def test_test_number_pool_update(db: InfrahubDatabase, default_branch: Bra
812842
assert "start_range can't be larger than end_range" in str(update_invalid_range.errors[0])
813843
assert update_ok.data
814844
assert not update_ok.errors
845+
846+
# Validate that we can delete a number pool that isn't tied to an attribute of kind NumberPool
847+
delete_ok = await graphql(
848+
schema=gql_params.schema,
849+
source=DELETE_NUMBER_POOL,
850+
context_value=gql_params.context,
851+
root_value=None,
852+
variable_values={
853+
"id": pool_id,
854+
},
855+
)
856+
assert not delete_ok.errors
857+
assert delete_ok.data
858+
assert delete_ok.data["CoreNumberPoolDelete"]["ok"]
859+
860+
query_after_delete = await graphql(
861+
schema=gql_params.schema,
862+
source=QUERY_NUMBER_POOL,
863+
context_value=gql_params.context,
864+
root_value=None,
865+
variable_values={
866+
"id": pool_id,
867+
},
868+
)
869+
assert not query_after_delete.errors
870+
assert query_after_delete.data
871+
assert query_after_delete.data["CoreNumberPool"]["count"] == 0
872+
873+
874+
async def test_delete_number_pool_in_use_by_numberpool_attribute(
875+
db: InfrahubDatabase, default_branch: Branch, register_core_models_schema: None
876+
) -> None:
877+
await load_schema(db=db, schema=SNOW_TICKET_SCHEMA)
878+
gql_params = await prepare_graphql_params(db=db, include_subscription=False, branch=default_branch)
879+
node_schema = registry.schema.get(name="SnowTask", branch=default_branch)
880+
number_pool_attribute = node_schema.get_attribute(name="number")
881+
assert isinstance(number_pool_attribute.parameters, NumberPoolParameters)
882+
registry.node[InfrahubKind.NUMBERPOOL] = CoreNumberPool
883+
query_before_creation = await graphql(
884+
schema=gql_params.schema,
885+
source=QUERY_NUMBER_POOL,
886+
context_value=gql_params.context,
887+
root_value=None,
888+
variable_values={
889+
"id": number_pool_attribute.parameters.number_pool_id,
890+
},
891+
)
892+
893+
assert not query_before_creation.errors
894+
assert query_before_creation.data
895+
assert query_before_creation.data["CoreNumberPool"]["count"] == 0
896+
897+
create_snow_incident_mutation = """
898+
mutation CreateSnowIncident(
899+
$title: String!,
900+
) {
901+
SnowIncidentCreate(
902+
data: {
903+
title: {value: $title},
904+
}
905+
) {
906+
object {
907+
title {
908+
value
909+
}
910+
number {
911+
value
912+
source {
913+
id
914+
}
915+
}
916+
identifier {
917+
value
918+
}
919+
}
920+
}
921+
}
922+
"""
923+
924+
create_snow_incident = await graphql(
925+
schema=gql_params.schema,
926+
source=create_snow_incident_mutation,
927+
context_value=gql_params.context,
928+
root_value=None,
929+
variable_values={
930+
"title": "Printer is saying PC load Letter",
931+
},
932+
)
933+
934+
assert not create_snow_incident.errors
935+
assert create_snow_incident.data
936+
assert (
937+
create_snow_incident.data["SnowIncidentCreate"]["object"]["title"]["value"]
938+
== "Printer is saying PC load Letter"
939+
)
940+
assert create_snow_incident.data["SnowIncidentCreate"]["object"]["number"]["value"] == 1
941+
assert (
942+
create_snow_incident.data["SnowIncidentCreate"]["object"]["number"]["source"]["id"]
943+
== number_pool_attribute.parameters.number_pool_id
944+
)
945+
assert create_snow_incident.data["SnowIncidentCreate"]["object"]["identifier"]["value"] == "INC1"
946+
947+
delete_fail = await graphql(
948+
schema=gql_params.schema,
949+
source=DELETE_NUMBER_POOL,
950+
context_value=gql_params.context,
951+
root_value=None,
952+
variable_values={
953+
"id": number_pool_attribute.parameters.number_pool_id,
954+
},
955+
)
956+
957+
assert delete_fail.errors
958+
assert "Unable to delete number pool SnowTask.number is in use (branches: main)" in str(delete_fail.errors)

0 commit comments

Comments
 (0)