Merge "feat: Support create port forwarding with internal/external port range"

This commit is contained in:
Zuul 2022-08-16 04:26:05 +00:00 committed by Gerrit Code Review
commit 5edda3c4da
15 changed files with 761 additions and 315 deletions

View File

@ -8,22 +8,26 @@
"1. The name of the custom resource class property should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_BAREMETAL_SMALL).": "1. The name of the custom resource class property should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_BAREMETAL_SMALL).",
"1. The name of the trait should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_TRAIT1).": "1. The name of the trait should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_TRAIT1).",
"1. The volume associated with the backup is available.": "1. The volume associated with the backup is available.",
"1. You can create {resources} using ports or port ranges.": "1. You can create {resources} using ports or port ranges.",
"10s": "10s",
"1D": "1D",
"1H": "1H",
"1min": "1min",
"2. In the same protocol, you cannot create multiple {resources} for the same source port or source port range.": "2. In the same protocol, you cannot create multiple {resources} for the same source port or source port range.",
"2. The trait of the scheduled node needs to correspond to the trait of the flavor used by the ironic instance; by injecting the necessary traits into the ironic instance, the computing service will only schedule the instance to the bare metal node with all the necessary traits (for example, the ironic instance which use the flavor that has CUSTOM_TRAIT1 as a necessary trait, can be scheduled to the node which has the trait of CUSTOM_TRAIT1).": "2. The trait of the scheduled node needs to correspond to the trait of the flavor used by the ironic instance; by injecting the necessary traits into the ironic instance, the computing service will only schedule the instance to the bare metal node with all the necessary traits (for example, the ironic instance which use the flavor that has CUSTOM_TRAIT1 as a necessary trait, can be scheduled to the node which has the trait of CUSTOM_TRAIT1).",
"2. The volume associated with the backup has been mounted, and the instance is shut down.": "2. The volume associated with the backup has been mounted, and the instance is shut down.",
"2. To ensure the integrity of the data, it is recommended that you suspend the write operation of all files when creating a backup.": "2. To ensure the integrity of the data, it is recommended that you suspend the write operation of all files when creating a backup.",
"2. You can customize the resource class name of the flavor, but it needs to correspond to the resource class of the scheduled node (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).": "2. You can customize the resource class name of the flavor, but it needs to correspond to the resource class of the scheduled node (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).",
"3. When using a port range to create a port mapping, the size of the external port range is required to be the same as the size of the internal port range. For example, the external port range is 80:90 and the internal port range is 8080:8090.": "3. When using a port range to create a port mapping, the size of the external port range is required to be the same as the size of the internal port range. For example, the external port range is 80:90 and the internal port range is 8080:8090.",
"4. When you use a port range to create {resources}, multiple {resources} will be created in batches. ": "4. When you use a port range to create {resources}, multiple {resources} will be created in batches. ",
"5min": "5min",
"8 to 16 characters, at least one uppercase letter, one lowercase letter, one number and one special character.": "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number and one special character.",
"8 to 16 characters, at least one uppercase letter, one lowercase letter, one number.": "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number.",
"A DNAT rule has been created for this port of this IP, please choose another port.": "A DNAT rule has been created for this port of this IP, please choose another port.",
"A command that will be sent to the container": "A command that will be sent to the container",
"A container with the same name already exists": "A container with the same name already exists",
"A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.": "A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.",
"A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.": "A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.",
"A port forwarding has been created for this port of this FIP, please choose another port.": "A port forwarding has been created for this port of this FIP, please choose another port.",
"A public container will allow anyone to use the objects in your container through a public URL.": "A public container will allow anyone to use the objects in your container through a public URL.",
"A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.": "A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.",
"A template is a YAML file that contains configuration information, please enter the correct format.": "A template is a YAML file that contains configuration information, please enter the correct format.",
@ -462,8 +466,6 @@
"Create Complete": "Create Complete",
"Create Configurations": "Create Configurations",
"Create Container": "Create Container",
"Create DNAT Rule": "Create DNAT Rule",
"Create DNAT rule": "Create DNAT rule",
"Create DSCP Marking Rule": "Create DSCP Marking Rule",
"Create Database": "Create Database",
"Create Database Backup": "Create Database Backup",
@ -489,6 +491,7 @@
"Create New Network": "Create New Network",
"Create Node": "Create Node",
"Create Port": "Create Port",
"Create Port Forwarding": "Create Port Forwarding",
"Create Port Group": "Create Port Group",
"Create Project": "Create Project",
"Create QoS Policy": "Create QoS Policy",
@ -631,7 +634,6 @@
"Delete Complete": "Delete Complete",
"Delete Configuration": "Delete Configuration",
"Delete Container": "Delete Container",
"Delete DNAT Rule": "Delete DNAT Rule",
"Delete DSCP Marking Rules": "Delete DSCP Marking Rules",
"Delete Database": "Delete Database",
"Delete Database Backup": "Delete Database Backup",
@ -658,6 +660,7 @@
"Delete Network": "Delete Network",
"Delete Node": "Delete Node",
"Delete Port": "Delete Port",
"Delete Port Forwarding": "Delete Port Forwarding",
"Delete Port Group": "Delete Port Group",
"Delete Project": "Delete Project",
"Delete QoS Policy": "Delete QoS Policy",
@ -788,7 +791,6 @@
"Edit Bare Metal Node": "Edit Bare Metal Node",
"Edit Consumer": "Edit Consumer",
"Edit Container": "Edit Container",
"Edit DNAT Rule": "Edit DNAT Rule",
"Edit DSCP Marking Rule": "Edit DSCP Marking Rule",
"Edit Default Pool": "Edit Default Pool",
"Edit Domain": "Edit Domain",
@ -806,6 +808,7 @@
"Edit Member": "Edit Member",
"Edit Metadata": "Edit Metadata",
"Edit Port": "Edit Port",
"Edit Port Forwarding": "Edit Port Forwarding",
"Edit Port Group": "Edit Port Group",
"Edit Project": "Edit Project",
"Edit QoS Policy": "Edit QoS Policy",
@ -911,6 +914,8 @@
"External Network Info": "External Network Info",
"External Networks": "External Networks",
"External Port": "External Port",
"External Port Range": "External Port Range",
"External Port/Port Range": "External Port/Port Range",
"Extra Infos": "Extra Infos",
"Extra Specs": "Extra Specs",
"FAKE": "FAKE",
@ -1177,6 +1182,8 @@
"Initialize Databases": "Initialize Databases",
"Initiator Mode": "Initiator Mode",
"Input destination port or port range(example: 80 or 80:160)": "Input destination port or port range(example: 80 or 80:160)",
"Input external port or port range(example: 80 or 80:160)": "Input external port or port range(example: 80 or 80:160)",
"Input internal port or port range(example: 80 or 80:160)": "Input internal port or port range(example: 80 or 80:160)",
"Input source port or port range(example: 80 or 80:160)": "Input source port or port range(example: 80 or 80:160)",
"Insecure Registry": "Insecure Registry",
"Inspect Failed": "Inspect Failed",
@ -1227,6 +1234,7 @@
"Internal Ip Address": "Internal Ip Address",
"Internal Network Bandwidth(Gbps)": "Internal Network Bandwidth(Gbps)",
"Internal Port": "Internal Port",
"Internal Port/Port Range": "Internal Port/Port Range",
"Internal Server Error (code: 500) ": "Internal Server Error (code: 500) ",
"Invalid": "Invalid",
"Invalid CIDR.": "Invalid CIDR.",
@ -1770,7 +1778,7 @@
"Port": "Port",
"Port Detail": "Port Detail",
"Port Forwarding": "Port Forwarding",
"Port Forwarding Rules": "Port Forwarding Rules",
"Port Forwardings": "Port Forwardings",
"Port Group": "Port Group",
"Port Groups": "Port Groups",
"Port ID": "Port ID",
@ -1780,6 +1788,7 @@
"Port Security Enabled": "Port Security Enabled",
"Port Type": "Port Type",
"Ports": "Ports",
"Ports are either single values or ranges": "Ports are either single values or ranges",
"Ports provide extra communication channels to your instances. You can select ports instead of networks or a mix of both (The port executes its own security group rules by default).": "Ports provide extra communication channels to your instances. You can select ports instead of networks or a mix of both (The port executes its own security group rules by default).",
"Portugal": "Portugal",
"Power Off": "Power Off",
@ -2276,7 +2285,7 @@
"The entire inspection process takes 5 to 10 minutes, so you need to be patient. After the registration is completed, the node configuration status will return to the manageable status.": "The entire inspection process takes 5 to 10 minutes, so you need to be patient. After the registration is completed, the node configuration status will return to the manageable status.",
"The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown.": "The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown.",
"The file with the same name will be overwritten.": "The file with the same name will be overwritten.",
"The floating IP configured with port forwarding rules cannot be bound": "The floating IP configured with port forwarding rules cannot be bound",
"The floating IP configured with port forwardings cannot be bound": "The floating IP configured with port forwardings cannot be bound",
"The format of the certificate content is: by \"----BEGIN CERTIFICATE-----\" as the beginning,\"-----END CERTIFICATE----\" as the end, 64 characters per line, the last line does not exceed 64 characters, and there cannot be blank lines.": "The format of the certificate content is: by \"----BEGIN CERTIFICATE-----\" as the beginning,\"-----END CERTIFICATE----\" as the end, 64 characters per line, the last line does not exceed 64 characters, and there cannot be blank lines.",
"The host name of this container": "The host name of this container",
"The http_proxy address to use for nodes in cluster": "The http_proxy address to use for nodes in cluster",
@ -2294,6 +2303,7 @@
"The ip of external members can be any, including the public network ip.": "The ip of external members can be any, including the public network ip.",
"The key pair allows you to SSH into your newly created instance. You can select an existing key pair, import a key pair, or generate a new key pair.": "The key pair allows you to SSH into your newly created instance. You can select an existing key pair, import a key pair, or generate a new key pair.",
"The kill signal to send": "The kill signal to send",
"The maximum batch size is {size}, that is, the size of the port range cannot exceed {size}.": "The maximum batch size is {size}, that is, the size of the port range cannot exceed {size}.",
"The maximum transmission unit (MTU) value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6.": "The maximum transmission unit (MTU) value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6.",
"The min size is {size} GiB": "The min size is {size} GiB",
"The name cannot be modified after creation": "The name cannot be modified after creation",
@ -2331,6 +2341,7 @@
"The server {name} is locked. Please unlock first.": "The server {name} is locked. Please unlock first.",
"The session has expired, please log in again.": "The session has expired, please log in again.",
"The shelved offloaded instance only supports immediate deletion": "The shelved offloaded instance only supports immediate deletion",
"The size of the external port range is required to be the same as the size of the internal port range": "The size of the external port range is required to be the same as the size of the internal port range",
"The start source is a template used to create an instance. You can choose an image or a bootable volume.": "The start source is a template used to create an instance. You can choose an image or a bootable volume.",
"The starting number must be less than the ending number": "The starting number must be less than the ending number",
"The timeout for cluster creation in minutes.": "The timeout for cluster creation in minutes.",
@ -2341,6 +2352,7 @@
"The unit suffix must be one of the following: Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it), KB, KiB, MB, MiB, GB, GiB, TB, TiB. If the unit suffix is not provided, it is assumed to be KB.": "The unit suffix must be one of the following: Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it), KB, KiB, MB, MiB, GB, GiB, TB, TiB. If the unit suffix is not provided, it is assumed to be KB.",
"The user has been disabled, please contact the administrator": "The user has been disabled, please contact the administrator",
"The user needs to ensure that the input is a shell script that can run completely and normally.": "The user needs to ensure that the input is a shell script that can run completely and normally.",
"The value of the upper limit of the range must be greater than the value of the lower limit of the range.": "The value of the upper limit of the range must be greater than the value of the lower limit of the range.",
"The volume associated with the backup is not available, unable to restore.": "The volume associated with the backup is not available, unable to restore.",
"The volume status can be reset to in-use only when the previous status is in-use.": "The volume status can be reset to in-use only when the previous status is in-use.",
"The volume type needs to be consistent with the volume type when the snapshot is created.": "The volume type needs to be consistent with the volume type when the snapshot is created.",
@ -2348,6 +2360,8 @@
"The working directory for commands to run in": "The working directory for commands to run in",
"The {action} instruction has been issued, instance: {name}. \n You can wait for a few seconds to follow the changes of the list data or manually refresh the data to get the final display result.": "The {action} instruction has been issued, instance: {name}. \n You can wait for a few seconds to follow the changes of the list data or manually refresh the data to get the final display result.",
"The {action} instruction has been issued. \n You can wait for a few seconds to follow the changes of the list data or manually refresh the data to get the final display result.": "The {action} instruction has been issued. \n You can wait for a few seconds to follow the changes of the list data or manually refresh the data to get the final display result.",
"The {name} has already been used by other {resource}({content}), please change.": "The {name} has already been used by other {resource}({content}), please change.",
"The {name} {ports} have already been used, please change.": "The {name} {ports} have already been used, please change.",
"There are resources that cannot {action} in the selected resources, such as:": "There are resources that cannot {action} in the selected resources, such as:",
"There are resources that cannot {action} in the selected resources.": "There are resources that cannot {action} in the selected resources.",
"There are resources under the project and cannot be deleted.": "There are resources under the project and cannot be deleted.",
@ -2669,7 +2683,6 @@
"database backups": "database backups",
"database instances": "database instances",
"delete": "delete",
"delete DNAT rule": "delete DNAT rule",
"delete allowed address pair": "delete allowed address pair",
"delete application credential": "delete application credential",
"delete bandwidth egress rules": "delete bandwidth egress rules",
@ -2691,6 +2704,7 @@
"delete load balancer": "delete load balancer",
"delete member": "delete member",
"delete network": "delete network",
"delete port forwarding": "delete port forwarding",
"delete project": "delete project",
"delete qos policy": "delete qos policy",
"delete role": "delete role",
@ -2729,6 +2743,8 @@
"enable cinder service": "enable cinder service",
"enable compute service": "enable compute service",
"enable neutron agent": "enable neutron agent",
"external port": "external port",
"external ports": "external ports",
"extra specs": "extra specs",
"flavor": "flavor",
"floating ip": "floating ip",
@ -2747,6 +2763,8 @@
"instance snapshots": "instance snapshots",
"instance: {name}.": "instance: {name}.",
"instances": "instances",
"internal port": "internal port",
"internal ports": "internal ports",
"ipsec site connection": "ipsec site connection",
"jump to the console": "jump to the console",
"keypair": "keypair",
@ -2778,6 +2796,7 @@
"please select network": "please select network",
"please select subnet": "please select subnet",
"port": "port",
"port forwarding": "port forwarding",
"port forwardings": "port forwardings",
"port groups": "port groups",
"ports": "ports",
@ -2895,7 +2914,7 @@
"{name} type capacity (GiB)": "{name} type capacity (GiB)",
"{name} type snapshots": "{name} type snapshots",
"{name} {id} could not be found.": "{name} {id} could not be found.",
"{number} port forwarding rules": "{number} port forwarding rules",
"{number} {resource}": "{number} {resource}",
"{pageSize} items/page": "{pageSize} items/page",
"{seconds} seconds": "{seconds} seconds"
}

