From 68590a6d278a865be2a8ec27d6e50cc4bdca41b7 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Wed, 5 Apr 2023 14:27:49 +0000 Subject: [PATCH] added CPU and MEM provisioning from env vars for SR OS --- common/vrnetlab.py | 67 +++++++++++++++-- sros/docker/launch.py | 164 +++++++++++++++++++++++------------------- 2 files changed, 150 insertions(+), 81 deletions(-) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index 91e968df..c91f5d6d 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -157,8 +157,7 @@ def start(self): # generate normal NICs cmd.extend(self.gen_nics()) - self.logger.debug(cmd) - self.logger.debug("joined cmd: {}".format(" ".join(cmd))) + self.logger.debug("qemu cmd: {}".format(" ".join(cmd))) self.p = subprocess.Popen( " ".join(cmd), @@ -409,7 +408,9 @@ def gen_mgmt(self): return res def nic_provision_delay(self) -> None: - self.logger.debug(f"number of provisioned data plane interfaces is {self.num_provisioned_nics}") + self.logger.debug( + f"number of provisioned data plane interfaces is {self.num_provisioned_nics}" + ) if self.num_provisioned_nics == 0: # no nics provisioned and/or not running from containerlab so we can bail @@ -427,7 +428,10 @@ def nic_provision_delay(self) -> None: provisioned_nics = list(inf_path.glob("eth*")) # if we see num provisioned +1 (for mgmt) we have all nics ready to roll! if len(provisioned_nics) >= self.num_provisioned_nics + 1: - nics = [int(re.search(pattern=r"\d+", string=nic.name).group()) for nic in provisioned_nics] + nics = [ + int(re.search(pattern=r"\d+", string=nic.name).group()) + for nic in provisioned_nics + ] # Ensure the max eth is in range of allocated eth index of VM LC nics = [nic for nic in nics if nic in range(start_eth, end_eth)] @@ -502,15 +506,16 @@ def gen_nics(self): "%(nic_type)s," "netdev=p%(i)02d," "bus=pci.%(pci_bus)s," - "addr=0x%(addr)x" % { + "addr=0x%(addr)x" + % { "nic_type": self.nic_type, "i": i, "pci_bus": pci_bus, "addr": addr, }, "-netdev", - "socket,id=p%(i)02d,listen=:%(j)02d" % {"i": i, "j": i + 10000} - ] + "socket,id=p%(i)02d,listen=:%(j)02d" % {"i": i, "j": i + 10000}, + ] ) continue @@ -725,3 +730,51 @@ def start(self, add_fwd_rules=True): class QemuBroken(Exception): """Our Qemu instance is somehow broken""" + + +# getMem returns the RAM size (in Mb) for a given VM mode. +# RAM can be specified in the variant dict, provided by a user via the custom type definition, +# or set via env vars. +# If set via env vars, the getMem will return this value as the most specific one. +# Otherwise, the ram provided to this function will be converted to Mb and returned. +def getMem(vmMode: str, ram: int) -> int: + if vmMode == "integrated": + # Integrated VM can use both MEMORY and CP_MEMORY env vars + if "MEMORY" in os.environ: + return 1024 * get_digits(os.getenv("MEMORY")) + if "CP_MEMORY" in os.environ: + return 1024 * get_digits(os.getenv("CP_MEMORY")) + if vmMode == "cp": + if "CP_MEMORY" in os.environ: + return 1024 * get_digits(os.getenv("CP_MEMORY")) + if vmMode == "lc": + if "LC_MEMORY" in os.environ: + return 1024 * get_digits(os.getenv("LC_MEMORY")) + return 1024 * ram + + +# getCpu returns the number of cpu cores for a given VM mode. +# Cpu can be specified in the variant dict, provided by a user via the custom type definition, +# or set via env vars. +# If set via env vars, the function will return this value as the most specific one. +# Otherwise, the number provided to this function via cpu param returned. +def getCpu(vsimMode: str, cpu: int) -> int: + if vsimMode == "integrated": + # Integrated VM can use both MEMORY and CP_MEMORY env vars + if "CPU" in os.environ: + return int(os.getenv("CPU")) + if "CP_CPU" in os.environ: + return int(os.getenv("CP_CPU")) + if vsimMode == "cp": + if "CP_CPU" in os.environ: + return int(os.getenv("CP_CPU")) + if vsimMode == "lc": + if "LC_CPU" in os.environ: + return int(os.getenv("LC_CPU")) + return cpu + + +# strip all non-numeric characters from a string +def get_digits(input_str: str) -> int: + non_string_chars = re.findall(r"\d", input_str) + return int("".join(non_string_chars)) diff --git a/sros/docker/launch.py b/sros/docker/launch.py index c80db5de..9eb4bee7 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -6,7 +6,6 @@ import re import signal import sys -import time import shutil import vrnetlab @@ -35,7 +34,8 @@ def trace(self, message, *args, **kws): logging.Logger.trace = trace -# + +# line_card_config is a convenience function that generates line card definition strings def line_card_config(chassis, card, mda, integrated=False, card_type=None): """ line_card_config is a convenience function that generates line card definition strings @@ -163,10 +163,11 @@ def line_card_config(chassis, card, mda, integrated=False, card_type=None): "timos_line": "slot=A chassis=SR-7s sfm=sfm-s card=cpm2-s", }, # line card (IOM/XCM) - "lcs": [ { - "min_ram": 6, - "timos_line": "slot=1 chassis=SR-7s sfm=sfm-s card=xcm-7s mda/1=s36-100gb-qsfp28", - "card_config": """/configure system power-shelf 1 power-shelf-type ps-a10-shelf-dc + "lcs": [ + { + "min_ram": 6, + "timos_line": "slot=1 chassis=SR-7s sfm=sfm-s card=xcm-7s mda/1=s36-100gb-qsfp28", + "card_config": """/configure system power-shelf 1 power-shelf-type ps-a10-shelf-dc /configure system power-shelf 1 power-module 1 power-module-type ps-a-dc-6000 /configure system power-shelf 1 power-module 2 power-module-type ps-a-dc-6000 /configure system power-shelf 1 power-module 3 power-module-type ps-a-dc-6000 @@ -199,7 +200,8 @@ def line_card_config(chassis, card, mda, integrated=False, card_type=None): /configure card 1 card-type xcm-7s /configure card 1 mda 1 mda-type s36-100gb-qsfp28 """, - } ], + } + ], }, "sr-14s": { "deployment_model": "distributed", @@ -307,10 +309,10 @@ def line_card_config(chassis, card, mda, integrated=False, card_type=None): /configure card 1 mda 1 mda-type m20-v /configure card 1 mda 2 mda-type isa-tunnel-v """, - # depending of the Network Function the Multi-Service Integrated Services Module (MS-ISM) card could be also defined as: - # isa-aa-v --> Application Assurance (Stateful Firewall) - # isa-bb-v --> Broadband (BNG, LAC, LNS) - # isa-tunnel-v (Already Configured) --> IP Tunneling (GRE, IPSec) + # depending of the Network Function the Multi-Service Integrated Services Module (MS-ISM) card could be also defined as: + # isa-aa-v --> Application Assurance (Stateful Firewall) + # isa-bb-v --> Broadband (BNG, LAC, LNS) + # isa-tunnel-v (Already Configured) --> IP Tunneling (GRE, IPSec) }, } @@ -487,9 +489,7 @@ def gen_bof_config(): class SROS_vm(vrnetlab.VM): def __init__(self, username, password, ram, conn_mode, cpu=2, num=0): - super(SROS_vm, self).__init__( - username, password, disk_image="/sros.qcow2", num=num, ram=ram - ) + super().__init__(username, password, disk_image="/sros.qcow2", num=num, ram=ram) self.nic_type = "virtio-net-pci" self.conn_mode = conn_mode self.uuid = "00000000-0000-0000-0000-000000000000" @@ -575,12 +575,14 @@ class SROS_integrated(SROS_vm): def __init__( self, hostname, username, password, mode, num_nics, variant, conn_mode ): - cpu = variant.get("cpu") - super(SROS_integrated, self).__init__( + ram: int = vrnetlab.getMem("integrated", variant.get("min_ram")) + cpu: int = vrnetlab.getCpu("integrated", variant.get("cpu")) + + super().__init__( username, password, cpu=cpu, - ram=1024 * int(variant["min_ram"]), + ram=ram, conn_mode=conn_mode, ) self.mode = mode @@ -615,7 +617,7 @@ def gen_mgmt(self): "detected ixr-r6 chassis, creating a dummy network device for SFM connection" ) res.append(f"-device virtio-net-pci,netdev=dummy,mac={vrnetlab.gen_mac(0)}") - res.append(f"-netdev tap,ifname=sfm-dummy,id=dummy,script=no,downscript=no") + res.append("-netdev tap,ifname=sfm-dummy,id=dummy,script=no,downscript=no") return res @@ -668,17 +670,18 @@ def bootstrap_config(self): class SROS_cp(SROS_vm): """Control plane for distributed VSR-SIM""" - def __init__( - self, hostname, username, password, mode, major_release, variant, conn_mode - ): + def __init__(self, hostname, username, password, mode, variant, conn_mode): # cp - control plane. role is used to create a separate overlay image name self.role = "cp" - cpu = variant["cp"].get("cpu") + + ram: int = vrnetlab.getMem(self.role, variant.get("cp").get("min_ram")) + cpu: int = vrnetlab.getCpu(self.role, variant.get("cp").get("cpu")) + super(SROS_cp, self).__init__( username, password, cpu=cpu, - ram=1024 * int(variant["cp"]["min_ram"]), + ram=ram, conn_mode=conn_mode, ) self.mode = mode @@ -783,13 +786,17 @@ class SROS_lc(SROS_vm): def __init__(self, lc_config, conn_mode, num_nics, slot=1, nic_eth_start=1): # role lc if for a line card. role is used to create a separate overlay image name self.role = "lc" + + ram: int = vrnetlab.getMem(self.role, lc_config.get("min_ram")) + cpu: int = vrnetlab.getCpu(self.role, lc_config.get("cpu")) + super(SROS_lc, self).__init__( None, None, - ram=1024 * int(lc_config["min_ram"]), + ram=ram, conn_mode=conn_mode, num=slot, - cpu=lc_config.get("cpu"), + cpu=cpu, ) self.smbios = ["type=1,product=TIMOS:{}".format(lc_config["timos_line"])] @@ -840,9 +847,10 @@ def bootstrap_spin(self): return +# SROS is main class for VSR-SIM class SROS(vrnetlab.VR): def __init__(self, hostname, username, password, mode, variant_name, conn_mode): - super(SROS, self).__init__(username, password) + super().__init__(username, password) if variant_name.lower() in SROS_VARIANTS: variant = SROS_VARIANTS[variant_name.lower()] @@ -856,18 +864,7 @@ def __init__(self, hostname, username, password, mode, variant_name, conn_mode): else: variant = parse_custom_variant(variant_name) - major_release = 0 - - # move files into place - for e in os.listdir("/"): - match = re.match(r"[^0-9]+([0-9]+)\S+\.qcow2$", e) - if match: - major_release = int(match.group(1)) - self.qcow_name = match.group(0) - if re.search(r"\.qcow2$", e): - os.rename("/" + e, "/sros.qcow2") - if re.search(r"\.license$", e): - shutil.move("/" + e, "/tftpboot/license.txt") + self.processFiles() self.license = False if os.path.isfile("/tftpboot/license.txt"): @@ -886,41 +883,7 @@ def __init__(self, hostname, username, password, mode, variant_name, conn_mode): self.logger.info(f"Number of NICs: {variant['max_nics']}") self.logger.info("Configuration mode: " + str(mode)) - # set up bridge for management interface to a localhost - self.logger.info("Creating br-mgmt bridge for management interface") - # This is to whitlist all bridges - vrnetlab.run_command(["mkdir", "-p", "/etc/qemu"]) - vrnetlab.run_command(["echo 'allow all' > /etc/qemu/bridge.conf"], shell=True) - # Enable IPv6 inside the container - vrnetlab.run_command(["sysctl net.ipv6.conf.all.disable_ipv6=0"], shell=True) - # Enable IPv6 routing inside the container - vrnetlab.run_command(["sysctl net.ipv6.conf.all.forwarding=1"], shell=True) - vrnetlab.run_command(["brctl", "addbr", "br-mgmt"]) - vrnetlab.run_command( - ["echo 16384 > /sys/class/net/br-mgmt/bridge/group_fwd_mask"], - shell=True, - ) - vrnetlab.run_command(["ip", "link", "set", "br-mgmt", "up"]) - vrnetlab.run_command( - [ - "ip", - "addr", - "add", - "dev", - "br-mgmt", - f"{BRIDGE_V4_ADDR}/{V4_PREFIX_LENGTH}", - ] - ) - vrnetlab.run_command( - [ - "ip", - "addr", - "add", - "dev", - "br-mgmt", - f"{BRIDGE_V6_ADDR}/{V6_PREFIX_LENGTH}", - ] - ) + self.setupMgmtBridge() if variant["deployment_model"] == "distributed": # CP VM instantiation @@ -930,7 +893,6 @@ def __init__(self, hostname, username, password, mode, variant_name, conn_mode): username, password, mode, - major_release, variant, conn_mode, ) @@ -1000,6 +962,60 @@ def __init__(self, hostname, username, password, mode, variant_name, conn_mode): ) ] + def setupMgmtBridge(self): + # set up bridge for management interface to a localhost + self.logger.info("Creating br-mgmt bridge for management interface") + # This is to whitlist all bridges + vrnetlab.run_command(["mkdir", "-p", "/etc/qemu"]) + vrnetlab.run_command(["echo 'allow all' > /etc/qemu/bridge.conf"], shell=True) + # Enable IPv6 inside the container + vrnetlab.run_command(["sysctl net.ipv6.conf.all.disable_ipv6=0"], shell=True) + # Enable IPv6 routing inside the container + vrnetlab.run_command(["sysctl net.ipv6.conf.all.forwarding=1"], shell=True) + vrnetlab.run_command(["brctl", "addbr", "br-mgmt"]) + vrnetlab.run_command( + ["echo 16384 > /sys/class/net/br-mgmt/bridge/group_fwd_mask"], + shell=True, + ) + vrnetlab.run_command(["ip", "link", "set", "br-mgmt", "up"]) + vrnetlab.run_command( + [ + "ip", + "addr", + "add", + "dev", + "br-mgmt", + f"{BRIDGE_V4_ADDR}/{V4_PREFIX_LENGTH}", + ] + ) + vrnetlab.run_command( + [ + "ip", + "addr", + "add", + "dev", + "br-mgmt", + f"{BRIDGE_V6_ADDR}/{V6_PREFIX_LENGTH}", + ] + ) + + # processFiles renames the qcow2 image to sros.qcow2 and the license file to license.txt + # as well as returning the major release number extracted from the qcow2 image name + def processFiles(self) -> int: + major_rel: int = 0 + + for e in os.listdir("/"): + match = re.match(r"[^0-9]+([0-9]+)\S+\.qcow2$", e) + if match: + major_rel = int(match.group(1)) + self.qcow_name = match.group(0) + if re.search(r"\.qcow2$", e): + os.rename("/" + e, "/sros.qcow2") + if re.search(r"\.license$", e): + shutil.move("/" + e, "/tftpboot/license.txt") + # returned major version isn't used currently + return major_rel + if __name__ == "__main__": import argparse