Skip to content

Commit 77fff96

Browse files
Merge pull request #1 from multiversx/init
Initial implementation
2 parents 21fb6d0 + d5ba824 commit 77fff96

16 files changed

+810
-0
lines changed

.flake8

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
ignore = E501

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,46 @@
11
# mx-chain-multistage-scripts
2+
23
Multi-staged sync & import-db (node versions manager).
4+
5+
These scripts allow one to use multiple versions of the MultiversX node, in sequence, to _sync_ (from the deep past) or run _import-db_ flows.
6+
7+
**Important:** these scripts are only suitable for observers, not for validators. Furthermore, the MultiversX proxy isn't handled.
8+
9+
## Python virtual environment
10+
11+
Create a virtual environment and install the dependencies:
12+
13+
```
14+
python3 -m venv ./venv
15+
source ./venv/bin/activate
16+
pip install -r ./requirements.txt --upgrade
17+
```
18+
19+
## Building the artifacts
20+
21+
Skip this flow if you choose to download the pre-built Node artifacts, instead of building them.
22+
23+
```
24+
PYTHONPATH=. python3 ./multistage/build.py --workspace=~/mvx-workspace --config=./multistage/samples/build.json
25+
```
26+
27+
## Set up an observer (or a squad)
28+
29+
```
30+
PYTHONPATH=. python3 ./multistage/driver.py --config=./multistage/samples/testnet_sync.json --lane=shard_0 --stage=andromeda
31+
32+
PYTHONPATH=. python3 ./multistage/driver.py --config=./multistage/samples/testnet_sync.json --lane=shard_1 --stage=andromeda
33+
...
34+
```
35+
36+
Once nodes are ready (synchronized to the network), switch to the regular node management scripts.
37+
38+
## Run import-db
39+
40+
```
41+
PYTHONPATH=. python3 ./multistage/driver.py --config=./multistage/samples/testnet_import_db.json --lane=shard_0 --stage=andromeda
42+
43+
PYTHONPATH=. python3 ./multistage/driver.py --config=./multistage/samples/testnet_import_db.json --lane=shard_1 --stage=andromeda
44+
45+
...
46+
```

