Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removing hard dependency on SCION lab packages, SCION build config #3

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
**/output/*
**/.scion_build_output/*
**/eth-states*/*
**/test_log*/*
**/__pycache__/*
Expand Down
187 changes: 185 additions & 2 deletions seedemu/layers/Scion.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
from __future__ import annotations
import requests
import logging
import os
import re
from urllib.parse import urlparse
from enum import Enum
from typing import Dict, Tuple, Union

from sys import version
from typing import Dict, Literal, Tuple, Union
from seedemu.core import (Emulator, Interface, Layer, Network, Registry,
Router, ScionAutonomousSystem, ScionRouter,
ScopedRegistry, Graphable)
from seedemu.core.ScionAutonomousSystem import IA
from seedemu.layers import ScionBase, ScionIsd
import shutil
import tempfile
from seedemu.utilities.BuildtimeDocker import BuildtimeDockerFile, BuildtimeDockerImage, sh


class LinkType(Enum):
Expand Down Expand Up @@ -47,6 +55,174 @@ def to_json(self, a_to_b: bool) -> str:
else:
return "PARENT"

class ScionBuildConfig():
"""!
@brief This class validates and builds scion from a configuration

checks the mode property and either downloads the binaries and builds it from source
Also supports local absolute directory file path to use instead in release mode
"""
mode: str
release_location: str
git_repo_url: str
checkout: str
version: str

def __init__(self):
self.mode = "release"
self.release_location = "https://github.com/scionproto/scion/releases/download/v0.12.0/scion_0.12.0_amd64_linux.tar.gz"
self.version = "v0.12.0"

def setBuildConfiguration(self, config: Dict[str,str]):
self.__validateBuildConfiguration(config)
self.mode = config["mode"]
if self.mode == "release":
self.release_location = config["releaseLocation"]
self.version = config["version"]
else:
self.git_repo_url = config["gitRepoUrl"]
self.checkout = config["checkout"]

def __validateBuildConfiguration(self, config: Dict[str,str]):
"""
validate build configuration dict by checking all the required keys and url validity
"""
if "mode" not in config:
raise KeyError("No SCION build configuration provided.")
if config["mode"] not in ["release", "build"]:
raise ValueError("Only two SCION build modes accepted. 'release'|'build'")
if config["mode"] == "release":
if "releaseLocation" not in config:
raise KeyError("releaseLocation must be set for the mode 'release'")
self.__validateReleaseLocation(config["releaseLocation"])
if "version" not in config:
raise KeyError("version must be set for the mode 'release'")
if config["mode"] == "build":
if "gitRepoUrl" not in config:
raise KeyError("gitRepoUrl must be set for the mode 'build'")
if "checkout" not in config:
raise KeyError("'checkout' must be set for the mode 'build'")
self.__validateGitURL(config["gitRepoUrl"])

def __validateReleaseLocation(self, path: str):
"""
check if the local path exists or the url is valid and reachable
"""
if (path) and self.__is_local_path(path):
if not os.path.exists(path):
raise ValueError("SCION local binary location is not valid.")
if not os.path.isabs(path):
raise ValueError("Absolute path required for the folder containing binaries")
elif self.__is_http_url(path):
try:
response = requests.head(path, allow_redirects=True, timeout=5)
if not response.status_code < 400:
raise Exception(f"SCION release url is valid but not reachable")
except requests.RequestException as e:
logging.error(e)
raise Exception(f"SCION release url is valid but not reachable")
else:
raise ValueError("Release location is Neither a valid HTTP URL nor a local path")

def __is_http_url(self, url: str) -> bool:
try:
result = urlparse(url)
return result.scheme in ("http", "https") and bool(result.netloc)
except ValueError:
return False

def __is_local_path(self, path: str) -> bool:
# A local path shouldn't be a URL but should exist in the filesystem
return not self.__is_http_url(path)

def __validateGitURL(self, url: str) :
# Ensure the URL ends with .git for Git repositories
if not url.endswith(".git"):
raise ValueError("URL does not look like a Git repository (missing .git)")
# Check the Git info/refs endpoint
git_service_url = f"{url}/info/refs?service=git-upload-pack"
try:
response = requests.get(git_service_url, timeout=10)
if not (response.status_code == 200 and "git-upload-pack" in response.text):
raise ValueError("SCION build repository not found (404)")
except requests.RequestException as e:
logging.error(e)
raise ValueError(f"Invalid SCION build repository")

def __classifyGitCheckout(self, checkout: str) -> str:
# Check if it's a commit (40 characters, hexadecimal)
if re.match(r'^[0-9a-fA-F]{40}$', checkout):
return "commit"
# Check if it's a tag (can be any string, usually without slashes and more descriptive)
if re.match(r'^[\w.-]+$', checkout):
return "tag"
# Check if it's a branch (can include slashes, dashes, or numbers)
if re.match(r'^[\w/.-]+$', checkout):
return "branch"

return "unknown"

def __generateGitCloneString(self, repo_url: str, checkout: str):
"""
Generates a Git clone string for the specified reference (branch, tag, or commit).
"""
checkout_type = self.__classifyGitCheckout(checkout)
if checkout_type == "branch":
return f"git clone -b {checkout} {repo_url} scion"
elif checkout_type == "tag":
return f"git clone --branch {checkout} {repo_url} scion"
elif checkout_type == "commit":
# Clone first, then checkout the commit
return f"git clone {repo_url} scion && cd scion && git checkout {checkout}"
else:
raise ValueError("Invalid reference type. Must be 'branch', 'tag', or 'commit'.")

def generateBuild(self) -> str :
"""
method to build all scion binaries and ouput to .scion_build_output based on the configuration mode
"""
if self.mode == "release":
if not self.__is_local_path(self.release_location):
if not os.path.isdir(f".scion_build_output/scion_binaries_{self.version}"):
SCION_RELEASE_TEMPLATE = f"""FROM alpine
RUN apk add --no-cache wget tar
WORKDIR /app
RUN wget -qO- {self.release_location} | tar xvz -C /app
"""
dockerfile = BuildtimeDockerFile(SCION_RELEASE_TEMPLATE)
container = BuildtimeDockerImage(f"scion-release-fetch-container_{self.version}").build(dockerfile).container()
current_dir = os.getcwd()
output_dir = os.path.join(current_dir, f".scion_build_output/scion_binaries_{self.version}")
container.entrypoint("sh").mountVolume(output_dir, "/build").run(
"-c \"cp -r /app/* /build\""
)
return output_dir

else:
output_dir = os.path.join(os.getcwd(), f".scion_build_output/scion_binaries_{self.version}")
return output_dir
else:
return self.release_location
else:
if not os.path.isdir(f".scion_build_output/scion_binaries_{self.checkout}"):
SCION_BUILD_TEMPLATE = f"""FROM golang:1.22-alpine
RUN apk add --no-cache git
RUN {self.__generateGitCloneString(self.git_repo_url,self.checkout)}
RUN cd scion && go mod tidy && CGO_ENABLED=0 go build -o bin ./router/... ./control/... ./dispatcher/... ./daemon/... ./scion/... ./scion-pki/... ./gateway/...
"""
dockerfile = BuildtimeDockerFile(SCION_BUILD_TEMPLATE)
container = BuildtimeDockerImage(f"scion-build-container-{self.checkout}").build(dockerfile).container()
current_dir = os.getcwd()
output_dir = os.path.join(current_dir, f".scion_build_output/scion_binaries_{self.checkout}")
container.entrypoint("sh").mountVolume(output_dir, "/build").run(
"-c \"cp -r scion/bin/* /build\""
)
return output_dir

else:
output_dir = os.path.join(os.getcwd(), f".scion_build_output/scion_binaries_{self.checkout}")
return output_dir


class Scion(Layer, Graphable):
"""!
Expand All @@ -58,6 +234,7 @@ class Scion(Layer, Graphable):

__links: Dict[Tuple[IA, IA, str, str, LinkType], int]
__ix_links: Dict[Tuple[int, IA, IA, str, str, LinkType], int]
__default_build_config : ScionBuildConfig

def __init__(self):
"""!
Expand All @@ -66,11 +243,17 @@ def __init__(self):
super().__init__()
self.__links = {}
self.__ix_links = {}
self.__default_build_config = ScionBuildConfig()
self.addDependency('ScionIsd', False, False)

def getName(self) -> str:
return "Scion"

def getBuildConfiguration(self) -> ScionBuildConfig:
return self.__default_build_config

def setBuildConfiguration(self, buildConfig: Dict[str,str]):
self.__default_build_config.setBuildConfiguration(buildConfig)

def addXcLink(self, a: Union[IA, Tuple[int, int]], b: Union[IA, Tuple[int, int]],
linkType: LinkType, count: int=1, a_router: str="", b_router: str="",) -> 'Scion':
Expand Down
25 changes: 11 additions & 14 deletions seedemu/layers/ScionRouting.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from seedemu.core import Emulator, Node, ScionAutonomousSystem, ScionRouter, Network
from seedemu.core.enums import NetworkType
from seedemu.layers import Routing, ScionBase, ScionIsd
from seedemu.layers.Scion import Scion, ScionBuildConfig


_Templates: Dict[str, str] = {}
Expand Down Expand Up @@ -78,42 +79,38 @@ def configure(self, emulator: Emulator):
@brief Install SCION on router, control service and host nodes.
"""
super().configure(emulator)

reg = emulator.getRegistry()

for ((scope, type, name), obj) in reg.getAll().items():
if type == 'rnode':
rnode: ScionRouter = obj
if not issubclass(rnode.__class__, ScionRouter):
rnode.__class__ = ScionRouter
rnode.initScionRouter()

self.__install_scion(rnode)
self.__install_scion(emulator,rnode)
name = rnode.getName()
rnode.appendStartCommand(_CommandTemplates['br'].format(name=name), fork=True)

elif type == 'csnode':
csnode: Node = obj
self.__install_scion(csnode)
self.__install_scion(emulator,csnode)
self.__append_scion_command(csnode)
name = csnode.getName()
csnode.appendStartCommand(_CommandTemplates['cs'].format(name=name), fork=True)

elif type == 'hnode':
hnode: Node = obj
self.__install_scion(hnode)
self.__install_scion(emulator,hnode)
self.__append_scion_command(hnode)

def __install_scion(self, node: Node):
def __install_scion(self, emulator: Emulator, node: Node):
"""Install SCION packages on the node."""
node.addBuildCommand(
'echo "deb [trusted=yes] https://packages.netsec.inf.ethz.ch/debian all main"'
' > /etc/apt/sources.list.d/scionlab.list')
node.addBuildCommand(
"apt-get update && apt-get install -y"
" scion-border-router scion-control-service scion-daemon scion-dispatcher scion-tools"
" scion-apps-bwtester")
node.addSoftware("apt-transport-https")
node.addSoftware("ca-certificates")
scion_layer: Scion = emulator.getLayer(layerName="Scion")
buildConfiguration: ScionBuildConfig = scion_layer.getBuildConfiguration()
build_dir = buildConfiguration.generateBuild()
node.addSharedFolder("/bin/scion/", build_dir)
node.addBuildCommand("export PATH=$PATH:/bin/scion/")

def __append_scion_command(self, node: Node):
"""Append commands for starting the SCION host stack on the node."""
Expand Down