Skip to content

Commit c123dc2

Browse files
committed
feat(oracle): add function to auto configure secondary vnic
1 parent eeff76d commit c123dc2

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed

pycloudlib/oci/instance.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
# This file is part of pycloudlib. See LICENSE file for license information.
33
"""OCI instance."""
44

5+
import json
56
from time import sleep
7+
import time
68
from typing import Dict, List, Optional
79

810
import oci
@@ -327,3 +329,46 @@ def remove_network_interface(self, ip_address: str):
327329
)
328330
return
329331
raise PycloudlibError(f"Network interface with ip_address={ip_address} did not detach")
332+
333+
def configure_secondary_vnic(self) -> str:
334+
if not self.secondary_vnic_private_ip:
335+
raise ValueError("Cannot configure secondary VNIC without a secondary VNIC attached")
336+
secondary_vnic_imds_data: Optional[Dict[str, str]] = None
337+
# it can take a bit for the
338+
for _ in range(60):
339+
# Fetch JSON data from the Oracle Cloud metadata service
340+
imds_req = self.execute("curl -s http://169.254.169.254/opc/v1/vnics").stdout
341+
vnics_data = json.loads(imds_req)
342+
if len(vnics_data) > 1:
343+
self._log.debug("Successfully fetched secondary VNIC data from IMDS")
344+
secondary_vnic_imds_data = vnics_data[1]
345+
break
346+
self._log.debug("No secondary VNIC data found from IMDS, retrying...")
347+
time.sleep(1)
348+
349+
if not secondary_vnic_imds_data:
350+
raise PycloudlibError(
351+
"Failed to fetch secondary VNIC data from IMDS. Cannot configure secondary VNIC"
352+
)
353+
354+
# Extract MAC address and private IP from the second VNIC
355+
mac_addr = secondary_vnic_imds_data["macAddr"]
356+
private_ip = secondary_vnic_imds_data["privateIp"]
357+
subnet_mask = secondary_vnic_imds_data["subnetCidrBlock"].split("/")[1]
358+
# Find the network interface corresponding to the MAC address
359+
interface = self.execute(
360+
f"ip link show | grep -B1 {mac_addr} | head -n1 | awk '{{print $2}}' | sed 's/://' "
361+
).stdout.strip()
362+
# Check if the interface was found
363+
if not interface:
364+
raise ValueError(f"No interface found for MAC address {mac_addr}")
365+
# Add the IP address to the interface
366+
self.execute(
367+
f"sudo ip addr add {private_ip}/{subnet_mask} dev {interface}"
368+
)
369+
# Verify that the IP address was added
370+
r = self.execute(f"ip addr show dev {interface}")
371+
if private_ip not in r.stdout:
372+
raise ValueError(f"IP {private_ip} was not successfully assigned to interface {interface}")
373+
self._log.info("Successfully assigned IP %s to interface %s", private_ip, interface)
374+
return private_ip