multistage/build.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import json
2+
import os
3+
import shutil
4+
import sys
5+
import traceback
6+
from argparse import ArgumentParser
7+
from pathlib import Path
8+
from typing import Any
9+
10+
from rich import print
11+
from rich.panel import Panel
12+
from rich.rule import Rule
13+
14+
from multistage import errors, golang
15+
from multistage.config import BuildConfigEntry
16+
from multistage.constants import FILE_MODE_NICE
17+
from multistage.shared import fetch_archive
18+
19+
20+
def main(cli_args: list[str] = sys.argv[1:]):
21+
try:
22+
_do_main(cli_args)
23+
except errors.KnownError as err:
24+
print(Panel(f"[red]{traceback.format_exc()}"))
25+
print(Panel(f"[red]{err.get_pretty()}"))
26+
return 1
27+
28+
29+
def _do_main(cli_args: list[str]):
30+
parser = ArgumentParser()
31+
parser.add_argument("--workspace", required=True, help="path of the build workspace")
32+
parser.add_argument("--config", required=True, help="path of the 'build' configuration file")
33+
args = parser.parse_args(cli_args)
34+
35+
workspace_path = Path(args.workspace).expanduser().resolve()
36+
workspace_path.mkdir(parents=True, exist_ok=True)
37+
38+
config_path = Path(args.config).expanduser().resolve()
39+
config_data = json.loads(config_path.read_text())
40+
config_entries = [BuildConfigEntry.new_from_dictionary(item) for item in config_data]
41+
42+
for entry in config_entries:
43+
print(Rule(f"[bold yellow]{entry.name}"))
44+
45+
golang.install_go(workspace_path, entry.go_url, environment_label=entry.name)
46+
build_environment = golang.acquire_environment(workspace_path, label=entry.name)
47+
48+
source_parent_folder = do_download(workspace_path, entry)
49+
cmd_node_folder = do_build(source_parent_folder, build_environment)
50+
copy_artifacts(cmd_node_folder, entry)
51+
52+
53+
def do_download(workspace: Path, entry: BuildConfigEntry) -> Path:
54+
url = entry.source_url
55+
extraction_folder = workspace / entry.name
56+
57+
fetch_archive(url, extraction_folder)
58+
return extraction_folder
59+
60+
61+
def do_build(source_parent_folder: Path, environment: golang.BuildEnvironment) -> Path:
62+
# If has one subfolder, that one is the source code
63+
subfolders = [Path(item.path) for item in os.scandir(source_parent_folder) if item.is_dir()]
64+
source_folder = subfolders[0] if len(subfolders) == 1 else source_parent_folder
65+
66+
cmd_node = source_folder / "cmd" / "node"
67+
go_mod = source_folder / "go.mod"
68+
69+
golang.build(cmd_node, environment)
70+
copy_wasmer_libraries(environment, go_mod, cmd_node)
71+
72+
return cmd_node
73+
74+
75+
def copy_wasmer_libraries(build_environment: golang.BuildEnvironment, go_mod: Path, destination: Path):
76+
go_path = Path(build_environment.go_path).expanduser().resolve()
77+
vm_go_folder_name = get_chain_vm_go_folder_name(go_mod)
78+
vm_go_path = go_path / "pkg" / "mod" / vm_go_folder_name
79+
libraries = list((vm_go_path / "wasmer").glob("*.so")) + list((vm_go_path / "wasmer2").glob("*.so"))
80+
81+
for library in libraries:
82+
shutil.copy(library, destination)
83+
84+
os.chmod(destination / library.name, FILE_MODE_NICE)
85+
86+
87+
def get_chain_vm_go_folder_name(go_mod: Path) -> str:
88+
lines = go_mod.read_text().splitlines()
89+
90+
matching_lines = [line for line in lines if "github.com/multiversx/mx-chain-vm-go" in line]
91+
if not matching_lines:
92+
raise errors.KnownError("cannot detect location of mx-chain-vm-go")
93+
94+
line_of_interest = matching_lines[0]
95+
parts = line_of_interest.split()
96+
return f"{parts[0]}@{parts[1]}"
97+
98+
99+
def copy_artifacts(cmd_node_folder: Path, entry: BuildConfigEntry):
100+
print(f"Copying artifacts to {entry.destination_folder} ...")
101+
102+
libraries = list(cmd_node_folder.glob("*.so"))
103+
executable = cmd_node_folder / "node"
104+
artifacts = libraries + [executable]
105+
106+
destination_folder = Path(entry.destination_folder).expanduser().resolve()
107+
shutil.rmtree(destination_folder, ignore_errors=True)
108+
destination_folder.mkdir(parents=True, exist_ok=True)
109+
110+
for artifact in artifacts:
111+
shutil.copy(artifact, destination_folder)
112+
113+
114+
if __name__ == "__main__":
115+
ret = main(sys.argv[1:])
116+
sys.exit(ret)

