feat(region,host,climc): support pod host random port mapping

This commit is contained in:
Zexi Li
2024-03-13 17:37:43 +08:00
parent 1bc14d251b
commit 749ec1849c
8 changed files with 867 additions and 78 deletions
+23 -9
View File
@@ -22,8 +22,9 @@ const (
)
const (
POD_METADATA_CRI_ID = "cri_id"
POD_METADATA_CRI_CONFIG = "cri_config"
POD_METADATA_CRI_ID = "cri_id"
POD_METADATA_CRI_CONFIG = "cri_config"
POD_METADATA_PORT_MAPPINGS = "port_mappings"
)
type PodContainerCreateInput struct {
@@ -35,16 +36,22 @@ type PodContainerCreateInput struct {
type PodPortMappingProtocol string
const (
PodPortMappingProtocolTCP = "tcp"
PodPortMappingProtocolUDP = "udp"
PodPortMappingProtocolSCTP = "sctp"
PodPortMappingProtocolTCP = "tcp"
PodPortMappingProtocolUDP = "udp"
//PodPortMappingProtocolSCTP = "sctp"
)
type PodPortMappingPortRange struct {
Start int `json:"start"`
End int `json:"end"`
}
type PodPortMapping struct {
Protocol PodPortMappingProtocol `json:"protocol"`
ContainerPort int32 `json:"container_port"`
HostPort int32 `json:"host_port"`
HostIp string `json:"host_ip"`
Protocol PodPortMappingProtocol `json:"protocol"`
ContainerPort int `json:"container_port"`
HostPort *int `json:"host_port,omitempty"`
HostIp string `json:"host_ip"`
HostPortRange *PodPortMappingPortRange `json:"host_port_range,omitempty"`
}
type PodCreateInput struct {
@@ -56,3 +63,10 @@ type PodStartResponse struct {
CRIId string `json:"cri_id"`
IsRunning bool `json:"is_running"`
}
type PodMetadataPortMapping struct {
Protocol PodPortMappingProtocol `json:"protocol"`
ContainerPort int32 `json:"container_port"`
HostPort int32 `json:"host_port,omitempty"`
HostIp string `json:"host_ip"`
}
+42 -6
View File
@@ -89,8 +89,19 @@ func (p *SPodDriver) ValidateCreateData(ctx context.Context, userCred mcclient.T
}
func (p *SPodDriver) validatePortMappings(input *api.PodCreateInput) error {
usedPorts := make(map[api.PodPortMappingProtocol]sets.Int)
for idx, pm := range input.PortMappings {
// TODO: 判断 host port 是否重复
ports, ok := usedPorts[pm.Protocol]
if !ok {
ports = sets.NewInt()
}
if pm.HostPort != nil {
if ports.Has(*pm.HostPort) {
return httperrors.NewInputParameterError("%s host_port %d is already specified", pm.Protocol, *pm.HostPort)
}
ports.Insert(*pm.HostPort)
}
usedPorts[pm.Protocol] = ports
if err := p.validatePortMapping(pm); err != nil {
return errors.Wrapf(err, "validate portmapping %d", idx)
}
@@ -98,6 +109,11 @@ func (p *SPodDriver) validatePortMappings(input *api.PodCreateInput) error {
return nil
}
func (p *SPodDriver) validateHostPortMapping(hostId string, pm *api.PodPortMapping) error {
// TODO:
return nil
}
func (p *SPodDriver) validateContainerData(ctx context.Context, userCred mcclient.TokenCredential, idx int, defaultNamePrefix string, ctr *api.PodContainerCreateInput, input *api.ServerCreateInput) error {
if ctr.Name == "" {
ctr.Name = fmt.Sprintf("%s-%d", defaultNamePrefix, idx)
@@ -137,7 +153,22 @@ func (p *SPodDriver) validateContainerVolumeMount(ctx context.Context, userCred
return nil
}
func (p *SPodDriver) validatePortRange(port int32) error {
func (p *SPodDriver) validatePortRange(portRange *api.PodPortMappingPortRange) error {
if portRange != nil {
if portRange.Start > portRange.End {
return httperrors.NewInputParameterError("port range start %d is large than %d", portRange.Start, portRange.End)
}
if portRange.Start <= 0 {
return httperrors.NewInputParameterError("port range start %d <= 0", portRange.Start)
}
if portRange.End > 65535 {
return httperrors.NewInputParameterError("port range end %d > 65535", portRange.End)
}
}
return nil
}
func (p *SPodDriver) validatePort(port int) error {
if port <= 0 || port > 65535 {
return httperrors.NewInputParameterError("port number %d isn't within 1 to 65535", port)
}
@@ -145,16 +176,21 @@ func (p *SPodDriver) validatePortRange(port int32) error {
}
func (p *SPodDriver) validatePortMapping(pm *api.PodPortMapping) error {
if err := p.validatePortRange(pm.HostPort); err != nil {
return errors.Wrap(err, "validate host_port")
if err := p.validatePortRange(pm.HostPortRange); err != nil {
return err
}
if err := p.validatePortRange(pm.ContainerPort); err != nil {
if pm.HostPort != nil {
if err := p.validatePort(*pm.HostPort); err != nil {
return errors.Wrap(err, "validate host_port")
}
}
if err := p.validatePort(pm.ContainerPort); err != nil {
return errors.Wrap(err, "validate container_port")
}
if pm.Protocol == "" {
pm.Protocol = api.PodPortMappingProtocolTCP
}
if !sets.NewString(api.PodPortMappingProtocolSCTP, api.PodPortMappingProtocolUDP, api.PodPortMappingProtocolTCP).Has(string(pm.Protocol)) {
if !sets.NewString(api.PodPortMappingProtocolUDP, api.PodPortMappingProtocolTCP).Has(string(pm.Protocol)) {
return httperrors.NewInputParameterError("unsupported protocol %s", pm.Protocol)
}
return nil
+169 -21
View File
@@ -1,3 +1,17 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package guestman
import (
@@ -13,6 +27,7 @@ import (
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/sets"
"yunion.io/x/onecloud/pkg/apis"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
@@ -30,6 +45,7 @@ import (
"yunion.io/x/onecloud/pkg/mcclient/auth"
computemod "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/util/fileutils2"
"yunion.io/x/onecloud/pkg/util/netutils2/getport"
"yunion.io/x/onecloud/pkg/util/pod"
)
@@ -243,6 +259,111 @@ func (s *sPodGuestInstance) getPodPrivilegedMode(input *computeapi.PodCreateInpu
return false
}
func (s *sPodGuestInstance) getPortMappings(input []*computeapi.PodPortMapping) ([]*runtimeapi.PortMapping, error) {
result := make([]*runtimeapi.PortMapping, len(input))
for idx := range input {
pm, err := s.getPortMapping(input[idx])
if err != nil {
return nil, errors.Wrapf(err, "get port mapping %s", jsonutils.Marshal(input[idx]))
}
result[idx] = pm
}
return result, nil
}
func (s *sPodGuestInstance) getOtherPods() []*sPodGuestInstance {
man := s.manager
otherPods := make([]*sPodGuestInstance, 0)
man.Servers.Range(func(id, value any) bool {
if id == s.Id {
return true
}
ins := value.(GuestRuntimeInstance)
pod, ok := ins.(*sPodGuestInstance)
if !ok {
return true
}
otherPods = append(otherPods, pod)
return true
})
return otherPods
}
func (s *sPodGuestInstance) getOtherPodsUsedPorts() (map[computeapi.PodPortMappingProtocol]sets.Int, error) {
otherPods := s.getOtherPods()
ret := make(map[computeapi.PodPortMappingProtocol]sets.Int)
for _, pod := range otherPods {
pms, err := pod.GetPodMetadataPortMappings()
if err != nil {
return nil, errors.Wrapf(err, "get pod %s port_mappins", pod.GetId())
}
for _, pm := range pms {
ps, ok := ret[pm.Protocol]
if !ok {
ps = sets.NewInt()
}
ps.Insert(int(pm.HostPort))
ret[pm.Protocol] = ps
}
}
return ret, nil
}
func (s *sPodGuestInstance) getPortMapping(pm *computeapi.PodPortMapping) (*runtimeapi.PortMapping, error) {
runtimePm := &runtimeapi.PortMapping{
ContainerPort: int32(pm.ContainerPort),
HostIp: pm.HostIp,
}
portProtocol := getport.TCP
switch pm.Protocol {
case computeapi.PodPortMappingProtocolTCP:
runtimePm.Protocol = runtimeapi.Protocol_TCP
portProtocol = getport.TCP
case computeapi.PodPortMappingProtocolUDP:
runtimePm.Protocol = runtimeapi.Protocol_UDP
portProtocol = getport.UDP
//case computeapi.PodPortMappingProtocolSCTP:
// runtimePm.Protocol = runtimeapi.Protocol_SCTP
default:
return nil, errors.Errorf("invalid protocol: %q", pm.Protocol)
}
// listen random port
otherPorts, err := s.getOtherPodsUsedPorts()
if err != nil {
return nil, errors.Wrap(err, "getOtherPodsUsedPorts")
}
if pm.HostPort != nil {
runtimePm.HostPort = int32(*pm.HostPort)
if getport.IsPortUsed(portProtocol, "", *pm.HostPort) {
return nil, httperrors.NewInputParameterError("host_port %d is used", pm.HostPort)
}
usedPorts, ok := otherPorts[pm.Protocol]
if ok {
if usedPorts.Has(*pm.HostPort) {
return nil, errors.Wrapf(err, "%s host_port %d is already used", pm.Protocol, *pm.HostPort)
}
}
return runtimePm, nil
} else {
start := 20000
end := 25000
if pm.HostPortRange != nil {
start = pm.HostPortRange.Start
end = pm.HostPortRange.End
}
otherPodPorts, ok := otherPorts[pm.Protocol]
if !ok {
otherPodPorts = sets.NewInt()
}
portResult, err := getport.GetPortByRangeBySets(portProtocol, start, end, otherPodPorts)
if err != nil {
return nil, errors.Wrapf(err, "listen %s port inside %d and %d", pm.Protocol, start, end)
}
runtimePm.HostPort = int32(portResult.Port)
return runtimePm, nil
}
}
func (s *sPodGuestInstance) startPod(ctx context.Context, userCred mcclient.TokenCredential) (*computeapi.PodStartResponse, error) {
podInput, err := s.getPodCreateParams()
if err != nil {
@@ -284,26 +405,11 @@ func (s *sPodGuestInstance) startPod(ctx context.Context, userCred mcclient.Toke
}
if len(podInput.PortMappings) != 0 {
podCfg.PortMappings = make([]*runtimeapi.PortMapping, len(podInput.PortMappings))
for idx := range podInput.PortMappings {
pm := podInput.PortMappings[idx]
runtimePm := &runtimeapi.PortMapping{
ContainerPort: pm.ContainerPort,
HostPort: pm.HostPort,
HostIp: pm.HostIp,
}
switch pm.Protocol {
case computeapi.PodPortMappingProtocolTCP:
runtimePm.Protocol = runtimeapi.Protocol_TCP
case computeapi.PodPortMappingProtocolUDP:
runtimePm.Protocol = runtimeapi.Protocol_UDP
case computeapi.PodPortMappingProtocolSCTP:
runtimePm.Protocol = runtimeapi.Protocol_SCTP
default:
return nil, errors.Errorf("invalid protocol: %q", pm.Protocol)
}
podCfg.PortMappings[idx] = runtimePm
pms, err := s.getPortMappings(podInput.PortMappings)
if err != nil {
return nil, errors.Wrap(err, "get port mappings")
}
podCfg.PortMappings = pms
}
criId, err := s.getCRI().RunPod(ctx, podCfg, "")
@@ -435,15 +541,41 @@ func (s *sPodGuestInstance) getCRIId() string {
return s.GetSourceDesc().Metadata[computeapi.POD_METADATA_CRI_ID]
}
func (s *sPodGuestInstance) convertToPodMetadataPortMappings(cfg *runtimeapi.PodSandboxConfig) []*computeapi.PodMetadataPortMapping {
if cfg.PortMappings == nil {
return []*computeapi.PodMetadataPortMapping{}
}
ret := make([]*computeapi.PodMetadataPortMapping, len(cfg.PortMappings))
for idx := range cfg.PortMappings {
pm := cfg.PortMappings[idx]
var proto computeapi.PodPortMappingProtocol = computeapi.PodPortMappingProtocolTCP
if pm.Protocol == runtimeapi.Protocol_UDP {
proto = computeapi.PodPortMappingProtocolUDP
}
ret[idx] = &computeapi.PodMetadataPortMapping{
Protocol: proto,
ContainerPort: pm.ContainerPort,
HostPort: pm.HostPort,
HostIp: pm.HostIp,
}
}
return ret
}
func (s *sPodGuestInstance) setCRIInfo(ctx context.Context, userCred mcclient.TokenCredential, criId string, cfg *runtimeapi.PodSandboxConfig) error {
s.Desc.Metadata[computeapi.POD_METADATA_CRI_ID] = criId
cfgStr := jsonutils.Marshal(cfg).String()
s.Desc.Metadata[computeapi.POD_METADATA_CRI_CONFIG] = cfgStr
pms := s.convertToPodMetadataPortMappings(cfg)
pmStr := jsonutils.Marshal(pms).String()
s.Desc.Metadata[computeapi.POD_METADATA_PORT_MAPPINGS] = pmStr
session := auth.GetSession(ctx, userCred, options.HostOptions.Region)
if _, err := computemod.Servers.SetMetadata(session, s.GetId(), jsonutils.Marshal(map[string]string{
computeapi.POD_METADATA_CRI_ID: criId,
computeapi.POD_METADATA_CRI_CONFIG: cfgStr,
computeapi.POD_METADATA_CRI_ID: criId,
computeapi.POD_METADATA_CRI_CONFIG: cfgStr,
computeapi.POD_METADATA_PORT_MAPPINGS: pmStr,
})); err != nil {
return errors.Wrapf(err, "set cri_id of pod %s", s.GetId())
}
@@ -473,6 +605,22 @@ func (s *sPodGuestInstance) getPodSandboxConfig() (*runtimeapi.PodSandboxConfig,
return podCfg, nil
}
func (s *sPodGuestInstance) GetPodMetadataPortMappings() ([]*computeapi.PodMetadataPortMapping, error) {
cfgStr := s.GetSourceDesc().Metadata[computeapi.POD_METADATA_PORT_MAPPINGS]
if cfgStr == "" {
return nil, nil
}
obj, err := jsonutils.ParseString(cfgStr)
if err != nil {
return nil, errors.Wrapf(err, "ParseString to json object: %s", cfgStr)
}
pms := make([]*computeapi.PodMetadataPortMapping, 0)
if err := obj.Unmarshal(pms); err != nil {
return nil, errors.Wrap(err, "Unmarshal to PodMetadataPortMappings")
}
return pms, nil
}
func (s *sPodGuestInstance) saveContainer(id string, criId string) error {
_, ok := s.containers[id]
if ok {
+114 -37
View File
@@ -34,50 +34,127 @@ type PodCreateOptions struct {
MEM string `help:"Memory size MB" metavar:"MEM" json:"-"`
VcpuCount int `help:"#CPU cores of VM server, default 1" default:"1" metavar:"<SERVER_CPU_COUNT>" json:"vcpu_count" token:"ncpu"`
AllowDelete *bool `help:"Unlock server to allow deleting" json:"-"`
PortMapping []string `help:"Port mapping of the pod and the format is: <host_port>:<container_port>/<tcp|udp>" short-token:"p"`
PortMapping []string `help:"Port mapping of the pod and the format is: host_port=8080,port=80,protocol=<tcp|udp>,host_port_range=<int>-<int>" short-token:"p"`
Arch string `help:"image arch" choices:"aarch64|x86_64"`
ContainerCreateCommonOptions
}
func parsePodPortMapping(input string) (*computeapi.PodPortMapping, error) {
segs := strings.Split(input, ":")
if len(segs) != 2 {
return nil, errors.Errorf("wrong format: %s", input)
func parsePodPortMappingDetails(input string) (*computeapi.PodPortMapping, error) {
pm := &computeapi.PodPortMapping{
Protocol: computeapi.PodPortMappingProtocolTCP,
}
hostPortStr := segs[0]
hostPort, err := strconv.Atoi(hostPortStr)
if err != nil {
return nil, errors.Wrapf(err, "host_port %s isn't integer", hostPortStr)
}
ctrPortPart := segs[1]
ctrPortSegs := strings.Split(ctrPortPart, "/")
if len(ctrPortSegs) > 2 {
return nil, errors.Wrapf(err, "wrong format: %s", ctrPortPart)
}
ctrPortStr := ctrPortSegs[0]
ctrPort, err := strconv.Atoi(ctrPortStr)
if err != nil {
return nil, errors.Wrapf(err, "container_port %s isn't integer", ctrPortStr)
}
var protocol computeapi.PodPortMappingProtocol = computeapi.PodPortMappingProtocolTCP
if len(ctrPortSegs) == 2 {
switch ctrPortSegs[1] {
case "tcp":
protocol = computeapi.PodPortMappingProtocolTCP
case "udp":
protocol = computeapi.PodPortMappingProtocolUDP
case "sctp":
protocol = computeapi.PodPortMappingProtocolSCTP
default:
return nil, errors.Wrapf(err, "wrong protocol: %s", ctrPortSegs[1])
for _, seg := range strings.Split(input, ",") {
info := strings.Split(seg, "=")
if len(info) != 2 {
return nil, errors.Errorf("invalid option %s", seg)
}
key := info[0]
val := info[1]
switch key {
case "host_port":
hp, err := strconv.Atoi(val)
if err != nil {
return nil, errors.Wrapf(err, "invalid host_port %s", val)
}
pm.HostPort = &hp
case "container_port", "port":
cp, err := strconv.Atoi(val)
if err != nil {
return nil, errors.Wrapf(err, "invalid container_port %s", val)
}
pm.ContainerPort = cp
case "proto", "protocol":
pm.Protocol = computeapi.PodPortMappingProtocol(val)
case "host_port_range":
rangeParts := strings.Split(val, "-")
if len(rangeParts) != 2 {
return nil, errors.Errorf("invalid range string %s", val)
}
start, err := strconv.Atoi(rangeParts[0])
if err != nil {
return nil, errors.Wrapf(err, "invalid host_port_range %s", rangeParts[0])
}
end, err := strconv.Atoi(rangeParts[1])
if err != nil {
return nil, errors.Wrapf(err, "invalid host_port_range %s", rangeParts[1])
}
pm.HostPortRange = &computeapi.PodPortMappingPortRange{
Start: start,
End: end,
}
}
}
return &computeapi.PodPortMapping{
Protocol: protocol,
ContainerPort: int32(ctrPort),
HostPort: int32(hostPort),
}, nil
if pm.ContainerPort == 0 {
return nil, errors.Error("container_port must specified")
}
return pm, nil
}
func ParsePodPortMapping(input string) (*computeapi.PodPortMapping, error) {
pm, err := parsePodPortMapping(input)
if err != nil {
return parsePodPortMappingDetails(input)
}
return pm, nil
}
func parsePodPortMapping(input string) (*computeapi.PodPortMapping, error) {
segs := strings.Split(input, ":")
parseCtrPart := func(ctrPortPart string) (computeapi.PodPortMappingProtocol, int, error) {
ctrPortSegs := strings.Split(ctrPortPart, "/")
if len(ctrPortSegs) > 2 {
return "", 0, errors.Errorf("wrong format: %s", ctrPortPart)
}
ctrPortStr := ctrPortSegs[0]
ctrPort, err := strconv.Atoi(ctrPortStr)
if err != nil {
return "", 0, errors.Wrapf(err, "container_port %s isn't integer", ctrPortStr)
}
var protocol computeapi.PodPortMappingProtocol = computeapi.PodPortMappingProtocolTCP
if len(ctrPortSegs) == 2 {
switch ctrPortSegs[1] {
case "tcp":
protocol = computeapi.PodPortMappingProtocolTCP
case "udp":
protocol = computeapi.PodPortMappingProtocolUDP
//case "sctp":
// protocol = computeapi.PodPortMappingProtocolSCTP
default:
return "", 0, errors.Wrapf(err, "wrong protocol: %s", ctrPortSegs[1])
}
}
return protocol, ctrPort, nil
}
if len(segs) == 1 {
protocol, ctrPort, err := parseCtrPart(segs[0])
if err != nil {
return nil, errors.Wrapf(err, "parse %s", segs[0])
}
return &computeapi.PodPortMapping{
Protocol: protocol,
ContainerPort: ctrPort,
}, nil
} else if len(segs) == 2 {
hostPortStr := segs[0]
hostPort, err := strconv.Atoi(hostPortStr)
if err != nil {
return nil, errors.Wrapf(err, "host_port %s isn't integer", hostPortStr)
}
ctrPortPart := segs[1]
protocol, ctrPort, err := parseCtrPart(ctrPortPart)
if err != nil {
return nil, errors.Wrapf(err, "parse %s", ctrPortPart)
}
return &computeapi.PodPortMapping{
Protocol: protocol,
ContainerPort: ctrPort,
HostPort: &hostPort,
}, nil
} else {
return nil, errors.Errorf("wrong format: %s", input)
}
}
func parseContainerDevice(dev string) (*computeapi.ContainerDevice, error) {
@@ -105,7 +182,7 @@ func (o *PodCreateOptions) Params() (*computeapi.ServerCreateInput, error) {
portMappings := make([]*computeapi.PodPortMapping, 0)
if len(o.PortMapping) != 0 {
for _, input := range o.PortMapping {
pm, err := parsePodPortMapping(input)
pm, err := ParsePodPortMapping(input)
if err != nil {
return nil, errors.Wrapf(err, "parse port mapping: %s", input)
}
@@ -8,6 +8,7 @@ import (
)
func Test_parsePodPortMapping(t *testing.T) {
var port80 int = 80
tests := []struct {
args string
want *computeapi.PodPortMapping
@@ -18,7 +19,7 @@ func Test_parsePodPortMapping(t *testing.T) {
want: &computeapi.PodPortMapping{
Protocol: computeapi.PodPortMappingProtocolTCP,
ContainerPort: 8080,
HostPort: 80,
HostPort: &port80,
},
wantErr: false,
},
@@ -27,7 +28,7 @@ func Test_parsePodPortMapping(t *testing.T) {
want: &computeapi.PodPortMapping{
Protocol: computeapi.PodPortMappingProtocolTCP,
ContainerPort: 8080,
HostPort: 80,
HostPort: &port80,
},
wantErr: false,
},
@@ -37,9 +38,11 @@ func Test_parsePodPortMapping(t *testing.T) {
wantErr: true,
},
{
args: "80",
want: nil,
wantErr: true,
args: "80",
want: &computeapi.PodPortMapping{
Protocol: computeapi.PodPortMappingProtocolTCP,
ContainerPort: 80,
},
},
{
args: "80s:ctrP",
@@ -60,3 +63,56 @@ func Test_parsePodPortMapping(t *testing.T) {
})
}
}
func Test_parsePodPortMappingDetails(t *testing.T) {
port8080 := 8080
tests := []struct {
input string
want *computeapi.PodPortMapping
wantErr bool
}{
{
input: "host_port=8080,port=80",
want: &computeapi.PodPortMapping{
Protocol: computeapi.PodPortMappingProtocolTCP,
ContainerPort: 80,
HostPort: &port8080,
},
},
{
input: "host_port=8080,port=80,protocol=udp",
want: &computeapi.PodPortMapping{
Protocol: computeapi.PodPortMappingProtocolUDP,
ContainerPort: 80,
HostPort: &port8080,
},
},
{
input: "host_port=8080,protocol=udp",
wantErr: true,
},
{
input: "container_port=80,protocol=udp,host_port_range=20000-25000",
want: &computeapi.PodPortMapping{
Protocol: computeapi.PodPortMappingProtocolUDP,
ContainerPort: 80,
HostPortRange: &computeapi.PodPortMappingPortRange{
Start: 20000,
End: 25000,
},
},
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := parsePodPortMappingDetails(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parsePodPortMappingDetails() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parsePodPortMappingDetails() got = %v, want %v", got, tt.want)
}
})
}
}
+1
View File
@@ -0,0 +1 @@
package getport // import "yunion.io/x/onecloud/pkg/util/netutils2/getport"
+263
View File
@@ -0,0 +1,263 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package getport
import (
"fmt"
"math/rand"
"net"
"strconv"
"strings"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/sets"
)
// REF: https://github.com/jsumners/go-getport/blob/master/getport.go
// Protocol indicates the communication protocol (tcp or udp) and network
// stack (IPv4, IPv6, or OS choice) to target when finding an available port.
type Protocol int
const (
// TCP indicates to let the OS decide between IPv4 and IPv6 when finding
// an open TCP based port.
TCP Protocol = iota
// TCP4 indicates to find an open IPv4 port.
TCP4
// TCP6 indicates to find an open IPv6 port.
TCP6
// UDP indicates to let the OS decide between IPv4 and IPv6 when finding
// an open UDP based port.
UDP
// UDP4 indicates to find an open IPv4 port.
UDP4
// UDP6 indicates to find an open IPv6 port.
UDP6
)
// PortResult represents the result of [GetPort]. It indicates the IP address
// and port number combination that resulted in finding an open port.
type PortResult struct {
// IP is either an IPv4 or IPv6 string as returned by [net.SplitHostPort].
IP string
// Port is the determined available port number.
Port int
}
// GetPort finds an open port for a given [Protocol] and address and returns
// that port number. If the [Protocol] is not recognized, or some problem is
// encountered while verifying the port, then the returned [PortResult.Port]
// number will be `-1` along with an error. The address parameter should be a
// simple IP address string, e.g. `127.0.0.1` or `::1`. The [PortResult.IP] will
// be set to the IP address that was actually used to find the open port. If
// address is the empty string (`""`), then the returned IP address will be the
// one determined by the OS when finding the port.
//
// Note: it is not guaranteed the port will remain open long enough to actually
// be used. Errors should still be checked when attempting to use the found
// port.
func GetPort(protocol Protocol, address string) (PortResult, error) {
return getPort(protocol, address, 0)
}
func getPort(protocol Protocol, address string, port int) (PortResult, error) {
stack := resolveProtocol(protocol)
result := PortResult{
IP: "",
Port: -1,
}
resolvedAddress, listenError := listen(
stack,
net.JoinHostPort(address, fmt.Sprintf("%d", port)),
)
if listenError != nil {
return result, listenError
}
// I do not see how it's possible to get an error from [net.SplitHostPort]
// here given how we have already validated the stack and successfully
// issued a [net.Listen].
addr, portStr, _ := net.SplitHostPort(resolvedAddress.String())
hPort, _ := strconv.Atoi(portStr)
result.IP = addr
result.Port = hPort
return result, nil
}
func IsPortUsed(protocol Protocol, address string, port int) bool {
_, err := getPort(protocol, address, port)
if err != nil {
return true
}
return false
}
func GetPortByRange(proto Protocol, start int, end int) (PortResult, error) {
return GetPortByRangeBySets(proto, start, end, sets.NewInt())
}
func GetPortByRangeBySets(proto Protocol, start int, end int, usedPorts sets.Int) (PortResult, error) {
for i := start; i <= end; i++ {
rPort := rand.Intn(end-start) + start
if usedPorts.Has(rPort) {
continue
}
result, err := getPort(proto, "", rPort)
if err != nil {
usedPorts.Insert(rPort)
log.Debugf("check random port %d: %v", rPort, err)
} else {
return result, nil
}
}
return PortResult{
IP: "",
Port: -1,
}, errors.Errorf("can't get free port in [%d, %d]", start, end)
}
// GetTcpPort gets a port for some random available address using either
// TCP4 or TCP6. See [GetPort] for more detail
func GetTcpPort() (PortResult, error) {
return GetPort(TCP, "")
}
// GetTcp4Port gets a port for some random available address using TCP4.
// See [GetPort] for more detail
func GetTcp4Port() (PortResult, error) {
return GetPort(TCP4, "")
}
// GetTcp6Port gets a port for some random available address using TCP6.
// See [GetPort] for more detail
func GetTcp6Port() (PortResult, error) {
return GetPort(TCP6, "")
}
// GetUdpPort gets a port for some random available address using either
// UDP4 or UDP6. See [GetPort] for more detail
func GetUdpPort() (PortResult, error) {
return GetPort(UDP, "")
}
// GetUdp4Port gets a port for some random available address using UDP4.
// See [GetPort] for more detail
func GetUdp4Port() (PortResult, error) {
return GetPort(UDP4, "")
}
// GetUdp6Port gets a port for some random available address using UDP6.
// See [GetPort] for more detail
func GetUdp6Port() (PortResult, error) {
return GetPort(UDP6, "")
}
// GetTcpPortForAddress gets either a TCP4 or TCP6 port for the given address.
// See [GetPort] for more detail.
func GetTcpPortForAddress(address string) (PortResult, error) {
return GetPort(TCP, address)
}
// GetTcp4PortForAddress gets a TCP4 port for the given address.
// See [GetPort] for more detail.
func GetTcp4PortForAddress(address string) (PortResult, error) {
return GetPort(TCP4, address)
}
// GetTcp6PortForAddress gets a TCP6 port for the given address.
// See [GetPort] for more detail.
func GetTcp6PortForAddress(address string) (PortResult, error) {
return GetPort(TCP6, address)
}
// GetUdpPortForAddress gets either a UDP4 or UDP6 port for the given address.
// See [GetPort] for more detail.
func GetUdpPortForAddress(address string) (PortResult, error) {
return GetPort(UDP, address)
}
// GetUdp4PortForAddress gets a UDP4 port for the given address.
// See [GetPort] for more detail.
func GetUdp4PortForAddress(address string) (PortResult, error) {
return GetPort(UDP4, address)
}
// GetUdp6PortForAddress gets a UDP6 port for the given address.
// See [GetPort] for more detail.
func GetUdp6PortForAddress(address string) (PortResult, error) {
return GetPort(UDP6, address)
}
// PortResultToAddress converts a [PortResult] into a traditional host:port
// string usable by [net.Listen] or [net.ListenPacket].
func PortResultToAddress(portResult PortResult) string {
return net.JoinHostPort(portResult.IP, fmt.Sprintf("%d", portResult.Port))
}
// listen is an internal wrapper for [net.Listen] and [net.ListenPacket].
func listen(stack string, addrWithPort string) (net.Addr, error) {
if strings.HasPrefix(stack, "tcp") {
l, err := net.Listen(stack, addrWithPort)
if err != nil {
return nil, err
}
defer l.Close()
return l.Addr(), nil
}
if strings.HasPrefix(stack, "udp") {
l, err := net.ListenPacket(stack, addrWithPort)
if err != nil {
return nil, err
}
defer l.Close()
return l.LocalAddr(), nil
}
return nil, errors.Errorf("stack not recognized: %s", stack)
}
// resolveProtocol maps the [Protocol] value to a network stack string
// that is supported by [net.Listen] and [net.ListenPacket].
func resolveProtocol(protocol Protocol) string {
var stack string
switch protocol {
case TCP:
stack = "tcp"
case TCP4:
stack = "tcp4"
case TCP6:
stack = "tcp6"
case UDP:
stack = "udp"
case UDP4:
stack = "udp4"
case UDP6:
stack = "udp6"
}
return stack
}
+194
View File
@@ -0,0 +1,194 @@
package getport
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
"yunion.io/x/log"
)
func TestGetPort(t *testing.T) {
t.Run("gets empty tcp port", func(t *testing.T) {
portResult, err := GetPort(TCP, "127.0.0.1")
assert.NoError(t, err)
assert.Contains(t, []string{"127.0.0.1", "::1"}, portResult.IP)
assert.Greater(t, portResult.Port, 0)
})
t.Run("get by empty address", func(t *testing.T) {
result, err := GetPort(TCP4, "")
assert.NoError(t, err)
log.Infof("result: %#v", result)
})
t.Run("gets empty tcp4 port", func(t *testing.T) {
portResult, err := GetPort(TCP4, "127.0.0.1")
assert.NoError(t, err)
assert.Equal(t, portResult.IP, "127.0.0.1")
assert.Greater(t, portResult.Port, 0)
})
t.Run("gets empty tcp6 port", func(t *testing.T) {
portResult, err := GetPort(TCP6, "::1")
assert.NoError(t, err)
assert.Equal(t, portResult.IP, "::1")
assert.Greater(t, portResult.Port, 0)
})
t.Run("gets empty udp port", func(t *testing.T) {
portResult, err := GetPort(UDP, "127.0.0.1")
assert.NoError(t, err)
assert.Contains(t, []string{"127.0.0.1", "::1"}, portResult.IP)
assert.Greater(t, portResult.Port, 0)
})
t.Run("gets empty udp4 port", func(t *testing.T) {
portResult, err := GetPort(UDP4, "127.0.0.1")
assert.NoError(t, err)
assert.Equal(t, portResult.IP, "127.0.0.1")
assert.Greater(t, portResult.Port, 0)
})
t.Run("gets empty udp6 port", func(t *testing.T) {
portResult, err := GetPort(UDP6, "::1")
assert.NoError(t, err)
assert.Equal(t, portResult.IP, "::1")
assert.Greater(t, portResult.Port, 0)
})
t.Run("errors for bad protocol", func(t *testing.T) {
portResult, err := GetPort(-1, "127.0.0.1")
assert.Equal(t, portResult.IP, "")
assert.Equal(t, portResult.Port, -1)
assert.Error(t, err, "stack not recognized")
})
t.Run("get port within 30000 to 31000", func(t *testing.T) {
portResult, err := GetPortByRange(TCP4, 30000, 31000)
assert.NoError(t, err)
log.Infof("port result: %#v", portResult)
assert.Greater(t, portResult.Port, 30000)
})
t.Run("get port within 30000 to 30001 except 30001", func(t *testing.T) {
var l net.Listener
var err error
go func() {
l, err = net.Listen("tcp4", net.JoinHostPort("", "30001"))
if err != nil {
t.Fatalf("listen 30001 tcp port: %v", err)
}
log.Infof("listen port 30001")
}()
time.Sleep(1 * time.Second)
portResult, err := GetPortByRange(TCP4, 30000, 30001)
assert.NoError(t, err)
log.Infof("port result: %#v", portResult)
assert.Equal(t, portResult.Port, 30000)
l.Close()
})
}
func TestGetTcpPort(t *testing.T) {
portResult, err := GetTcpPort()
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetTcp4Port(t *testing.T) {
portResult, err := GetTcp4Port()
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetTcp6Port(t *testing.T) {
portResult, err := GetTcp6Port()
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetUdpPort(t *testing.T) {
portResult, err := GetUdpPort()
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetUdp4Port(t *testing.T) {
portResult, err := GetUdp4Port()
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetUdp6Port(t *testing.T) {
portResult, err := GetUdp6Port()
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetTcpPortForAddress(t *testing.T) {
portResult, err := GetTcpPortForAddress("127.0.0.1")
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetTcp4PortForAddress(t *testing.T) {
portResult, err := GetTcp4PortForAddress("127.0.0.1")
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetTcp6PortForAddress(t *testing.T) {
portResult, err := GetTcp6PortForAddress("::1")
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetUdpPortForAddress(t *testing.T) {
portResult, err := GetUdpPortForAddress("127.0.0.1")
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetUdp4PortForAddress(t *testing.T) {
portResult, err := GetUdp4PortForAddress("127.0.0.1")
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestGetUdp6PortForAddress(t *testing.T) {
portResult, err := GetUdp6PortForAddress("::1")
assert.NoError(t, err)
assert.Greater(t, portResult.Port, 0)
}
func TestPortResultToAddress(t *testing.T) {
portResult := PortResult{
IP: "::1",
Port: 3000,
}
address := PortResultToAddress(portResult)
assert.Equal(t, address, "[::1]:3000")
}
func TestListen(t *testing.T) {
t.Run("errors for bad tcp stack", func(t *testing.T) {
addr, err := listen("tcp-bad", "127.0.0.1:0")
assert.Empty(t, addr)
assert.Error(t, err, "listen tcp-bad: unknown network tcp-bad")
})
t.Run("errors for bad udp stack", func(t *testing.T) {
addr, err := listen("udp-bad", "127.0.0.1:0")
assert.Empty(t, addr)
assert.Error(t, err, "listen udp-bad: unknown network udp-bad")
})
t.Run("errors for bad stack", func(t *testing.T) {
addr, err := listen("unix", "127.0.0.1:0")
assert.Empty(t, addr)
assert.Error(t, err, "stack not recognized")
})
}