esp-test-utils 0.2.3__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/CHANGELOG.md +9 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/PKG-INFO +3 -1
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/PKG-INFO +3 -1
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/SOURCES.txt +3 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/requires.txt +2 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/port/base_port.py +13 -1
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/common/decorators.py +20 -0
- esp_test_utils-0.3.0/esptest/devices/switch.py +544 -0
- esp_test_utils-0.3.0/esptest/network/mac.py +31 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/scripts/downbin.py +10 -1
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/scripts/list_ports.py +20 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/tools/download_bin.py +33 -6
- esp_test_utils-0.3.0/esptest/tools/uart_monitor.py +324 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/pyproject.toml +5 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/adapter/test_Dut.py +21 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/basic/test_decorators.py +25 -1
- esp_test_utils-0.3.0/tests/devices/test_switch.py +217 -0
- esp_test_utils-0.2.3/esptest/network/mac.py +0 -5
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/.github/.gitkeep +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/.github/workflows/pypi-publish.yml +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/.gitignore +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/.gitlab-ci.yml +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/.pre-commit-config.yaml +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/CONTRIBUTING.md +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/LICENSE +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/README.md +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/docs/Makefile +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/docs/conf.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/docs/index.rst +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/docs/make.bat +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/dependency_links.txt +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/entry_points.txt +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/top_level.txt +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/__main__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/dut/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/dut/create_dut.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/dut/dut_base.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/dut/esp_dut.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/dut/esp_mixin.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/dut/esp_port.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/dut/mac_mixin.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/dut/wrapper.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/port/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/port/serial_port.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/adapter/port/shell_port.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/all.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/common/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/common/compat_typing.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/common/data_monitor.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/common/encoding.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/common/generator.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/common/shell.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/common/timestamp.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/config/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/config/default_config.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/config/env_config.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/db/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/db/runners.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/devices/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/devices/attenuator.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/devices/esp_serial.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/devices/serial_dut.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/devices/serial_tools.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/env/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/env/base_env.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/env/wifi_env.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/esp_console/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/esp_console/wifi_cmd.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/interface/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/interface/dut.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/interface/port.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/iperf_utility/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/iperf_utility/iperf_results.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/iperf_utility/iperf_test.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/iperf_utility/iperf_test.test.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/iperf_utility/line_chart.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/logger/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/logger/logger.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/network/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/network/netif.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/network/nic.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/scripts/monitor.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/scripts/set_att.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/tools/copy_bin.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/tools/http_download.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/tools/pip_check.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/utility/gen_esp32part.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/esptest/utility/parse_bin_path.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/example/jap_test.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/example/restart_test.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/setup.cfg +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/__init__.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/adapter/test_shell_port.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/basic/test_network.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/conftest.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/db/test_db_runners.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/esp_console/conftest.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/esp_console/test_WifiCmd.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/iperf_utility/test_chart.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/iperf_utility/test_iperf_results.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/iperf_utility/test_iperf_util.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/test_EnvConfig.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/test_common.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/test_import.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/tools/test_download_file.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/tools/test_pip_check.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/utility/_files/test-bin.zip +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tests/utility/test_parse_bin_path.py +0 -0
- {esp_test_utils-0.2.3 → esp_test_utils-0.3.0}/tools/ci/check_dev_version.py +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## v0.3.0 (2025-12-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
- feat: esp-listports support monitor mode
|
|
5
|
+
- feat: add more logs to H3CSwitch
|
|
6
|
+
- feat: esp-downbin support argument --force-no-stub
|
|
7
|
+
- feat: add h3c switch device control
|
|
8
|
+
- feat: add decorator timeit
|
|
9
|
+
|
|
1
10
|
## v0.2.3 (2025-11-14)
|
|
2
11
|
|
|
3
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: esp-test-utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: ESP Test Utils
|
|
5
5
|
Author-email: Chen Yudong <chenyudong@espressif.com>
|
|
6
6
|
License: Apache License
|
|
@@ -228,8 +228,10 @@ Requires-Dist: pyserial
|
|
|
228
228
|
Requires-Dist: PyYAML
|
|
229
229
|
Requires-Dist: pexpect
|
|
230
230
|
Requires-Dist: pyusb
|
|
231
|
+
Requires-Dist: pyudev
|
|
231
232
|
Requires-Dist: esptool
|
|
232
233
|
Requires-Dist: packaging
|
|
234
|
+
Requires-Dist: rich
|
|
233
235
|
Requires-Dist: sqlalchemy
|
|
234
236
|
Requires-Dist: typing_extensions; python_version < "3.11"
|
|
235
237
|
Provides-Extra: idfci
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: esp-test-utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: ESP Test Utils
|
|
5
5
|
Author-email: Chen Yudong <chenyudong@espressif.com>
|
|
6
6
|
License: Apache License
|
|
@@ -228,8 +228,10 @@ Requires-Dist: pyserial
|
|
|
228
228
|
Requires-Dist: PyYAML
|
|
229
229
|
Requires-Dist: pexpect
|
|
230
230
|
Requires-Dist: pyusb
|
|
231
|
+
Requires-Dist: pyudev
|
|
231
232
|
Requires-Dist: esptool
|
|
232
233
|
Requires-Dist: packaging
|
|
234
|
+
Requires-Dist: rich
|
|
233
235
|
Requires-Dist: sqlalchemy
|
|
234
236
|
Requires-Dist: typing_extensions; python_version < "3.11"
|
|
235
237
|
Provides-Extra: idfci
|
|
@@ -52,6 +52,7 @@ esptest/devices/attenuator.py
|
|
|
52
52
|
esptest/devices/esp_serial.py
|
|
53
53
|
esptest/devices/serial_dut.py
|
|
54
54
|
esptest/devices/serial_tools.py
|
|
55
|
+
esptest/devices/switch.py
|
|
55
56
|
esptest/env/__init__.py
|
|
56
57
|
esptest/env/base_env.py
|
|
57
58
|
esptest/env/wifi_env.py
|
|
@@ -79,6 +80,7 @@ esptest/tools/copy_bin.py
|
|
|
79
80
|
esptest/tools/download_bin.py
|
|
80
81
|
esptest/tools/http_download.py
|
|
81
82
|
esptest/tools/pip_check.py
|
|
83
|
+
esptest/tools/uart_monitor.py
|
|
82
84
|
esptest/utility/gen_esp32part.py
|
|
83
85
|
esptest/utility/parse_bin_path.py
|
|
84
86
|
example/jap_test.py
|
|
@@ -93,6 +95,7 @@ tests/adapter/test_shell_port.py
|
|
|
93
95
|
tests/basic/test_decorators.py
|
|
94
96
|
tests/basic/test_network.py
|
|
95
97
|
tests/db/test_db_runners.py
|
|
98
|
+
tests/devices/test_switch.py
|
|
96
99
|
tests/esp_console/conftest.py
|
|
97
100
|
tests/esp_console/test_WifiCmd.py
|
|
98
101
|
tests/esp_console/_files/wifi_cmd_connected_1.log
|
|
@@ -26,6 +26,13 @@ NEVER_MATCHED_MAGIC_STRING = 'o6K,Q.(w+~yr~N9R'
|
|
|
26
26
|
class ExpectTimeout(TimeoutError):
|
|
27
27
|
"""raise same ExpectTimeout rather than different Exception from different framework"""
|
|
28
28
|
|
|
29
|
+
def __init__(self, message: str, data_in_buffer: t.Union[str, bytes] = b'') -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.data_in_buffer: t.Union[str, bytes] = data_in_buffer
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
return f'{super().__str__()}\n data_in_buffer={repr(self.data_in_buffer)}'
|
|
35
|
+
|
|
29
36
|
|
|
30
37
|
class RawPort(metaclass=abc.ABCMeta):
|
|
31
38
|
"""Define a minimum Dut class, the dut objects should at least support these methods
|
|
@@ -408,7 +415,12 @@ class BasePort(PortInterface, t.Generic[T]):
|
|
|
408
415
|
try:
|
|
409
416
|
result = func(self, *args, **kwargs)
|
|
410
417
|
except self.expect_timeout_exceptions as e:
|
|
411
|
-
|
|
418
|
+
try:
|
|
419
|
+
data_in_buffer = self._pexpect_spawn.before # pylint: disable=protected-access
|
|
420
|
+
except AttributeError:
|
|
421
|
+
data_in_buffer = ''
|
|
422
|
+
self.logger.debug(f'ExpectTimeout: {str(e)}, data_in_buffer={repr(data_in_buffer)}')
|
|
423
|
+
raise ExpectTimeout(str(e), data_in_buffer=data_in_buffer) from e
|
|
412
424
|
return result
|
|
413
425
|
|
|
414
426
|
return wrap
|
|
@@ -109,3 +109,23 @@ def suppress_stdout() -> t.Callable[[GenericFunc], GenericFunc]:
|
|
|
109
109
|
return t.cast(GenericFunc, wrapper)
|
|
110
110
|
|
|
111
111
|
return decorator
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def timeit(
|
|
115
|
+
print_func: t.Callable[[str], None] = logger.critical,
|
|
116
|
+
format_str: str = 'Func {func_name} time used: {time_used:.2f} s',
|
|
117
|
+
) -> t.Callable[[GenericFunc], GenericFunc]:
|
|
118
|
+
"""Show time used when method is called"""
|
|
119
|
+
|
|
120
|
+
def decorator(func: GenericFunc) -> GenericFunc:
|
|
121
|
+
@wraps(func)
|
|
122
|
+
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
123
|
+
start_time = time.perf_counter()
|
|
124
|
+
ret = func(*args, **kwargs)
|
|
125
|
+
end_time = time.perf_counter()
|
|
126
|
+
print_func(format_str.format(func_name=func.__name__, time_used=end_time - start_time))
|
|
127
|
+
return ret
|
|
128
|
+
|
|
129
|
+
return t.cast(GenericFunc, wrapper)
|
|
130
|
+
|
|
131
|
+
return decorator
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import esptest.common.compat_typing as t
|
|
5
|
+
|
|
6
|
+
from ..adapter.port.shell_port import ShellPort
|
|
7
|
+
from ..all import get_logger
|
|
8
|
+
from ..network.mac import format_mac_to_h3c, normalize_mac
|
|
9
|
+
from ..network.netif import ip_in_network
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
KNOWN_INTERFACE_PREFIXS = [
|
|
14
|
+
'BAG',
|
|
15
|
+
'XGE',
|
|
16
|
+
'HGE',
|
|
17
|
+
'GE',
|
|
18
|
+
]
|
|
19
|
+
LINK_TYPE_MAP = {
|
|
20
|
+
'A': 'access',
|
|
21
|
+
'T': 'trunk',
|
|
22
|
+
'H': 'hybrid',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SwitchConfig:
|
|
28
|
+
ip: str
|
|
29
|
+
port: int
|
|
30
|
+
login_method: str = 'telnet'
|
|
31
|
+
login_username: str = ''
|
|
32
|
+
login_password: str = ''
|
|
33
|
+
timeout: float = 10
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
if self.login_method not in ['ssh', 'telnet']:
|
|
37
|
+
raise ValueError(f'login_method must be ssh or telnet, got {self.login_method}')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class VlanInfo:
|
|
42
|
+
id: int # 1-4094
|
|
43
|
+
interface_name: str = '' # Vlan1
|
|
44
|
+
name: str = ''
|
|
45
|
+
type: str = '' # Static
|
|
46
|
+
status: str = '' # UP / DOWN
|
|
47
|
+
ip: str = '' # 10.0.0.1
|
|
48
|
+
mask: str = '' # 255.255.255.0
|
|
49
|
+
description: str = ''
|
|
50
|
+
# TODO: support more fields
|
|
51
|
+
tagged_ports: str = ''
|
|
52
|
+
untagged_ports: str = ''
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def parse_interface_brief_line(cls, line: str) -> t.Optional['VlanInfo']:
|
|
56
|
+
"""Interface Link Protocol Primary IP Description"""
|
|
57
|
+
if not line.startswith('Vlan'):
|
|
58
|
+
return None
|
|
59
|
+
parts = line.split(maxsplit=4)
|
|
60
|
+
if len(parts) not in [4, 5]:
|
|
61
|
+
# description can be empty
|
|
62
|
+
return None
|
|
63
|
+
vlan_id = int(parts[0].replace('Vlan', ''))
|
|
64
|
+
interface_name = parts[0]
|
|
65
|
+
status = parts[1]
|
|
66
|
+
assert status in ['UP', 'DOWN'], f'Invalid status {status}'
|
|
67
|
+
ip = parts[3]
|
|
68
|
+
# mask is not shown in the output
|
|
69
|
+
description = parts[4] if len(parts) == 5 else ''
|
|
70
|
+
return cls(vlan_id, interface_name, '', '', status, ip, '', description)
|
|
71
|
+
|
|
72
|
+
def parse_vlan_details(self, line: str) -> None:
|
|
73
|
+
"""VLAN ID: 1
|
|
74
|
+
VLAN type: Static
|
|
75
|
+
Route interface: Configured
|
|
76
|
+
IPv4 address: 10.0.0.1
|
|
77
|
+
IPv4 subnet mask: 255.255.255.0
|
|
78
|
+
Description: Server
|
|
79
|
+
Name: VLAN 0001
|
|
80
|
+
"""
|
|
81
|
+
match = re.search(r'VLAN ID: (\d+)', line)
|
|
82
|
+
if not match or int(match.group(1)) != self.id:
|
|
83
|
+
raise AssertionError(f'VLAN ID does not match current VLAN ID {self.id}')
|
|
84
|
+
# name
|
|
85
|
+
match = re.search(r'Name:\s*([\S ]+)', line)
|
|
86
|
+
assert match
|
|
87
|
+
self.name = match.group(1).strip()
|
|
88
|
+
# ip and mask
|
|
89
|
+
match = re.search(r'IPv4 address:\s*(\d+\.\d+\.\d+\.\d+)', line)
|
|
90
|
+
assert match and match.group(1) == self.ip, f'IP address does not match current IP address {self.ip}'
|
|
91
|
+
match = re.search(r'IPv4 subnet mask:\s*(\d+\.\d+\.\d+\.\d+)', line)
|
|
92
|
+
assert match
|
|
93
|
+
self.mask = match.group(1).strip()
|
|
94
|
+
# other fields
|
|
95
|
+
match = re.search(r'VLAN type:\s*(\w+)', line)
|
|
96
|
+
if match:
|
|
97
|
+
self.type = match.group(1).strip()
|
|
98
|
+
match = re.search(r'Description:\s*([\S ]+)', line)
|
|
99
|
+
if match:
|
|
100
|
+
self.description = match.group(1).strip()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class PoolInfo:
|
|
105
|
+
name: str
|
|
106
|
+
ip: str = '' # 10.0.0.1
|
|
107
|
+
mask: str = '' # 255.255.255.0
|
|
108
|
+
gateway: str = '' # 10.0.0.1
|
|
109
|
+
dns_list: str = '' # "8.8.8.8 114.114.114.114"
|
|
110
|
+
# needs vlan info
|
|
111
|
+
vlan_id: int = 0 # which vlan the pool belongs to
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def parse_pool_info(cls, output: str) -> 'PoolInfo':
|
|
115
|
+
"""Pool name: 111
|
|
116
|
+
Network: 10.0.0.0 mask 255.255.254.0
|
|
117
|
+
dns-list 8.8.8.8 114.114.114.114
|
|
118
|
+
expired day 1 hour 0 minute 0 second 0
|
|
119
|
+
gateway-list 10.0.0.1
|
|
120
|
+
static bindings:
|
|
121
|
+
ip-address 10.0.0.10 mask 255.255.254.0
|
|
122
|
+
hardware-address 1122-3344-aabb ethernet
|
|
123
|
+
"""
|
|
124
|
+
match = re.search(r'Pool name:\s*(\w+)', output)
|
|
125
|
+
assert match, f'Failed to parse pool name from output: {output}'
|
|
126
|
+
pool_name = match.group(1).strip()
|
|
127
|
+
match = re.search(r'gateway-list\s*(\d+\.\d+\.\d+\.\d+)', output)
|
|
128
|
+
assert match, f'Failed to parse gateway from output: {output}'
|
|
129
|
+
gateway = match.group(1).strip()
|
|
130
|
+
match = re.search(r'Network:\s*(\d+\.\d+\.\d+\.\d+) mask (\d+\.\d+\.\d+\.\d+)', output)
|
|
131
|
+
if match:
|
|
132
|
+
ip = match.group(1).strip()
|
|
133
|
+
mask = match.group(2).strip()
|
|
134
|
+
else:
|
|
135
|
+
logger.warning(
|
|
136
|
+
f'Failed to parse network info from pool {pool_name}, trying parse ip/mask from static bindings'
|
|
137
|
+
)
|
|
138
|
+
ip = gateway.split(' ')[0]
|
|
139
|
+
mask_match = re.search(r'mask (\d+\.\d+\.\d+\.\d+)', output)
|
|
140
|
+
assert mask_match, f'Failed to parse ip/mask from pool: {pool_name}, Please set network config to the pool'
|
|
141
|
+
mask = mask_match.group(1).strip()
|
|
142
|
+
match = re.search(r'dns-list\s*([\d\. ]+)', output)
|
|
143
|
+
assert match
|
|
144
|
+
dns_list = match.group(1).strip()
|
|
145
|
+
return cls(pool_name, ip, mask, gateway, dns_list)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class InterfaceInfo:
|
|
150
|
+
name: str # XGE1/0/1
|
|
151
|
+
full_name: str = '' # Ten-GigabitEthernet1/0/1
|
|
152
|
+
description: str = ''
|
|
153
|
+
status: str = '' # UP / DOWN
|
|
154
|
+
speed: str = '' # 1000M
|
|
155
|
+
duplex: str = '' # F(a) / A
|
|
156
|
+
link_mode: str = '' # bridge
|
|
157
|
+
link_type: str = '' # access/trunk
|
|
158
|
+
pvid: int = 0 # which vlan the interface belongs to
|
|
159
|
+
permit_vlan: str = '' # 1, 205 to 206
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def parse_interface_line(cls, line: str) -> t.Optional['InterfaceInfo']:
|
|
163
|
+
"""Interface Link Speed Duplex Type PVID Description"""
|
|
164
|
+
if not any(line.startswith(prefix) for prefix in KNOWN_INTERFACE_PREFIXS):
|
|
165
|
+
return None
|
|
166
|
+
parts = line.split(maxsplit=6)
|
|
167
|
+
if len(parts) not in [6, 7]:
|
|
168
|
+
# description can be empty
|
|
169
|
+
return None
|
|
170
|
+
interface_name = parts[0]
|
|
171
|
+
status = parts[1]
|
|
172
|
+
assert status in ['UP', 'DOWN'], f'Invalid status {status} ({line})'
|
|
173
|
+
speed = parts[2]
|
|
174
|
+
# Duplex: (a)/A - auto; H - half; F - full
|
|
175
|
+
duplex = parts[3]
|
|
176
|
+
if parts[4] in ['A', 'T', 'H']:
|
|
177
|
+
# Type: A - access; T - trunk; H - hybrid
|
|
178
|
+
link_type = LINK_TYPE_MAP[parts[4]]
|
|
179
|
+
else:
|
|
180
|
+
link_type = ''
|
|
181
|
+
try:
|
|
182
|
+
pvid = int(parts[5])
|
|
183
|
+
except ValueError:
|
|
184
|
+
pvid = 0
|
|
185
|
+
# mask is not shown in the output
|
|
186
|
+
description = parts[6] if len(parts) == 7 else ''
|
|
187
|
+
return cls(interface_name, '', description, status, speed, duplex, '', link_type, pvid, '')
|
|
188
|
+
|
|
189
|
+
def parse_interface_details(self, data: str) -> None:
|
|
190
|
+
"""
|
|
191
|
+
interface Ten-GigabitEthernet1/0/1
|
|
192
|
+
description test
|
|
193
|
+
port link-mode bridge
|
|
194
|
+
port link-type trunk
|
|
195
|
+
undo port trunk permit vlan 1
|
|
196
|
+
port trunk permit vlan 111 to 112 2000
|
|
197
|
+
port link-aggregation group 1
|
|
198
|
+
"""
|
|
199
|
+
# full name
|
|
200
|
+
match = re.search(r'interface\s+(\S+)', data)
|
|
201
|
+
assert match
|
|
202
|
+
self.full_name = match.group(1).strip()
|
|
203
|
+
# vlan
|
|
204
|
+
match = re.search(r'port trunk permit vlan\s+([\S ]+)', data)
|
|
205
|
+
assert match
|
|
206
|
+
self.permit_vlan = match.group(1).strip()
|
|
207
|
+
# link mode
|
|
208
|
+
match = re.search(r'port link-mode\s+(\w+)', data)
|
|
209
|
+
if match:
|
|
210
|
+
self.link_mode = match.group(1).strip()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class ArpInfo:
|
|
215
|
+
ip: str
|
|
216
|
+
mac: str
|
|
217
|
+
vlan_id: str
|
|
218
|
+
interface: str = ''
|
|
219
|
+
# aging: str = '' # ignored
|
|
220
|
+
type: str = '' # D
|
|
221
|
+
# needs pool info
|
|
222
|
+
pool_name: str = '' # which pool the arp entry belongs to
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def parse_arp_line(cls, line: str) -> t.Optional['ArpInfo']:
|
|
226
|
+
"""IP address MAC address VLAN/VSI name Interface Aging Type"""
|
|
227
|
+
parts = line.split(maxsplit=5)
|
|
228
|
+
if len(parts) != 6 or not re.match(r'\d+\.\d+\.\d+\.\d+', parts[0]):
|
|
229
|
+
return None
|
|
230
|
+
ip = parts[0]
|
|
231
|
+
mac = normalize_mac(parts[1])
|
|
232
|
+
vlan_id = parts[2]
|
|
233
|
+
interface = parts[3]
|
|
234
|
+
# aging = parts[4] # ignored
|
|
235
|
+
typ = parts[5].strip()
|
|
236
|
+
return cls(ip, mac, vlan_id, interface, typ)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class StaticBindInfo:
|
|
241
|
+
ip: str
|
|
242
|
+
mask: str
|
|
243
|
+
hardware_address: str
|
|
244
|
+
# needs pool info
|
|
245
|
+
pool_name: str = '' # which pool the arp entry belongs to
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def mac(self) -> str:
|
|
249
|
+
return normalize_mac(self.hardware_address)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class H3CSwitch:
|
|
253
|
+
def __init__(self, config: SwitchConfig, log_file: str = '') -> None:
|
|
254
|
+
self.config = config
|
|
255
|
+
self.ip = config.ip
|
|
256
|
+
self.port = config.port
|
|
257
|
+
self.login_method = config.login_method
|
|
258
|
+
self.username = config.login_username
|
|
259
|
+
self.password = config.login_password
|
|
260
|
+
self.timeout = config.timeout
|
|
261
|
+
self.log_file = log_file
|
|
262
|
+
self.session: t.Optional[ShellPort] = None
|
|
263
|
+
self.sysname = ''
|
|
264
|
+
self.need_save = False
|
|
265
|
+
# cache
|
|
266
|
+
self._vlan_info_list: t.List[VlanInfo] = []
|
|
267
|
+
self._interface_info_list: t.List[InterfaceInfo] = []
|
|
268
|
+
self._pool_name_list: t.List[str] = []
|
|
269
|
+
self._pool_info_list: t.List[PoolInfo] = []
|
|
270
|
+
self._arp_info_list: t.List[ArpInfo] = []
|
|
271
|
+
self._static_bind_info_list: t.List[StaticBindInfo] = []
|
|
272
|
+
|
|
273
|
+
def connect(self) -> None:
|
|
274
|
+
"""
|
|
275
|
+
Connect to the switch.
|
|
276
|
+
"""
|
|
277
|
+
_switch_name = f'H3C-Switch-{self.ip}-{self.port}'
|
|
278
|
+
# Use TERM=xterm to avoid "'xterm-256color': unknown terminal type.".
|
|
279
|
+
if self.login_method == 'telnet':
|
|
280
|
+
self.session = ShellPort(f'TERM=xterm telnet {self.ip}', name=_switch_name, log_file=self.log_file)
|
|
281
|
+
self.session.timeout = self.timeout
|
|
282
|
+
self.session.expect('Login: ')
|
|
283
|
+
self.session.write_line(self.username)
|
|
284
|
+
else:
|
|
285
|
+
self.session = ShellPort(f'TERM=xterm ssh {self.username}@{self.ip}', log_file=self.log_file)
|
|
286
|
+
|
|
287
|
+
self.session.expect('Password:')
|
|
288
|
+
self.session.write_line(self.password)
|
|
289
|
+
match = self.session.expect(re.compile(r'<(\w+)>'))
|
|
290
|
+
self.sysname = match.group(1)
|
|
291
|
+
# Disable pagination
|
|
292
|
+
self.session.write_line('screen-length disable')
|
|
293
|
+
self.session.expect(f'<{self.sysname}>')
|
|
294
|
+
self.session.write_line('system-view')
|
|
295
|
+
self.session.expect(f'[{self.sysname}]')
|
|
296
|
+
logger.info(f'Connected to switch: {self.ip}:{self.port}')
|
|
297
|
+
|
|
298
|
+
def disconnect(self) -> None:
|
|
299
|
+
"""Disconnect from the switch, and save the configuration if needed."""
|
|
300
|
+
if self.session:
|
|
301
|
+
if self.need_save:
|
|
302
|
+
self.save()
|
|
303
|
+
self.session.close()
|
|
304
|
+
logger.info(f'Disconnected from switch: {self.ip}:{self.port}')
|
|
305
|
+
self.session = None
|
|
306
|
+
self.sysname = ''
|
|
307
|
+
|
|
308
|
+
def save(self) -> None:
|
|
309
|
+
"""Save the configuration of the switch."""
|
|
310
|
+
if self.session:
|
|
311
|
+
self.session.write_line('save f')
|
|
312
|
+
self.session.expect('successfully.')
|
|
313
|
+
self.session.expect(self.sysname)
|
|
314
|
+
self.need_save = False
|
|
315
|
+
logger.info('Switch configuration saved successfully.')
|
|
316
|
+
|
|
317
|
+
def reset_cache(self) -> None:
|
|
318
|
+
"""Reset the cache of the switch."""
|
|
319
|
+
self._vlan_info_list = []
|
|
320
|
+
self._interface_info_list = []
|
|
321
|
+
self._pool_name_list = []
|
|
322
|
+
self._pool_info_list = []
|
|
323
|
+
self._arp_info_list = []
|
|
324
|
+
self._static_bind_info_list = []
|
|
325
|
+
|
|
326
|
+
def __enter__(self) -> 'H3CSwitch':
|
|
327
|
+
self.connect()
|
|
328
|
+
return self
|
|
329
|
+
|
|
330
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore
|
|
331
|
+
self.disconnect()
|
|
332
|
+
|
|
333
|
+
def execute_command(self, command: str, timeout: float = -1) -> str:
|
|
334
|
+
"""Execute a command on the switch and return the result."""
|
|
335
|
+
if timeout == -1:
|
|
336
|
+
timeout = self.timeout
|
|
337
|
+
if self.session:
|
|
338
|
+
# ensure internal buffers are flushed before sending a command
|
|
339
|
+
self.session.flush_data()
|
|
340
|
+
self.session.write_line(command)
|
|
341
|
+
# escape special chars for regex matching
|
|
342
|
+
# limit the length to 20 chars, because H3C echo may insert new line
|
|
343
|
+
_command_escaped = re.escape(command[:20])
|
|
344
|
+
match = self.session.expect(
|
|
345
|
+
re.compile(rf'({_command_escaped}[\s\S]+[\[<]{self.sysname}\S*[\]>])'), timeout=timeout
|
|
346
|
+
)
|
|
347
|
+
# return the captured output between the echoed command and the prompt
|
|
348
|
+
return match.group(1).strip()
|
|
349
|
+
return ''
|
|
350
|
+
|
|
351
|
+
def system_view(self) -> bool:
|
|
352
|
+
"""Enter system view of the switch."""
|
|
353
|
+
if not self.session:
|
|
354
|
+
return False
|
|
355
|
+
self.session.flush_data()
|
|
356
|
+
self.session.write_line('')
|
|
357
|
+
match = self.session.expect(re.compile(rf'([\[<]{self.sysname}[-\w]*[\]>])'))
|
|
358
|
+
data = match.group(1)
|
|
359
|
+
if data.startswith('<'):
|
|
360
|
+
self.session.write_line('system-view')
|
|
361
|
+
self.session.expect(f'[{self.sysname}]')
|
|
362
|
+
return True
|
|
363
|
+
if data == f'[{self.sysname}]':
|
|
364
|
+
# already in system view
|
|
365
|
+
return True
|
|
366
|
+
assert data.startswith(f'[{self.sysname}')
|
|
367
|
+
self.session.write_line('qu')
|
|
368
|
+
self.session.expect(f'[{self.sysname}]')
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
def get_vlan_info(self) -> t.List[VlanInfo]:
|
|
372
|
+
"""Get VLAN (interface) information from the switch."""
|
|
373
|
+
if self._vlan_info_list:
|
|
374
|
+
return self._vlan_info_list
|
|
375
|
+
# show vlan interfaces
|
|
376
|
+
command = 'display interface Vlan-interface brief'
|
|
377
|
+
output = self.execute_command(command)
|
|
378
|
+
self._vlan_info_list = []
|
|
379
|
+
for line in output.splitlines():
|
|
380
|
+
if not line.startswith('Vlan'):
|
|
381
|
+
continue
|
|
382
|
+
new_vlan = VlanInfo.parse_interface_brief_line(line)
|
|
383
|
+
assert new_vlan
|
|
384
|
+
output = self.execute_command(f'display vlan {new_vlan.id}')
|
|
385
|
+
new_vlan.parse_vlan_details(output)
|
|
386
|
+
self._vlan_info_list.append(new_vlan)
|
|
387
|
+
logger.info(f'Get vlan [interface] info: {len(self._vlan_info_list)} vlans')
|
|
388
|
+
return self._vlan_info_list
|
|
389
|
+
|
|
390
|
+
def get_pool_name_list(self) -> t.List[str]:
|
|
391
|
+
"""Get pool name list from the switch."""
|
|
392
|
+
if self._pool_name_list:
|
|
393
|
+
return self._pool_name_list
|
|
394
|
+
# show pool
|
|
395
|
+
command = 'display dhcp server pool | include name'
|
|
396
|
+
output = self.execute_command(command)
|
|
397
|
+
self._pool_name_list = []
|
|
398
|
+
for match in re.finditer(r'Pool name:\s*(\S+)', output):
|
|
399
|
+
pool_name = match.group(1)
|
|
400
|
+
self._pool_name_list.append(pool_name)
|
|
401
|
+
return self._pool_name_list
|
|
402
|
+
|
|
403
|
+
def get_pool_info(self) -> t.List[PoolInfo]:
|
|
404
|
+
"""Get pool information from the switch."""
|
|
405
|
+
if self._pool_info_list:
|
|
406
|
+
return self._pool_info_list
|
|
407
|
+
# show pool
|
|
408
|
+
pool_names = self.get_pool_name_list()
|
|
409
|
+
for pool_name in pool_names:
|
|
410
|
+
output = self.execute_command(f'display dhcp server pool {pool_name}')
|
|
411
|
+
new_pool = PoolInfo.parse_pool_info(output)
|
|
412
|
+
assert new_pool
|
|
413
|
+
# try to add vlan_id to the pool
|
|
414
|
+
for vlan in self.get_vlan_info():
|
|
415
|
+
if vlan.ip in new_pool.gateway:
|
|
416
|
+
new_pool.vlan_id = vlan.id
|
|
417
|
+
break
|
|
418
|
+
self._pool_info_list.append(new_pool)
|
|
419
|
+
logger.info(f'Get pool list: {len(self._pool_info_list)} pools')
|
|
420
|
+
return self._pool_info_list
|
|
421
|
+
|
|
422
|
+
def get_interface_info(self, detail: bool = False) -> t.List[InterfaceInfo]:
|
|
423
|
+
"""Get interface information from the switch."""
|
|
424
|
+
if self._interface_info_list:
|
|
425
|
+
return self._interface_info_list
|
|
426
|
+
# show pool
|
|
427
|
+
command = 'display interface brief'
|
|
428
|
+
output = self.execute_command(command)
|
|
429
|
+
self._interface_info_list = []
|
|
430
|
+
for line in output.splitlines():
|
|
431
|
+
new_interface = InterfaceInfo.parse_interface_line(line)
|
|
432
|
+
if new_interface:
|
|
433
|
+
if detail:
|
|
434
|
+
# add interface vlan info (display this in interface view)
|
|
435
|
+
self.system_view()
|
|
436
|
+
self.execute_command(f'interface {new_interface.name}')
|
|
437
|
+
output = self.execute_command('display this')
|
|
438
|
+
new_interface.parse_interface_details(output)
|
|
439
|
+
self.system_view()
|
|
440
|
+
self._interface_info_list.append(new_interface)
|
|
441
|
+
logger.info(f'Get interface list: {len(self._interface_info_list)} interfaces')
|
|
442
|
+
return self._interface_info_list
|
|
443
|
+
|
|
444
|
+
def get_arp_info(self) -> t.List[ArpInfo]:
|
|
445
|
+
"""Get ARP information from the switch."""
|
|
446
|
+
if self._arp_info_list:
|
|
447
|
+
return self._arp_info_list
|
|
448
|
+
# show pool
|
|
449
|
+
command = 'display arp'
|
|
450
|
+
output = self.execute_command(command)
|
|
451
|
+
self._arp_info_list = []
|
|
452
|
+
for line in output.splitlines():
|
|
453
|
+
new_arp = ArpInfo.parse_arp_line(line)
|
|
454
|
+
if new_arp:
|
|
455
|
+
self._arp_info_list.append(new_arp)
|
|
456
|
+
logger.info(f'Get ARP list: {len(self._arp_info_list)} ARP entries')
|
|
457
|
+
return self._arp_info_list
|
|
458
|
+
|
|
459
|
+
def get_static_bind_info(self) -> t.List[StaticBindInfo]:
|
|
460
|
+
"""Get static bind information from the switch."""
|
|
461
|
+
if self._static_bind_info_list:
|
|
462
|
+
return self._static_bind_info_list
|
|
463
|
+
self._static_bind_info_list = []
|
|
464
|
+
pattern = re.compile(r'ip-address\s+([\d\.]+)\s+mask\s+([\d\.]+)\s+hardware-address\s+(\S+)\s')
|
|
465
|
+
for pool in self.get_pool_name_list():
|
|
466
|
+
command = f'display dhcp server pool {pool}'
|
|
467
|
+
output = self.execute_command(command)
|
|
468
|
+
for match in pattern.finditer(output):
|
|
469
|
+
ip_address = match.group(1)
|
|
470
|
+
mask = match.group(2)
|
|
471
|
+
hardware_address = match.group(3)
|
|
472
|
+
new_bind = StaticBindInfo(ip_address, mask, hardware_address, pool)
|
|
473
|
+
self._static_bind_info_list.append(new_bind)
|
|
474
|
+
logger.info(f'Get static bind list: {len(self._static_bind_info_list)} static binds')
|
|
475
|
+
return self._static_bind_info_list
|
|
476
|
+
|
|
477
|
+
def get_pool_by_ip(self, ip_address: str) -> PoolInfo:
|
|
478
|
+
"""Get pool name by IP address."""
|
|
479
|
+
for pool in self.get_pool_info():
|
|
480
|
+
if ip_in_network(ip_address, f'{pool.ip}/{pool.mask}'):
|
|
481
|
+
return pool
|
|
482
|
+
raise ValueError(f'IP address {ip_address} not found in any pool')
|
|
483
|
+
|
|
484
|
+
def get_arp_info_by_ip(self, ip_address: str) -> ArpInfo:
|
|
485
|
+
"""Get ARP information by IP address."""
|
|
486
|
+
if self._arp_info_list:
|
|
487
|
+
for arp_info in self._arp_info_list:
|
|
488
|
+
if arp_info.ip == ip_address:
|
|
489
|
+
return arp_info
|
|
490
|
+
output = self.execute_command(f'display arp {ip_address}')
|
|
491
|
+
for line in output.splitlines():
|
|
492
|
+
if line.startswith(ip_address):
|
|
493
|
+
arp_info = ArpInfo.parse_arp_line(line) # type: ignore
|
|
494
|
+
if arp_info:
|
|
495
|
+
return arp_info
|
|
496
|
+
raise ValueError(f'IP address {ip_address} not found in ARP table')
|
|
497
|
+
|
|
498
|
+
def add_one_static_bind( # pylint: disable=too-many-positional-arguments
|
|
499
|
+
self,
|
|
500
|
+
ip_address: str,
|
|
501
|
+
hardware_address: str = '',
|
|
502
|
+
mask: str = '',
|
|
503
|
+
pool_name: str = '',
|
|
504
|
+
remove_existing: bool = False,
|
|
505
|
+
) -> bool:
|
|
506
|
+
"""Add static bind information to the switch.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
ip_address: IP address to bind.
|
|
510
|
+
hardware_address: Hardware address to bind.
|
|
511
|
+
mask: Subnet mask.
|
|
512
|
+
pool_name: Pool name.
|
|
513
|
+
remove_existing: Remove existing bind information for the IP address.
|
|
514
|
+
"""
|
|
515
|
+
if pool_name:
|
|
516
|
+
assert mask, 'Mask is required when pool_name is specified'
|
|
517
|
+
assert pool_name in self.get_pool_name_list(), f'Pool {pool_name} not found on this switch'
|
|
518
|
+
else:
|
|
519
|
+
pool = self.get_pool_by_ip(ip_address)
|
|
520
|
+
pool_name = pool.name
|
|
521
|
+
mask = pool.mask
|
|
522
|
+
if not hardware_address:
|
|
523
|
+
hardware_address = self.get_arp_info_by_ip(ip_address).mac
|
|
524
|
+
hardware_address = format_mac_to_h3c(hardware_address)
|
|
525
|
+
result = True
|
|
526
|
+
self.system_view()
|
|
527
|
+
command = f'dhcp server ip-pool {pool_name}'
|
|
528
|
+
self.execute_command(command)
|
|
529
|
+
try:
|
|
530
|
+
if remove_existing:
|
|
531
|
+
command = f'undo static-bind ip-address {ip_address}'
|
|
532
|
+
self.execute_command(command)
|
|
533
|
+
logger.info(f'Bind static dhcp {ip_address} {mask} {hardware_address} to pool {pool_name}.')
|
|
534
|
+
command = f'static-bind ip-address {ip_address} mask {mask} hardware-address {hardware_address}'
|
|
535
|
+
output = self.execute_command(command)
|
|
536
|
+
if 'The IP address has already been bound' in output:
|
|
537
|
+
logger.error(f'IP address {ip_address} has already been bound, pool:{pool_name}')
|
|
538
|
+
result = False
|
|
539
|
+
except TimeoutError as e:
|
|
540
|
+
logger.error(f'Failed to bind {ip_address} {mask} {hardware_address}, pool:{pool_name}, error:{str(e)}')
|
|
541
|
+
result = False
|
|
542
|
+
self.need_save = bool(result)
|
|
543
|
+
self.system_view() # return to system view
|
|
544
|
+
return result
|