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

Add options extensions to ros2launch and extensibility to the node action #216

Merged
merged 23 commits into from
Apr 5, 2021

Conversation

gbiggs
Copy link
Member

@gbiggs gbiggs commented Jan 26, 2021

This PR is a response to #180. In that PR, a request was made that extensibility be added to the launch system and the --secure option be added as an extension. This PR provides the extensibility support. The security extension is provided in a PR on a new repository.

It adds:

  • The ability to load extensions that provide additional options to the ros2 launch command. For example, an extension can add an option --secure. Aside from getting the chance to add as many options as desired to the command line flags, extensions are given the chance to execute code prior to the LaunchService starting, prior to the launch being executed, and after the launch process completes (i.e. on shutdown?).
  • The ability to load extensions that add additional command line flags to the command lines that launch nodes, and execute code just before the node is launched. This is a short-term solution to the need for extensibility. In the long term we plan to refactor all of launch to be more amenable to extensibility.

For an example, see this PR which adds a security extension.

Once both PRs are accepted, the ros2launch_security repository will need to be transferred to the security WG.

@gbiggs gbiggs changed the title WIP: Add options extensions to ros2launch and extensibility to the node action Add options extensions to ros2launch and extensibility to the node action Feb 2, 2021
@gbiggs
Copy link
Member Author

gbiggs commented Feb 2, 2021

Ready for review.

@kyrofa
Copy link
Member

kyrofa commented Feb 2, 2021

@gbiggs thank you for doing this. I will leave a code review to more qualified people, but I'll say we do believe this covers the functionality we would need for what we're doing.

@gbiggs
Copy link
Member Author

gbiggs commented Feb 2, 2021

@kyrofa Let us know who you want to set as maintainer, and if the security WG wants to take the plugins one.

@kyrofa
Copy link
Member

kyrofa commented Feb 2, 2021

@gbiggs I can take it and add the necessary people if that works.

@gbiggs
Copy link
Member Author

gbiggs commented Feb 7, 2021

Great, thanks! I've added you as maintainer to the new package.

Copy link

@Arnatious Arnatious left a comment

Choose a reason for hiding this comment

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

Looking great, only some minor nitpicks.

@@ -14,8 +14,10 @@

"""Module for the Node action."""

import importlib_metadata

Choose a reason for hiding this comment

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

Third party package, should be in the same block as yaml

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in ded9e1d

import os
import pathlib
from packaging.version import Version

Choose a reason for hiding this comment

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

Third party package, should be in the same block as yaml

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in ded9e1d

)
launch_description = result[0]
if len(result) > 1:
tails[name] = result[1:]

Choose a reason for hiding this comment

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

Is this variable used solely for debugging? It isn't ever read from.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it was. Fixed in 22d14af

@ros-discourse
Copy link

This pull request has been mentioned on ROS Discourse. There might be relevant details there:

https://discourse.ros.org/t/ros-2-tsc-meeting-minutes-2021-02-18/19102/1

Copy link
Contributor

@hidmic hidmic left a comment

Choose a reason for hiding this comment

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

First pass!

* `NAME` (will be set to the entry point name)

The following methods may be defined:
* `add_arguments` to add arguments to the argparse parser
Copy link
Contributor

Choose a reason for hiding this comment

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

@gbiggs nit: consider moving documentation to each method, and only listing which ones are required here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in 1e18575

@@ -145,6 +150,7 @@ def main(self, *, parser, args):
else:
raise RuntimeError('unexpected mode')
launch_arguments.extend(args.launch_arguments)

Copy link
Contributor

Choose a reason for hiding this comment

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

@gbiggs unrelated?

Copy link
Member Author

Choose a reason for hiding this comment

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

This vanished when I rebased.

def prelaunch(self, launch_description, options, args):
return (launch_description,)

def postlaunch(self, launch_return_code, options, args):
Copy link
Contributor

Choose a reason for hiding this comment

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

@gbiggs hmm, what is this options argument for? BTW it doesn't seem like prelaunch and postlaunch are provided with it in any call site.

Copy link
Member Author

Choose a reason for hiding this comment

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

It looks like it was vestigial of an earlier iteration of the changes. I've removed the arguments in 40ee929.

* `NAME` (will be set to the entry point name)

The following methods may be defined:
* `command_extension` to extend the command used to launch the node's
Copy link
Contributor

Choose a reason for hiding this comment

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

@gbiggs nit: consider moving documentation to each method, and only listing which ones are required here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in 6754e9a.

@@ -408,6 +453,10 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]:
ros_specific_arguments['name'] = '__node:={}'.format(self.__expanded_node_name)
if self.__expanded_node_namespace != '':
ros_specific_arguments['ns'] = '__ns:={}'.format(self.__expanded_node_namespace)
node_info = NodeActionExtension.NodeInfo(self.__package, self.__node_executable, self.node_name)
for extension in self.__extensions.values():
ros_specific_arguments = extension.execute(context, ros_specific_arguments, node_info)
Copy link
Contributor