multistage/config.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
2+
3+
from pathlib import Path
4+
from typing import Any
5+
6+
from multistage import errors
7+
8+
9+
class BuildConfigEntry:
10+
def __init__(self, name: str, go_url: str, source_url: str, destination_folder: str) -> None:
11+
if not name:
12+
raise errors.KnownError("build 'name' is required")
13+
if not go_url:
14+
raise errors.KnownError("build 'go url' is required")
15+
if not source_url:
16+
raise errors.KnownError("build 'source' is required")
17+
if not destination_folder:
18+
raise errors.KnownError("build 'destination' is required")
19+
20+
self.name = name
21+
self.go_url = go_url
22+
self.source_url = source_url
23+
self.destination_folder = destination_folder
24+
25+
@classmethod
26+
def new_from_dictionary(cls, data: dict[str, Any]):
27+
name = data.get("name") or ""
28+
go_url = data.get("goUrl") or ""
29+
source_url = data.get("sourceUrl") or ""
30+
destination_folder = data.get("destinationFolder") or ""
31+
32+
return cls(
33+
name=name,
34+
go_url=go_url,
35+
source_url=source_url,
36+
destination_folder=destination_folder,
37+
)
38+
39+
40+
class DriverConfig:
41+
def __init__(self, lanes: list["LaneConfig"]) -> None:
42+
lanes_names = [lane.name for lane in lanes]
43+
44+
if not lanes:
45+
raise errors.BadConfigurationError("'lanes' are required")
46+
if len(lanes_names) > len(set(lanes_names)):
47+
raise errors.BadConfigurationError("lanes names must be unique")
48+
49+
self.lanes = lanes
50+
self.lanes_by_name = {lane.name: lane for lane in lanes}
51+
52+
@classmethod
53+
def new_from_dictionary(cls, data: dict[str, Any]):
54+
lanes_records = data.get("lanes") or []
55+
lanes = [LaneConfig.new_from_dictionary(record) for record in lanes_records]
56+
57+
return cls(
58+
lanes=lanes,
59+
)
60+
61+
def get_lanes_names(self) -> list[str]:
62+
return [lane.name for lane in self.lanes]
63+
64+
def get_lane(self, name: str) -> "LaneConfig":
65+
return self.lanes_by_name[name]
66+
67+
68+
class LaneConfig:
69+
def __init__(self, name: str, working_directory: str, stages: list["StageConfig"]) -> None:
70+
stages_names = [stage.name for stage in stages]
71+
72+
if not name:
73+
raise errors.BadConfigurationError("for all lanes, 'name' is required")
74+
if not working_directory:
75+
raise errors.BadConfigurationError(f"for lane {name}, 'working directory' is required")
76+
if not stages:
77+
raise errors.BadConfigurationError(f"for lane {name}, 'stages' are required")
78+
if len(stages) > len(set(stages_names)):
79+
raise errors.BadConfigurationError("stages names must be unique")
80+
81+
self.name = name
82+
self.working_directory = Path(working_directory).expanduser().resolve()
83+
self.stages = stages
84+
self.stages_by_name = {stage.name: stage for stage in stages}
85+
86+
@classmethod
87+
def new_from_dictionary(cls, data: dict[str, Any]):
88+
name = data.get("name") or ""
89+
working_directory = data.get("workingDirectory") or ""
90+
stages_records = data.get("stages") or []
91+
stages = [StageConfig.new_from_dictionary(record) for record in stages_records]
92+
93+
return cls(
94+
name=name,
95+
working_directory=working_directory,
96+
stages=stages,
97+
)
98+
99+
def get_stages_names(self) -> list[str]:
100+
return [stage.name for stage in self.stages]
101+
102+
def get_stages_including_and_after(self, initial_stage_name: str) -> list["StageConfig"]:
103+
stages_names = self.get_stages_names()
104+
index_of_initial_stage_name = stages_names.index(initial_stage_name)
105+
return self.stages[index_of_initial_stage_name:]
106+
107+
108+
class StageConfig:
109+
def __init__(self,
110+
name: str,
111+
until_epoch: int,
112+
node_status_url: str,
113+
configuration_archive: str,
114+
bin: str,
115+
node_arguments: list[str],
116+
with_db_lookup_extensions: bool,
117+
with_indexing: bool) -> None:
118+
if not name:
119+
raise errors.BadConfigurationError("for all stages, 'name' is required")
120+
if not until_epoch:
121+
raise errors.BadConfigurationError(f"for stage {name}, 'until epoch' is required")
122+
if not node_status_url:
123+
raise errors.BadConfigurationError(f"for stage {name}, 'node status url' is required")
124+
if not configuration_archive:
125+
raise errors.BadConfigurationError(f"for stage {name}, 'configuration archive' is required")
126+
if not bin:
127+
raise errors.BadConfigurationError(f"for stage {name}, 'bin' is required")
128+
129+
self.name = name
130+
self.until_epoch = until_epoch
131+
self.node_status_url = node_status_url
132+
self.configuration_archive = configuration_archive
133+
self.bin = Path(bin).expanduser().resolve()
134+
self.node_arguments = node_arguments
135+
self.with_db_lookup_extensions = with_db_lookup_extensions
136+
self.with_indexing = with_indexing
137+
138+
@classmethod
139+
def new_from_dictionary(cls, data: dict[str, Any]):
140+
name = data.get("name") or ""
141+
until_epoch = data.get("untilEpoch") or 0
142+
node_status_url = data.get("nodeStatusUrl") or ""
143+
configuration_archive = data.get("configurationArchive") or ""
144+
bin = data.get("bin") or ""
145+
node_arguments = data.get("nodeArguments") or []
146+
with_db_lookup_extensions = data.get("withDbLookupExtensions") or False
147+
with_indexing = data.get("withIndexing") or False
148+
149+
return cls(
150+
name=name,
151+
until_epoch=until_epoch,
152+
node_status_url=node_status_url,
153+
configuration_archive=configuration_archive,
154+
bin=bin,
155+
node_arguments=node_arguments,
156+
with_db_lookup_extensions=with_db_lookup_extensions,
157+
with_indexing=with_indexing,
158+
)

