feat: add op-conductor-ops script with updated readme and example config file (#44)

This commit is contained in:
Jacob Elias 2024-08-21 18:47:16 -05:00 committed by GitHub
parent 9a36d674b5
commit ad47122943
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 950 additions and 0 deletions

@ -0,0 +1,46 @@
# op-conductor-ops
op-conductor-ops is a CLI tool for managing op-conductor sequencer clusters.
**WARNING!!! This tool can cause a network outage if used improperly. Please consult #pod-devinfra before using.**
## Setup
Requires [poetry](https://github.com/python-poetry/poetry).
Install python dependencies with `poetry install`.
## Usage
After installing dependencies with `poetry`, the tool can be invoked with `./op-conductor-ops`,
which just calls `poetry run python main.py` and passes on any arguments.
### Example Usage
* Example usage with implicit config file with lookup at ./config.toml
```./op-conductor-ops status <network-name>```
* Usage with explicit path to config and certificate
```./op-conductor-ops -c ./<path>/config.toml --cert ./<path>/cacert.pem <command> <network-name>```
## Example Configuration File: example.config.toml
This configuration file is used to set up the networks and sequencers for your application.
### Structure
The configuration file is divided into two main sections:
1. **Networks**: This section defines the networks that your application will use. There is an example network configuration (`op-network-1`) and a blank network configuration (`op-network-N`) for you to fill out.
2. **Sequencers**: This section defines the sequencers for each network. Again, there is an example sequencer configuration for `op-network-1` and a blank sequencer configuration for `op-network-N`.
Is is recommended to update the network name and sequencer names for your specifc configuration in the toml object declaration
### Config Usage
1. Copy this file to `config.toml` in your application's root directory.
2. Modify the example configurations or fill out the blank configurations as needed for your application.
3. Save the `config.toml` file and use it to configure your application's networks and sequencers.
Remember, the example configurations are provided for your convenience, but you should review and update them to match your specific requirements.

@ -0,0 +1,29 @@
from network import Network
from sequencer import Sequencer
import toml
def read_config(config_path: str) -> tuple[dict[str, Sequencer], str]:
config = toml.load(config_path)
cert_path = config.get('cert_path', "")
# load sequencers into a map
sequencers = {}
for name, seq_config in config['sequencers'].items():
sequencers[name] = Sequencer(
sequencer_id=name,
raft_addr=seq_config['raft_addr'],
conductor_rpc_url=seq_config['conductor_rpc_url'],
node_rpc_url=seq_config['node_rpc_url'],
voting=seq_config['voting']
)
# Initialize network, with list of sequencers
networks = {}
for network_name, network_config in config['networks'].items():
network_sequencers = [sequencers[seq_name]
for seq_name in network_config['sequencers']]
networks[network_name] = Network(network_name, network_sequencers)
return networks, cert_path

@ -0,0 +1,62 @@
# Path to the SSL/TLS certificate file
cert_path = "./cacert.pem"
# Network configurations
[networks]
# Example network configuration
[networks.op-network-1]
sequencers = [
"op-network-1-sequencer-0",
"op-network-1-sequencer-1",
"op-network-1-sequencer-2",
]
# Blank network configuration
[networks.op-network-N]
sequencers = [
"op-network-N-sequencer-0",
"op-network-N-sequencer-1",
"op-network-N-sequencer-2",
]
# Sequencer configurations
[sequencers]
# Example sequencer configuration for op-network-1 with three sequencers
[sequencers.op-network-1-sequencer-0]
raft_addr = "op-network-1-sequencer-0-op-conductor-raft:50050"
conductor_rpc_url = "https://op-network-1-sequencer-0-op-conductor"
node_rpc_url = "https://op-network-1-sequencer-0-op-node"
voting = true
[sequencers.op-network-1-sequencer-1]
raft_addr = "op-network-1-sequencer-1-op-conductor-raft.50050"
conductor_rpc_url = "https://op-network-1-sequencer-1-op-conductor"
node_rpc_url = "https://op-network-1-sequencer-1-op-node"
voting = false
[sequencers.op-network-1-sequencer-2]
raft_addr = "op-network-1-sequencer-2-op-conductor-raft:50050"
conductor_rpc_url = "https://op-network-1-sequencer-2-op-conductor"
node_rpc_url = "https://op-network-1-sequencer-2-op-node"
voting = true
# Blank sequencer configuration for op-network-N with three blank sequencers
[sequencers.op-network-N-sequencer-0]
raft_addr = ""
conductor_rpc_url = ""
node_rpc_url = ""
voting = true
[sequencers.op-network-N-sequencer-1]
raft_addr = ""
conductor_rpc_url = ""
node_rpc_url = ""
voting = true
[sequencers.op-network-N-sequencer-2]
raft_addr = ""
conductor_rpc_url = ""
node_rpc_url = ""
voting = true

@ -0,0 +1,29 @@
import concurrent.futures
class Network:
def __init__(self, name, sequencers):
self.name = name
self.sequencers = sequencers
def update(self):
def _update(sequencer):
sequencer.update()
with concurrent.futures.ThreadPoolExecutor() as executor:
list(executor.map(_update, self.sequencers))
def get_sequencer_by_id(self, sequencer_id: str):
return next(
(
sequencer
for sequencer in self.sequencers
if sequencer.sequencer_id == sequencer_id
),
None,
)
def find_conductor_leader(self):
return next(
(sequencer for sequencer in self.sequencers if sequencer.conductor_leader),
None,
)

@ -0,0 +1,3 @@
#!/bin/sh
poetry run python op-conductor-ops.py "${@}"

@ -0,0 +1,326 @@
#!/usr/bin/env python
import os
import requests
from rich.console import Console
from rich.table import Table
import typer
from typing_extensions import Annotated
from config import read_config
from utils import make_rpc_payload, print_boolean, print_warn, print_error
app = typer.Typer(
help="CLI for managing OP Conductor sequencers. WARNING: This tool can cause a network outage if used improperly. Please consult #pod-devinfra before using."
)
console = Console()
@app.callback()
def load_config(
cert: Annotated[str, typer.Option(
"--cert",
help="[Optional] Certificate file path for https. Takes precedece over cert_path config",
envvar="CONDUCTOR_CERT",
)] = "",
config_path: Annotated[str, typer.Option(
"--config", "-c",
help="Path to config file.",
envvar="CONDUCTOR_CONFIG",
)] = "./config.toml",
):
networks, config_cert_path = read_config(config_path)
global NETWORKS
NETWORKS = networks
# Use the cert path from the command line if provided,
# otherwise use the one from the config
# Export the certificate for https connections
cert_path = cert or config_cert_path
if cert_path:
os.environ["REQUESTS_CA_BUNDLE"] = cert_path
os.environ["SSL_CERT_FILE"] = cert_path
def get_network(network: str):
if network not in NETWORKS:
typer.echo(f"Network must be one of {', '.join(NETWORKS.keys())}")
raise typer.Exit(code=1)
network_obj = NETWORKS[network]
network_obj.update()
return network_obj
@app.command()
def status(network: str):
"""Print the status of all sequencers in a network."""
network_obj = get_network(network)
sequencers = network_obj.sequencers
table = Table(
"Sequencer ID",
"Conductor Active",
"Sequencer Healthy",
"Conductor Leader",
"Active Sequencer",
"Unsafe Number",
"Unsafe Hash",
)
for sequencer in sequencers:
table.add_row(
sequencer.sequencer_id,
print_boolean(sequencer.conductor_active),
print_boolean(sequencer.sequencer_healthy),
print_boolean(sequencer.conductor_leader),
print_boolean(sequencer.sequencer_active),
str(sequencer.unsafe_l2_number),
str(sequencer.unsafe_l2_hash),
)
console.print(table)
leader = network_obj.find_conductor_leader()
if leader is None:
print_warn(f"Could not find current leader in network {network}")
else:
display_correction = False
membership = {x["id"]: x for x in leader.cluster_membership()}
for sequencer in sequencers:
if sequencer.sequencer_id in membership:
if (
int(not sequencer.voting)
!= membership[sequencer.sequencer_id]["suffrage"]
):
print_error(
f": {sequencer.sequencer_id} does not have the correct voting status.")
display_correction = True
else:
print_warn(
f": {sequencer.sequencer_id} is not in the cluster")
display_correction = True
if display_correction:
print_warn(
"Run 'update-cluster-membership' to correct membership issues")
@app.command()
def transfer_leader(network: str, sequencer_id: str):
"""Transfer leadership to a specific sequencer."""
network_obj = get_network(network)
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"Sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
if sequencer.voting is False:
print_error(f"Sequencer {sequencer_id} is not a voter")
raise typer.Exit(code=1)
healthy = sequencer.sequencer_healthy
if not healthy:
print_error(f"Target sequencer {sequencer_id} is not healthy")
raise typer.Exit(code=1)
leader = network_obj.find_conductor_leader()
if leader is None:
print_error(f"Could not find current leader in network {network}")
raise typer.Exit(code=1)
resp = requests.post(
leader.conductor_rpc_url,
json=make_rpc_payload(
"conductor_transferLeaderToServer",
params=[sequencer.sequencer_id, sequencer.raft_addr],
),
)
resp.raise_for_status()
if "error" in resp.json():
print_error(
f"Failed to transfer leader to {sequencer_id}: {resp.json()['error']}"
)
raise typer.Exit(code=1)
typer.echo(f"Successfully transferred leader to {sequencer_id}")
@app.command()
def pause(network: str, sequencer_id: str = None):
"""Pause all conductors.
If --sequencer-id is provided, only pause conductor for that sequencer.
"""
network_obj = get_network(network)
sequencers = network_obj.sequencers
if sequencer_id is not None:
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"Sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
sequencers = [sequencer]
error = False
for sequencer in sequencers:
resp = requests.post(
sequencer.conductor_rpc_url,
json=make_rpc_payload("conductor_pause"),
)
try:
resp.raise_for_status()
if "error" in resp.json():
raise Exception(resp.json()["error"])
typer.echo(f"Successfully paused {sequencer.sequencer_id}")
except Exception as e:
typer.echo(f"Failed to pause {sequencer.sequencer_id}: {e}")
if error:
raise typer.Exit(code=1)
@app.command()
def resume(network: str, sequencer_id: str = None):
"""Resume all conductors.
If --sequencer-id is provided, only resume conductor for that sequencer.
"""
network_obj = get_network(network)
sequencers = network_obj.sequencers
if sequencer_id is not None:
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
sequencers = [sequencer]
error = False
for sequencer in sequencers:
resp = requests.post(
sequencer.conductor_rpc_url,
json=make_rpc_payload("conductor_resume"),
)
try:
resp.raise_for_status()
if "error" in resp.json():
raise Exception(resp.json()["error"])
typer.echo(f"Successfully resumed {sequencer.sequencer_id}")
except Exception as e:
print_error(f"Failed to resume {sequencer.sequencer_id}: {e}")
if error:
raise typer.Exit(code=1)
@app.command()
def override_leader(network: str, sequencer_id: str):
"""Override the conductor_leader response for a sequencer to True.
Note that this does not affect consensus and it should only be used for disaster recovery purposes.
"""
network_obj = get_network(network)
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
resp = requests.post(
sequencer.conductor_rpc_url,
json=make_rpc_payload("conductor_overrideLeader"),
)
resp.raise_for_status()
if "error" in resp.json():
print_error(
f"Failed to override conductor leader status for {sequencer_id}: {resp.json()['error']}"
)
raise typer.Exit(code=1)
resp = requests.post(
sequencer.node_rpc_url,
json=make_rpc_payload("admin_overrideLeader"),
)
resp.raise_for_status()
if "error" in resp.json():
print_error(
f"Failed to override sequencer leader status for {sequencer_id}: {resp.json()['error']}"
)
raise typer.Exit(code=1)
typer.echo(f"Successfully overrode leader for {sequencer_id}")
@app.command()
def remove_server(network: str, sequencer_id: str):
"""Remove a sequencer from the cluster."""
network_obj = get_network(network)
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
leader = network_obj.find_conductor_leader()
resp = requests.post(
leader.conductor_rpc_url,
json=make_rpc_payload("conductor_removeServer",
params=[sequencer_id, 0]),
)
resp.raise_for_status()
if "error" in resp.json():
print_error(f"Failed to remove {sequencer_id}: {resp.json()['error']}")
raise typer.Exit(code=1)
typer.echo(f"Successfully removed {sequencer_id}")
@app.command()
def update_cluster_membership(network: str):
"""Update the cluster membership to match the sequencer configuration."""
network_obj = get_network(network)
sequencers = network_obj.sequencers
leader = network_obj.find_conductor_leader()
if leader is None:
print_error(f"Could not find current leader in network {network}")
raise typer.Exit(code=1)
membership = {x["id"]: x for x in leader.cluster_membership()}
error = False
for sequencer in sequencers:
if sequencer.sequencer_id in membership:
if (
int(not sequencer.voting)
!= membership[sequencer.sequencer_id]["suffrage"]
):
typer.echo(
f"Removing {sequencer.sequencer_id} from cluster to update voting status"
)
remove_server(network, sequencer.sequencer_id)
method = (
"conductor_addServerAsVoter"
if sequencer.voting
else "conductor_addServerAsNonvoter"
)
resp = requests.post(
leader.conductor_rpc_url,
json=make_rpc_payload(
method,
params=[sequencer.sequencer_id, sequencer.raft_addr, 0],
),
)
try:
resp.raise_for_status()
if "error" in resp.json():
raise Exception(resp.json()["error"])
typer.echo(
f"Successfully added {sequencer.sequencer_id} as {'voter' if sequencer.voting else 'non-voter'}"
)
except Exception as e:
print_warn(f"Failed to add {sequencer.sequencer_id} as voter: {e}")
if error:
raise typer.Exit(code=1)
if __name__ == "__main__":
app()

307
op-conductor-ops/poetry.lock generated Normal file

@ -0,0 +1,307 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "certifi"
version = "2024.6.2"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
{file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
]
[[package]]
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "idna"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "pygments"
version = "2.18.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "requests"
version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rich"
version = "13.7.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "shellingham"
version = "1.5.4"
description = "Tool to Detect Surrounding Shell"
optional = false
python-versions = ">=3.7"
files = [
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
[[package]]
name = "typer"
version = "0.12.3"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.7"
files = [
{file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"},
{file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"},
]
[package.dependencies]
click = ">=8.0.0"
rich = ">=10.11.0"
shellingham = ">=1.3.0"
typing-extensions = ">=3.7.4.3"
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "urllib3"
version = "2.2.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "b56021bf3dca52508e895f8746030fedc8d5d4a7b5e91cd9e25874ed1970f580"

@ -0,0 +1,17 @@
[tool.poetry]
name = "op-conductor-ops"
description = "CLI tool for managing op-conductor sequencer clusters"
authors = ["Zach Howard <zach@oplabs.co>"]
license = "MIT"
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
typer = "^0.12.3"
requests = "^2.31.0"
toml = "^0.10.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

@ -0,0 +1,105 @@
import concurrent.futures
import requests
import typer
from utils import make_rpc_payload
class Sequencer:
def __init__(
self, sequencer_id, raft_addr, conductor_rpc_url, node_rpc_url, voting
):
self.sequencer_id = sequencer_id
self.raft_addr = raft_addr
self.conductor_rpc_url = conductor_rpc_url
self.node_rpc_url = node_rpc_url
self.voting = voting
self.conductor_active = None
self.conductor_leader = None
self.sequencer_healthy = None
self.sequencer_active = None
self.unsafe_l2_hash = None
self.unsafe_l2_number = None
def _get_sequencer_active(self):
resp = requests.post(
self.node_rpc_url,
json=make_rpc_payload("admin_sequencerActive"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
self.sequencer_active = resp.json()["result"]
def _get_sequencer_healthy(self):
resp = requests.post(
self.conductor_rpc_url,
json=make_rpc_payload("conductor_sequencerHealthy"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
self.sequencer_healthy = resp.json()["result"]
def _get_conductor_active(self):
resp = requests.post(
self.conductor_rpc_url,
json=make_rpc_payload("conductor_active"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
self.conductor_active = resp.json()["result"]
def _get_conductor_leader(self):
resp = requests.post(
self.conductor_rpc_url,
json=make_rpc_payload("conductor_leader"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
self.conductor_leader = resp.json()["result"]
def _get_unsafe_l2(self):
resp = requests.post(
self.node_rpc_url,
json=make_rpc_payload("optimism_syncStatus"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
result = resp.json()["result"]
self.unsafe_l2_number = result["unsafe_l2"]["number"]
self.unsafe_l2_hash = result["unsafe_l2"]["hash"]
def cluster_membership(self):
resp = requests.post(
self.conductor_rpc_url,
json=make_rpc_payload("conductor_clusterMembership"),
)
resp.raise_for_status()
return resp.json()["result"]["servers"]
def update(self):
functions = [
self._get_conductor_active,
self._get_conductor_leader,
self._get_sequencer_healthy,
self._get_sequencer_active,
self._get_unsafe_l2,
]
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {executor.submit(func): func for func in functions}
for future in concurrent.futures.as_completed(futures):
func = futures[future]
try:
result = future.result()
except Exception as e:
typer.echo(f"{func.__name__} raised an exception: {e}")

26
op-conductor-ops/utils.py Normal file

@ -0,0 +1,26 @@
from rich import print
def print_boolean(value):
if value is None:
return ""
return "" if value else ""
def make_rpc_payload(method: str, params: list = None):
if params is None:
params = []
return {
"id": 1,
"jsonrpc": "2.0",
"method": method,
"params": params,
}
def print_error(msg: str):
print(f"[bold red]ERROR![/bold red] {msg}")
def print_warn(msg: str):
print(f"[bold yellow]WARNING![/bold yellow] {msg}")