Choose a reason for hiding this comment

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

@gbiggs why not passing the node action itself instead of this ad-hoc data structure?

Copy link
Member Author

Choose a reason for hiding this comment

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

It seemed cleaner to pass in a minimal data structure rather than have extensions need to know about the entire Node action class and its internal variables.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. I will point out that this data in particular (e.g. the package name) could be exposed via public API. You wouldn't have to access internal implementations details from the extension in that case.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added public API to expose the package and executable, and removed the node_info structure in 2d9f3d1.

@@ -342,6 +383,10 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
raise
self.__final_node_name = prefix_namespace(
self.__expanded_node_namespace, self.__expanded_node_name)

for extension in self.__extensions.values():
self.cmd.extend(extension.command_extension(context))
Copy link
Contributor

Choose a reason for hiding this comment

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

@gbiggs why a method to specifically extend the command? Isn't it possible to do so (even resolving substitutions) upon extension execution?

Copy link
Member Author

Choose a reason for hiding this comment

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

I can't remember specifically, but I suspect I was following the existing pattern of the Node class, which splits up building the command and executing it. Possibly it does things that way for debug output?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, neither NodeExtension.command_extension nor NodeExtension.execute deal with execution. Both configure the command line, in direct and indirect ways respectively.

Copy link
Contributor

Choose a reason for hiding this comment

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

SecurityNodeActionExtension.execute could fetch an enclave and extend the command line w/o the extra step.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not just fetching an enclave, it's doing stuff that has side effects. In the case of the security extension, it's actually creating and using files on disc. There's also a need to add an option to the ros2 launch command line for the user to use, which is why there is the separate command_extension method. The function documentation for that was wrong; I've fixed it in c2b2b7f.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I'm a bit confused. From what I can see in this patch, NodeActionExtension.command_extension is not used by ros2 launch directly, but within Node._perform_substitutions to extend Node.cmd. In which case, my point is that both NodeActionExtension.command and NodeActionExtension.pre_execute happen on Action.execute. There's no configuration stage in between. An indirection through context locals would make sense if there was a context global to override (or not).

Anyhow, I won't block on this. But we do need that documentation sorted out.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, I was the one confused. Sorry about that. I was getting it mixed up with the ros2 launch command line flag adding, which is handled by the options extension.

I looked over the code and I agree that it is possible to combine the two functions. I can make an argument against combining them that the current way follows the structure of the Node action in that it partitions preparing the command line from preparing ROS-specific arguments and executing the command, but it's a weak argument. So I went ahead and changed it to match your suggestion in da09c67.

@@ -432,3 +481,86 @@ def expanded_node_namespace(self):
def expanded_remapping_rules(self):
"""Getter for expanded_remappings."""
return self.__expanded_remappings

def _get_extensions(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

@gbiggs nit: consider making this a free function or staticmethod (better not, too much support logic inside the Node class) that returns the list of extensions.

Copy link
Member Author

Choose a reason for hiding this comment

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

This function accesses the node's logger to output an error message that is specific to the function's internal implementation. I could refactor that out into returning an optional error message, but I'm wary of going to far for something that's meant to be temporary. How strongly do you feel about this?

Copy link
Contributor

Choose a reason for hiding this comment

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

How strongly do you feel about this?

It's a bit too much unrelated code. Also, as far as I can see the logger is fixed for the entire module. It should be straightforward to fetch it again if need be.

for something that's meant to be temporary.

Until we get around to replace it, it is permanent.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've made the two functions free in 0d9cb67.

Copy link
Member Author

Choose a reason for hiding this comment

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

Until we get around to replace it, it is permanent

Fair point! :)

"supports '%s'" % (extension_point_version, extension_version))


def get_upper_bound_caret_version(version):
Copy link
Contributor

Choose a reason for hiding this comment

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

@gbiggs consider moving these functions to another file.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in 6754e9a.

@hidmic
Copy link
Contributor

hidmic commented Mar 15, 2021

Also, this patch needs conflicts resolved.

Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
@gbiggs
Copy link
Member Author

gbiggs commented Mar 17, 2021

Also, this patch needs conflicts resolved.

Rebased to fix this. I'll start addressing your other comments now.

Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
@gbiggs
Copy link
Member Author

gbiggs commented Mar 18, 2021

@hidmic Thanks for the comments. This is ready for another pass.

Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Copy link
Contributor

@hidmic hidmic left a comment

Choose a reason for hiding this comment

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

Overall LGTM but for a few comments.

Once we settle those we need CI.

@@ -432,3 +481,86 @@ def expanded_node_namespace(self):
def expanded_remapping_rules(self):
"""Getter for expanded_remappings."""
return self.__expanded_remappings

