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

Added interface for connecting parachain nodes to relaychain nodes. #35

Merged
merged 14 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 4 additions & 0 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ provides:
interface: cos_agent
rpc-url:
interface: rpc-url

requires:
relay-rpc-url:
interface: rpc-url
jakobilobi marked this conversation as resolved.
Show resolved Hide resolved
30 changes: 17 additions & 13 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from interface_prometheus import PrometheusProvider
from interface_rpc_url_provider import RpcUrlProvider
from interface_rpc_url_requirer import RpcUrlRequirer
from polkadot_rpc_wrapper import PolkadotRpcWrapper
import utils
from service_args import ServiceArgs
Expand All @@ -40,6 +41,7 @@ def __init__(self, *args):
self.prometheus_node_provider = PrometheusProvider(self, 'node-prometheus', 9100, '/metrics')
self.prometheus_polkadot_provider = PrometheusProvider(self, 'polkadot-prometheus', 9615, '/metrics')
self.rpc_url_provider = RpcUrlProvider(self, 'rpc_url'),
self.rpc_url_requirer = RpcUrlRequirer(self, 'relay_rpc_url'),

self.cos_agent_provider = COSAgentProvider(
self,
Expand Down Expand Up @@ -70,11 +72,12 @@ def __init__(self, *args):

self._stored.set_default(binary_url=self.config.get('binary-url'),
docker_tag=self.config.get('docker-tag'),
service_args=self.config.get('service-args'))
service_args=self.config.get('service-args'),
relay_rpc_urls=dict())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Due to the data bug in departed hook we need a dict to for each url also store information which unit it is.


def _on_install(self, event: ops.InstallEvent) -> None:
self.unit.status = ops.MaintenanceStatus("Begin installing charm")
service_args_obj = ServiceArgs(self.config.get('service-args'))
service_args_obj = ServiceArgs(self.config.get('service-args'), self._stored.relay_rpc_urls)
# Setup polkadot group and user, disable login
utils.setup_group_and_user()
# Create environment file for polkadot service arguments
Expand All @@ -93,7 +96,7 @@ def _on_install(self, event: ops.InstallEvent) -> None:

def _on_config_changed(self, event: ops.ConfigChangedEvent) -> None:
try:
service_args_obj = ServiceArgs(self.config.get('service-args'))
service_args_obj = ServiceArgs(self.config.get('service-args'), self._stored.relay_rpc_urls)
except ValueError as e:
self.unit.status = ops.BlockedStatus(str(e))
event.defer()
Expand Down Expand Up @@ -124,7 +127,7 @@ def _on_update_status(self, event: ops.UpdateStatusEvent) -> None:

def update_status(self, connection_attempts: int = 4) -> None:
if utils.service_started():
rpc_port = ServiceArgs(self._stored.service_args).rpc_port
rpc_port = ServiceArgs(self._stored.service_args, self._stored.relay_rpc_urls).rpc_port
for i in range(connection_attempts):
time.sleep(5)
try:
Expand All @@ -143,7 +146,7 @@ def update_status(self, connection_attempts: int = 4) -> None:
break
except RequestsConnectionError as e:
logger.warning(e)
self.unit.status = ops.MaintenanceStatus("Client not responding to HTTP (attempt {}/{})".format(i, connection_attempts))
self.unit.status = ops.MaintenanceStatus("Client not responding to HTTP (attempt {}/{})".format(i+1, connection_attempts))
jakobilobi marked this conversation as resolved.
Show resolved Hide resolved
if type(self.unit.status) != ops.ActiveStatus:
self.unit.status = ops.WaitingStatus("Service running, client starting up")
else:
Expand All @@ -155,11 +158,11 @@ def _on_start(self, event: ops.StartEvent) -> None:

def _on_stop(self, event: ops.StopEvent) -> None:
utils.stop_service()
self.unit.status = ops.ActiveStatus("Service stopped")
self.update_status()
Maharacha marked this conversation as resolved.
Show resolved Hide resolved

