Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions extreme/exos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# vrnetlab / Extreme-EXOS (exos)

This is the vrnetlab docker image for Extreme EXOS.

## Building the docker image

Download the QCOW2 image from Extreme Networks:

```bash
curl -O https://akamai-ep.extremenetworks.com/Extreme_P/github-en/Virtual_EXOS/EXOS-VM_v32.7.3.126.qcow2
```

Place the QCOW2 image into this folder, then run:

```bash
make
```

The image will be tagged based on the version in the filename (e.g., `vrnetlab/extreme_exos:v32.6.3.126`).

## Tested versions

- `EXOS-VM_v32.6.3.126.qcow2`
- `EXOS-VM_32.7.2.19.qcow2`
- `EXOS-VM_v33.1.1.31.qcow2` - this image seems to take a long time to boot.
18 changes: 1 addition & 17 deletions extreme/exos/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
FROM public.ecr.aws/docker/library/debian:bookworm-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qy \
&& apt-get install --no-install-recommends -y \
iproute2 \
python3 \
socat \
qemu-kvm \
qemu-utils \
telnet \
&& rm -rf /var/lib/apt/lists/*
FROM ghcr.io/srl-labs/vrnetlab-base:0.2.1

ARG IMAGE
COPY $IMAGE* /
COPY *.py /

EXPOSE 22 80 161/udp 443 830
HEALTHCHECK CMD ["/healthcheck.py"]
ENTRYPOINT ["/launch.py"]
101 changes: 68 additions & 33 deletions extreme/exos/docker/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ class EXOS_vm(vrnetlab.VM):
def __init__(self, username, password, hostname, conn_mode):
disk_image = None
for e in sorted(os.listdir("/")):
if not disk_image and re.search(".qcow2$", e):
if not disk_image and re.search(".qcow2$", e):
disk_image = "/" + e

super(EXOS_vm, self).__init__(
username,
password,
disk_image=disk_image,
ram=512,
cpu="core2duo",
username,
password,
disk_image=disk_image,
ram=512,
cpu="core2duo",
driveif="ide",
)

Expand All @@ -59,6 +59,37 @@ def __init__(self, username, password, hostname, conn_mode):
self.num_nics = 13
self.nic_type = "rtl8139"

def wait_write_config(self, cmd, wait="#", retries=30, delay=5):
"""Execute command with retry logic for 'configuration load' state.

EXOS can enter a 'configuration load' state during boot where config
commands are rejected. This wrapper retries the command until success.
"""
for attempt in range(retries):
self.tn.write(f"{cmd}\r".encode())
try:
res = self.tn.read_until(wait.encode(), timeout=30)
response_str = res.decode(errors="ignore")

# Check if we got the config load error
if "configuration load" in response_str.lower():
if attempt < retries - 1: # Don't log on last attempt
self.logger.info(
f"'configuration load' detected, retrying '{cmd}' in {delay}s..."
)
time.sleep(delay)
continue

# Success - command completed
return
except Exception as e:
self.logger.warning(f"Timeout waiting for prompt after '{cmd}': {e}")
if attempt < retries - 1:
time.sleep(delay)
continue

self.logger.error(f"Command '{cmd}' failed after {retries} retries")

def bootstrap_spin(self):
""" This function should be called periodically to do work.
"""
Expand All @@ -69,24 +100,27 @@ def bootstrap_spin(self):
self.start()
return

(ridx, match, res) = self.tn.expect([rb'node is now available for login.',
rb'\[[yY]\/[nN]\/q\]'], 1)
(ridx, match, res) = self.tn.expect(
[rb'node is now available for login.', rb'\[[yY]\/[nN]\/q\]'], 1
)

if match: # got a match!
if ridx == 0:
time.sleep(1)
self.wait_write(cmd='', wait=None)
self.wait_write(cmd='admin', wait='login:')
self.wait_write(cmd='', wait='password:')
self.wait_write(cmd="", wait=None)
self.wait_write(cmd="admin", wait="login:")
self.wait_write(cmd="", wait="password:")
else:
self.wait_write(cmd='q', wait=None)
self.wait_write(cmd='', wait='#')
self.wait_write(cmd="q", wait=None)
self.wait_write(cmd="", wait="#")
self.logger.info("Found config prompt")
# run main config!
self.logger.info("Running bootstrap_config()")
self.bootstrap_config()
self.startup_config()
(ridx, match, res) = self.tn.expect([rb'node is now available for login.'],1)
(ridx, match, res) = self.tn.expect(
[rb'node is now available for login.'], 1
)
time.sleep(1)
# close telnet connection
self.tn.close()
Expand All @@ -99,7 +133,7 @@ def bootstrap_spin(self):

# no match, if we saw some output from the router it's probably
# booting, so let's give it some more time
if res != b'':
if res != b"":
self.logger.trace("OUTPUT: %s" % res.decode())
# reset spins if we saw some output
self.spins = 0
Expand All @@ -111,19 +145,23 @@ def bootstrap_spin(self):
def bootstrap_config(self):
""" Do the actual bootstrap config
"""
self.wait_write(cmd=f"configure snmp sysName {self.hostname}", wait=None)
self.wait_write(cmd="configure vlan Mgmt ipaddress 10.0.0.15/24", wait="#")
self.wait_write(cmd="configure iproute add default 10.0.0.2 vr VR-Mgmt", wait="#")
if self.username == 'admin':
self.wait_write_config(cmd=f"configure snmp sysName {self.hostname}")
self.wait_write_config(cmd="unconfigure vlan Mgmt ipaddress")
self.wait_write_config(cmd="configure vlan Mgmt ipaddress 10.0.0.15/24")
self.wait_write_config(cmd="configure iproute add default 10.0.0.2 vr VR-Mgmt")
if self.username == "admin":
self.wait_write(cmd="configure account admin password", wait="#")
self.wait_write(cmd="", wait="Current user's password:")
self.wait_write(cmd=self.password, wait="New password:")
self.wait_write(cmd=self.password, wait="Reenter password:")
else:
self.wait_write(cmd=f"create account admin {self.username} {self.password}", wait="#")
self.wait_write(cmd="disable cli prompting", wait="#")
self.wait_write(cmd="enable ssh2", wait="#")
self.wait_write(cmd="save", wait="#")
self.wait_write(
cmd=f"create account admin {self.username} {self.password}", wait="#"
)
self.wait_write_config(cmd="disable cli prompting")
self.wait_write_config(cmd="configure ssh2 key")
self.wait_write_config(cmd="enable ssh2")
self.wait_write_config(cmd="save")

def startup_config(self):
if not os.path.exists(STARTUP_CONFIG_FILE):
Expand All @@ -140,16 +178,15 @@ def startup_config(self):
class EXOS(vrnetlab.VR):
def __init__(self, hostname, username, password, conn_mode):
super(EXOS, self).__init__(username, password)
self.vms = [EXOS_vm(username, password,hostname, conn_mode)]
self.vms = [EXOS_vm(username, password, hostname, conn_mode)]


if __name__ == '__main__':
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='')
parser.add_argument(
"--trace", action="store_true", help="enable trace level logging"
)
parser.add_argument('--hostname', default='vr-exos', help='Router hostname')

parser = argparse.ArgumentParser(description="")
parser.add_argument("--trace", action="store_true", help="enable trace level logging")
parser.add_argument("--hostname", default="vr-exos", help="Router hostname")
parser.add_argument('--username', default='vrnetlab', help='Username')
parser.add_argument('--password', default='VR-netlab9', help='Password')
parser.add_argument(
Expand All @@ -169,7 +206,5 @@ def __init__(self, hostname, username, password, conn_mode):
if args.trace:
logger.setLevel(1)

vr = EXOS(
args.hostname, args.username, args.password, conn_mode=args.connection_mode
)
vr = EXOS(args.hostname, args.username, args.password, conn_mode=args.connection_mode)
vr.start()