View File

@ -8,22 +8,26 @@
"1. The name of the custom resource class property should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_BAREMETAL_SMALL).": "1. 自定义资源属性的命名应该以 CUSTOM_ 开头、只能包含大写字母A ~ Z、数字0 ~ 9或下划线、长度不超过255个字符比如CUSTOM_BAREMETAL_SMALL。",
"1. The name of the trait should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_TRAIT1).": "1. 特性的命名应该以 CUSTOM_ 开头、只能包含大写字母A ~ Z、数字0 ~ 9或下划线、长度不超过255个字符比如CUSTOM_TRAIT1。",
"1. The volume associated with the backup is available.": "1. 备份关联的云硬盘处于可用状态。",
"1. You can create {resources} using ports or port ranges.": "1. 可以使用端口或端口范围创建{resources}。",
"10s": "10秒",
"1D": "1天",
"1H": "1小时",
"1min": "1分钟",
"2. In the same protocol, you cannot create multiple {resources} for the same source port or source port range.": "2. 相同协议下,同一个源端口或源端口范围不可创建多个{resources}。",
"2. The trait of the scheduled node needs to correspond to the trait of the flavor used by the ironic instance; by injecting the necessary traits into the ironic instance, the computing service will only schedule the instance to the bare metal node with all the necessary traits (for example, the ironic instance which use the flavor that has CUSTOM_TRAIT1 as a necessary trait, can be scheduled to the node which has the trait of CUSTOM_TRAIT1).": "2. 被调度节点的特性需要与裸机实例使用的云主机类型的特性对应;通过给裸机实例注入必需特性,计算服务将只调度实例到具有所有必需特性的裸金属节点(比如:调度节点的有 CUSTOM_TRAIT1 特性, 云主机类型添加CUSTOM_TRAIT1为必要特性可以调度到此节点。",
"2. The volume associated with the backup has been mounted, and the instance is shut down.": "2. 备份关联的云硬盘已被挂载,且云主机处于关机状态。",
"2. To ensure the integrity of the data, it is recommended that you suspend the write operation of all files when creating a backup.": "2. 为了保证数据的完整性,建议您在创建备份时暂停所有文件的写操作。",
"2. You can customize the resource class name of the flavor, but it needs to correspond to the resource class of the scheduled node (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).": "2. 你可以自定义云主机类型的资源类名称,但需要与被调度节点的资源类对应;(比如:调度节点的资源类名称为 baremetal.with-GPU云主机类型的自定义资源类名称为CUSTOM_BAREMETAL_WITH_GPU。",
"3. When using a port range to create a port mapping, the size of the external port range is required to be the same as the size of the internal port range. For example, the external port range is 80:90 and the internal port range is 8080:8090.": "3. 使用端口范围创建端口映射时要求源端口范围大小与目标端口范围大小一致源端口范围为80:90目标端口范围为8080:8090。",
"4. When you use a port range to create {resources}, multiple {resources} will be created in batches. ": "4. 使用端口范围创建{resources}时,会批量创建多个{resources}。",
"5min": "5分钟",
"8 to 16 characters, at least one uppercase letter, one lowercase letter, one number and one special character.": "8个到16个字符至少一个大写字母一个小写字母一个数字和一个特殊字符。",
"8 to 16 characters, at least one uppercase letter, one lowercase letter, one number.": "8个到16个字符至少一个大写字母一个小写字母一个数字。",
"A DNAT rule has been created for this port of this IP, please choose another port.": "此IP的这个端口已经创建了DNAT规则请选择另一个端口。",
"A command that will be sent to the container": "将发送到容器的命令",
"A container with the same name already exists": "已存在同名容器",
"A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.": "通过当前活跃的连接数来估计服务器负载情况的一种动态调度算法,系统把新的连接请求分配给当前连接数目最少的服务器。常用于长连接服务,例如数据库连接等服务。",
"A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.": "一个主机集合最多可以与一个AZ建立关联一旦建立了关联无法再取消关联AZ。",
"A port forwarding has been created for this port of this FIP, please choose another port.": "该端口已经创建了端口转发,请使用另一个端口。",
"A public container will allow anyone to use the objects in your container through a public URL.": "一个公有容器会允许任何人通过公共 URL 去使用您容器里面的对象。",
"A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.": "云主机当前状态的磁盘数据保存,创建镜像文件,以备将来启动新的云主机使用。",
"A template is a YAML file that contains configuration information, please enter the correct format.": "模板是包含配置信息的YAML文件 请输入正确的格式。",
@ -462,8 +466,6 @@
"Create Complete": "创建完成",
"Create Configurations": "创建配置",
"Create Container": "创建容器",
"Create DNAT Rule": "创建DNAT规则",
"Create DNAT rule": "创建DNAT规则",
"Create DSCP Marking Rule": "创建DSCP标记规则",
"Create Database": "创建数据库",
"Create Database Backup": "创建数据库备份",
@ -489,6 +491,7 @@
"Create New Network": "创建新网络",
"Create Node": "注册节点",
"Create Port": "创建端口",
"Create Port Forwarding": "创建端口转发",
"Create Port Group": "创建端口组",
"Create Project": "创建项目",
"Create QoS Policy": "创建QoS策略",
@ -631,7 +634,6 @@
"Delete Complete": "删除完成",
"Delete Configuration": "删除配置",
"Delete Container": "删除容器",
"Delete DNAT Rule": "删除DNAT规则",
"Delete DSCP Marking Rules": "删除DSCP标记规则",
"Delete Database": "删除数据库",
"Delete Database Backup": "删除数据库备份",
@ -658,6 +660,7 @@
"Delete Network": "删除网络",
"Delete Node": "删除节点",
"Delete Port": "删除端口",
"Delete Port Forwarding": "删除端口转发",
"Delete Port Group": "删除端口组",
"Delete Project": "删除项目",
"Delete QoS Policy": "删除QoS策略",
@ -788,7 +791,6 @@
"Edit Bare Metal Node": "编辑裸机节点",
"Edit Consumer": "编辑消费者",
"Edit Container": "编辑容器",
"Edit DNAT Rule": "编辑DNAT规则",
"Edit DSCP Marking Rule": "编辑DSCP标记规则",
"Edit Default Pool": "编辑资源池",
"Edit Domain": "编辑域",
@ -806,6 +808,7 @@
"Edit Member": "编辑成员",
"Edit Metadata": "编辑元数据",
"Edit Port": "编辑端口",
"Edit Port Forwarding": "编辑端口转发",
"Edit Port Group": "编辑端口组",
"Edit Project": "编辑项目",
"Edit QoS Policy": "编辑",
@ -911,6 +914,8 @@
"External Network Info": "外部网络信息",
"External Networks": "外部网络",
"External Port": "源端口",
"External Port Range": "源端口范围",
"External Port/Port Range": "源端口/端口范围",
"Extra Infos": "额外信息",
"Extra Specs": "额外规格",
"FAKE": "FAKE",
@ -1177,6 +1182,8 @@
"Initialize Databases": "初始数据库",
"Initiator Mode": "发起模式",
"Input destination port or port range(example: 80 or 80:160)": "目的端口或端口范围例如80 或 80:160",
"Input external port or port range(example: 80 or 80:160)": "源端口或端口范围例如80 或 80:160",
"Input internal port or port range(example: 80 or 80:160)": "目标端口或端口范围例如80 或 80:160",
"Input source port or port range(example: 80 or 80:160)": "源端口或源端口范围(例如: 80 或 80:160)",
"Insecure Registry": "不安全的注册表",
"Inspect Failed": "检查失败",
@ -1227,6 +1234,7 @@
"Internal Ip Address": "目标IP",
"Internal Network Bandwidth(Gbps)": "内网带宽Gbps",
"Internal Port": "目标端口",
"Internal Port/Port Range": "目标端口/端口范围",
"Internal Server Error (code: 500) ": "服务器错误错误码500",
"Invalid": "失效",
"Invalid CIDR.": "无效的CIDR",
@ -1770,7 +1778,7 @@
"Port": "端口",
"Port Detail": "端口详情",
"Port Forwarding": "端口转发",
"Port Forwarding Rules": "端口转发规则",
"Port Forwardings": "端口转发",
"Port Group": "端口组",
"Port Groups": "端口组",
"Port ID": "端口ID",
@ -1780,6 +1788,7 @@
"Port Security Enabled": "启用端口安全",
"Port Type": "端口方式",
"Ports": "端口",
"Ports are either single values or ranges": "端口要么都是单一数值,要么都是范围",
"Ports provide extra communication channels to your instances. You can select ports instead of networks or a mix of both (The port executes its own security group rules by default).": "端口为您的云主机提供了额外的通信渠道。您可以选择已创建的端口而非网络或者二者都选(端口默认执行本身的安全组规则)。",
"Portugal": "葡萄牙",
"Power Off": "关机",
@ -2276,7 +2285,7 @@
"The entire inspection process takes 5 to 10 minutes, so you need to be patient. After the registration is completed, the node configuration status will return to the manageable status.": "检查的整个过程需要耗费 5 到 10 分钟时间,您需要耐心等待。在完成注册后,节点配置状态会重新回到可管理状态。",
"The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown.": "镜像中的cloud-init或cloudbase-init服务的预制配置未同步至镜像属性, 登录名未知",
"The file with the same name will be overwritten.": "对同名文件将会进行文件覆盖操作。",
"The floating IP configured with port forwarding rules cannot be bound": "不允许绑定配置了端口转发规则的浮动IP",
"The floating IP configured with port forwardings cannot be bound": "不允许绑定配置了端口转发的浮动IP",
"The format of the certificate content is: by \"----BEGIN CERTIFICATE-----\" as the beginning,\"-----END CERTIFICATE----\" as the end, 64 characters per line, the last line does not exceed 64 characters, and there cannot be blank lines.": "证书内容格式为:以”-----BEGIN CERTIFICATE-----”作为开头,以“-----END CERTIFICATE----”作为结尾每行64字符最后一行不超过64字符不能有空行。",
"The host name of this container": "容器的主机名",
"The http_proxy address to use for nodes in cluster": "用于集群中节点的 http_proxy 地址",
@ -2294,6 +2303,7 @@
"The ip of external members can be any, including the public network ip.": "外部成员的IP可以是任何IP包括公网IP。",
"The key pair allows you to SSH into your newly created instance. You can select an existing key pair, import a key pair, or generate a new key pair.": "密钥对允许您SSH到您新创建的实例。 您可以选择一个已存在的密钥对、导入一个密钥对或生成一个新的密钥对。",
"The kill signal to send": "要发送的终止信号",
"The maximum batch size is {size}, that is, the size of the port range cannot exceed {size}.": "批量的上限为{size}个,即端口范围大小不可超过{size}。",
"The maximum transmission unit (MTU) value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6.": "地址片段的最大传输单位。IPv4最小68IPv6最小1280。",
"The min size is {size} GiB": "最小内存为 {size} GiB",
"The name cannot be modified after creation": "名称创建后不可修改",
@ -2331,6 +2341,7 @@
"The server {name} is locked. Please unlock first.": "云主机{name}已被锁定,请先解锁。",
"The session has expired, please log in again.": "会话已过期,请重新登录。",
"The shelved offloaded instance only supports immediate deletion": "已归档的云主机仅支持立即删除",
"The size of the external port range is required to be the same as the size of the internal port range": "源端口范围的大小要与目标端口范围的大小相同",
"The start source is a template used to create an instance. You can choose an image or a bootable volume.": "启动源是用来创建云主机的模板, 您可以选择镜像或者可启动的卷。",
"The starting number must be less than the ending number": "起始数字必须小于结束数字",
"The timeout for cluster creation in minutes.": "集群创建超时时间,以分钟为单位。",
@ -2341,6 +2352,7 @@
"The unit suffix must be one of the following: Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it), KB, KiB, MB, MiB, GB, GiB, TB, TiB. If the unit suffix is not provided, it is assumed to be KB.": "单位后缀必须是以下之一Kb(it)、Kib(it)、Mb(it)、Mib(it)、Gb(it)、Gib(it)、Tb(it)、Tib(it)、KB、 KiB、MB、MiB、GB、GiB、TB、TiB。如果未提供单位后缀则假定为千字节。",
"The user has been disabled, please contact the administrator": "用户已被禁用,请联系管理员",
"The user needs to ensure that the input is a shell script that can run completely and normally.": "请确保输入的是能完整正常运行的 shell 脚本。",
"The value of the upper limit of the range must be greater than the value of the lower limit of the range.": "范围上限的数值必须要大于范围下限的数值",
"The volume associated with the backup is not available, unable to restore.": "云硬盘不处于可用状态,不支持恢复备份操作。",
"The volume status can be reset to in-use only when the previous status is in-use.": "只有当之前的状态为使用中时,才将云硬盘状态重置为使用中。",
"The volume type needs to be consistent with the volume type when the snapshot is created.": "创建云硬盘的云硬盘类型需要和创建快照时间点的云硬盘类型保持一致。",
@ -2348,6 +2360,8 @@
"The working directory for commands to run in": "用于运行命令的工作目录",
"The {action} instruction has been issued, instance: {name}. \n You can wait for a few seconds to follow the changes of the list data or manually refresh the data to get the final display result.": "{action}指令已下发,实例名称:{name}。 \n 您可等待几秒关注列表数据的变更或是手动刷新数据,以获取最终展示结果。",
"The {action} instruction has been issued. \n You can wait for a few seconds to follow the changes of the list data or manually refresh the data to get the final display result.": "{action}指令已下发。 \n 您可等待几秒关注列表数据的变更或是手动刷新数据,以获取最终展示结果。",
"The {name} has already been used by other {resource}({content}), please change.": "{name} 已经被其他{resource}使用({content}),请修改。",
"The {name} {ports} have already been used, please change.": "{name} {ports} 已经被使用,请修改。",
"There are resources that cannot {action} in the selected resources, such as:": "您选中的资源中有无法{action}的资源,如:",
"There are resources that cannot {action} in the selected resources.": "您选中的资源中有无法{action}的资源。",
"There are resources under the project and cannot be deleted.": "项目下存在资源,无法执行删除操作。",
@ -2669,7 +2683,6 @@
"database backups": "数据库备份",
"database instances": "数据库实例",
"delete": "删除",
"delete DNAT rule": "删除DNAT规则",
"delete allowed address pair": "删除可用地址对",
"delete application credential": "删除应用凭证",
"delete bandwidth egress rules": "删除出方向带宽限制规则",
@ -2691,6 +2704,7 @@
"delete load balancer": "删除负载均衡",
"delete member": "删除成员",
"delete network": "删除网络",
"delete port forwarding": "删除端口转发",
"delete project": "删除项目",
"delete qos policy": "删除QoS策略",
"delete role": "删除角色",
@ -2729,6 +2743,8 @@
"enable cinder service": "启用存储服务",
"enable compute service": "启用计算服务",
"enable neutron agent": "启用网络服务",
"external port": "源端口",
"external ports": "源端口",
"extra specs": "额外规格",
"flavor": "云主机类型",
"floating ip": "浮动IP",
@ -2747,6 +2763,8 @@
"instance snapshots": "云主机快照",
"instance: {name}.": "实例名称:{name}。",
"instances": "云主机",
"internal port": "目标端口",
"internal ports": "目标端口",
"ipsec site connection": "IPsec站点连接",
"jump to the console": "跳转到控制台",
"keypair": "密钥",
@ -2778,6 +2796,7 @@
"please select network": "请选择网络",
"please select subnet": "请选择子网",
"port": "端口",
"port forwarding": "端口转发",
"port forwardings": "端口转发",
"port groups": "端口组",
"ports": "端口",
@ -2895,7 +2914,7 @@
"{name} type capacity (GiB)": "{name} 类型容量 (GiB)",
"{name} type snapshots": "{name} 类型快照",
"{name} {id} could not be found.": "您查看的资源{name} {id} 无法获取",
"{number} port forwarding rules": "{number}个端口转发规则",
"{number} {resource}": "{number}个{resource}",
"{pageSize} items/page": "{pageSize} 条/页",
"{seconds} seconds": "{seconds}秒"
}