tests/unit_tests/oci/test_instance.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,77 @@ def test_oci_instance_ip_parametrized(
487487
ip = oci_instance.ip
488488
# the following will only run if no error is raised
489489
assert ip == expected_ip
490+
491+
492+
class TestConfigureSecondaryVnic:
493+
def test_configure_secondary_vnic_success(self, oci_instance:OciInstance):
494+
with mock.patch.object(
495+
OciInstance, "secondary_vnic_private_ip", new_callable=mock.PropertyMock
496+
) as mock_secondary_ip:
497+
mock_secondary_ip.return_value = "10.0.0.5"
498+
# Provide a side_effect matching the four execute() calls
499+
oci_instance.execute = mock.Mock(side_effect=[
500+
mock.Mock(stdout='[{"macAddr": "00:16:3e:aa:bb:cc","privateIp":"10.0.0.4",'
501+
'"subnetCidrBlock":"10.0.0.0/24"},'
502+
'{"macAddr":"00:16:3e:ee:ff:gg","privateIp":"10.0.0.5",'
503+
'"subnetCidrBlock":"10.0.1.0/24"}]'),
504+
mock.Mock(stdout="eth1"),
505+
mock.Mock(stdout=""),
506+
mock.Mock(stdout="10.0.0.5"),
507+
])
508+
ip = oci_instance.configure_secondary_vnic()
509+
assert ip == "10.0.0.5"
510+
511+
def test_configure_secondary_vnic_no_secondary(self, oci_instance:OciInstance):
512+
with mock.patch.object(
513+
OciInstance, "secondary_vnic_private_ip", new_callable=mock.PropertyMock
514+
) as mock_secondary_ip:
515+
mock_secondary_ip.return_value = None
516+
with pytest.raises(ValueError, match="Cannot configure secondary VNIC"):
517+
oci_instance.configure_secondary_vnic()
518+
519+
# patch out time.sleep
520+
@mock.patch("time.sleep", mock.MagicMock())
521+
def test_configure_secondary_vnic_unavailable_imds(self, oci_instance:OciInstance):
522+
with mock.patch.object(
523+
OciInstance, "secondary_vnic_private_ip", new_callable=mock.PropertyMock
524+
) as mock_secondary_ip:
525+
mock_secondary_ip.return_value = "10.0.0.5"
526+
# Return empty IMDS data each time
527+
oci_instance.execute = mock.Mock(side_effect=[mock.Mock(stdout="[]")] * 60)
528+
with pytest.raises(PycloudlibError, match="Failed to fetch secondary VNIC data"):
529+
oci_instance.configure_secondary_vnic()
530+
531+
def test_configure_secondary_vnic_no_interface_found(self, oci_instance:OciInstance):
532+
with mock.patch.object(
533+
OciInstance, "secondary_vnic_private_ip", new_callable=mock.PropertyMock
534+
) as mock_secondary_ip:
535+
mock_secondary_ip.return_value = "10.0.0.5"
536+
# IMDS returns one entry
537+
oci_instance.execute = mock.Mock(side_effect=[
538+
mock.Mock(stdout='[{"macAddr": "00:16:3e:aa:bb:cc","privateIp":"10.0.0.4",'
539+
'"subnetCidrBlock":"10.0.0.0/24"},'
540+
'{"macAddr":"00:16:3e:ee:ff:gg","privateIp":"10.0.0.5",'
541+
'"subnetCidrBlock":"10.0.1.0/24"}]'),
542+
mock.Mock(stdout=""), # No interface found
543+
])
544+
with pytest.raises(ValueError, match="No interface found for MAC address"):
545+
oci_instance.configure_secondary_vnic()
546+
547+
def test_configure_secondary_vnic_ip_not_assigned(self, oci_instance:OciInstance):
548+
with mock.patch.object(
549+
OciInstance, "secondary_vnic_private_ip", new_callable=mock.PropertyMock
550+
) as mock_secondary_ip:
551+
mock_secondary_ip.return_value = "10.0.0.5"
552+
# Returns the single IMDS entry, then interface, then IP add, then empty IP check
553+
oci_instance.execute = mock.Mock(side_effect=[
554+
mock.Mock(stdout='[{"macAddr": "00:16:3e:aa:bb:cc","privateIp":"10.0.0.4",'
555+
'"subnetCidrBlock":"10.0.0.0/24"},'
556+
'{"macAddr":"00:16:3e:ee:ff:gg","privateIp":"10.0.0.5",'
557+
'"subnetCidrBlock":"10.0.1.0/24"}]'),
558+
mock.Mock(stdout="eth1"),
559+
mock.Mock(stdout=""),
560+
mock.Mock(stdout=""), # Nothing found
561+
])
562+
with pytest.raises(ValueError, match="was not successfully assigned"):
563+
oci_instance.configure_secondary_vnic()

0 commit comments

Comments
 (0)