Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/+from-pool-sent-once.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A `from_pool` request is sent once: after a successful save, the next update no longer sends it again.
1 change: 1 addition & 0 deletions changelog/+number-pool-detach-attach.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Number attributes have `attach_to_pool()` and `detach_from_pool()`. `attach_to_pool()` makes a number pool track the number the attribute holds, without allocating a new one, and `detach_from_pool()` makes the pool stop tracking it while the attribute keeps its number. Both take effect when the node is saved, and require Infrahub 1.12 or later.
1 change: 1 addition & 0 deletions changelog/+number-pool-inline-ranges.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A `CoreNumberPool` can be created with its ranges in one call, by passing `ranges` as a list of dictionaries with `start`, `end` and `allocation_weight`, any of which can be left out. Saving with `allow_upsert=True` sets the ranges of an existing pool to the declared list. Before, the SDK sent each range as an empty object, which Infrahub read as one range without a start or an end. A single range of an existing pool is still created, changed or deleted through its `CoreNumberPoolRange` node. Requires Infrahub 1.12 or later.
1 change: 1 addition & 0 deletions changelog/+number-pool-value-and-from-pool.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
An attribute created with both `value` and `from_pool` now sends both, so the number pool tracks the given number instead of allocating a new one.
1 change: 1 addition & 0 deletions changelog/+pool-allocations-provenance.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Resource pool nodes have `get_pool_allocations()`, which lists the resources a pool holds and, for a number pool, whether the pool allocated each number or a user attached it. Requires Infrahub 1.12 or later. `get_pool_allocated_resources()` is unchanged.
163 changes: 163 additions & 0 deletions docs/docs/python-sdk/guides/resource-manager.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,88 @@ query {

Notice that we have one IP address allocated by the Resource manager in the test branch. The query in the main branch shows us this allocation, indicating that it has been allocated and the resource cannot be allocated again. However, the IP address does not exist itself within the main branch.

## Create a number pool with ranges

A number pool allocates from its ranges. Pass `ranges` as a list of dictionaries with `start`, `end` and `allocation_weight`. A range without a `start` or an `end` is open on that side, and ranges with a higher `allocation_weight` are allocated from first. Without `ranges`, the pool has one range with no start and no end.

<Tabs groupId="async-sync">
<TabItem value="Async" default>

```python
pool = await client.create(
kind="CoreNumberPool",
name="VLAN IDs",
node="InfraVLAN",
node_attribute="vlan_id",
ranges=[
{"start": 100, "end": 199, "allocation_weight": 10},
{"start": 200, "end": 2999},
],
)
await pool.save()
```

</TabItem>
<TabItem value="Sync" default>

```python
pool = client.create(
kind="CoreNumberPool",
name="VLAN IDs",
node="InfraVLAN",
node_attribute="vlan_id",
ranges=[
{"start": 100, "end": 199, "allocation_weight": 10},
{"start": 200, "end": 2999},
],
)
pool.save()
```

</TabItem>
</Tabs>

The declared list is the complete set of ranges. Saving the same pool with `allow_upsert=True` sets the ranges of an existing pool to that list: Infrahub keeps the ranges whose bounds match, creates the new ones and deletes the others.

To add, change or delete a single range of an existing pool, work with its `CoreNumberPoolRange` node. Infrahub refuses changes made through the pool's `ranges` relationship with `add()` or `remove()`, because the pool takes its ranges as bounds, not as references to range nodes.

<Tabs groupId="async-sync">
<TabItem value="Async" default>

```python
# Add a range
new_range = await client.create(kind="CoreNumberPoolRange", start=3000, end=3999, pool=pool)
await new_range.save()

# Change its weight
new_range.allocation_weight.value = 50
await new_range.save()

# Delete it
await new_range.delete()
```

</TabItem>
<TabItem value="Sync" default>

```python
# Add a range
new_range = client.create(kind="CoreNumberPoolRange", start=3000, end=3999, pool=pool)
new_range.save()

# Change its weight
new_range.allocation_weight.value = 50
new_range.save()

# Delete it
new_range.delete()
```

</TabItem>
</Tabs>

Infrahub refuses a range that overlaps another range of the pool, and refuses a change or a deletion that would leave a number the pool tracks outside every range. The ranges of a pool generated from a schema attribute are set in the attribute's `parameters.ranges` instead, and cannot be changed through `CoreNumberPoolRange` nodes.

## CoreNumberPool and attribute allocation

`CoreNumberPool` allocates integer values (such as VLAN IDs or AS numbers) directly to node attributes. The pool assigns the integer value at the moment the node is created on the server.
Expand All @@ -323,6 +405,87 @@ vlan = await client.create(
await vlan.save()
```

### Attach a number you choose to a pool

To keep a number you choose and have the pool track it, pass both `value` and `from_pool`. The pool records the number as one a user provided, and does not allocate a new one.

<Tabs groupId="async-sync">
<TabItem value="Async" default>

```python
vlan = await client.create(
kind="InfraVLAN",
name="VLAN-200",
vlan_id={"value": 200, "from_pool": {"id": pool_id}},
)
await vlan.save()
```

</TabItem>
<TabItem value="Sync" default>

```python
vlan = client.create(
kind="InfraVLAN",
name="VLAN-200",
vlan_id={"value": 200, "from_pool": {"id": pool_id}},
)
vlan.save()
```

</TabItem>
</Tabs>

On an existing node, call `attach_to_pool()` on the attribute with the pool or its ID. The pool tracks the number the attribute holds when the node is saved.

<Tabs groupId="async-sync">
<TabItem value="Async" default>

```python
vlan = await client.get(kind="InfraVLAN", name__value="VLAN-300")
vlan.vlan_id.attach_to_pool(pool)
await vlan.save()
```

</TabItem>
<TabItem value="Sync" default>

```python
vlan = client.get(kind="InfraVLAN", name__value="VLAN-300")
vlan.vlan_id.attach_to_pool(pool)
vlan.save()
```

</TabItem>
</Tabs>

### Detach a number from its pool

Call `detach_from_pool()` on the attribute. The attribute keeps its number, and the pool stops tracking it when the node is saved.

<Tabs groupId="async-sync">
<TabItem value="Async" default>

```python
vlan = await client.get(kind="InfraVLAN", name__value="VLAN-200")
vlan.vlan_id.detach_from_pool()
await vlan.save()
```

</TabItem>
<TabItem value="Sync" default>

```python
vlan = client.get(kind="InfraVLAN", name__value="VLAN-200")
vlan.vlan_id.detach_from_pool()
vlan.save()
```

</TabItem>
</Tabs>

To find which pool tracks a number on a node you fetched, query the node with `property=True` and read the attribute's `source`, which is the number pool.

### Limitation: `allow_upsert=True` with a pool-sourced HFID attribute

`CoreNumberPool` assigns the integer value at server creation time, and a new value on each creation. When a node's human-friendly identifier (HFID) includes a pool-sourced attribute, the HFID is never stable, so an upsert can never match an existing node by it. Rather than updating, each `save(allow_upsert=True)` would create another node. The SDK blocks this up front to avoid silently duplicating data.
Expand Down
36 changes: 34 additions & 2 deletions docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/attribute.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,35 @@ Never raises and sends no request.
- was assigned since, when the attribute does not belong to a node, or when its node has no
- ``id`` yet. ``False`` otherwise.

#### `attach_to_pool`

```python
attach_to_pool(self, pool: CoreNodeBase | str) -> None
```

Attach the number this attribute holds to a number pool when the node is next saved.

The pool then tracks the number as one a user provided, without allocating a new one.

**Args:**

- `pool`: The number pool node, or its ID.

**Raises:**

- `ValueError`: If the attribute holds no number, or if ``pool`` is a node that is not a resource pool or
has not been saved.

#### `detach_from_pool`

```python
detach_from_pool(self) -> None
```

Detach the number this attribute holds from its number pool when the node is next saved.

The attribute keeps its number, and the pool stops tracking it.

#### `is_from_pool_attribute`

```python
Expand All @@ -77,6 +106,9 @@ is_from_pool_attribute(self) -> bool

Check whether this attribute's value is sourced from a resource pool.

This reflects only the pool requests made through this object. For a node fetched from the server, query it
with ``property=True`` and read the attribute's ``source``, which is the number pool tracking the value.

**Returns:**

- True if the attribute value is a resource pool node or was explicitly allocated from a pool.
Expand All @@ -94,5 +126,5 @@ A pool-backed attribute is unresolved when:
- its value is a pool node object (the pool reference itself, not an allocated scalar), or
- its value is None and the from_pool allocation dict is set.

An attribute whose _from_pool dict is set but whose value has already been populated
with the allocated scalar (e.g. after a prior save) is considered resolved.
An attribute whose _from_pool dict is set but which holds a number, either the one allocated by a
prior save or one given to attach to the pool, is considered resolved.
60 changes: 60 additions & 0 deletions docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/node.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,30 @@ Fetch all nodes that were allocated for the pool and a given resource.

- `ValueError`: If the node is not a resource pool.

#### `get_pool_allocations`

```python
get_pool_allocations(self, resource: InfrahubNode) -> list[PoolAllocation]
```

Fetch the resources the pool holds for a given resource, with how each one came to be held.

Requires Infrahub 1.12 or later. Unlike :meth:`get_pool_allocated_resources`, it does not fetch the
allocated nodes.

**Args:**

- `resource`: The resource from which the nodes were allocated\: the pool itself, or one of
its ranges or prefixes.

**Returns:**

- list\[PoolAllocation]: One entry per resource the pool holds.

**Raises:**

- `ValueError`: If the node is not a resource pool.

#### `get_pool_resources_utilization`

```python
Expand Down Expand Up @@ -1006,6 +1030,30 @@ Fetch all nodes that were allocated for the pool and a given resource.

- `ValueError`: If the node is not a resource pool.

#### `get_pool_allocations`

```python
get_pool_allocations(self, resource: InfrahubNodeSync) -> list[PoolAllocation]
```

Fetch the resources the pool holds for a given resource, with how each one came to be held.

Requires Infrahub 1.12 or later. Unlike :meth:`get_pool_allocated_resources`, it does not fetch the
allocated nodes.

**Args:**

- `resource`: The resource from which the nodes were allocated\: the pool itself, or one
of its ranges or prefixes.

**Returns:**

- list\[PoolAllocation]: One entry per resource the pool holds.

**Raises:**

- `ValueError`: If the node is not a resource pool.

#### `get_pool_resources_utilization`

```python
Expand Down Expand Up @@ -1097,6 +1145,18 @@ fetched via ``client.get(...)``. A server-side change to the file
between the fetch and the call will not be detected unless the
caller re-fetches the node first.

### `PoolAllocation`

One resource a pool holds, as returned by :meth:`InfrahubNode.get_pool_allocations` and its sync twin.

**Methods:**

#### `from_graphql`

```python
from_graphql(cls, data: dict[str, Any]) -> PoolAllocation
```

### `InfrahubNodeBase`

Base class for :class:`InfrahubNode` and :class:`InfrahubNodeSync`.
Expand Down
7 changes: 7 additions & 0 deletions infrahub_sdk/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ class InfrahubClientMode(str, enum.Enum):
# IDEMPOTENT = "idempotent"


class PoolAllocationProvenance(str, enum.Enum):
"""Whether a number pool allocated a number or a user provided it and attached it to the pool."""

ALLOCATED = "allocated"
PROVIDED = "provided"


class Priority(str, enum.Enum):
"""Request priority emitted as the ``X-Priority`` header.

Expand Down
3 changes: 2 additions & 1 deletion infrahub_sdk/node/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
SAFE_VALUE,
UPLOAD_IF_CHANGED_FEATURE_NOT_SUPPORTED_MESSAGE,
)
from .node import InfrahubNode, InfrahubNodeBase, InfrahubNodeSync, UploadResult
from .node import InfrahubNode, InfrahubNodeBase, InfrahubNodeSync, PoolAllocation, UploadResult
from .parsers import parse_human_friendly_id
from .property import NodeProperty
from .related_node import (
Expand Down Expand Up @@ -43,6 +43,7 @@
"InfrahubNodeBase",
"InfrahubNodeSync",
"NodeProperty",
"PoolAllocation",
"RelatedNode",
"RelatedNodeBase",
"RelatedNodeSync",
Expand Down
Loading
Loading