def _get_extensions(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

How strongly do you feel about this?

It's a bit too much unrelated code. Also, as far as I can see the logger is fixed for the entire module. It should be straightforward to fetch it again if need be.

for something that's meant to be temporary.

Until we get around to replace it, it is permanent.

@@ -408,6 +453,10 @@ def execute(self, context: LaunchContext) -> Optional[List[Action]]:
ros_specific_arguments['name'] = '__node:={}'.format(self.__expanded_node_name)
if self.__expanded_node_namespace != '':
ros_specific_arguments['ns'] = '__ns:={}'.format(self.__expanded_node_namespace)
node_info = NodeActionExtension.NodeInfo(self.__package, self.__node_executable, self.node_name)
for extension in self.__extensions.values():
ros_specific_arguments = extension.execute(context, ros_specific_arguments, node_info)
Copy link
Contributor

Choose a reason for hiding this comment

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

I see. I will point out that this data in particular (e.g. the package name) could be exposed via public API. You wouldn't have to access internal implementations details from the extension in that case.

@@ -342,6 +383,10 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
raise
self.__final_node_name = prefix_namespace(
self.__expanded_node_namespace, self.__expanded_node_name)

for extension in self.__extensions.values():
self.cmd.extend(extension.command_extension(context))
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, neither NodeExtension.command_extension nor NodeExtension.execute deal with execution. Both configure the command line, in direct and indirect ways respectively.

Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
@@ -342,6 +383,10 @@ def _perform_substitutions(self, context: LaunchContext) -> None:
raise
self.__final_node_name = prefix_namespace(
self.__expanded_node_namespace, self.__expanded_node_name)

for extension in self.__extensions.values():
self.cmd.extend(extension.command_extension(context))
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I'm a bit confused. From what I can see in this patch, NodeActionExtension.command_extension is not used by ros2 launch directly, but within Node._perform_substitutions to extend Node.cmd. In which case, my point is that both NodeActionExtension.command and NodeActionExtension.pre_execute happen on Action.execute. There's no configuration stage in between. An indirection through context locals would make sense if there was a context global to override (or not).

Anyhow, I won't block on this. But we do need that documentation sorted out.

Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
Copy link
Contributor

@hidmic hidmic left a comment

Choose a reason for hiding this comment

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

CI up to launch_ros, test_launch_ros, and ros2launch:

  • Linux Build Status
  • Linux-aarch64 Build Status
  • macOS Build Status
  • Windows Build Status

@hidmic
Copy link
Contributor

hidmic commented Mar 30, 2021

@gbiggs flake8 isn't happy, mind to check?

Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
@gbiggs
Copy link
Member Author

gbiggs commented Apr 1, 2021

@hidmic I fixed the two flake8 errors I could see in the output.

Signed-off-by: Chris Lalancette <clalancette@openrobotics.org>
@clalancette
Copy link
Contributor

I fixed a few more flake8 fixes that I saw locally (and in CI).

However, I am still seeing this warning:

launch_ros/actions/test_extension.py:18
  /home/ubuntu/ros2_ws/src/ros2/launch_ros/launch_ros/launch_ros/actions/test_extension.py:18: PytestCollectionWarning: cannot collect test class 'TestNodeActionExtension' because it has a __init__ constructor (from: launch_ros/actions/test_extension.py)
    class TestNodeActionExtension(NodeActionExtension):

I think what is happening here is that pytest is getting confused because the class starts with the name Test. Looking around, I don't see this class or file being used anywhere. Is it?

@Arnatious
Copy link

I think what is happening here is that pytest is getting confused because the class starts with the name Test. Looking around, I don't see this class or file being used anywhere. Is it?

For reference setting the class atrtibute __test__ = False will squelch that error

@gbiggs
Copy link
Member Author

gbiggs commented Apr 1, 2021

Sorry about that. That's a class I was using to do some quick tests. I thought I had deleted it, turns out I had committed it instead by mistake. I've pushed the removal of them.

Signed-off-by: Geoffrey Biggs <gbiggs@killbots.net>
@clalancette
Copy link
Contributor

All right, tests are green locally. Let's run them in CI again and see what happens:

  • Linux Build Status
  • Linux-aarch64 Build Status
  • macOS Build Status
  • Windows Build Status

@clalancette clalancette added this to In progress in Galactic via automation Apr 5, 2021
@clalancette
Copy link
Contributor

All right, this is approved by @hidmic, and CI is clean (other than the Windows failure, but that is unrelated to this PR). I'm going to go ahead and merge this, thanks for all of the work @gbiggs , and thanks for the reviews @hidmic.

@clalancette clalancette merged commit cedfedf into ros2:master Apr 5, 2021
@clalancette clalancette moved this from In progress to Needs Release in Galactic Apr 5, 2021
@hidmic
Copy link
Contributor

hidmic commented Apr 5, 2021

@clalancette shall we run a combined CI with osrf/ros2launch_security#1?

@clalancette
Copy link
Contributor

Good idea, I'll go ahead and do that over on that pull request.

@clalancette clalancette moved this from Needs Release to Done in Galactic May 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Galactic
  
Done
Development

Successfully merging this pull request may close these issues.

None yet

6 participants