Skip to content

chore: Sync data availability and integrity tests with latest Nomos node changes #3

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

Merged
merged 28 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ae11280
fix: remove skip for test_da_sampling_determines_data_presence
romanzac Feb 4, 2025
a5a04b2
fix: set default docker image
romanzac Feb 4, 2025
3ee9852
test: reconstruct command
romanzac Feb 6, 2025
f3a5117
test: nomos cli wrapper
romanzac Feb 7, 2025
b84df0a
fix: command composition
romanzac Feb 7, 2025
f227425
fix: close rest connection
romanzac Feb 10, 2025
5ad2456
fix: refactor api_clients
romanzac Feb 10, 2025
d05a078
fix: refactor docker_manager
romanzac Feb 10, 2025
0ce5280
fix: add log stream parsing
romanzac Feb 10, 2025
c84f461
fix: add remove_padding
romanzac Feb 11, 2025
ca8eabf
fix: use 4 node instead of 5 node cluster
romanzac Feb 11, 2025
51d108c
fix: add response content check
romanzac Feb 12, 2025
9e85b0a
fix: adjust delays
romanzac Feb 12, 2025
3189548
test: test_data_integrity in workflow
romanzac Feb 12, 2025
1144a7a
fix: move prune-vm into correct dir
romanzac Feb 12, 2025
e55e6b6
fix: checkout first
romanzac Feb 12, 2025
4824aff
fix: change to public image
romanzac Feb 12, 2025
eb51c6b
fix: rename workflow
romanzac Feb 12, 2025
ebd56ce
fix: indexing for ensure_nodes_ready
romanzac Feb 12, 2025
d340256
fix: refactor run for NomosCli
romanzac Feb 13, 2025
cdc7836
fix: conversion functions for index and app_id
romanzac Feb 13, 2025
f74002c
test: with different data
romanzac Feb 13, 2025
f7d54c8
fix: change data to assert
romanzac Feb 13, 2025
9c61ae1
fix: move command param to kwargs
romanzac Feb 13, 2025
5a8455c
fix: refactor stop and kill
romanzac Feb 13, 2025
4b0678a
fix: reduce params in start_container
romanzac Feb 13, 2025
dd3c820
test: daily workflow
romanzac Feb 13, 2025
b8719dd
test: rename daily workflow
romanzac Feb 13, 2025
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
27 changes: 27 additions & 0 deletions .github/actions/prune-vm/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Inspired by https://github.com/AdityaGarg8/remove-unwanted-software
# to free up disk space. Currently removes Dotnet, Android and Haskell.
name: Remove unwanted software
description: Default GitHub runners come with a lot of unnecessary software
runs:
using: "composite"
steps:
- name: Disk space report before modification
shell: bash
run: |
echo "==> Available space before cleanup"
echo
df -h
- name: Maximize build disk space
shell: bash
run: |
set -euo pipefail
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/.ghcup
- name: Disk space report after modification
shell: bash
run: |
echo "==> Available space after cleanup"
echo
df -h
11 changes: 11 additions & 0 deletions .github/workflows/nomos_daily.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: Nomos E2E Tests Daily

on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:

jobs:
test-common:
uses: ./.github/workflows/test_common.yml

32 changes: 32 additions & 0 deletions .github/workflows/test_common.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: E2E Tests Common

on:
workflow_call:

env:
FORCE_COLOR: "1"

jobs:
tests:
name: tests
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4

- name: Remove unwanted software
uses: ./.github/actions/prune-vm

- uses: actions/setup-python@v4
with:
python-version: '3.12'
cache: 'pip'

- run: |
pip install -r requirements.txt
mkdir -p kzgrs
wget https://raw.githubusercontent.com/logos-co/nomos-node/master/tests/kzgrs/kzgrs_test_params -O kzgrs/kzgrs_test_params

- name: Run tests
run: |
pytest
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
port: 4400
n_hosts: 5
n_hosts: 4
timeout: 30

# ConsensusConfig related parameters
Expand Down
2 changes: 1 addition & 1 deletion cluster_config/cfgsync.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
port: 4400
n_hosts: 5
n_hosts: 2
timeout: 30

# ConsensusConfig related parameters
Expand Down
File renamed without changes.
File renamed without changes.
7 changes: 3 additions & 4 deletions src/node/api_clients/rest.py → src/api_clients/rest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from src.libs.custom_logger import get_custom_logger
import json
from urllib.parse import quote
from src.node.api_clients.base_client import BaseClient
from src.api_clients.base_client import BaseClient

logger = get_custom_logger(__name__)

Expand All @@ -12,12 +11,12 @@ def __init__(self, rest_port):

def rest_call(self, method, endpoint, payload=None):
url = f"http://127.0.0.1:{self._rest_port}/{endpoint}"
headers = {"Content-Type": "application/json"}
headers = {"Content-Type": "application/json", "Connection": "close"}
return self.make_request(method, url, headers=headers, data=payload)