View File

@ -0,0 +1,581 @@
// Copyright 2021 99cloud
//
// 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.
import React from 'react';
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import { isNull, isObject } from 'lodash';
import { getCanReachSubnetIdsWithRouterIdInComponent } from 'resources/neutron/router';
import { PortStore } from 'stores/neutron/port';
import { getPortFormItem, getPortsAndReasons } from 'resources/neutron/port';
import { getInterfaceWithReason } from 'resources/neutron/floatingip';
import globalPortForwardingStore from 'stores/neutron/port-forwarding';
import { enablePFW } from 'resources/neutron/neutron';
import { regex } from 'utils/validate';
const { portRangeRegex } = regex;
export class CreatePortForwarding extends ModalAction {
static id = 'create-port-forwarding';
static title = t('Create Port Forwarding');
get name() {
return t('Create Port Forwarding');
}
get resource() {
return t('port forwarding');
}
get resources() {
return t('port forwardings');
}
init() {
this.portStore = new PortStore();
this.state = {
...this.state,
alreadyUsedPorts: [],
portFixedIPs: [],
canReachSubnetIdsWithRouterId: [],
routerIdWithExternalNetworkInfo: [],
supportRange: true,
};
this.getRangeSupport();
this.getFipAlreadyUsedPorts();
getCanReachSubnetIdsWithRouterIdInComponent.call(this, (router) => {
const { item } = this;
return (
router.external_gateway_info &&
router.external_gateway_info.network_id === item.floating_network_id
);
});
}
async getFipAlreadyUsedPorts() {
const detail = await globalPortForwardingStore.fetchList({
fipId: this.item.id,
});
this.setState({
alreadyUsedPorts: detail || [],
});
}
get instanceName() {
return this.item.floating_ip_address || this.values.name;
}
static get modalSize() {
return 'large';
}
getModalSize() {
return 'large';
}
portsDisableFunc = (i) => i.fixed_ips.length === 0;
get defaultValue() {
const { floating_ip_address } = this.item;
const value = {
floatingIp: floating_ip_address,
};
return value;
}
static policy = 'create_floatingip_port_forwarding';
static allowed = (item) => {
return Promise.resolve(isNull(item.fixed_ip_address) && enablePFW());
};
getSubmitData(values) {
const {
floatingIp,
virtual_adapter: { selectedRows = [] } = {},
fixed_ip_address: { selectedRows: fixedIPAddressSelectedRows = [] } = {},
external_port,
internal_port,
...rest
} = values;
const data = {
...rest,
};
if (external_port.includes(':')) {
data.external_port_range = external_port;
} else {
data.external_port = external_port;
}
if (internal_port.includes(':')) {
data.internal_port_range = internal_port;
} else {
data.internal_port = internal_port;
}
data.internal_ip_address = fixedIPAddressSelectedRows[0].fixed_ip_address;
data.internal_port_id = selectedRows[0].id;
return data;
}
onSubmit = (data) => {
const { external_port_range, internal_port_range, ...rest } = data;
if (!external_port_range || this.supportRange) {
return globalPortForwardingStore.create({
id: this.item.id,
data,
});
}
const externalPorts = this.getPortsByInput(external_port_range);
const internalPorts = this.getPortsByInput(internal_port_range);
const reqs = externalPorts.map((externalPort, index) => {
return globalPortForwardingStore.create({
id: this.item.id,
data: {
...rest,
external_port: externalPort,
internal_port: internalPorts[index],
},
});
});
return Promise.all(reqs);
};
get nameForStateUpdate() {
return ['protocol'];
}
async getRangeSupport() {
try {
await globalPortForwardingStore.fetchListByPage({
limit: 1,
fipId: this.item.id,
external_port_range: '80:81',
});
this.setState({
supportRange: true,
});
} catch (e) {
console.log(e);
this.setState({
supportRange: false,
});
}
}
handlePortSelect = async (data) => {
this.setState({
fixedIpLoading: true,
});
const { canReachSubnetIdsWithRouterId } = this.state;
const interfacesWithReason = await getInterfaceWithReason(
data.selectedRows
);
const portFixedIPs = getPortsAndReasons(
interfacesWithReason,
canReachSubnetIdsWithRouterId,
true
);
this.setState({
portFixedIPs,
fixed_ip_address: undefined,
fixedIpLoading: false,
});
this.formRef.current &&
this.formRef.current.resetFields(['fixed_ip_address', 'internal_port']);
};
checkPortUsed = (val, type) => {
const { alreadyUsedPorts: usedPorts, protocol } = this.state;
const port = parseInt(val, 10);
const checkInternal = (baseCheck, pf) => {
if (!baseCheck) {
return false;
}
const formData = this.formRef.current.getFieldsValue([
'virtual_adapter',
'fixed_ip_address',
]);
const internalIpAddress =
formData.fixed_ip_address.selectedRows[0].fixed_ip_address;
const internalPortId = formData.virtual_adapter.selectedRows[0].id;
return (
pf.internal_port_id === internalPortId &&
pf.internal_ip_address === internalIpAddress
);
};
return usedPorts.find((pf) => {
const {
external_port,
internal_port,
external_port_range,
internal_port_range,
} = pf;
const range =
type === 'external' ? external_port_range : internal_port_range;
const pfPort = type === 'external' ? external_port : internal_port;
if (range) {
const [start, end] = this.getRangeFromString(range);
const baseCheck =
port >= start && port <= end && pf.protocol === protocol;
return type === 'external' ? baseCheck : checkInternal(baseCheck, pf);
}
const baseCheck = port === pfPort && pf.protocol === protocol;
return type === 'external' ? baseCheck : checkInternal(baseCheck, pf);
});
};
checkExtPortUsed = (val) => {
return this.checkPortUsed(val, 'external');
};
getRangeFromString = (value) => {
const tmp = (value || '').split(':');
if (!tmp.length || tmp.length > 2) {
return [];
}
const start = parseInt(tmp[0], 10);
const end = parseInt(tmp[1], 10);
return [start, end];
};
getPortForwardingContent = (item) => {
const {
external_port,
external_port_range,
internal_ip_address,
internal_port,
internal_port_range,
} = item;
return `${external_port || external_port_range} => ${internal_ip_address}:${
internal_port || internal_port_range
}`;
};
getUsedError = (items, name) => {
if (items.length === 1 && isObject(items[0])) {
return t(
'The {name} has already been used by other {resource}({content}), please change.',
{
name,
resource: this.resource,
content: this.getPortForwardingContent(items[0]),
}
);
}
return t('The {name} {ports} have already been used, please change.', {
name,
ports: items.join(','),
});
};
checkRangeInput = (input) => {
const [start, end] = this.getRangeFromString(input);
const length = end - start + 1;
if (length <= 1) {
return {
error: t(
'The value of the upper limit of the range must be greater than the value of the lower limit of the range.'
),
};
}
if (length > this.maxRangeSize) {
return { error: this.maxRangeSizeTip };
}
return {
length,
start,
end,
};
};
getPortsByRange = (start, length) => {
return Array.from({ length }, (_, i) => start + i);
};
getPortsByInput = (input) => {
const { length, start } = this.checkRangeInput(input);
return this.getPortsByRange(start, length);
};
checkPortRangeUsed = (start, length, type) => {
const ports = this.getPortsByRange(start, length);
const usedPorts = ports.filter((port) => {
if (type === 'external') {
return this.checkExtPortUsed(port);
}
return this.checkInternalPortUsed(port);
});
if (usedPorts.length) {
const name =
type === 'external' ? t('external ports') : t('internal ports');
return {
error: this.getUsedError(usedPorts, name),
};
}
return {
ports,
length,
};
};
checkTwoRangeLength = (externalLength, internalLength) => {
if (externalLength !== internalLength) {
return t(
'The size of the external port range is required to be the same as the size of the internal port range'
);
}
return '';
};
checkExternalPortInput = (externalPortInput, internalPortInput) => {
const externalIsRange = externalPortInput.includes(':');
const internalIsRange = internalPortInput.includes(':');
if (internalPortInput && externalIsRange !== internalIsRange) {
return t('Ports are either single values or ranges');
}
if (!externalIsRange) {
const ret = this.checkExtPortUsed(externalPortInput);
if (ret) {
return this.getUsedError([ret], t('external port'));
}
return '';
}
const {
start,
length,
error: lengthError,
} = this.checkRangeInput(externalPortInput);
if (lengthError) {
return lengthError;
}
const { error } = this.checkPortRangeUsed(start, length, 'external');
if (error) {
return error;
}
if (!portRangeRegex.test(internalPortInput)) {
return '';
}
const { length: internalLength } = this.checkRangeInput(internalPortInput);
if (!internalLength) {
return '';
}
return this.checkTwoRangeLength(length, internalLength);
};
validateExternalPort = (rule, val) => {
const { internal_port: internalPort } = this.formRef.current.getFieldsValue(
['internal_port']
);
if (!portRangeRegex.test(val)) {
return Promise.resolve(true);
}
const result = this.checkExternalPortInput(val, internalPort || '');
if (result) {
return Promise.reject(result);
}
return Promise.resolve(true);
};
checkInternalPortUsed = (val) => {
return this.checkPortUsed(val, 'internal');
};
checkInternalPortInput = (externalPortInput, internalPortInput) => {
const externalIsRange = externalPortInput.includes(':');
const internalIsRange = internalPortInput.includes(':');
if (externalPortInput && externalIsRange !== internalIsRange) {
return t('Ports are either single values or ranges');
}
if (!internalIsRange) {
const ret = this.checkInternalPortUsed(internalPortInput);
if (ret) {
return this.getUsedError([ret], t('internal port'));
}
return '';
}
const {
start,
length,
error: lengthError,
} = this.checkRangeInput(internalPortInput);
if (lengthError) {
return lengthError;
}
const { error } = this.checkPortRangeUsed(start, length, 'internal');
if (error) {
return error;
}
if (!portRangeRegex.test(externalPortInput)) {
return '';
}
const { length: externalLength } = this.checkRangeInput(externalPortInput);
if (!externalLength) {
return '';
}
return this.checkTwoRangeLength(length, externalLength);
};
validateInternalPort = (_, val) => {
if (!portRangeRegex.test(val)) {
return Promise.resolve(true);
}
const { external_port: externalPort } = this.formRef.current.getFieldsValue(
['external_port']
);
const result = this.checkInternalPortInput(externalPort || '', val);
if (result) {
return Promise.reject(result);
}
return Promise.resolve();
};
onFixedIpChange = (e) => {
this.setState(
{
fixed_ip_address: e,
},
() => {
this.formRef.current.resetFields(['internal_port']);
}
);
};
get supportRange() {
const { supportRange } = this.state;
return supportRange;
}
get maxRangeSize() {
if (this.supportRange) {
return Infinity;
}
return 20;
}
get maxRangeSizeTip() {
return t(
'The maximum batch size is {size}, that is, the size of the port range cannot exceed {size}.',
{ size: this.maxRangeSize }
);
}
get tips() {
return (
<div>
<p>
{t('1. You can create {resources} using ports or port ranges.', {
resources: this.resources,
})}
</p>
<p>
{t(
'2. In the same protocol, you cannot create multiple {resources} for the same source port or source port range.',
{
resources: this.resources,
}
)}
</p>
<p>
{t(
'3. When using a port range to create a port mapping, the size of the external port range is required to be the same as the size of the internal port range. For example, the external port range is 80:90 and the internal port range is 8080:8090.'
)}
</p>
{!this.supportRange && (
<p>
{t(
'4. When you use a port range to create {resources}, multiple {resources} will be created in batches. ',
{
resources: this.resources,
}
) + this.maxRangeSizeTip}
</p>
)}
</div>
);
}
get formItems() {
const { fixed_ip_address = { selectedRows: [] } } = this.state;
const externalPortExtra = t(
'Input external port or port range(example: 80 or 80:160)'
);
const internalPortExtra = t(
'Input internal port or port range(example: 80 or 80:160)'
);
const ret = [
{
name: 'floatingIp',
label: t('Floating Ip'),
type: 'label',
iconType: 'floatingIp',
},
{
name: 'description',
label: t('Description'),
type: 'textarea',
},
{
name: 'protocol',
label: t('Protocol'),
type: 'select',
options: [
{
label: 'TCP',
value: 'tcp',
},
{
label: 'UDP',
value: 'udp',
},
],
required: true,
},
{
name: 'external_port',
label: t('External Port/Port Range'),
type: 'port-range',
required: true,
validator: this.validateExternalPort,
dependencies: ['protocol', 'internal_port'],
placeholder: externalPortExtra,
extra: externalPortExtra,
},
{
name: 'internal_port',
label: t('Internal Port/Port Range'),
type: 'port-range',
hidden: fixed_ip_address.selectedRows.length === 0,
required: true,
validator: this.validateInternalPort,
dependencies: ['protocol', 'external_port'],
placeholder: internalPortExtra,
extra: internalPortExtra,
},
];
const [virtualAdapterItem, fixedIpItem] = getPortFormItem.call(this, [
'compute:nova',
'',
]);
virtualAdapterItem.label = t('Target Port');
fixedIpItem.label = t('Target IP Address');
fixedIpItem.onChange = this.onFixedIpChange;
ret.splice(4, 0, ...[virtualAdapterItem, fixedIpItem]);
return ret;
}
}
export default inject('rootStore')(observer(CreatePortForwarding));

View File

@ -15,13 +15,13 @@
import { ConfirmAction } from 'containers/Action';
import globalPortForwardingStore from 'stores/neutron/port-forwarding';
export default class DeleteAction extends ConfirmAction {
export default class Delete extends ConfirmAction {
get id() {
return 'delete';
}
get title() {
return t('Delete DNAT Rule');
return t('Delete Port Forwarding');
}
get isDanger() {
@ -33,7 +33,7 @@ export default class DeleteAction extends ConfirmAction {
}
get actionName() {
return t('delete DNAT rule');
return t('delete port forwarding');
}
policy = 'delete_floatingip_port_forwarding';

View File

@ -21,12 +21,12 @@ import { getPortFormItem, getPortsAndReasons } from 'resources/neutron/port';
import { ModalAction } from 'containers/Action';
export class Edit extends ModalAction {
static id = 'editDnat';
static id = 'edit';
static title = t('Edit');
get name() {
return t('Edit DNAT Rule');
return t('Edit Port Forwarding');
}
init() {
@ -287,7 +287,7 @@ export class Edit extends ModalAction {
return Promise.reject(
new Error(
t(
'A DNAT rule has been created for this port of this IP, please choose another port.'
'A port forwarding has been created for this port of this FIP, please choose another port.'
)
)
);

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import Create from 'pages/network/containers/FloatingIp/actions/CreateDNAT';
import Create from './Create';
import Edit from './Edit';
import Delete from './Delete';

View File

@ -44,6 +44,18 @@ export class PortForwarding extends Base {
return true;
}
get isSortByBackend() {
return true;
}
get defaultSortKey() {
return 'external_port';
}
get defaultSortOrder() {
return 'ascend';
}
get actionConfigs() {
return this.isAdminPage
? actionConfigs.actionConfigsAdmin
@ -52,24 +64,42 @@ export class PortForwarding extends Base {
getColumns = () => [
{
title: t('External Port'),
title: t('ID'),
dataIndex: 'id',
},
{
title: t('External Port/Port Range'),
dataIndex: 'external_port',
render: (value, record) => {
return value || record.external_port_range;
},
},
{
title: t('Internal Ip Address'),
dataIndex: 'internal_ip_address',
isHideable: true,
sorter: false,
},
{
title: t('Internal Port'),
title: t('Internal Port/Port Range'),
dataIndex: 'internal_port',
isHideable: true,
sorter: false,
render: (value, record) => {
return value || record.internal_port_range;
},
},
{
title: t('Protocol'),
dataIndex: 'protocol',
isHideable: true,
},
{
title: t('Description'),
dataIndex: 'description',
sorter: false,
isHideable: true,
},
];
get searchFilters() {
@ -92,6 +122,14 @@ export class PortForwarding extends Base {
label: t('External Port'),
name: 'external_port',
},
{
label: t('External Port Range'),
name: 'external_port_range',
},
{
label: t('Description'),
name: 'description',
},
];
}
}

View File

@ -79,8 +79,8 @@ export class FloatingIpDetail extends Base {
];
if (enablePFW() && isNull(this.detailData.fixed_ip_address)) {
tabs.push({
title: t('Port Forwarding Rules'),
key: 'port_forwarding_rules',
title: t('Port Forwardings'),
key: 'port_forwarding',
component: PortForwarding,
});
}

View File

@ -1,259 +0,0 @@
// Copyright 2021 99cloud
//
// 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.
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import { isNull } from 'lodash';
import { getCanReachSubnetIdsWithRouterIdInComponent } from 'resources/neutron/router';
import { PortStore } from 'stores/neutron/port';
import { getPortFormItem, getPortsAndReasons } from 'resources/neutron/port';
import { getInterfaceWithReason } from 'resources/neutron/floatingip';
import globalPortForwardingStore from 'stores/neutron/port-forwarding';
import { enablePFW } from 'resources/neutron/neutron';
export class CreateDNAT extends ModalAction {
static id = 'createDNAT';
static title = t('Create DNAT Rule');
get name() {
return t('Create DNAT rule');
}
init() {
this.portStore = new PortStore();
getCanReachSubnetIdsWithRouterIdInComponent.call(this, (router) => {
const { item } = this;
return (
router.external_gateway_info &&
router.external_gateway_info.network_id === item.floating_network_id
);
});
this.getFipAlreadyUsedPorts();
this.state = {
alreadyUsedPorts: [],
instanceFixedIPs: [],
portFixedIPs: [],
canReachSubnetIdsWithRouterId: [],
routerIdWithExternalNetworkInfo: [],
};
}
async getFipAlreadyUsedPorts() {
const detail = await globalPortForwardingStore.fetchList({
fipId: this.item.id,
});
this.setState({
alreadyUsedPorts: detail || [],
});
}
get instanceName() {
return this.item.floating_ip_address || this.values.name;
}
static get modalSize() {
return 'large';
}
getModalSize() {
return 'large';
}
portsDisableFunc = (i) => i.fixed_ips.length === 0;
get defaultValue() {
const { floating_ip_address } = this.item;
const value = {
floatingIp: floating_ip_address,
};
return value;
}
handlePortSelect = async (data) => {
this.setState({
fixedIpLoading: true,
});
const { canReachSubnetIdsWithRouterId } = this.state;
const interfacesWithReason = await getInterfaceWithReason(
data.selectedRows
);
const portFixedIPs = getPortsAndReasons(
interfacesWithReason,
canReachSubnetIdsWithRouterId,
true
);
this.setState({
portFixedIPs,
fixed_ip_address: undefined,
fixedIpLoading: false,
});
this.formRef.current &&
this.formRef.current.resetFields(['fixed_ip_address', 'internal_port']);
};
static policy = 'create_floatingip_port_forwarding';
static allowed = (item) => {
return Promise.resolve(isNull(item.fixed_ip_address) && enablePFW());
};
onSubmit = (values) => {
const {
floatingIp,
virtual_adapter: { selectedRows = [] } = {},
fixed_ip_address: { selectedRows: fixedIPAddressSelectedRows = [] } = {},
...rest
} = values;
const data = {
...rest,
};
data.internal_ip_address = fixedIPAddressSelectedRows[0].fixed_ip_address;
data.internal_port_id = selectedRows[0].id;
return globalPortForwardingStore.create({
id: this.item.id,
data,
});
};
get nameForStateUpdate() {
return ['protocol'];
}
get formItems() {
const { fixed_ip_address = { selectedRows: [] } } = this.state;
const ret = [
{
name: 'floatingIp',
label: t('Floating Ip'),
type: 'label',
iconType: 'floatingIp',
},
{
name: 'protocol',
label: t('Protocol'),
type: 'select',
options: [
{
label: 'TCP',
value: 'tcp',
},
{
label: 'UDP',
value: 'udp',
},
],
required: true,
},
{
name: 'external_port',
label: t('External Port'),
type: 'input-number',
min: 1,
max: 65535,
required: true,
validator: (_, val) => {
if (!val) {
return Promise.reject(
new Error(`${t('Please input')} ${t('External Port')}`)
);
}
const { alreadyUsedPorts, protocol } = this.state;
const flag = alreadyUsedPorts.some(
(pf) => pf.external_port === val && pf.protocol === protocol
);
if (flag) {
return Promise.reject(
new Error(
t('The port of this fip is in use, Please change another port.')
)
);
}
return Promise.resolve(true);
},
dependencies: ['protocol'],
},
{
name: 'internal_port',
label: t('Internal Port'),
type: 'input-number',
hidden: fixed_ip_address.selectedRows.length === 0,
min: 1,
max: 65535,
required: true,
validator: (_, val) => {
if (!val) {
return Promise.reject(
new Error(`${t('Please input')} ${t('Internal Port')}`)
);
}
const formData = this.formRef.current.getFieldsValue([
'virtual_adapter',
'fixed_ip_address',
]);
const internal_ip_address =
formData.fixed_ip_address.selectedRows[0].fixed_ip_address;
const internal_port_id = formData.virtual_adapter.selectedRows[0].id;
const { alreadyUsedPorts, protocol } = this.state;
// determine whether the FIP has been bound to the port of the port
const flag = alreadyUsedPorts.some(
(pf) =>
pf.internal_port === val &&
pf.internal_port_id === internal_port_id &&
pf.internal_ip_address === internal_ip_address &&
pf.protocol === protocol
);
if (flag) {
return Promise.reject(
new Error(
t(
'A DNAT rule has been created for this port of this IP, please choose another port.'
)
)
);
}
return Promise.resolve(true);
},
dependencies: ['protocol'],
},
];
const extraColumn = getPortFormItem.call(this, ['compute:nova', '']);
const portIndex = extraColumn.findIndex(
(i) => i.name === 'virtual_adapter'
);
extraColumn[portIndex].label = t('Target Port');
const fixedIPAddressIndex = extraColumn.findIndex(
(i) => i.name === 'fixed_ip_address'
);
extraColumn[fixedIPAddressIndex].label = t('Target IP Address');
extraColumn[fixedIPAddressIndex].onChange = (e) => {
this.setState(
{
fixed_ip_address: e,
},
() => {
this.formRef.current.resetFields(['internal_port']);
}
);
};
ret.splice(3, 0, ...extraColumn);
return ret;
}
}
export default inject('rootStore')(observer(CreateDNAT));

View File

@ -18,7 +18,7 @@ import Associate from './Associate';
import Release from './Release';
import Disassociate from './Disassociate';
import Edit from './Edit';
import CreateDNAT from './CreateDNAT';
import CreatePortForwarding from '../Detail/PortForwarding/actions/Create';
const rowActions = {
firstAction: Edit,
@ -30,7 +30,7 @@ const rowActions = {
action: Disassociate,
},
{
action: CreateDNAT,
action: CreatePortForwarding,
},
{
action: Release,

View File

@ -21,11 +21,11 @@ import {
} from 'resources/neutron/floatingip';
import { FloatingIpStore } from 'stores/neutron/floatingIp';
import { emptyActionConfig } from 'utils/constants';
import { Col, Popover, Row } from 'antd';
import { Popover, List } from 'antd';
import { FileTextOutlined } from '@ant-design/icons';
import { qosEndpoint } from 'client/client/constants';
import { getOptions } from 'utils';
import styles from './styles.less';
import { isEmpty } from 'lodash';
import actionConfigs from './actions';
export class FloatingIps extends Base {
@ -150,33 +150,78 @@ export class FloatingIps extends Base {
}
getPortForwardingDetail(record, detail) {
const { key, ...rest } = detail;
if (isEmpty(rest)) {
return '';
}
const { floating_ip_address: fip } = record;
const { protocol, external_port, internal_ip_address, internal_port } =
detail;
return `${protocol}: ${fip}:${external_port} => ${internal_ip_address}:${internal_port}`;
const {
protocol,
external_port,
external_port_range,
internal_ip_address,
internal_port,
internal_port_range,
} = detail;
return `${protocol}: ${fip}:${
external_port || external_port_range
} => ${internal_ip_address}:${internal_port || internal_port_range}`;
}
get portForwardingResourceName() {
return t('Port Forwarding');
}
get portForwardingResourcesName() {
return t('Port Forwardings');
}
getPortForwardingRender(record) {
const data = this.getRecordPortForwarding(record);
if (!data.length) {
const { length } = data;
if (!length) {
return null;
}
const pageSize = 10;
const zeroLength =
length > pageSize ? pageSize - (length % pageSize) : pageSize;
const zeroData = Array.from({ length: zeroLength }, (i) => ({
key: `zero-${i}`,
}));
const dataWithKey = data.map((d) => ({
...d,
key: d.external_port || d.external_port_range,
}));
const newData = [...dataWithKey, ...zeroData];
const content = (
<List
itemLayout="vertical"
size="small"
pagination={{
hideOnSinglePage: true,
pageSize,
size: 'small',
}}
dataSource={newData}
renderItem={(item) => {
return (
<div style={{ height: '30px', lineHeight: '30px' }}>
{this.getPortForwardingDetail(record, item)}
</div>
);
}}
/>
);
return (
<Popover
content={
<Row className={styles['popover-row']} gutter={[8, 8]}>
{data.map((i, idx) => (
<Col span={24} key={`pfw-${idx}`}>
{this.getPortForwardingDetail(record, i)}
</Col>
))}
</Row>
}
title={t('Port Forwarding')}
content={content}
title={this.portForwardingResourceName}
destroyTooltipOnHide
placement="right"
>
{t('{number} port forwarding rules', {
{t('{number} {resource}', {
number: data.length,
resource: this.portForwardingResourcesName,
})}
&nbsp;
<FileTextOutlined />
@ -190,8 +235,9 @@ export class FloatingIps extends Base {
return '';
}
const ret = data.map((i) => this.getPortForwardingDetail(record, i));
const total = t('{number} port forwarding rules', {
const total = t('{number} {resource}', {
number: data.length,
resource: this.portForwardingResourcesName,
});
return [total, ...ret].join('\n');
}

View File

@ -1,5 +0,0 @@
.popover-row {
max-width: 320px;
margin: 0 !important;
text-align: center;
}

View File

@ -298,7 +298,7 @@ export const getFixedIPFormItemForAssociate = (label, self) => {
export const getFIPFormItemExtra = () => {
if (enablePFW()) {
return t(
'The floating IP configured with port forwarding rules cannot be bound'
'The floating IP configured with port forwardings cannot be bound'
);
}
return '';

View File

@ -35,6 +35,13 @@ export class PortForwardingStore extends Base {
};
}
updateParamsSortPage = (params, sortKey, sortOrder) => {
if (sortKey && sortOrder) {
params.sort_key = sortKey;
params.sort_dir = sortOrder === 'descend' ? 'desc' : 'asc';
}
};
listDidFetch(items, allProjects, filters) {
if (items.length === 0) {
return items;

View File

@ -49,7 +49,7 @@ describe('The Router Page', () => {
'Static Routes',
'staticRoutes'
);
// .clickDetailTab('DNAT Rules', 'dnat');
// .clickDetailTab('Port Forwardings', 'port_forwarding');
cy.goBackToList(listUrl);
});