Skip to content

Commit b3a1ab5

Browse files
rafabrRafael Brandaohellt
authored
Add support for Huawei NE40E (#250)
* Add support for Huawei NE40E * DeepSource suggestions/fixes * simplify sed and format python * reuse parent mgmt interface generator * Improve startup config * Add support to CE12800 * added platform name in the tag added cleanup of the docker build context after the image is built * use tftp instead of sftp --------- Co-authored-by: Rafael Brandao <[email protected]> Co-authored-by: Roman Dodin <[email protected]>
1 parent 47b0b2b commit b3a1ab5

File tree

5 files changed

+334
-1
lines changed

5 files changed

+334
-1
lines changed

huawei_vrp/Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
VENDOR=Huawei
2+
NAME=VRP
3+
IMAGE_FORMAT=qcow2
4+
IMAGE_GLOB=*.qcow2
5+
6+
# match versions like:
7+
# huawei_ne40e-<any version string>.qcow2
8+
# huawei_ce12800-<any version string>.qcow2
9+
VERSION=$(shell echo $(IMAGE) | sed -e 's/huawei_\(ne40e\|ce12800\)-\(.*\)\.qcow2/\1-\2/')
10+
11+
-include ../makefile-sanity.include
12+
-include ../makefile.include

huawei_vrp/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Huawei NE40E
2+
3+
Rename your qcow2 disk image to conform to the following pattern:
4+
5+
```
6+
huawei_ne40e-<version>.qcow2
7+
or
8+
huawei_ce12800-<version>.qcow2
9+
```
10+
11+
Build the image with:
12+
13+
```
14+
make
15+
```
16+
17+
The resulting image will be tagged as:
18+
19+
```
20+
vrnetlab/huawei_vrp:<platform>-<version>
21+
```
22+
23+
for example, if the qcow2 image is named `huawei_ne40e-8.180.qcow2`, then the image will be tagged as:
24+
25+
```
26+
vrnetlab/huawei_vrp:ne40e-8.180
27+
```

huawei_vrp/docker/Dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM public.ecr.aws/docker/library/debian:bookworm-slim
2+
3+
ENV DEBIAN_FRONTEND=noninteractive
4+
5+
RUN apt-get update -qy \
6+
&& apt-get install --no-install-recommends -y \
7+
iproute2 \
8+
python3 \
9+
socat \
10+
qemu-kvm \
11+
qemu-utils \
12+
telnet \
13+
&& rm -rf /var/lib/apt/lists/*
14+
15+
ARG IMAGE
16+
COPY $IMAGE* /
17+
COPY *.py /
18+
19+
EXPOSE 22 80 161/udp 443 830
20+
HEALTHCHECK CMD ["/healthcheck.py"]
21+
ENTRYPOINT ["/launch.py"]

huawei_vrp/docker/launch.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
#!/usr/bin/env python3
2+
3+
import datetime
4+
import logging
5+
import os
6+
import re
7+
import signal
8+
import sys
9+
import time
10+
11+
import vrnetlab
12+
13+
STARTUP_CONFIG_FILE = "/config/startup-config.cfg"
14+
15+
16+
def handle_SIGCHLD(signal, frame):
17+
os.waitpid(-1, os.WNOHANG)
18+
19+
20+
def handle_SIGTERM(signal, frame):
21+
sys.exit(0)
22+
23+
24+
signal.signal(signal.SIGINT, handle_SIGTERM)
25+
signal.signal(signal.SIGTERM, handle_SIGTERM)
26+
signal.signal(signal.SIGCHLD, handle_SIGCHLD)
27+
28+
TRACE_LEVEL_NUM = 9
29+
logging.addLevelName(TRACE_LEVEL_NUM, "TRACE")
30+
31+
32+
def trace(self, message, *args, **kws):
33+
# Yes, logger takes its '*args' as 'args'.
34+
if self.isEnabledFor(TRACE_LEVEL_NUM):
35+
self._log(TRACE_LEVEL_NUM, message, args, **kws)
36+
37+
38+
logging.Logger.trace = trace
39+
40+
41+
class VRP_vm(vrnetlab.VM):
42+
def __init__(self, username, password, hostname, conn_mode):
43+
disk_image = None
44+
self.vm_type = "UNKNOWN"
45+
for e in sorted(os.listdir("/")):
46+
if not disk_image and re.search(".qcow2$", e):
47+
disk_image = "/" + e
48+
if "huawei_ne40e" in e:
49+
self.vm_type = "NE40E"
50+
if "huawei_ce12800" in e:
51+
self.vm_type = "CE12800"
52+
53+
super(VRP_vm, self).__init__(
54+
username,
55+
password,
56+
disk_image=disk_image,
57+
ram=2048,
58+
smp="2",
59+
driveif="virtio",
60+
)
61+
62+
self.hostname = hostname
63+
self.conn_mode = conn_mode
64+
self.num_nics = 14
65+
self.nic_type = "virtio-net-pci"
66+
67+
def bootstrap_spin(self):
68+
"""This function should be called periodically to do work."""
69+
70+
if self.spins > 300:
71+
# too many spins with no result -> give up
72+
self.stop()
73+
self.start()
74+
return
75+
76+
(ridx, match, res) = self.tn.expect([b"<HUAWEI>"], 1)
77+
78+
if match and ridx == 0: # got a match!
79+
# run main config!
80+
self.logger.info("Running bootstrap_config()")
81+
self.startup_config()
82+
self.bootstrap_config()
83+
time.sleep(1)
84+
# close telnet connection
85+
self.tn.close()
86+
# startup time?
87+
startup_time = datetime.datetime.now() - self.start_time
88+
self.logger.info("Startup complete in: %s" % startup_time)
89+
# mark as running
90+
self.running = True
91+
return
92+
93+
time.sleep(5)
94+
95+
# no match, if we saw some output from the router it's probably
96+
# booting, so let's give it some more time
97+
if res != b"":
98+
self.logger.trace("OUTPUT: %s" % res.decode())
99+
# reset spins if we saw some output
100+
self.spins = 0
101+
102+
self.spins += 1
103+
104+
return
105+
106+
def bootstrap_mgmt_interface(self):
107+
self.wait_write(cmd="mmi-mode enable", wait=None)
108+
self.wait_write(cmd="system-view", wait=">")
109+
self.wait_write(cmd="ip vpn-instance __MGMT_VPN__", wait="]")
110+
self.wait_write(cmd="ipv4-family", wait="]")
111+
self.wait_write(cmd="quit", wait="]")
112+
self.wait_write(cmd="quit", wait="]")
113+
if self.vm_type == "CE12800":
114+
mgmt_interface = "MEth"
115+
if self.vm_type == "NE40E":
116+
mgmt_interface = "GigabitEthernet"
117+
self.wait_write(cmd=f"interface {mgmt_interface} 0/0/0", wait="]")
118+
# Error: The system is busy in building configuration. Please wait for a moment...
119+
while True:
120+
self.wait_write(cmd="clear configuration this", wait=None)
121+
(idx, match, res) = self.tn.expect([rb"Error"], 1)
122+
if match and idx == 0:
123+
time.sleep(5)
124+
else:
125+
break
126+
self.wait_write(cmd="undo shutdown", wait=None)
127+
self.wait_write(cmd="ip binding vpn-instance __MGMT_VPN__", wait="]")
128+
self.wait_write(cmd="ip address 10.0.0.15 24", wait="]")
129+
self.wait_write(cmd="quit", wait="]")
130+
self.wait_write(
131+
cmd="ip route-static vpn-instance __MGMT_VPN__ 0.0.0.0 0 10.0.0.2", wait="]"
132+
)
133+
134+
def bootstrap_config(self):
135+
"""Do the actual bootstrap config"""
136+
self.bootstrap_mgmt_interface()
137+
self.wait_write(cmd=f"sysname {self.hostname}", wait="]")
138+
139+
if self.vm_type == "CE12800":
140+
self.wait_write(cmd="aaa", wait="]")
141+
self.wait_write(cmd="undo local-user policy security-enhance", wait="]")
142+
self.wait_write(cmd="quit", wait="]")
143+
if self.vm_type == "NE40E":
144+
self.wait_write(cmd="undo user-security-policy enable", wait="]")
145+
146+
self.wait_write(cmd="aaa", wait="]")
147+
self.wait_write(cmd=f"undo local-user {self.username}", wait="]")
148+
self.wait_write(
149+
cmd=f"local-user {self.username} password irreversible-cipher {self.password}",
150+
wait="]",
151+
)
152+
self.wait_write(cmd=f"local-user {self.username} service-type ssh", wait="]")
153+
self.wait_write(
154+
cmd=f"local-user {self.username} user-group manage-ug", wait="]"
155+
)
156+
self.wait_write(cmd="quit", wait="]")
157+
158+
# SSH
159+
self.wait_write(cmd="user-interface vty 0 4", wait="]")
160+
self.wait_write(cmd="authentication-mode aaa", wait="]")
161+
self.wait_write(cmd="protocol inbound ssh", wait="]")
162+
self.wait_write(cmd="quit", wait="]")
163+
self.wait_write(cmd=f"undo ssh user {self.username}", wait="]")
164+
self.wait_write(
165+
cmd=f"ssh user {self.username} authentication-type password ", wait="]"
166+
)
167+
self.wait_write(cmd=f"ssh user {self.username} service-type all ", wait="]")
168+
self.wait_write(cmd="stelnet server enable", wait="]")
169+
170+
# NETCONF
171+
self.wait_write(cmd="snetconf server enable", wait="]")
172+
self.wait_write(cmd="netconf", wait="]")
173+
self.wait_write(cmd="protocol inbound ssh port 830", wait="]")
174+
self.wait_write(cmd="quit", wait="]")
175+
176+
self.wait_write(cmd="commit", wait="]")
177+
self.wait_write(cmd="return", wait="]")
178+
self.wait_write(cmd="save", wait=">")
179+
self.wait_write(cmd="undo mmi-mode enable", wait=">")
180+
181+
def startup_config(self):
182+
if not os.path.exists(STARTUP_CONFIG_FILE):
183+
self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} not found")
184+
return
185+
186+
187+
vrnetlab.run_command(["cp", STARTUP_CONFIG_FILE, "/tftpboot/containerlab.cfg"])
188+
189+
190+
if self.vm_type == "CE12800":
191+
with open(STARTUP_CONFIG_FILE, "r+") as file:
192+
cfg = file.read()
193+
modified = False
194+
195+
if "device board 1 " not in cfg:
196+
cfg = "device board 1 board-type CE-LPUE\n" + cfg
197+
modified = True
198+
199+
if "interface NULL0" not in cfg:
200+
cfg = cfg + "\ninterface NULL0"
201+
modified = True
202+
203+
if modified:
204+
file.seek(0)
205+
file.write(cfg)
206+
file.truncate()
207+
208+
209+
self.bootstrap_mgmt_interface()
210+
self.wait_write(cmd="commit", wait="]")
211+
212+
213+
self.wait_write(cmd=f"return", wait="]")
214+
time.sleep(1)
215+
self.wait_write(cmd=f"tftp 10.0.0.2 vpn-instance __MGMT_VPN__ get containerlab.cfg", wait=">")
216+
self.wait_write(cmd="startup saved-configuration containerlab.cfg", wait=">")
217+
self.wait_write(cmd="reboot fast", wait=">")
218+
self.wait_write(cmd="reboot", wait="#")
219+
self.wait_write(cmd="", wait="The current login time is")
220+
print(f"File '{STARTUP_CONFIG_FILE}' successfully loaded")
221+
222+
def gen_mgmt(self):
223+
"""Generate qemu args for the mgmt interface(s)"""
224+
# call parent function to generate the mgmt interface
225+
res = super().gen_mgmt()
226+
227+
# Creates required dummy interface
228+
res.append(f"-device virtio-net-pci,netdev=dummy,mac={vrnetlab.gen_mac(0)}")
229+
res.append("-netdev tap,ifname=vrp-dummy,id=dummy,script=no,downscript=no")
230+
231+
return res
232+
233+
234+
class VRP(vrnetlab.VR):
235+
def __init__(self, hostname, username, password, conn_mode):
236+
super(VRP, self).__init__(username, password)
237+
self.vms = [VRP_vm(username, password, hostname, conn_mode)]
238+
239+
240+
if __name__ == "__main__":
241+
import argparse
242+
243+
parser = argparse.ArgumentParser(description="")
244+
parser.add_argument(
245+
"--trace", action="store_true", help="enable trace level logging"
246+
)
247+
parser.add_argument("--hostname", default="vr-VRP", help="Router hostname")
248+
parser.add_argument("--username", default="vrnetlab", help="Username")
249+
parser.add_argument("--password", default="VR-netlab9", help="Password")
250+
parser.add_argument(
251+
"--connection-mode",
252+
default="tc",
253+
help="Connection mode to use in the datapath",
254+
)
255+
256+
args = parser.parse_args()
257+
258+
LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s"
259+
logging.basicConfig(format=LOG_FORMAT)
260+
logger = logging.getLogger()
261+
262+
logger.setLevel(logging.DEBUG)
263+
264+
if args.trace:
265+
logger.setLevel(1)
266+
267+
vr = VRP(
268+
args.hostname, args.username, args.password, conn_mode=args.connection_mode
269+
)
270+
vr.start()

makefile.include

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ docker-image:
1111
for IMAGE in $(IMAGES); do \
1212
echo "Making $$IMAGE"; \
1313
$(MAKE) IMAGE=$$IMAGE docker-build; \
14+
$(MAKE) IMAGE=$$IMAGE docker-clean-build; \
1415
done
1516
endif
1617

1718
docker-clean-build:
19+
@echo "--> Cleaning docker build context"
1820
-rm -f docker/*.qcow2* docker/*.tgz* docker/*.vmdk* docker/*.iso docker/*.xml docker/*.bin
21+
-rm -f docker/healthcheck.py docker/vrnetlab.py
1922

2023
docker-pre-build: ;
2124

@@ -35,7 +38,7 @@ endif
3538
$(MAKE) IMAGE=$$IMAGE docker-build-image-copy
3639
(cd docker; docker build --build-arg http_proxy=$(http_proxy) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg https_proxy=$(https_proxy) --build-arg HTTPS_PROXY=$(HTTPS_PROXY) --build-arg IMAGE=$(IMAGE) --build-arg VERSION=$(VERSION) --label "vrnetlab-version=$(VRNETLAB_VERION)" -t $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) .)
3740

38-
docker-build: docker-build-common
41+
docker-build: docker-build-common docker-clean-build
3942

4043
docker-push:
4144
for IMAGE in $(IMAGES); do \

0 commit comments

Comments
 (0)