package service import ( "context" "fmt" "log" "time" "oci-portal/internal/oci" ) // ReservedIPs 列出指定区间的保留公网 IP(compartmentID 空为生效 compartment)。 func (s *OciConfigService) ReservedIPs(ctx context.Context, id uint, region, compartmentID string) ([]oci.ReservedIP, error) { cred, err := s.credentialsByID(ctx, id) if err != nil { return nil, err } return s.client.ListReservedIPs(ctx, cred, region, compartmentID) } // CreateReservedIP 在指定区间创建保留公网 IP。 func (s *OciConfigService) CreateReservedIP(ctx context.Context, id uint, region, compartmentID, displayName string) (oci.ReservedIP, error) { cred, err := s.credentialsByID(ctx, id) if err != nil { return oci.ReservedIP{}, err } return s.client.CreateReservedIP(ctx, cred, region, compartmentID, displayName) } // AssignReservedIP 绑定保留 IP:vnicID 非空绑到该网卡主私有 IP, // 否则绑实例主网卡(instanceID 为空表示解绑)。 func (s *OciConfigService) AssignReservedIP(ctx context.Context, id uint, region, publicIPID, instanceID, vnicID string) error { if publicIPID == "" { return fmt.Errorf("assign reserved ip: publicIpId is required") } cred, err := s.credentialsByID(ctx, id) if err != nil { return err } if vnicID != "" { return s.client.AssignReservedIPToVnic(ctx, cred, region, publicIPID, vnicID) } return s.client.AssignReservedIP(ctx, cred, region, publicIPID, instanceID) } // DeleteReservedIP 删除保留公网 IP。 func (s *OciConfigService) DeleteReservedIP(ctx context.Context, id uint, region, publicIPID string) error { if publicIPID == "" { return fmt.Errorf("delete reserved ip: publicIpId is required") } cred, err := s.credentialsByID(ctx, id) if err != nil { return err } return s.client.DeleteReservedIP(ctx, cred, region, publicIPID) } // 保留 IP 后台绑定的轮询节奏与超时上限。 const ( reservedBindInterval = 10 * time.Second reservedBindTimeout = 8 * time.Minute ) // goBind 启动保留 IP 后台绑定 goroutine 并纳入 WaitGroup,供 Stop 等待收尾。 func (s *OciConfigService) goBind(fn func()) { s.bindWG.Add(1) go func() { defer s.bindWG.Done() fn() }() } // Stop 通知在途的保留 IP 绑定 goroutine 提前退出并等待全部收尾;仅关服时调用一次。 func (s *OciConfigService) Stop() { close(s.bindStop) s.bindWG.Wait() } // bindReservedIPToVnicWhenReady 等次要 VNIC 就绪后绑定保留 IP;附加网卡的后台编排, // 失败只记日志(附加请求本身已成功返回)。 func (s *OciConfigService) bindReservedIPToVnicWhenReady(cred oci.Credentials, region, instanceID, attachmentID, publicIPID string) { ctx, cancel := context.WithTimeout(context.Background(), reservedBindTimeout) defer cancel() for { vnics, err := s.client.ListInstanceVnics(ctx, cred, region, instanceID) if err == nil { if vnicID := readyVnicID(vnics, attachmentID); vnicID != "" { if err := s.client.AssignReservedIPToVnic(ctx, cred, region, publicIPID, vnicID); err != nil { log.Printf("[reserved-ip] bind %s to vnic %s failed: %v", publicIPID, vnicID, err) } return } } select { case <-ctx.Done(): log.Printf("[reserved-ip] bind %s timed out waiting for attachment %s", publicIPID, attachmentID) return case <-s.bindStop: return case <-time.After(reservedBindInterval): } } } // readyVnicID 从网卡列表找出指定附加关系中已就绪的 VNIC。 func readyVnicID(vnics []oci.Vnic, attachmentID string) string { for _, v := range vnics { if v.AttachmentID == attachmentID && v.VnicID != "" && v.LifecycleState == "ATTACHED" { return v.VnicID } } return "" } // bindReservedIPWhenReady 等实例 RUNNING 后绑定保留 IP;创建实例的后台编排, // 失败只记日志(创建请求本身已成功返回)。 func (s *OciConfigService) bindReservedIPWhenReady(cred oci.Credentials, region, instanceID, publicIPID string) { ctx, cancel := context.WithTimeout(context.Background(), reservedBindTimeout) defer cancel() for { inst, err := s.client.GetInstance(ctx, cred, region, instanceID) if err == nil && inst.LifecycleState == "RUNNING" { if err := s.client.AssignReservedIP(ctx, cred, region, publicIPID, instanceID); err != nil { log.Printf("[reserved-ip] bind %s to instance %s failed: %v", publicIPID, instanceID, err) } return } select { case <-ctx.Done(): log.Printf("[reserved-ip] bind %s timed out waiting for instance %s", publicIPID, instanceID) return case <-s.bindStop: return case <-time.After(reservedBindInterval): } } }