Skip to content

Commit a6a603b

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 52998d4 commit a6a603b

File tree

7 files changed

+746
-74
lines changed

7 files changed

+746
-74
lines changed

pycloudlib/oci/cloud.py

Lines changed: 38 additions & 1 deletion
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(
@@ -263,6 +266,7 @@ def launch(
263266
username: Optional[str] = None,
264267
cluster_id: Optional[str] = None,
265268
subnet_name: Optional[str] = None,
269+
primary_network_config: Optional[NetworkingConfig] = None,
266270
**kwargs,
267271
) -> OciInstance:
268272
"""Launch an instance.
@@ -273,12 +277,14 @@ def launch(
273277
https://docs.cloud.oracle.com/en-us/iaas/Content/Compute/References/computeshapes.htm
274278
user_data: used by Cloud-Init to run custom scripts or
275279
provide custom Cloud-Init configuration
276-
subnet_name: string, name of subnet to use for instance.
277280
retry_strategy: a retry strategy from oci.retry module
278281
to apply for this operation
279282
username: username to use when connecting via SSH
280283
vcn_name: Name of the VCN to use. If not provided, the first VCN
281284
found will be used
285+
subnet_name: string, name of subnet to use for instance.
286+
primary_network_config: NetworkingConfig object to use for configuring the primary
287+
network interface
282288
**kwargs: dictionary of other arguments to pass as
283289
LaunchInstanceDetails
284290
@@ -297,6 +303,7 @@ def launch(
297303
self.compartment_id,
298304
self.availability_domain,
299305
vcn_name=self.vcn_name,
306+
networking_config=primary_network_config,
300307
)
301308
metadata = {
302309
"ssh_authorized_keys": self.key_pair.public_key_content,
@@ -314,6 +321,10 @@ def launch(
314321
image_id=image_id,
315322
metadata=metadata,
316323
compute_cluster_id=cluster_id,
324+
create_vnic_details=generate_create_vnic_details(
325+
subnet_id=subnet_id,
326+
networking_config=primary_network_config,
327+
),
317328
**kwargs,
318329
)
319330

@@ -328,6 +339,32 @@ def launch(
328339
self.created_instances.append(instance)
329340
return instance
330341

342+
def find_compatible_subnet(self, networking_config: NetworkingConfig) -> str:
343+
"""
344+
Automatically select a subnet that is compatible with the given networking_config.
345+
346+
In this case, compatible means that the subnet can support the necessary networking type
347+
(ipv4 only, ipv6 only, or dual stack) and the private or public requirement.
348+
This method will select the first subnet that matches the criteria.
349+
350+
Args:
351+
networking_config: NetworkingConfig object to use for finding a subnet
352+
353+
Returns:
354+
id of the subnet selected
355+
356+
Raises:
357+
`PycloudlibError` if unable to determine `subnet_id` for the given `networking_config`
358+
"""
359+
subnet_id = get_subnet_id(
360+
network_client=self.network_client,
361+
compartment_id=self.compartment_id,
362+
availability_domain=self.availability_domain,
363+
vcn_name=self.vcn_name,
364+
networking_config=networking_config,
365+
)
366+
return subnet_id
367+
331368
def snapshot(self, instance, clean=True, name=None):
332369
"""Snapshot an instance and generate an image from it.
333370

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)