Skip to content

Enhanced Interfaces: Add support for Linode-related endpoints and fields #533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: proj/enhanced-interfaces
Choose a base branch
from

Conversation

lgarber-akamai
Copy link
Contributor

@lgarber-akamai lgarber-akamai commented Apr 21, 2025

📝 Description

This pull request adds support for the following API interfaces and fields introduced as part of the VPC Linodes Enhanced Interfaces project:

  • POST linode/instances/{id}/interfaces
  • GET linode/instances/{id}/interfaces
  • GET linode/instances/{id}/interfaces/settings
  • PUT linode/instances/{id}/interfaces/settings
  • GET linode/instances/{id}/interfaces/{interfaceId}
  • PUT linode/instances/{id}/interfaces/{interfaceId}
  • DELETE linode/instances/{id}/interfaces/{interfaceId}
  • GET linode/instances/{id}/interfaces/{interfaceId}/firewalls
  • POST linode/instances/{id}/upgrade-interfaces
  • POST linode/instances/{id}
  • GET linode/instances/{id}

This includes the following additions:

  • New Models:
    • LinodeInterface and associated JSONObjects
    • LinodeInterfacesSettings and associated JSONObjects
  • New Methods
    • Instance().interface_create(...)
    • Instance().interfaces_settings (property method)
    • Instance().interfaces (property method)
    • NetworkingGroup().ipv6_range_allocate(...) - Unrelated; dependency
  • New Arguments / Fields
    • LinodeGroup().instance_create(...) - interfaces, interface_generation, network_helper

Integration Test Suite Run: https://github.com/linode/linode_api4-python/actions/runs/14862125137/job/41729485455

✔️ How to Test

The following test steps assume you have pulled down this PR locally and run make install.

Unit Testing

make test-unit

Integration Testing

make test-int TEST_COMMAND=models/linode

Manual Testing

  1. In a linode_api4-python sandbox environment (e.g. dx-devenv), run the following:
import os
import time

from linode_api4 import (
    AccountSettingsInterfacesForNewLinodes,
    InterfaceGeneration,
    LinodeClient,
    LinodeInterfaceDefaultRouteOptions,
    LinodeInterfaceOptions,
    LinodeInterfacePublicIPv4AddressOptions,
    LinodeInterfacePublicIPv6Options,
    LinodeInterfacePublicIPv6RangeOptions,
    LinodeInterfacePublicOptions,
    LinodeInterfaceVLANOptions,
    LinodeInterfaceVPCOptions,
)

label = f"manual-test-{int(time.time())}"

linode_client = LinodeClient(
    os.getenv("LINODE_TOKEN"), base_url="https://api.linode.com/v4beta"
)

linode_client.account.settings().interfaces_for_new_linodes = (
    AccountSettingsInterfacesForNewLinodes.linode_default_but_legacy_config_allowed
)

print(
    "Account Settings (interfaces_for_new_linodes):",
    linode_client.account.settings().interfaces_for_new_linodes,
)

firewall = linode_client.networking.firewall_create(
    label,
    rules={
        "inbound_policy": "DROP",
        "outbound_policy": "DROP",
    },
)

print(firewall)

vpc = linode_client.vpcs.create(label, region="us-mia")

subnet = vpc.subnet_create(label, ipv4="10.10.0.0/24")

print(vpc)
print(subnet)

inst, _ = linode_client.linode.instance_create(
    label=label,
    ltype="g6-nanode-1",
    region="us-mia",
    image="linode/ubuntu24.04",
    interface_generation=InterfaceGeneration.LINODE,
    booted=False,
    interfaces=[
        LinodeInterfaceOptions(
            firewall_id=firewall.id,
            default_route=LinodeInterfaceDefaultRouteOptions(ipv6=True),
            public=LinodeInterfacePublicOptions(
                ipv6=LinodeInterfacePublicIPv6Options(
                    ranges=[LinodeInterfacePublicIPv6RangeOptions(range="/64")]
                )
            ),
        ),
        LinodeInterfaceOptions(vlan=LinodeInterfaceVLANOptions(vlan_label="test-vlan")),
    ],
)

print("Linode Interface Generation:", inst.interface_generation)

print(
    "Linode Interface Settings " "(default_route.ipv4_eligible_interface_ids):",
    inst.interfaces_settings.default_route.ipv4_eligible_interface_ids,
)

print(
    "Linode Interface Settings " "(default_route.ipv6_eligible_interface_ids):",
    inst.interfaces_settings.default_route.ipv6_eligible_interface_ids,
)

print("Linode Interfaces:", list(inst.interfaces))

interface = inst.interface_create(
    firewall=firewall.id,
    default_route=LinodeInterfaceDefaultRouteOptions(ipv4=True),
    vpc=LinodeInterfaceVPCOptions(subnet_id=subnet.id),
)

print("New Interface:", interface)
print("New Interface MAC Address:", interface.mac_address)
print(f"New Interface Firewalls:", list(interface.get_firewalls()))