def rest_call_text(self, method, endpoint, payload=None):
url = f"http://127.0.0.1:{self._rest_port}/{endpoint}"
headers = {"accept": "text/plain"}
headers = {"accept": "text/plain", "Connection": "close"}
return self.make_request(method, url, headers=headers, data=payload)

def info(self):
Expand Down
Empty file added src/cli/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions src/cli/cli_vars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from src.env_vars import NOMOS_IMAGE

nomos_cli = {
"reconstruct": {
"image": NOMOS_IMAGE,
"flags": [{"--app-blobs": [0]}], # Value [] is a list of indexes into list of values required for the flag
"volumes": [],
"ports": [],
"entrypoint": "",
},
}
111 changes: 111 additions & 0 deletions src/cli/nomos_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import json
import os
import re

from src.data_storage import DS
from src.libs.common import generate_log_prefix
from src.libs.custom_logger import get_custom_logger
from tenacity import retry, stop_after_delay, wait_fixed

from src.cli.cli_vars import nomos_cli
from src.docker_manager import DockerManager, stop, kill
from src.env_vars import DOCKER_LOG_DIR, NOMOS_CLI
from src.steps.da import remove_padding

logger = get_custom_logger(__name__)


class NomosCli:
def __init__(self, **kwargs):
if "command" not in kwargs:
raise ValueError("The command parameter is required")

command = kwargs["command"]
if command not in nomos_cli:
raise ValueError("Unknown command provided")