def _on_get_session_key_action(self, event: ops.ActionEvent) -> None:
event.log("Getting new session key through rpc...")
rpc_port = ServiceArgs(self._stored.service_args).rpc_port
rpc_port = ServiceArgs(self._stored.service_args, self._stored.relay_rpc_urls).rpc_port
key = PolkadotRpcWrapper(rpc_port).get_session_key()
if key:
event.set_results(results={'session-key': key})
Expand All @@ -172,7 +175,7 @@ def _on_has_session_key_action(self, event: ops.ActionEvent) -> None:
if not re.match(keypattern, key):
event.fail("Illegal key pattern, did your key start with 0x ?")
else:
rpc_port = ServiceArgs(self._stored.service_args).rpc_port
rpc_port = ServiceArgs(self._stored.service_args, self._stored.relay_rpc_urls).rpc_port
has_session_key = PolkadotRpcWrapper(rpc_port).has_session_key(key)
event.set_results(results={'has-key': has_session_key})

Expand All @@ -183,32 +186,33 @@ def _on_insert_key_action(self, event: ops.ActionEvent) -> None:
if not re.match(keypattern, address):
event.fail("Illegal key pattern, did your public key/address start with 0x ?")
else:
rpc_port = ServiceArgs(self._stored.service_args).rpc_port
rpc_port = ServiceArgs(self._stored.service_args, self._stored.relay_rpc_urls).rpc_port
PolkadotRpcWrapper(rpc_port).insert_key(mnemonic, address)

def _on_restart_node_service_action(self, event: ops.ActionEvent) -> None:
utils.restart_service()
if not utils.service_started():
event.fail("Could not restart service")
self.unit.status = ops.ActiveStatus("Node service restarted")
self.update_status()
Maharacha marked this conversation as resolved.
Show resolved Hide resolved

def _on_start_node_service_action(self, event: ops.ActionEvent) -> None:
utils.start_service()
if not utils.service_started():
event.fail("Could not start service")
self.unit.status = ops.ActiveStatus("Node service started")
self.update_status()

def _on_stop_node_service_action(self, event: ops.ActionEvent) -> None:
utils.stop_service()
if utils.service_started(iterations=1):
event.fail("Could not stop service")
self.unit.status = ops.BlockedStatus("Node service stopped")
self.update_status()

def _on_set_node_key_action(self, event: ops.ActionEvent) -> None:
key = event.params['key']
utils.stop_service()
utils.write_node_key_file(key)
utils.start_service()
self.update_status()

def _on_find_validator_address_action(self, event: ops.ActionEvent) -> None:
event.log("Checking sessions key through rpc...")
Expand Down Expand Up @@ -279,7 +283,7 @@ def _on_get_node_info_action(self, event: ops.ActionEvent) -> None:
event.set_results(results={'node-relay': utils.get_relay_for_parachain()})
# On-chain info
try:
rpc_port = ServiceArgs(self._stored.service_args).rpc_port
rpc_port = ServiceArgs(self._stored.service_args, self._stored.relay_rpc_urls).rpc_port
block_height = PolkadotRpcWrapper(rpc_port).get_block_height()
if block_height:
event.set_results(results={'chain-block-height': block_height})
Expand Down
27 changes: 17 additions & 10 deletions src/interface_rpc_url_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from service_args import ServiceArgs
from ops.framework import Object
from ops.charm import RelationChangedEvent
from ops.charm import RelationJoinedEvent
import utils

class RpcUrlProvider(Object):
"""RPC url provider interface."""
Expand All @@ -17,17 +18,23 @@ def __init__(self, charm, relation_name):
charm.on[relation_name].relation_joined, self._on_relation_joined
)

def _on_relation_joined(self, event: RelationChangedEvent) -> None:
def _on_relation_joined(self, event: RelationJoinedEvent) -> None:
"""This event is used to send the ws or http rpc url to another client."""

service_args_obj = ServiceArgs(self._charm.config.get('service-args'))
ingress_address = event.relation.data.get(self.model.unit)['ingress-address']
if service_args_obj.ws_port:
url = f'ws://{ingress_address}:{service_args_obj.ws_port}'
elif service_args_obj.rpc_port:
url = f'http://{ingress_address}:{service_args_obj.rpc_port}'
else:
service_args_obj = ServiceArgs(self._charm.config.get('service-args'), "")

ws_port = service_args_obj.ws_port
rpc_port = service_args_obj.rpc_port
if not ws_port and not rpc_port:
event.defer()
return

event.relation.data[self.model.unit]['url'] = url
# In newer version of Polkadot the ws options are removed, and ws and http uses the same port specified by --rpc-port instead.
if "--ws-port" not in utils.get_client_binary_help_output():
ws_port = rpc_port
jakobilobi marked this conversation as resolved.
Show resolved Hide resolved