multistage/constants.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import stat
2+
3+
METACHAIN_ID = 4294967295
4+
NODE_PROCESS_ULIMIT = 1024 * 512
5+
NODE_MONITORING_PERIOD = 5
6+
NODE_RETURN_CODE_SUCCESS = 0
7+
NODE_RETURN_CODE_SIGKILL = -9
8+
TEMPORARY_DIRECTORIES_PREFIX = "mx_chain_scripts_multistage_"
9+
10+
# Read, write and execute by owner, read and execute by group and others
11+
FILE_MODE_NICE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH

multistage/driver.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import json
2+
import sys
3+
import traceback
4+
from argparse import ArgumentParser
5+
from pathlib import Path
6+
from typing import Any
7+
8+
from rich import print
9+
from rich.panel import Panel
10+
11+
from multistage import errors
12+
from multistage.config import DriverConfig
13+
from multistage.lane_controller import LaneController
14+
15+
16+
def main(cli_args: list[str] = sys.argv[1:]):
17+
try:
18+
_do_main(cli_args)
19+
except errors.KnownError as err:
20+
print(Panel(f"[red]{traceback.format_exc()}"))
21+
print(Panel(f"[red]{err.get_pretty()}"))
22+
return 1
23+
24+
25+
def _do_main(cli_args: list[str]):
26+
parser = ArgumentParser()
27+
parser.add_argument("--config", required=True, help="path of the 'driver' configuration file")
28+
parser.add_argument("--lane", required=True, help="which lane to handle")
29+
parser.add_argument("--stage", required=True, help="initial stage on the lane")
30+
args = parser.parse_args(cli_args)
31+
32+
config_path = Path(args.config).expanduser().resolve()
33+
config_data = json.loads(config_path.read_text())
34+
driver_config = DriverConfig.new_from_dictionary(config_data)
35+
lane_name = args.lane
36+
initial_stage_name = args.stage
37+
38+
if lane_name not in driver_config.get_lanes_names():
39+
raise errors.BadConfigurationError(f"unknown lane: {lane_name}")
40+
41+
lane_config = driver_config.get_lane(lane_name)
42+
43+
if initial_stage_name not in lane_config.get_stages_names():
44+
raise errors.BadConfigurationError(f"unknown stage: {initial_stage_name}")
45+
46+
print(f"[bold yellow]Lane: {lane_name}")
47+
print(f"[bold yellow]Initial stage: {initial_stage_name}")
48+
49+
lane = LaneController(lane_config, initial_stage_name)
50+
lane.start()
51+
52+
53+
if __name__ == "__main__":
54+
ret = main(sys.argv[1:])
55+
sys.exit(ret)

0 commit comments

Comments
 (0)