-
Notifications
You must be signed in to change notification settings - Fork 912
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
Reckless #5647
Reckless #5647
Conversation
A simple standalone python executable to track plugin repositories, clone to /tmp, install requirements, test plugin runs, then install and enable in lightningd and in the config. Changelog-Added: Reckless - a Core Lightning plugin manager
The user should be informed that their config now has a new source, but but any config files created downstream should be automatically populated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @endothermicdev, this is a great first iteration of reckless
. Since this is the first python built-in tool destined for the end-user I went through it with a fine-toothed comb, so don't worry about the many (optional) comments.
Let me know if I can help, or some of the comments aren't clear 🤗
tools/reckless
Outdated
|
||
if __name__ == '__main__': | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument(dest='command', help='install/uninstall/search/enable' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to customize the error when the command is missing? It'd be very useful for the error to spell out the available commands so we don't have to guess that help
will get us that list.
tools/reckless
Outdated
if cmd[0] == 'add': | ||
for src in cmd[1:]: | ||
add_source(src) | ||
elif cmd[0] == 'remove' or cmd[0] == 'rem' or cmd[0] == 'rm': | ||
for src in cmd[1:]: | ||
remove_source(src) | ||
elif cmd[0] == 'list': | ||
list_source() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add a help
one? I had to look at the source to find out what the options are. Subparsers are your friend here :-)
def verbose(*args): | ||
if not IS_VERBOSE: | ||
return | ||
print(*args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Python has quite the powerful logging
library at your disposal: https://docs.python.org/3/library/logging.html
tools/reckless
Outdated
def lightning_cli_available(): | ||
clncli = Popen(['lightning-cli'], stdout=PIPE, stderr=PIPE) | ||
clncli.wait(timeout=1) | ||
if clncli.returncode == 0: | ||
return True | ||
else: | ||
return False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also check if we have an executable on the $PATH
:
from distutils.spawn import find_executable
find_executable('lightning-cli')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, this is so much nicer!
This enables compatibility with startup_regtest.sh among other uses. The lightning-cli network flag is also set in case there is no config file.
Regtest environments commonly use explicit definition of the config file for lightningd. This can be queried and utilized by default, saving redundant definitions between lightning and reckless.
A more pythonic approach which should also enable additional help context for subcommands.
`reckless help <cmd>` previously called the function docstring. This could be updated to use the subparser help, but would require a strict naming convention or a dictionary. Providing a hint to use the built-in contextual help via the option flag is hopefully sufficient.
More pythonic than returning mixed types.
The goal was to support passing a list to install, enable, etc. in order to improve performance. Passing lists to most of the functions was less practical than iterating through the items from the top level.
This change makes it easier to follow retrieval of parent directories. Additional os.path operations replaced with their pathlib.Path equivalents to keep module usage consistent.
This also simplifies dynamic enable/disable by catching the exception raised when the cli is unable to connect to RPC (lightningd offline or misconfigured relative to reckless).
While loading the appropriate lightningconfig file, it is now checked against the active config file in lightningd. Because a deviation from the default file structure would not be possible, a -conf option is also added to explicitly pass the lightningd config file into reckless.
b89911a
to
1e8ea76
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the fixups @endothermicdev, I only found a couple of cleanups that could also be done in a separate cleanup PR. I think the user-visible interface looks good, and we can clean up under the covers.
Which brings us to the question of whether you're happy to merge this as is, and I'd add it to the milestone for the release, or whether you'd like to hold it back, and we can include it in two months. WDYT?
@@ -82,14 +82,14 @@ class InstInfo: | |||
|
|||
def create_dir(r: int, directory: str) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could make directory
a Path
instance, so you avoid converting it multiple times here. But that's more of a cleanup for later and not a blocker here.
@@ -196,14 +196,14 @@ class RecklessConfig(Config): | |||
def __init__(self, path: Union[str, None] = None, | |||
default_text: Union[str, None] = None): | |||
if path is None: | |||
path = os.path.join(LIGHTNING_DIR, 'reckless', | |||
'bitcoin-reckless.conf') | |||
path = Path(LIGHTNING_DIR).joinpath('reckless', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works also as Path(LIGHTNING_DIR) / 'reckless' / 'bitcoin-reckless.conf'
since the Path
library overrides __div__
giving it a custom interpretation of concatenation. Makes it look very unix-y :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and the right hand side can be a Path
or str
allowing you to avoid overly verbose type conversions too :-)
clncli = Popen(cmd, stdout=PIPE, stderr=PIPE) | ||
clncli.wait(timeout=timeout) | ||
out = clncli.stdout.read().decode() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could replace these 3 lines with subprocess.check_output()
which does the stdout
handling by itself, and returns the output as bytes
directly. Though since you have the code anyway, it might not be worth it.
See comment below regarding the handling of errors. check_output
is likely not worth it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing that might be worthwhile is to set the text
argument to have subprocess
decode automatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the use of check_output
: that would raise an Exception, potentially making the differentiation of error types harder further down. So you most likely don't want to change that.
tools/reckless
Outdated
cmd = LIGHTNING_CLI_CALL.copy() | ||
cmd.extend(['plugin', 'start', path]) | ||
clncli = Popen(cmd, stdout=PIPE) | ||
clncli.wait(timeout=3) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worthwhile encapsulating the RPC into a separate class, or rely on pyln-client
to interact with lightningd
in a pythonic way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And of course this is in direct contradiction with my earlier comment 🤦 Feel free to ignore this 😉
tools/reckless
Outdated
RECKLESS_CONFIG.enable_plugin(path) | ||
print('{} enabled'.format(plugin_name)) | ||
else: | ||
err = eval(clncli.stdout.read().decode().replace('\n', ''))['message'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uff, how did I miss eval
here? That opens you up to arbitrary code executions should a plugin be malicious it might be able to use this as a way to escalate privileges from an unprivileged user running the node, to a user with more privileges that is managing the node through reckless
. Good think you removed it here.
Reckless was failing to install multiple plugins due to git not appreciating the cwd being a now removed dir after the first plugin tmp files were cleaned up.
37d64ca
to
6725934
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good fixes, but I think we probably want to lose that last commit.
tools/reckless
Outdated
os.chdir(plugin_path) | ||
assert os.getcwd() == str(plugin_path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is likely unexpected for the user since they'll be dropped into a temporary directory if anything goes wrong. You can use the cwd
argument to Popen
to have it change the directory in the subprocess before exec'ing the executable, thus leaving the parent process in its current working directory unchanged.
Let's do that in a cleanup PR, since this is just a minor fix.
tools/Makefile
Outdated
tools/pip-install: | ||
@if which $(PIP_EXE) >> /dev/null; then \ | ||
echo "installing python dependencies"; \ | ||
$(PIP_EXE) $(PIP_ARGS) install -r tools/requirements.txt ; \ | ||
else \ | ||
(echo "Pip not installed, skipping python dependencies") ; \ | ||
fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I see where this comes from, but it most likely isn't what we want:
- Developer dependencies are tracked in
pyproject.toml
- There is currently no user dependencies (other than libraries from the distro), so we need something here. On the other hand the users will not be the ones to run
Makefile
s, so we better look for some better option. Alternatively we could just useurllib3
and not have dependencies at all.
Potentially reckless could be a completely separate tool that we distribute on PyPI where it can have its own dependencies, and users would just be a pip install reckless
away from being able to run it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm certainly open to a better option for installing python dependencies for the user (or at least prompting a pip install
.) I think we'll want that at some point. Let's just switch to urllib3
for the time being? Distributing reckless via PyPI is fine, but I would still prefer it to be updated and working by default with the latest CLN release if possible.
dedf801
to
6725934
Compare
reckless
A Core-Lightning plugin manager
Features
Love Core-Lightning? Want to install and test a CLN plugin with only three words and a command line? Reckless has you covered!
Commands
reckless install <plugin>
searches available repositories, if one matches, clones it, installs dependencies, and tests that the plugin executes and exits normally. If so, the plugin is permanently installed, added to the CLN config, and dynamically activated.reckless uninstall <plugin>
disables the plugin, removes the directory.reckless search <plugin>
looks through all available sources for a plugin matching this name.reckless enable <plugin>
/reckless disable <plugin>
dynamically enables/disables the reckless-installed plugin and updates the config to match.reckless source list
list all plugin repositories.reckless source add <repo url>
add another plugin repository for reckless to search or install from.Let's Get Reckless!
Running the first time will prompt the user that their lightningd's bitcoin config will be appended (or created) to inherit the reckless config file (this config is specific to bitcoin by default.) Management of plugins will subsequently modify this file.
Start with a simple installation such as
reckless install summary
Troubleshooting tips
Plugins must be executable. For python plugins, the shebang is invoked, so
python3
should be available in your environment. This can be verified withwhich Python3
. The default reckless directory is /home//.lightning/reckless and it should be possible for the lightningd user to execute files located here. If this is a problem, the option flagreckless -d=<my_alternate_dir>
may be used to relocate the reckless directory from its default. Consider creating a permanent alias in this case.For Developers
Tips for making your plugin compatible with reckless install:
pip install -e .
- Poetry looks for additional files in the working directory, whereas with pip, any references to these will require something likepackages = [{ include = "*.py" }]
under the[tool.poetry]
section.reckless source add https://my.repo.url/here
however, https://github.com/lightningd/plugins is included by default. Consider adding your plugin lightningd/plugins to make installation simpler.To Do
This is the minimum viable product of the utility outlined by Rusty.
Still to come: