diff --git a/perfkitbenchmarker/linux_benchmarks/iperf_benchmark.py b/perfkitbenchmarker/linux_benchmarks/iperf_benchmark.py index 5e08e3981..84417698f 100644 --- a/perfkitbenchmarker/linux_benchmarks/iperf_benchmark.py +++ b/perfkitbenchmarker/linux_benchmarks/iperf_benchmark.py @@ -118,6 +118,9 @@ IPERF_UDP_PORT = 25000 IPERF_RETRIES = 5 +IPERF_PORT_IPV6 = IPERF_PORT + 1 +IPERF_UDP_PORT_IPV6 = IPERF_UDP_PORT + 1 + def GetConfig(user_config): return configs.LoadConfig(BENCHMARK_CONFIG, user_config, BENCHMARK_NAME) @@ -145,6 +148,12 @@ def Prepare(benchmark_spec): if UDP in FLAGS.iperf_benchmarks: vm.AllowPort(IPERF_UDP_PORT) + if vm_util.ShouldRunOnExternalIpv6Address(vm): + if TCP in FLAGS.iperf_benchmarks: + vm.AllowPort(IPERF_PORT_IPV6, source_range = ['::/0']) + if UDP in FLAGS.iperf_benchmarks: + vm.AllowPort(IPERF_UDP_PORT_IPV6, source_range = ['::/0']) + if TCP in FLAGS.iperf_benchmarks: stdout, _ = vm.RemoteCommand( f'nohup iperf --server --port {IPERF_PORT} &> /dev/null & echo $!' @@ -154,6 +163,17 @@ def Prepare(benchmark_spec): vm.iperf_tcp_server_pid = stdout.strip() # Check that the server is actually running vm.RemoteCommand(f'ps -p {vm.iperf_tcp_server_pid}') + + if vm_util.ShouldRunOnExternalIpv6Address(vm): + stdout, _ = vm.RemoteCommand( + f'nohup iperf -V --server --port {IPERF_PORT_IPV6} &> /dev/null & echo $!' + ) + + # TODO(user): store this in a better place + vm.iperf_tcp_server_pid_ipv6 = stdout.strip() + # Check that the server is actually running + vm.RemoteCommand(f'ps -p {vm.iperf_tcp_server_pid_ipv6}') + if UDP in FLAGS.iperf_benchmarks: stdout, _ = vm.RemoteCommand( f'nohup iperf --server --bind {vm.internal_ip} --udp ' @@ -164,6 +184,16 @@ def Prepare(benchmark_spec): # Check that the server is actually running vm.RemoteCommand(f'ps -p {vm.iperf_udp_server_pid}') + if vm_util.ShouldRunOnExternalIpv6Address(vm): + stdout, _ = vm.RemoteCommand( + f'nohup iperf -V --server --bind {vm.ipv6_address} --udp ' + f'--port {IPERF_UDP_PORT_IPV6} &> /dev/null & echo $!' + ) + # TODO(user): store this in a better place + vm.iperf_udp_server_pid_ipv6 = stdout.strip() + # Check that the server is actually running + vm.RemoteCommand(f'ps -p {vm.iperf_udp_server_pid_ipv6}') + @vm_util.Retry(max_retries=IPERF_RETRIES) def _RunIperf( @@ -174,6 +204,7 @@ def _RunIperf( ip_type, protocol, interval_size=None, + using_ipv6 = False, ): """Run iperf using sending 'vm' to connect to 'ip_address'. @@ -185,6 +216,7 @@ def _RunIperf( ip_type: The IP type of 'ip_address' (e.g. 'internal', 'external') protocol: The protocol for Iperf to use. Either 'TCP' or 'UDP' interval_size: Time interval at which to output stats. + using_ipv6: Whether receiving_ip_address is IPv6 or not Returns: A Sample. @@ -199,6 +231,7 @@ def _RunIperf( 'sending_zone': sending_vm.zone, 'runtime_in_seconds': FLAGS.iperf_runtime_in_seconds, 'ip_type': ip_type, + 'using_ipv6': using_ipv6, } if protocol == TCP: @@ -208,6 +241,13 @@ def _RunIperf( f'--parallel {thread_count}' ) + if using_ipv6: + iperf_cmd = ( + f'iperf -V --enhancedreports --client {receiving_ip_address} --port ' + f'{IPERF_PORT_IPV6} --format m --time {FLAGS.iperf_runtime_in_seconds} ' + f'--parallel {thread_count}' + ) + if FLAGS.iperf_interval: iperf_cmd += f' --interval {FLAGS.iperf_interval}' @@ -437,6 +477,13 @@ def _RunIperf( f' {IPERF_UDP_PORT} --format m --time {FLAGS.iperf_runtime_in_seconds}' f' --parallel {thread_count} ' ) + + if using_ipv6: + iperf_cmd = ( + f'iperf -V --enhancedreports --udp --client {receiving_ip_address} --port' + f' {IPERF_UDP_PORT_IPV6} --format m --time {FLAGS.iperf_runtime_in_seconds}' + f' --parallel {thread_count} ' + ) if FLAGS.iperf_udp_per_stream_bandwidth: iperf_cmd += f' --bandwidth {FLAGS.iperf_udp_per_stream_bandwidth}M' @@ -603,6 +650,22 @@ def Run(benchmark_spec): time.sleep(FLAGS.iperf_sleep_time) + # Send using external IPv6 addresses + if vm_util.ShouldRunOnExternalIpv6Address(receiving_vm): + results.append( + _RunIperf( + sending_vm, + receiving_vm, + receiving_vm.ipv6_address, + thread_count, + vm_util.IpAddressMetadata.EXTERNAL, + protocol, + interval_size=FLAGS.iperf_interval, + using_ipv6 = True, + ) + ) + time.sleep(FLAGS.iperf_sleep_time) + return results @@ -619,7 +682,18 @@ def Cleanup(benchmark_spec): vm.RemoteCommand( f'kill -9 {vm.iperf_tcp_server_pid}', ignore_failure=True ) + + if vm_util.ShouldRunOnExternalIpv6Address(vm): + vm.RemoteCommand( + f'kill -9 {vm.iperf_tcp_server_pid_ipv6}', ignore_failure=True + ) + if UDP in FLAGS.iperf_benchmarks: vm.RemoteCommand( f'kill -9 {vm.iperf_udp_server_pid}', ignore_failure=True ) + + if vm_util.ShouldRunOnExternalIpv6Address(vm): + vm.RemoteCommand( + f'kill -9 {vm.iperf_udp_server_pid_ipv6}', ignore_failure=True + ) diff --git a/perfkitbenchmarker/linux_benchmarks/netperf_benchmark.py b/perfkitbenchmarker/linux_benchmarks/netperf_benchmark.py index 5062f2356..2353cce96 100644 --- a/perfkitbenchmarker/linux_benchmarks/netperf_benchmark.py +++ b/perfkitbenchmarker/linux_benchmarks/netperf_benchmark.py @@ -265,6 +265,9 @@ def PrepareServerVM(server_vm, client_vm_internal_ips, client_vm_ip_address): if vm_util.ShouldRunOnExternalIpAddress(): # Open all of the command and data ports server_vm.AllowPort(PORT_START, PORT_START + num_streams * 2 - 1) + + if vm_util.ShouldRunOnExternalIpv6Address(server_vm): + server_vm.AllowPort(PORT_START, PORT_START + num_streams * 2 - 1, ['::/0']) port_end = PORT_START + num_streams * 2 - 1 netserver_cmd = ( @@ -773,6 +776,7 @@ def RunClientServerVMs(client_vm, server_vm): external_ip_result.metadata['ip_type'] = ( vm_util.IpAddressMetadata.EXTERNAL ) + external_ip_result.metadata['using_ipv6'] = (False) external_ip_result.metadata.update(metadata) results.extend(external_ip_results) @@ -789,8 +793,26 @@ def RunClientServerVMs(client_vm, server_vm): internal_ip_result.metadata['ip_type'] = ( vm_util.IpAddressMetadata.INTERNAL ) + internal_ip_result.metadata['using_ipv6'] = (False) results.extend(internal_ip_results) + if vm_util.ShouldRunOnExternalIpv6Address(server_vm) and vm_util.ShouldRunOnExternalIpv6Address(client_vm): + external_ipv6_results = RunNetperf( + client_vm, + netperf_benchmark, + [server_vm.ipv6_address], + num_streams, + # NAT translates internal to external IP when remote IP is external + [client_vm.ipv6_address], + ) + for external_ipv6_result in external_ipv6_results: + external_ipv6_result.metadata['ip_type'] = ( + vm_util.IpAddressMetadata.EXTERNAL + ) + external_ipv6_result.metadata['using_ipv6'] = (True) + external_ipv6_result.metadata.update(metadata) + results.extend(external_ipv6_results) + return results diff --git a/perfkitbenchmarker/linux_benchmarks/ping_benchmark.py b/perfkitbenchmarker/linux_benchmarks/ping_benchmark.py index 330c881d2..057a49732 100644 --- a/perfkitbenchmarker/linux_benchmarks/ping_benchmark.py +++ b/perfkitbenchmarker/linux_benchmarks/ping_benchmark.py @@ -63,6 +63,10 @@ def Prepare(benchmark_spec): # pylint: disable=unused-argument vms = benchmark_spec.vms for vm in vms: vm.AllowIcmp() + vms = benchmark_spec.vms + for vm in vms: + if vm_util.ShouldRunOnExternalIpv6Address(vm): + vm.AllowIcmpIpv6() def Run(benchmark_spec): @@ -88,10 +92,15 @@ def Run(benchmark_spec): results = results + _RunPing( sending_vm, receiving_vm, receiving_vm.internal_ip, ip_type ) + if vm_util.ShouldRunOnExternalIpv6Address(receiving_vm): + ip_type = vm_util.IpAddressMetadata.EXTERNAL + results = results + _RunPing( + sending_vm, receiving_vm, receiving_vm.ipv6_address, ip_type, using_ipv6=True + ) return results -def _RunPing(sending_vm, receiving_vm, receiving_ip, ip_type): +def _RunPing(sending_vm, receiving_vm, receiving_ip, ip_type, using_ipv6 = False): """Run ping using 'sending_vm' to connect to 'receiving_ip'. Args: @@ -100,6 +109,7 @@ def _RunPing(sending_vm, receiving_vm, receiving_ip, ip_type): receiving_ip: The IP address to be pinged. ip_type: The type of 'receiving_ip', (either 'vm_util.IpAddressSubset.INTERNAL or vm_util.IpAddressSubset.EXTERNAL') + using_ipv6: True if receiving_ip is IPv6, False if not Returns: A list of samples, with one sample for each metric. @@ -120,6 +130,7 @@ def _RunPing(sending_vm, receiving_vm, receiving_ip, ip_type): metadata = { 'ip_type': ip_type, + 'using_ipv6': using_ipv6, 'receiving_zone': receiving_vm.zone, 'sending_zone': sending_vm.zone, } diff --git a/perfkitbenchmarker/network.py b/perfkitbenchmarker/network.py index b3008aae5..3d11fc26d 100644 --- a/perfkitbenchmarker/network.py +++ b/perfkitbenchmarker/network.py @@ -77,6 +77,14 @@ def AllowIcmp(self, vm): """ pass + def AllowIcmpIpv6(self, vm): + """Opens the ICMP for IPv6 protocol on the firewall + + Args: + vm: The BaseVirtualMachine object to open the ICMP for IPv6 protocol for. + """ + pass + def AllowPort( self, vm, diff --git a/perfkitbenchmarker/providers/gcp/gce_network.py b/perfkitbenchmarker/providers/gcp/gce_network.py index e9b7a2b73..909e1d6cc 100644 --- a/perfkitbenchmarker/providers/gcp/gce_network.py +++ b/perfkitbenchmarker/providers/gcp/gce_network.py @@ -692,6 +692,8 @@ def AllowPort( start_port, end_port, ) + if source_range is not None: + firewall_name += f'-{source_range.replace(':','c').replace('/','s')}' key = (vm.project, vm.cidr, start_port, end_port, source_range) else: firewall_name = 'perfkit-firewall-%s-%d-%d' % ( @@ -699,6 +701,8 @@ def AllowPort( start_port, end_port, ) + if source_range is not None: + firewall_name += f'-{source_range.replace(':','c').replace('/','s')}' key = (vm.project, start_port, end_port, source_range) if key in self.firewall_rules: return @@ -753,6 +757,37 @@ def AllowIcmp(self, vm): self.firewall_icmp_rules[key] = firewall_rule firewall_rule.Create() + def AllowIcmpIpv6(self, vm): + """Open the ICMP for IPv6 protocl on the firewall. + Note: Protocol Number for ICMP for IPv6 is 58. + (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml) + + Args: + vm: The BaseVirtualMachine object to open the ICMP for IPv6 protocol for. + """ + if vm.is_static: + return + with self._lock: + if vm.cidr: + cidr_string = network.BaseNetwork.FormatCidrString(vm.cidr) + firewall_name = 'perfkit-firewall-icmp-ipv6-%s-%s' % ( + cidr_string, + FLAGS.run_uri, + ) + key = (vm.project, vm.cidr, 'ipv6') + else: + firewall_name = 'perfkit-firewall-icmp-ipv6-%s' % FLAGS.run_uri + key = (vm.project, 'ipv6') + + if key in self.firewall_icmp_rules: + return + + allow = '58' # ICMP for IPv6 Protocol Number + firewall_rule = GceFirewallRule( + firewall_name, vm.project, allow, vm.network.network_resource.name, "::/0" + ) + self.firewall_icmp_rules[key] = firewall_rule + firewall_rule.Create() class GceNetworkSpec(network.BaseNetworkSpec): """Object representing a GCE Network specification.""" @@ -873,6 +908,9 @@ def _Create(self): cmd.flags['network'] = self.network_name cmd.flags['region'] = self.region cmd.flags['range'] = self.addr_range + if FLAGS.assign_external_ipv6: + cmd.flags['stack-type'] = 'IPV4_IPV6' + cmd.flags['ipv6-access-type'] = 'EXTERNAL' cmd.Issue() def _Exists(self) -> bool: diff --git a/perfkitbenchmarker/providers/gcp/gce_virtual_machine.py b/perfkitbenchmarker/providers/gcp/gce_virtual_machine.py index 8d0136a4c..5b2476a74 100644 --- a/perfkitbenchmarker/providers/gcp/gce_virtual_machine.py +++ b/perfkitbenchmarker/providers/gcp/gce_virtual_machine.py @@ -734,6 +734,9 @@ def _GenerateCreateCommand(self, ssh_keys_path): no_address_arg = [] if not self.assign_external_ip or idx > 0: no_address_arg = ['no-address'] + ipv6_args = [] + if self.assign_external_ipv6: + ipv6_args = ['stack-type=IPV4_IPV6', 'ipv6-network-tier=PREMIUM'] cmd.additional_flags += [ '--network-interface', ','.join( @@ -744,6 +747,7 @@ def _GenerateCreateCommand(self, ssh_keys_path): ] + gce_nic_queue_count_arg + no_address_arg + + ipv6_args ), ] @@ -1106,6 +1110,9 @@ def _ParseDescribeResponse(self, describe_response): for network_interface in describe_response['networkInterfaces']: self.internal_ips.append(network_interface['networkIP']) + + if 'ipv6AccessConfigs' in network_interface: + self.ipv6_address = network_interface['ipv6AccessConfigs'][0]['externalIpv6'] @property def HasIpAddress(self): @@ -1118,6 +1125,7 @@ def _NeedsToParseDescribeResponse(self): not self.id or not self.GetInternalIPs() or (self.assign_external_ip and not self.ip_address) + or (self.assign_external_ipv6 and not self.ipv6_address) ) @vm_util.Retry() diff --git a/perfkitbenchmarker/virtual_machine.py b/perfkitbenchmarker/virtual_machine.py index 65e84305e..738c49d17 100644 --- a/perfkitbenchmarker/virtual_machine.py +++ b/perfkitbenchmarker/virtual_machine.py @@ -138,6 +138,12 @@ def ValidateVmMetadataFlag(options_list): 'If True, an external (public) IP will be created for VMs. ' 'If False, --connect_via_internal_ip may also be needed.', ) +_ASSIGN_EXTERNAL_IPV6 = flags.DEFINE_boolean( + 'assign_external_ipv6', + False, + 'If True, an external (public) IPv6 address will be created for VMs. ' + '(initial support only for GCP VMs)' +) flags.DEFINE_string( 'boot_startup_script', None, @@ -334,6 +340,7 @@ class BaseVmSpec(spec.BaseSpec): gpu_type: None or string. Type of gpus to attach to the VM. image: The disk image to boot from. assign_external_ip: Bool. If true, create an external (public) IP. + assign_external_ipv6: Bool. If true, create an external (public) IPv6 address. install_packages: If false, no packages will be installed. This is useful if benchmark dependencies have already been installed. background_cpu_threads: The number of threads of background CPU usage while @@ -362,6 +369,7 @@ def __init__(self, *args, **kwargs): self.gpu_type = None self.image = None self.assign_external_ip = None + self.assign_external_ipv6 = None self.install_packages = None self.background_cpu_threads = None self.background_network_mbits_per_sec = None @@ -420,6 +428,8 @@ def _ApplyFlags(cls, config_values, flag_values): config_values['gpu_count'] = flag_values.gpu_count if flag_values['assign_external_ip'].present: config_values['assign_external_ip'] = flag_values.assign_external_ip + if flag_values['assign_external_ipv6'].present: + config_values['assign_external_ipv6'] = flag_values.assign_external_ipv6 if flag_values['disable_interrupt_moderation'].present: config_values['disable_interrupt_moderation'] = ( flag_values.disable_interrupt_moderation @@ -479,6 +489,10 @@ def _GetOptionDecoderConstructions(cls): option_decoders.BooleanDecoder, {'default': True}, ), + 'assign_external_ipv6': ( + option_decoders.BooleanDecoder, + {'default': False}, + ), 'gpu_type': ( option_decoders.EnumDecoder, {'valid_values': VALID_GPU_TYPES, 'default': None}, @@ -545,6 +559,8 @@ class BaseVirtualMachine(os_mixin.BaseOsMixin, resource.BaseResource): internal_ip: Internal IP address. assign_external_ip: If True, create an external (public) IP. ip_address: Public (external) IP address. + assign_external_ipv6: If True, create an external (public) IPv6 Address. + ipv6_address: Public (external) IPv6 address. machine_type: The provider-specific instance type (e.g. n1-standard-8). project: The provider-specific project associated with the VM (e.g. artisanal-lightbulb-883). @@ -630,7 +646,9 @@ def __init__(self, vm_spec: BaseVmSpec): ) self.boot_completion_ip_subset = _BOOT_COMPLETION_IP_SUBSET.value self.assign_external_ip = vm_spec.assign_external_ip + self.assign_external_ipv6 = vm_spec.assign_external_ipv6 self.ip_address = None + self.ipv6_address = None self.internal_ip = None self.internal_ips = [] self.user_name = _VM_USER_NAME.value @@ -860,6 +878,11 @@ def AllowIcmp(self): if self.firewall and not FLAGS.skip_firewall_rules: self.firewall.AllowIcmp(self) + def AllowIcmpIpv6(self): + """Opens ICMP for IPv6 protocol on the firewall corresponding to the VM if exists""" + if self.firewall and not FLAGS.skip_firewall_rules and self.ipv6_address is not None: + self.firewall.AllowIcmpIpv6(self) + def AllowPort(self, start_port, end_port=None, source_range=None): """Opens the port on the firewall corresponding to the VM if one exists. @@ -939,6 +962,8 @@ def GetResourceMetadata(self): result['name'] = self.name if self.ip_address is not None: result['ip_address'] = self.ip_address + if self.ipv6_address is not None: + result['ipv6_address'] = self.ipv6_address if pkb_flags.RECORD_PROCCPU.value and self.cpu_version: result['cpu_version'] = self.cpu_version if self.create_operation_name is not None: diff --git a/perfkitbenchmarker/vm_util.py b/perfkitbenchmarker/vm_util.py index a21bb4036..b25b2412a 100644 --- a/perfkitbenchmarker/vm_util.py +++ b/perfkitbenchmarker/vm_util.py @@ -721,6 +721,10 @@ def ShouldRunOnExternalIpAddress(ip_type=None): IpAddressSubset.REACHABLE, ) +def ShouldRunOnExternalIpv6Address(receiving_vm): + """Returns whether a test should be run on an instance's external IPv6 address. + """ + return ShouldRunOnExternalIpAddress() and receiving_vm.ipv6_address is not None def ShouldRunOnInternalIpAddress(sending_vm, receiving_vm, ip_type=None): """Returns whether a test should be run on an instance's internal IP.