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
2 changes: 1 addition & 1 deletion linode_api4/objects/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# isort: skip_file
from .base import Base, Property, MappedObject, DATE_FORMAT
from .base import Base, Property, MappedObject, DATE_FORMAT, ExplicitNullValue
from .dbase import DerivedBase
from .filtering import and_, or_
from .region import Region
Expand Down
39 changes: 33 additions & 6 deletions linode_api4/objects/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
volatile_refresh_timeout = timedelta(seconds=15)


class ExplicitNullValue:
"""
An explicitly null value to set a property to.
Instances of `NullValue` differ from None as they will be explicitly
included in the resource PUT requests.
"""


class Property:
def __init__(
self,
Expand All @@ -22,6 +30,7 @@ def __init__(
filterable=False,
id_relationship=False,
slug_relationship=False,
nullable=False,
):
"""
A Property is an attribute returned from the API, and defines metadata
Expand All @@ -39,6 +48,7 @@ def __init__(
id_relationship - This Property should create a relationship with this key as the ID
(This should be used on fields ending with '_id' only)
slug_relationship - This property is a slug related for a given type.
nullable - This property can be explicitly null on PUT requests.
"""
self.mutable = mutable
self.identifier = identifier
Expand Down Expand Up @@ -244,19 +254,36 @@ def _serialize(self):
A helper method to build a dict of all mutable Properties of
this object
"""
result = {
a: getattr(self, a)
for a in type(self).properties
if type(self).properties[a].mutable
}

result = {}

# Aggregate mutable values into a dict
for k, v in type(self).properties.items():
if not v.mutable:
continue

value = getattr(self, k)

if not value:
continue

# Let's allow explicit null values as both classes and instances
if (
isinstance(value, ExplicitNullValue)
or value == ExplicitNullValue
):
value = None

result[k] = value

# Resolve the underlying IDs of results
for k, v in result.items():
if isinstance(v, Base):
result[k] = v.id
elif isinstance(v, MappedObject):
result[k] = v.dict

return {k: v for k, v in result.items() if v}
return result

def _api_get(self):
"""
Expand Down
13 changes: 13 additions & 0 deletions linode_api4/objects/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ class IPv6Range(Base):
class IPAddress(Base):
"""
note:: This endpoint is in beta. This will only function if base_url is set to `https://api.linode.com/v4beta`.

Represents a Linode IP address object.

When attempting to reset the `rdns` field to default, consider using the ExplicitNullValue class::

ip = IPAddress(client, "127.0.0.1")
ip.rdns = ExplicitNullValue
ip.save()

# Re-populate all attributes with new information from the API
ip.invalidate()

API Documentation: https://www.linode.com/docs/api/networking/#ip-address-view
"""

api_endpoint = "/networking/ips/{address}"
Expand Down
11 changes: 11 additions & 0 deletions test/fixtures/networking_ips_127.0.0.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"address": "127.0.0.1",
"gateway": "127.0.0.1",
"linode_id": 123,
"prefix": 24,
"public": true,
"rdns": "test.example.org",
"region": "us-east",
"subnet_mask": "255.255.255.0",
"type": "ipv4"
}
27 changes: 27 additions & 0 deletions test/objects/networking_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from test.base import ClientBaseCase

from linode_api4 import ExplicitNullValue
from linode_api4.objects import Firewall, IPAddress, IPv6Pool, IPv6Range


Expand Down Expand Up @@ -43,3 +44,29 @@ def test_get_rules(self):
self.assertEqual(result["outbound"], [])
self.assertEqual(result["inbound_policy"], "DROP")
self.assertEqual(result["outbound_policy"], "DROP")

def test_rdns_reset(self):
"""
Tests that the RDNS of an IP and be reset using an explicit null value.
"""

ip = IPAddress(self.client, "127.0.0.1")

with self.mock_put("/networking/ips/127.0.0.1") as m:
ip.rdns = ExplicitNullValue()
ip.save()

self.assertEqual(m.call_url, "/networking/ips/127.0.0.1")

# We need to assert of call_data_raw because
# call_data drops keys with null values
self.assertEqual(m.call_data_raw, '{"rdns": null}')

# Ensure that everything works as expected with a class reference
with self.mock_put("/networking/ips/127.0.0.1") as m:
ip.rdns = ExplicitNullValue
ip.save()

self.assertEqual(m.call_url, "/networking/ips/127.0.0.1")

self.assertEqual(m.call_data_raw, '{"rdns": null}')