ingress_address = event.relation.data.get(self.model.unit)['ingress-address']
if rpc_port:
event.relation.data[self.model.unit]['rpc_url'] = f'http://{ingress_address}:{rpc_port}'
if ws_port:
event.relation.data[self.model.unit]['ws_url'] = f'ws://{ingress_address}:{ws_port}'
51 changes: 51 additions & 0 deletions src/interface_rpc_url_requirer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/python3

"""RPC url interface (requirers side)."""

from service_args import ServiceArgs
from ops.framework import Object
from ops.charm import RelationChangedEvent, RelationDepartedEvent
import utils

class RpcUrlRequirer(Object):
"""RPC url requirer interface."""

def __init__(self, charm, relation_name):
super().__init__(charm, relation_name)
self._charm = charm
self._relation_name = relation_name
self.framework.observe(
charm.on[relation_name].relation_changed, self._on_relation_changed
)
self.framework.observe(
charm.on[relation_name].relation_departed, self._on_relation_departed
)

def _on_relation_changed(self, event: RelationChangedEvent) -> None:
"""This event is used to receive the http rpc url from another client."""
Maharacha marked this conversation as resolved.
Show resolved Hide resolved

if not event.unit in event.relation.data:
event.defer()
return

# The --relay-chain-rpc-urls option currently only supports ws, hence using ws_url and not rpc_url.
try:
ws_url = event.relation.data[event.unit]["ws_url"]
except KeyError:
event.defer()
return
Copy link
Collaborator

Choose a reason for hiding this comment

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

What feedback from juju do you get when adding the relation if this happens?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure. Can try.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added some logging messages if it receives or can not receive an URL. Not sure how we can make the feedback better. I mean receiving the URL is not a guarantee that the connection actually works. Might be other stuff that makes the Parachain node not be able to use the Relaychain node. So then the question is should we make it clear for the operator that it is not able to use one of the Relaynodes in the list?

# Storing the unitname+relation_id is a workaround because the relation data is already removed before the departed hook is called.
# This is to know which url to remove.
# This "bug" (unclear if it's a bug or a removed feature) is reported to the Juju team.
dict_key = event.unit.name + ':' + str(event.relation.id)
self._charm._stored.relay_rpc_urls[dict_key] = ws_url
Maharacha marked this conversation as resolved.
Show resolved Hide resolved
service_args_obj = ServiceArgs(self._charm.config.get('service-args'), self._charm._stored.relay_rpc_urls)
utils.update_service_args(service_args_obj.service_args_string)
self._charm.update_status()

def _on_relation_departed(self, event: RelationDepartedEvent) -> None:
dict_key = event.unit.name + ':' + str(event.relation.id)
self._charm._stored.relay_rpc_urls.pop(dict_key)
service_args_obj = ServiceArgs(self._charm.config.get('service-args'), self._charm._stored.relay_rpc_urls)
utils.update_service_args(service_args_obj.service_args_string)
self._charm.update_status()
5 changes: 4 additions & 1 deletion src/service_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

class ServiceArgs():

def __init__(self, service_args: str):
def __init__(self, service_args: str, relay_rpc_urls: dict):
Copy link
Contributor Author

@Maharacha Maharacha Jan 11, 2024

Choose a reason for hiding this comment

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

I'm not very happy with this solution. But could not come up with something more beautiful but still easy. We might probably need to do some surgency to this class anyway when we bring in snap support.

service_args = self.__encode_for_emoji(service_args)
self._relay_rpc_urls = relay_rpc_urls
self.__check_service_args(service_args)
self.service_args_list = self.__service_args_to_list(service_args)
self.__check_service_args(self.service_args_list)
Expand Down Expand Up @@ -90,6 +91,8 @@ def __add_secondchain_args(self, args: list):

def __customize_service_args(self):
self.__add_firstchain_args(['--node-key-file', utils.NODE_KEY_PATH])
if self._relay_rpc_urls:
self.__add_firstchain_args(['--relay-chain-rpc-urls'] + list(self._relay_rpc_urls.values()))
jakobilobi marked this conversation as resolved.
Show resolved Hide resolved

if self.chain_name == 'peregrine':
self.__peregrine()
Expand Down