inst.invalidate()

print("Updated Linode Interfaces:", inst.interfaces)

interface = inst.interfaces[0]

print("Public Interface Addresses:", interface.public.ipv4.addresses)

interface.public.ipv4.addresses += [
    LinodeInterfacePublicIPv4AddressOptions(address="auto")
]

interface.save()
interface.invalidate()

print("Updated VPC Interface Ranges:", inst.interfaces[0].public.ipv4.addresses)
  1. Ensure the script finishes successfully and the output looks similar to the following:
Account Settings (interfaces_for_new_linodes): legacy_config_default_but_linode_allowed
Firewall: 2487383
VPC: 183845
VPCSubnet: 181313
Linode Interface Generation: linode
Linode Interface Settings (default_route.ipv4_eligible_interface_ids): [4349]
Linode Interface Settings (default_route.ipv6_eligible_interface_ids): [4349]
Linode Interfaces: [LinodeInterface: 4349, LinodeInterface: 4350]
New Interface: LinodeInterface: 4351
New Interface MAC Address: 22:00:34:35:0f:17
New Interface Firewalls: [Firewall: 2487383]
Updated Linode Interfaces: [LinodeInterface: 4349, LinodeInterface: 4350, LinodeInterface: 4351]
Public Interface Addresses: [LinodeInterfacePublicIPv4Address(address='172.233.184.207', primary=True)]
Updated VPC Interface Ranges: [LinodeInterfacePublicIPv4Address(address='172.233.184.207', primary=True), LinodeInterfacePublicIPv4Address(address='172.233.184.226', primary=False)]
  1. Make arbitrary changes to the script and re-run.

@lgarber-akamai lgarber-akamai added the new-feature for new features in the changelog. label Apr 21, 2025
@lgarber-akamai lgarber-akamai force-pushed the new/enhanced-interfaces-linodes branch from 9d231d3 to 0d67823 Compare April 22, 2025 20:28
Comment on lines +183 to +189
def ipv6_range_allocate(
self,
prefix_length: int,
route_target: Optional[str] = None,
linode: Optional[Union[Instance, int]] = None,
**kwargs,
) -> IPv6Range:
Copy link
Contributor Author

@lgarber-akamai lgarber-akamai May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint wasn't added as part of Enhanced Interfaces but it was oddly missing from the SDK. I added it here to unblock some of the IPv6 interface test cases below.

Let me know if this is worth splitting out into a separate PR 🙂

Comment on lines +1950 to +1951
# NOTE: We do not implement this as a Property because Property does
# not currently have a mechanism for 1:1 sub-entities.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on whether this would be worth implementing as a separate body of work?

@lgarber-akamai lgarber-akamai force-pushed the new/enhanced-interfaces-linodes branch from 1b68419 to ed25e3b Compare May 5, 2025 20:23
@lgarber-akamai lgarber-akamai force-pushed the new/enhanced-interfaces-linodes branch from ed25e3b to 5739a51 Compare May 5, 2025 20:24
"""

params = {
"firewall_id": firewall,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API complains about the firewall_id key if it's fully excluded from the request, but doesn't complain when the key if explicitly specified as null:

[400] firewall_id: No default firewall has been defined for this interface type. Please define a default firewall or provide an ID (or 'null') for a specific firewall.

Does anyone know if this was an intentional API decision or if it's just a bug?

vpc=LinodeInterfaceVPCOptions(
subnet_id=subnet.id,
ipv4=LinodeInterfaceVPCIPv4Options(
# TODO (Enhanced Interfaces): Not currently working as expected
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was running into something that seemed like an API bug here; will report when I have some time

"vpc": Property(mutable=True, json_object=LinodeInterfaceVPC),
}

def get_firewalls(self, *filters) -> List[Firewall]:
Copy link
Contributor Author

@lgarber-akamai lgarber-akamai May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the name get_firewalls(...) rather than firewalls(...) here to prevent ambiguity between this and similar property methods.

Any thoughts on this? I'd be happy to revert it if we feel it's too out of line with the rest of the SDK 🙂

edit: Reverted to align with Instance(...) non-property firewalls(...) method.

@lgarber-akamai lgarber-akamai force-pushed the new/enhanced-interfaces-linodes branch from 37c3a31 to bbb021e Compare May 6, 2025 14:07
Comment on lines -314 to -319
interfaces = kwargs.get("interfaces", None)
if interfaces is not None and isinstance(interfaces, Iterable):
kwargs["interfaces"] = [
i._serialize() if isinstance(i, ConfigInterface) else i
for i in interfaces
]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer necessary due to _flatten_request_body_recursive(drop_null_keys(params)) below

@lgarber-akamai lgarber-akamai marked this pull request as ready for review May 6, 2025 14:20
@lgarber-akamai lgarber-akamai requested a review from a team as a code owner May 6, 2025 14:20
@lgarber-akamai lgarber-akamai removed the request for review from a team May 6, 2025 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new-feature for new features in the changelog.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants