package oci import ( "context" "fmt" "time" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/core" ) // instanceVnics 列出实例的全部 VNIC,并一并返回实例所在 compartment, // 供调用方复用为后续按实例定位查询的 compartmentId,避免重复 GetInstance。 func (c *RealClient) instanceVnics(ctx context.Context, cred Credentials, region, instanceID string) ([]core.Vnic, string, error) { cc, err := c.computeClient(cred, region) if err != nil { return nil, "", err } vn, err := c.vcnClient(cred, region) if err != nil { return nil, "", err } // VNIC 挂载记录在实例所在 compartment,先取实例定位,传租户根会漏查 instResp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instanceID}) if err != nil { return nil, "", fmt.Errorf("get instance %s: %w", instanceID, err) } compartmentID := hostCompartment(cred, deref(instResp.CompartmentId)) resp, err := cc.ListVnicAttachments(ctx, core.ListVnicAttachmentsRequest{ CompartmentId: compartmentID, InstanceId: &instanceID, }) if err != nil { return nil, "", fmt.Errorf("list vnic attachments: %w", err) } return attachedVnics(ctx, vn, resp.Items), *compartmentID, nil } // attachedVnics 逐挂载记录查询 VNIC 详情,跳过查询失败的记录。 func attachedVnics(ctx context.Context, vn core.VirtualNetworkClient, atts []core.VnicAttachment) []core.Vnic { vnics := make([]core.Vnic, 0, len(atts)) for _, att := range atts { if att.VnicId == nil { continue } vnicResp, err := vn.GetVnic(ctx, core.GetVnicRequest{VnicId: att.VnicId}) if err != nil { continue } vnics = append(vnics, vnicResp.Vnic) } return vnics } // lookupPublicIP 查询私有 IP 绑定的公网 IP,未绑定返回 nil。 func lookupPublicIP(ctx context.Context, vn core.VirtualNetworkClient, privateIPID *string) *core.PublicIp { resp, err := vn.GetPublicIpByPrivateIpId(ctx, core.GetPublicIpByPrivateIpIdRequest{ GetPublicIpByPrivateIpIdDetails: core.GetPublicIpByPrivateIpIdDetails{PrivateIpId: privateIPID}, }) if err != nil || resp.Id == nil { return nil } return &resp.PublicIp } // ChangeInstancePublicIP 实现 Client:为实例主 VNIC 主私有 IP 更换临时公网 IP。 // 旧临时 IP 删除、旧保留 IP 自动解绑(资源保留在账户),随后分配新的临时 IP。 func (c *RealClient) ChangeInstancePublicIP(ctx context.Context, cred Credentials, region, instanceID string) (string, error) { vn, err := c.vcnClient(cred, region) if err != nil { return "", err } priv, err := c.primaryPrivateIP(ctx, cred, region, instanceID) if err != nil { return "", err } return replaceEphemeralPublicIP(ctx, vn, cred, priv) } // ChangeVnicPublicIP 实现 Client:为指定 VNIC 的主私有 IP 更换临时公网 IP(次要网卡场景)。 func (c *RealClient) ChangeVnicPublicIP(ctx context.Context, cred Credentials, region, vnicID string) (string, error) { vn, err := c.vcnClient(cred, region) if err != nil { return "", err } priv, err := vnicPrimaryPrivateIP(ctx, vn, vnicID) if err != nil { return "", err } return replaceEphemeralPublicIP(ctx, vn, cred, priv) } // replaceEphemeralPublicIP 释放私有 IP 上现有公网 IP(临时删除、保留解绑)并分配新临时 IP。 func replaceEphemeralPublicIP(ctx context.Context, vn core.VirtualNetworkClient, cred Credentials, priv core.PrivateIp) (string, error) { if err := detachExistingPublicIP(ctx, vn, priv.Id); err != nil { return "", err } // OCI 要求临时公网 IP 与其私有 IP 同 compartment,故不能用生效 compartment resp, err := vn.CreatePublicIp(ctx, core.CreatePublicIpRequest{ CreatePublicIpDetails: core.CreatePublicIpDetails{ CompartmentId: hostCompartment(cred, deref(priv.CompartmentId)), Lifetime: core.CreatePublicIpDetailsLifetimeEphemeral, PrivateIpId: priv.Id, }, }) if err != nil { return "", fmt.Errorf("create public ip: %w", err) } return deref(resp.IpAddress), nil } // detachExistingPublicIP 释放私有 IP 上已绑定的公网 IP: // 临时 IP 直接删除;保留 IP 仅解绑(回到未分配状态,资源不删除)。 func detachExistingPublicIP(ctx context.Context, vn core.VirtualNetworkClient, privateIPID *string) error { existing := lookupPublicIP(ctx, vn, privateIPID) if existing == nil { return nil } if existing.Lifetime != core.PublicIpLifetimeReserved { if _, err := vn.DeletePublicIp(ctx, core.DeletePublicIpRequest{PublicIpId: existing.Id}); err != nil { return fmt.Errorf("delete old public ip: %w", err) } return nil } _, err := vn.UpdatePublicIp(ctx, core.UpdatePublicIpRequest{ PublicIpId: existing.Id, UpdatePublicIpDetails: core.UpdatePublicIpDetails{PrivateIpId: common.String("")}, }) if err != nil { return fmt.Errorf("unassign reserved public ip: %w", err) } waitPublicIPDetached(ctx, vn, privateIPID) return nil } // waitPublicIPDetached 短轮询等私有 IP 上的旧绑定释放;保留 IP 解绑为异步操作, // 立即创建新临时 IP 可能撞上未释放的旧绑定。超时不报错,交由后续创建操作反馈。 func waitPublicIPDetached(ctx context.Context, vn core.VirtualNetworkClient, privateIPID *string) { for i := 0; i < 10; i++ { if lookupPublicIP(ctx, vn, privateIPID) == nil { return } select { case <-ctx.Done(): return case <-time.After(500 * time.Millisecond): } } } // primaryVnic 返回实例的主 VNIC。 func (c *RealClient) primaryVnic(ctx context.Context, cred Credentials, region, instanceID string) (core.Vnic, error) { vnics, _, err := c.instanceVnics(ctx, cred, region, instanceID) if err != nil { return core.Vnic{}, err } for i := range vnics { if vnics[i].IsPrimary != nil && *vnics[i].IsPrimary { return vnics[i], nil } } return core.Vnic{}, fmt.Errorf("instance %s has no primary vnic", instanceID) } // primaryPrivateIP 返回实例主 VNIC 上的主私有 IP 对象; // 保留完整对象是为了让调用方能拿到私有 IP 所在的 compartment。 func (c *RealClient) primaryPrivateIP(ctx context.Context, cred Credentials, region, instanceID string) (core.PrivateIp, error) { vn, err := c.vcnClient(cred, region) if err != nil { return core.PrivateIp{}, err } vnic, err := c.primaryVnic(ctx, cred, region, instanceID) if err != nil { return core.PrivateIp{}, err } privResp, err := vn.ListPrivateIps(ctx, core.ListPrivateIpsRequest{VnicId: vnic.Id}) if err != nil { return core.PrivateIp{}, fmt.Errorf("list private ips: %w", err) } for _, priv := range privResp.Items { if priv.IsPrimary != nil && *priv.IsPrimary { return priv, nil } } return core.PrivateIp{}, fmt.Errorf("change public ip: primary vnic has no primary private ip") }