logger.debug(f"Cli is going to be initialized with this config {nomos_cli[command]}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to have some error handling to exit with a meaningfull message if command is not a key of nomos_cli

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at 9c61ae1

self._command = command
self._image_name = nomos_cli[command]["image"]
self._internal_ports = nomos_cli[command]["ports"]
self._volumes = nomos_cli[command]["volumes"]
self._entrypoint = nomos_cli[command]["entrypoint"]

container_name = "nomos-cli-" + generate_log_prefix()
self._log_path = os.path.join(DOCKER_LOG_DIR, f"{container_name}__{self._image_name.replace('/', '_')}.log")
self._docker_manager = DockerManager(self._image_name)
self._container_name = container_name
self._container = None

cwd = os.getcwd()
self._volumes = [cwd + "/" + volume for volume in self._volumes]

def run(self, input_values=None, **kwargs):
logger.debug(f"NomosCli starting with log path {self._log_path}")

self._port_map = {}

cmd = [NOMOS_CLI, self._command]
for flag in nomos_cli[self._command]["flags"]:
for f, indexes in flag.items():
cmd.append(f)
for j in indexes:
cmd.append(input_values[j])

logger.debug(f"NomosCli command to run {cmd}")

self._container = self._docker_manager.start_container(
self._docker_manager.image,
port_bindings=self._port_map,
args=None,
log_path=self._log_path,
volumes=self._volumes,
entrypoint=self._entrypoint,
remove_container=True,
name=self._container_name,
command=cmd,
)

DS.nomos_nodes.append(self)

match self._command:
case "reconstruct":
decode_only = kwargs.get("decode_only", False)
return self.reconstruct(input_values=input_values, decode_only=decode_only)
case _:
return

def reconstruct(self, input_values=None, decode_only=False):
keywords = ["Reconstructed data"]

log_stream = self._container.logs(stream=True)

matches = self._docker_manager.search_log_for_keywords(self._log_path, keywords, False, log_stream)
assert len(matches) > 0, f"Reconstructed data not found {matches}"

# Use regular expression that captures the byte list after "Reconstructed data"
result = re.sub(r".*Reconstructed data\s*(\[[^\]]+\]).*", r"\1", matches[keywords[0]][0])

result_bytes = []
try:
result_bytes = json.loads(result)
except Exception as ex:
logger.debug(f"Conversion to bytes failed with exception {ex}")

if decode_only:
result_bytes = result_bytes[:-31]

result_bytes = remove_padding(result_bytes)
result = bytes(result_bytes).decode("utf-8")

DS.nomos_nodes.remove(self)

return result

@retry(stop=stop_after_delay(5), wait=wait_fixed(0.1), reraise=True)
def stop(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't those methods exactly the same with the ones from src/node
/nomos_node.py ?
Maybe you should move them to docker manager to not have duplication
Applies to other stuff as well if it's duplicated

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at 5a8455c

self._container = stop(self._container)

@retry(stop=stop_after_delay(5), wait=wait_fixed(0.1), reraise=True)
def kill(self):
self._container = kill(self._container)
116 changes: 83 additions & 33 deletions src/node/docker_mananger.py → src/docker_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,41 @@ def create_network(self, network_name=NETWORK_NAME):
logger.debug(f"Network {network_name} created")
return network

def start_container(self, image_name, port_bindings, args, log_path, volumes, entrypoint, remove_container=True, name=None):
def start_container(self, image_name, port_bindings, args, log_path, volumes, entrypoint, **kwargs):
remove_container = kwargs.get("remove_container", True)
name = kwargs.get("name")
command = kwargs.get("command")

cli_args = []
for key, value in args.items():
if isinstance(value, list): # Check if value is a list
cli_args.extend([f"--{key}={item}" for item in value]) # Add a command for each item in the list
elif value is None:
cli_args.append(f"{key}") # Add simple command as it is passed in the key
else:
cli_args.append(f"--{key}={value}") # Add a single command
if command is None:
for key, value in args.items():
if isinstance(value, list): # Check if value is a list
cli_args.extend([f"--{key}={item}" for item in value]) # Add a command for each item in the list
elif value is None:
cli_args.append(f"{key}") # Add simple command as it is passed in the key
else:
cli_args.append(f"--{key}={value}") # Add a single command
else:
cli_args = command

cli_args_str_for_log = " ".join(cli_args)
logger.debug(f"docker run -i -t {port_bindings} {image_name} {cli_args_str_for_log}")
container = self._client.containers.run(
image_name,
command=cli_args,
ports=port_bindings,
detach=True,
remove=remove_container,
auto_remove=remove_container,
volumes=volumes,
entrypoint=entrypoint,
name=name,
network=NETWORK_NAME,
)
logger.debug(f"docker run -i -t --entrypoint {entrypoint} {port_bindings} {image_name} {cli_args_str_for_log}")

try:
container = self._client.containers.run(
image_name,
command=cli_args,
ports=port_bindings,
detach=True,
remove=remove_container,
auto_remove=remove_container,
volumes=volumes,
entrypoint=entrypoint,
name=name,
network=NETWORK_NAME,
)
except Exception as ex:
logger.debug(f"Docker container run failed with exception {ex}")

logger.debug(f"Container started with ID {container.short_id}. Setting up logs at {log_path}")
log_thread = threading.Thread(target=self._log_container_output, args=(container, log_path))
Expand Down Expand Up @@ -125,19 +136,32 @@ def is_container_running(self, container):
def image(self):
return self._image

def search_log_for_keywords(self, log_path, keywords, use_regex=False):
def find_keywords_in_line(self, keywords, line, use_regex=False):
matches = {keyword: [] for keyword in keywords}

# Open the log file and search line by line
with open(log_path, "r") as log_file:
for line in log_file:
for keyword in keywords:
if use_regex:
if re.search(keyword, line, re.IGNORECASE):
matches[keyword].append(line.strip())
else:
if keyword.lower() in line.lower():
matches[keyword].append(line.strip())
for keyword in keywords:
if use_regex:
if re.search(keyword, line, re.IGNORECASE):
matches[keyword].append(line.strip())
else:
if keyword.lower() in line.lower():
matches[keyword].append(line.strip())

return matches

def search_log_for_keywords(self, log_path, keywords, use_regex=False, log_stream=None):
matches = {}

# Read from stream
if log_stream is not None:
for line in log_stream:
matches = self.find_keywords_in_line(keywords, line.decode("utf-8"), use_regex=use_regex)

else:
# Open the log file and search line by line
with open(log_path, "r") as log_file:
for line in log_file:
matches = self.find_keywords_in_line(keywords, line, use_regex=use_regex)

# Check if there were any matches
if any(matches[keyword] for keyword in keywords):
Expand All @@ -146,5 +170,31 @@ def search_log_for_keywords(self, log_path, keywords, use_regex=False):
logger.debug(f"Found matches for keyword '{keyword}': {lines}")
return matches
else:
logger.debug("No errors found in the nomos logs.")
logger.debug("No keywords found in the nomos logs.")
return None


def stop(container):
if container:
logger.debug(f"Stopping container with id {container.short_id}")
container.stop()
try:
container.remove()
except:
pass
logger.debug("Container stopped.")

return None


def kill(container):
if container:
logger.debug(f"Killing container with id {container.short_id}")
container.kill()
try:
container.remove()
except:
pass
logger.debug("Container killed.")

return None
6 changes: 6 additions & 0 deletions src/env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ def get_env_var(var_name, default=None):
NOMOS_EXECUTOR = "nomos_executor"
CFGSYNC = "cfgsync"

DEFAULT_IMAGE = "ghcr.io/logos-co/nomos-node:latest"

NODE_1 = get_env_var("NODE_1", NOMOS)
NODE_2 = get_env_var("NODE_2", NOMOS_EXECUTOR)
NODE_3 = get_env_var("NODE_3", CFGSYNC)

NOMOS_IMAGE = get_env_var("NOMOS_IMAGE", DEFAULT_IMAGE)

NOMOS_CLI = "/usr/bin/nomos-cli"

ADDITIONAL_NODES = get_env_var("ADDITIONAL_NODES", f"{NOMOS},{NOMOS}")
# more nodes need to follow the NODE_X pattern
DOCKER_LOG_DIR = get_env_var("DOCKER_LOG_DIR", "./log/docker")
Expand Down
Loading