Skip to content

Commit cef8a8c

Browse files
committed
feat(oracle): allow for specifying network configuration
This commit adds the ability to specify network configuration for launching instances and for attaching vnics to instances. This allows for more control over the network configuration of instances and vnics.
1 parent cf61f58 commit cef8a8c

File tree

7 files changed

+747
-77
lines changed

7 files changed

+747
-77
lines changed

pycloudlib/oci/cloud.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
)
2121
from pycloudlib.oci.instance import OciInstance
2222
from pycloudlib.oci.utils import (
23+
generate_create_vnic_details,
2324
get_subnet_id,
2425
get_subnet_id_by_name,
2526
parse_oci_config_from_env_vars,
2627
wait_till_ready,
2728
)
29+
from pycloudlib.types import NetworkingConfig
2830
from pycloudlib.util import UBUNTU_RELEASE_VERSION_MAP, subp
2931

3032

@@ -251,6 +253,7 @@ def get_instance(self, instance_id, *, username: Optional[str] = None, **kwargs)
251253
availability_domain=self.availability_domain,
252254
oci_config=self.oci_config,
253255
username=username,
256+
vcn_name=self.vcn_name,
254257
)
255258

256259
def launch(
@@ -265,6 +268,7 @@ def launch(
265268
subnet_id: Optional[str] = None,
266269
subnet_name: Optional[str] = None,
267270
metadata: Dict = {},
271+
primary_network_config: Optional[NetworkingConfig] = None,
268272
**kwargs,
269273
) -> OciInstance:
270274
"""Launch an instance.
@@ -286,6 +290,9 @@ def launch(
286290
username: username to use when connecting via SSH
287291
vcn_name: Name of the VCN to use. If not provided, the first VCN
288292
found will be used
293+
subnet_name: string, name of subnet to use for instance.
294+
primary_network_config: NetworkingConfig object to use for configuring the primary
295+
network interface
289296
**kwargs: dictionary of other arguments to pass as
290297
LaunchInstanceDetails
291298
@@ -296,18 +303,16 @@ def launch(
296303
if not image_id:
297304
raise ValueError(f"{self._type} launch requires image_id param. Found: {image_id}")
298305

299-
# provided subnet_id takes the highest precendence
300306
if not subnet_id:
301307
if subnet_name:
302-
subnet_id = get_subnet_id_by_name(
303-
self.network_client, self.compartment_id, subnet_name
304-
)
308+
subnet_id = get_subnet_id_by_name(self.network_client, self.compartment_id, subnet_name)
305309
else:
306310
subnet_id = get_subnet_id(
307311
self.network_client,
308312
self.compartment_id,
309313
self.availability_domain,
310314
vcn_name=self.vcn_name,
315+
networking_config=primary_network_config,
311316
)
312317
default_metadata = {
313318
"ssh_authorized_keys": self.key_pair.public_key_content,
@@ -327,6 +332,10 @@ def launch(
327332
image_id=image_id,
328333
metadata={**default_metadata, **metadata},
329334
compute_cluster_id=cluster_id,
335+
create_vnic_details=generate_create_vnic_details(
336+
subnet_id=subnet_id,
337+
networking_config=primary_network_config,
338+
),
330339
**kwargs,
331340
)
332341

@@ -341,6 +350,32 @@ def launch(
341350
self.created_instances.append(instance)
342351
return instance
343352

353+
def find_compatible_subnet(self, networking_config: NetworkingConfig) -> str:
354+
"""
355+
Automatically select a subnet that is compatible with the given networking_config.
356+
357+
In this case, compatible means that the subnet can support the necessary networking type
358+
(ipv4 only, ipv6 only, or dual stack) and the private or public requirement.
359+
This method will select the first subnet that matches the criteria.
360+
361+
Args:
362+
networking_config: NetworkingConfig object to use for finding a subnet
363+
364+
Returns:
365+
id of the subnet selected
366+
367+
Raises:
368+
`PycloudlibError` if unable to determine `subnet_id` for the given `networking_config`
369+
"""
370+
subnet_id = get_subnet_id(
371+
network_client=self.network_client,
372+
compartment_id=self.compartment_id,
373+
availability_domain=self.availability_domain,
374+
vcn_name=self.vcn_name,
375+
networking_config=networking_config,
376+
)
377+
return subnet_id
378+
344379
def snapshot(self, instance, clean=True, name=None):
345380
"""Snapshot an instance and generate an image from it.
346381

pycloudlib/oci/instance.py

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010

1111
from pycloudlib.errors import PycloudlibError
1212
from pycloudlib.instance import BaseInstance
13-
from pycloudlib.oci.utils import get_subnet_id, get_subnet_id_by_name, wait_till_ready
13+
from pycloudlib.oci.utils import (
14+
generate_create_vnic_details,
15+
get_subnet_id,
16+
get_subnet_id_by_name,
17+
wait_till_ready,
18+
)
19+
from pycloudlib.types import NetworkingConfig
1420

1521

1622
class OciInstance(BaseInstance):
@@ -27,6 +33,7 @@ def __init__(
2733
oci_config=None,
2834
*,
2935
username: Optional[str] = None,
36+
vcn_name: Optional[str] = None,
3037
):
3138
"""Set up the instance.
3239
@@ -46,6 +53,7 @@ def __init__(
4653
self.availability_domain = availability_domain
4754
self._fault_domain = None
4855
self._ip = None
56+
self._vcn_name: Optional[str] = vcn_name
4957

5058
if oci_config is None:
5159
oci_config = oci.config.from_file("~/.oci/config") # noqa: E501
@@ -145,7 +153,8 @@ def secondary_vnic_private_ip(self) -> Optional[str]:
145153
for vnic_attachment in vnic_attachments
146154
]
147155
secondary_vnic_attachment = [vnic for vnic in vnics if not vnic.is_primary][0]
148-
return secondary_vnic_attachment.private_ip
156+
self._log.debug("secondary vnic attachment data:\n%s", secondary_vnic_attachment)
157+
return secondary_vnic_attachment.private_ip or secondary_vnic_attachment.ipv6_addresses[0]
149158

150159
@property
151160
def instance_data(self):
@@ -258,7 +267,7 @@ def get_secondary_vnic_ip(self) -> str:
258267
def add_network_interface(
259268
self,
260269
nic_index: int = 0,
261-
use_private_subnet: bool = False,
270+
networking_config: Optional[NetworkingConfig] = None,
262271
subnet_name: Optional[str] = None,
263272
**kwargs: Any,
264273
) -> str:
@@ -270,13 +279,19 @@ def add_network_interface(
270279
271280
Args:
272281
nic_index: The index of the NIC to add
273-
subnet_name: Name of the subnet to add the NIC to. If not provided,
274-
will use `use_private_subnet` to select first available subnet.
275-
use_private_subnet: If True, will select the first available private
276-
subnet. If False, will select the first available public subnet.
277-
This is only used if `subnet_name` is not provided.
282+
networking_config: Networking configuration to use when selecting subnet. This specifies
283+
the networking type (ipv4, ipv6, or dualstack) and whether to use a public or
284+
private subnet. If not provided, will default to selecting the first public subnet
285+
found.
286+
subnet_name: Name of the subnet to add the NIC to. If provided, this subnet will
287+
blindly be selected and networking_config will be ignored.
288+
289+
Returns:
290+
str: The private IP address of the added network interface.
278291
"""
279292
if subnet_name:
293+
if networking_config:
294+
self._log.debug("Ignoring networking_config when subnet_name is provided.")
280295
subnet_id = get_subnet_id_by_name(
281296
self.network_client,
282297
self.compartment_id,
@@ -287,10 +302,11 @@ def add_network_interface(
287302
self.network_client,
288303
self.compartment_id,
289304
self.availability_domain,
290-
private=use_private_subnet,
305+
networking_config=networking_config,
306+
vcn_name=self._vcn_name,
291307
)
292-
create_vnic_details = oci.core.models.CreateVnicDetails( # noqa: E501
293-
subnet_id=subnet_id,
308+
create_vnic_details = generate_create_vnic_details(
309+
subnet_id=subnet_id, networking_config=networking_config
294310
)
295311
attach_vnic_details = oci.core.models.AttachVnicDetails( # noqa: E501
296312
create_vnic_details=create_vnic_details,
@@ -304,13 +320,29 @@ def add_network_interface(
304320
desired_state=vnic_attachment_data.LIFECYCLE_STATE_ATTACHED,
305321
)
306322
vnic_data = self.network_client.get_vnic(vnic_attachment_data.vnic_id).data
323+
self._log.debug(
324+
"Newly attached vnic data:\n%s",
325+
vnic_data,
326+
)
327+
try:
328+
new_ip = vnic_data.private_ip or vnic_data.ipv6_addresses[0]
329+
except IndexError:
330+
err_msg = (
331+
"Unexpected error occurred when trying to retrieve local IP address of the "
332+
"newly attached NIC. No private IP or IPv6 address found."
333+
)
334+
self._log.error(
335+
err_msg + "Full vnic data for debugging purposes:\n%s",
336+
vnic_data,
337+
)
338+
raise PycloudlibError(err_msg)
307339
self._log.info(
308-
"Added network interface with private IP %s to instance %s on nic #%s",
309-
vnic_data.private_ip,
340+
"Added network interface with IP %s to instance %s on nic #%s",
341+
new_ip,
310342
self.instance_id,
311343
nic_index,
312344
)
313-
return vnic_data.private_ip
345+
return new_ip
314346

315347
def remove_network_interface(self, ip_address: str):
316348
"""Remove network interface based on IP address.
@@ -355,14 +387,20 @@ def configure_secondary_vnic(self) -> str:
355387
or if the IP address was not successfully assigned to the interface.
356388
PycloudlibError: If failed to fetch secondary VNIC data from the Oracle Cloud metadata service.
357389
"""
358-
if not self.secondary_vnic_private_ip:
390+
secondary_ip = self.secondary_vnic_private_ip
391+
if not secondary_ip:
359392
raise ValueError("Cannot configure secondary VNIC without a secondary VNIC attached")
393+
if ":" in secondary_ip:
394+
imds_url = "http://[fd00:c1::a9fe:a9fe]/opc/v1/vnics"
395+
else:
396+
imds_url = "http://169.254.169.254/opc/v1/vnics"
397+
360398
secondary_vnic_imds_data: Optional[Dict[str, str]] = None
361399
# it can take a bit for the secondary VNIC to show up in the IMDS
362400
# so we need to retry fetching the data for roughly a minute
363401
for _ in range(60):
364402
# Fetch JSON data from the Oracle Cloud metadata service
365-
imds_req = self.execute("curl -s http://169.254.169.254/opc/v1/vnics").stdout
403+
imds_req = self.execute(f"curl -s {imds_url}").stdout
366404
vnics_data = json.loads(imds_req)
367405
if len(vnics_data) > 1:
368406
self._log.debug("Successfully fetched secondary VNIC data from IMDS")

0 commit comments

Comments
 (0)