From fcd279f5bf3aa5e978a7aa4c89e405cbd7169fc5 Mon Sep 17 00:00:00 2001 From: uhryniuk Date: Tue, 28 Oct 2025 14:59:19 -0500 Subject: [PATCH 1/4] feat(oci): add instance property to get all ip addresses --- pycloudlib/oci/instance.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/pycloudlib/oci/instance.py b/pycloudlib/oci/instance.py index c401da0b..5060fe50 100644 --- a/pycloudlib/oci/instance.py +++ b/pycloudlib/oci/instance.py @@ -46,6 +46,7 @@ def __init__( self.availability_domain = availability_domain self._fault_domain = None self._ip = None + self._ips: List[str] = [] if oci_config is None: oci_config = oci.config.from_file("~/.oci/config") # noqa: E501 @@ -93,20 +94,32 @@ def ip(self): ] # select vnic with is_primary = True primary_vnic = [vnic for vnic in vnics if vnic.is_primary][0] - # if not public IP, check for ipv6 - # None is specifically returned by OCI when ipv6 only vnic - if primary_vnic.public_ip is None: - if primary_vnic.ipv6_addresses: - self._ip = primary_vnic.ipv6_addresses[0] - self._log.info("Using ipv6 address: %s", self._ip) - else: - raise PycloudlibError("No public ipv4 address or ipv6 address found") - else: + + # attempt to use the IPv4 address if available + if primary_vnic.public_ip: self._ip = primary_vnic.public_ip + self._ips.append(primary_vnic.public_ip) self._log.info("Using ipv4 address: %s", self._ip) - return self._ip + + # fallback to ipv6 address if possible + if primary_vnic.ipv6_addresses: + if self._ip is None: + self._ip = primary_vnic.ipv6_addresses[0] + self._log.info("Using ipv6 address: %s", self._ip) + self.ips.extend(primary_vnic.ipv6_addresses) + + if self._ip is None: + raise PycloudlibError("No public ipv4 address or ipv6 address found") + return self._ip + @property + def ips(self): + """Return IP address of instance.""" + if self._ip is None: + return [] + return self._ips + @property def private_ip(self): """Return private IP address of instance.""" From aa5130d3c5308fe1187e20f8e4054d9c38a937f2 Mon Sep 17 00:00:00 2001 From: uhryniuk Date: Tue, 28 Oct 2025 14:59:46 -0500 Subject: [PATCH 2/4] feat(oci): conditionally allocate ips for v4, v6 and dual stack --- pycloudlib/oci/cloud.py | 16 ++++++++++++++++ pycloudlib/oci/instance.py | 4 ++-- tests/unit_tests/oci/test_cloud.py | 26 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/pycloudlib/oci/cloud.py b/pycloudlib/oci/cloud.py index 79c90605..901503e7 100644 --- a/pycloudlib/oci/cloud.py +++ b/pycloudlib/oci/cloud.py @@ -309,6 +309,17 @@ def launch( self.availability_domain, vcn_name=self.vcn_name, ) + # Configure vNIC for IPv4, IPv6 or dual stack. + subnet = self.network_client.get_subnet(subnet_id).data + vnic_kwargs = {} + + # When no IPv4 CIDR is present, the value is assigned to '' str instead of None. + if "null" not in subnet.cidr_block: + vnic_kwargs["assign_public_ip"] = True + + if subnet.ipv6_cidr_block is not None: + vnic_kwargs["assign_ipv6_ip"] = True + default_metadata = { "ssh_authorized_keys": self.key_pair.public_key_content, } @@ -327,6 +338,11 @@ def launch( image_id=image_id, metadata={**default_metadata, **metadata}, compute_cluster_id=cluster_id, + create_vnic_details=oci.core.models.CreateVnicDetails( + subnet_id=subnet_id, + display_name="primary-vnic", + **vnic_kwargs, + ), **kwargs, ) diff --git a/pycloudlib/oci/instance.py b/pycloudlib/oci/instance.py index 5060fe50..2e1bf508 100644 --- a/pycloudlib/oci/instance.py +++ b/pycloudlib/oci/instance.py @@ -106,7 +106,7 @@ def ip(self): if self._ip is None: self._ip = primary_vnic.ipv6_addresses[0] self._log.info("Using ipv6 address: %s", self._ip) - self.ips.extend(primary_vnic.ipv6_addresses) + self._ips.extend(primary_vnic.ipv6_addresses) if self._ip is None: raise PycloudlibError("No public ipv4 address or ipv6 address found") @@ -116,7 +116,7 @@ def ip(self): @property def ips(self): """Return IP address of instance.""" - if self._ip is None: + if self.ip is None: return [] return self._ips diff --git a/tests/unit_tests/oci/test_cloud.py b/tests/unit_tests/oci/test_cloud.py index d73f7920..d9c1cc7d 100644 --- a/tests/unit_tests/oci/test_cloud.py +++ b/tests/unit_tests/oci/test_cloud.py @@ -40,6 +40,7 @@ def oci_mock(): mock_network_client = mock.Mock() mock_compute_client_class.return_value = mock_compute_client mock_network_client_class.return_value = mock_network_client + mock_network_client.get_subnet.return_value = create_mock_subnet_response() yield mock_compute_client, mock_network_client, oci_config @@ -77,6 +78,26 @@ def oci_cloud(oci_mock, tmp_path): yield oci_cloud +def create_mock_subnet_response( + cidr_block: str = "10.0.0.0/16", + ipv6_cidr_block: str = "2603:c010:2004:8500::/56", + display_name: str = "test-display-name", + subnet_id: str = "subnet-id", + vcn_id: str = "vcn-id" +): + """Creates a mock OCI Response containing a Subnet model. + + Since both `cidr_block` and `ipv6_cidr_block` are provided, the default + subnet is dual stack. + """ + mock_subnet_data = oci.core.models.Subnet( + cidr_block=cidr_block, + ipv6_cidr_block=ipv6_cidr_block, + display_name=display_name, + id=subnet_id, + vcn_id=vcn_id + ) + return oci.response.Response(status=200, headers={}, data=mock_subnet_data, request=None) OCI_PYCLOUDLIB_CONFIG = """\ [oci] @@ -304,6 +325,11 @@ def test_launch_instance(self, mock_wait_till_ready, oci_cloud, oci_mock): args, _ = oci_cloud.compute_client.launch_instance.call_args launch_instance_details = args[0] assert launch_instance_details.subnet_id == "subnet-id" + + # confirm vnic options have been provided for ipv4, ipv6 and dual stack subnets + assert launch_instance_details.create_vnic_details.assign_public_ip + assert launch_instance_details.create_vnic_details.assign_ipv6_ip + assert oci_cloud.get_instance.call_count == 3 From 769b0e69b2a406dede5c7ab0bc15908a3cd4edc4 Mon Sep 17 00:00:00 2001 From: uhryniuk Date: Tue, 28 Oct 2025 15:00:29 -0500 Subject: [PATCH 3/4] feat(oci): bump version file for due to api change --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 0eee9fed..ae096aa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1!10.13.3 +1!10.14.0 From 01c3443809d855ff250a7415768d30e2b2357786 Mon Sep 17 00:00:00 2001 From: uhryniuk Date: Mon, 27 Oct 2025 14:39:55 -0500 Subject: [PATCH 4/4] fix(azure): add None check for new azure library return type --- pycloudlib/azure/instance.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pycloudlib/azure/instance.py b/pycloudlib/azure/instance.py index fe2cc41a..dd0a4b2a 100644 --- a/pycloudlib/azure/instance.py +++ b/pycloudlib/azure/instance.py @@ -184,6 +184,8 @@ def _get_boot_diagnostics(self) -> Optional[str]: ) # Azure has a 60 secs delay for the boot diagnostics to be active. time.sleep(BOOT_DIAGNOSTICS_URI_DELAY) + if not diagnostics.serial_console_log_blob_uri: + raise PycloudlibError("No serial console log blob uri has been set.") response = requests.get(diagnostics.serial_console_log_blob_uri, timeout=10) except ResourceExistsError: self._log.warning("Boot diagnostics not enabled, so none is collected.")