From 3d7816dd351f3a2c9242a71eba86f20929784985 Mon Sep 17 00:00:00 2001 From: MontrealSergiy Date: Tue, 11 Nov 2025 08:58:22 -0500 Subject: [PATCH 1/4] Add support for bindmounts in ToolConfig #1568 --- BrainPortal/app/models/cluster_task.rb | 18 ++++++++- BrainPortal/app/models/tool_config.rb | 38 ++++++++++++++++++- .../views/tool_configs/_form_fields.html.erb | 9 +++-- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/BrainPortal/app/models/cluster_task.rb b/BrainPortal/app/models/cluster_task.rb index d23df027d..ed6d80618 100644 --- a/BrainPortal/app/models/cluster_task.rb +++ b/BrainPortal/app/models/cluster_task.rb @@ -2357,6 +2357,7 @@ def singularity_commands(command_script) # (3) The root of the GridShare area (all tasks workdirs) gridshare_dir = self.bourreau.cms_shared_dir # not mounted explicitely + # (6) Ext3 capture mounts, if any. # These will look like "-B .capt_abcd.ext3:/path/workdir/abcd:image-src=/" # While we are building these options, we're also creating @@ -2382,6 +2383,16 @@ def singularity_commands(command_script) sing_opts end + # add tool config mounts, specified in 'overlay' + esc_local_dp_mountpoints += mountpoints.inject("") do |sing_opts,mount| + b_path, c_path = mount.split(":", 2) + sing_opts += " -B #{b_path.bash_escape}:#{c_path.bash_escape}:ro" + sing_opts + end + + + + # (5) Overlays defined in the ToolConfig # Some of them might be patterns (e.g. /a/b/data*.squashfs) that need to # be resolved locally. @@ -2507,7 +2518,7 @@ def singularity_commands(command_script) -B #{cache_dir.bash_escape} \\ -B #{cache_dir.bash_escape}:/DP_Cache \\ -B #{gridshare_dir.bash_escape} \\ - #{esc_local_dp_mountpoints} \\ + #{esc_local_dp_mountpoints} \\ #{overlay_mounts} \\ -B #{task_workdir.bash_escape}:#{effect_workdir.bash_escape} \\ #{esc_capture_mounts} \\ @@ -2566,6 +2577,11 @@ def ext3capture_basenames self.tool_config.ext3capture_basenames end + # Just invokes the same method on the task's ToolConfig. + def bindmounts + self.tool_config.bindmounts + end + # This method creates an empty +filename+ with +size+ bytes # (where size is specified like what the unix 'truncate' command accepts) # and then formats it with a ext3 filesystem. If the filename already exists, diff --git a/BrainPortal/app/models/tool_config.rb b/BrainPortal/app/models/tool_config.rb index 7a2574d55..6b69b690a 100644 --- a/BrainPortal/app/models/tool_config.rb +++ b/BrainPortal/app/models/tool_config.rb @@ -386,6 +386,8 @@ def cbrain_task_class # userfile:1234 # # A ext3 capture filesystem, will NOT be returned here as an overlay # ext3capture:basename=12G + # # A readonly bind mount, will NOT be returned here as an overlay + # bindmount:/local/basename:/container/basename def singularity_overlays_full_paths specs = parsed_overlay_specs specs.map do |knd, id_or_name| @@ -410,6 +412,8 @@ def singularity_overlays_full_paths { userfile.cache_full_path() => "registered userfile" } when 'ext3capture' [] # handled separately + when 'bindmount' + [] # hanlded separatery else cb_error "Invalid '#{knd}:#{id_or_name}' overlay." end @@ -428,6 +432,31 @@ def data_providers_with_overlays end.compact end + def additional_apptainer_mounts + specs = parsed_overlay_specs + return [] if specs.empty? + specs.map do |kind, id_or_name| + case kind + when 'dp' + dp = DataProvider.where_id_or_name(id_or_name).first + cb_error "Can't find DataProvider #{id_or_name} for fetching overlays" if ! dp + dp.additional_apptainer_mounts rescue nil + when 'file' + cb_error "Provide absolute path for overlay file '#{id_or_name}'." if (Pathname.new id_or_name).relative? + + end + end + end + + def bindmounts + specs = parsed_overlay_specs + return [] if specs.empty? + specs + .map { |pair| pair[1] if pair[0] == 'bindmount' } + .compact + .map { |basename_basename | basename_and_basename.split(":") } + end + # Returns pairs [ [ basename, size], ...] as in [ [ 'work', '28g' ] def ext3capture_basenames specs = parsed_overlay_specs @@ -486,8 +515,9 @@ def parsed_overlay_specs end # Verifies that the admin has entered a set of - # overlay specifications properly. One or several of: + # overlay and bindmount specifications properly. One or several of: # + # #### the overlay rules # file:/full/path/to/something.squashfs # file:/full/path/to/pattern*/data?.squashfs # userfile:333 @@ -497,6 +527,8 @@ def parsed_overlay_specs # ext3capture:work=30G # ext3capture:tool_1.1.2=15M # + # bindmount:/bourreau/path/to:/container/path/to # bindmount rules ( additionally introduced ) + # def validate_overlays_specs #:nodoc: specs = parsed_overlay_specs @@ -542,6 +574,10 @@ def validate_overlays_specs #:nodoc: self.errors.add(:singularity_overlays_specs, "contains invalid ext3capture specification (must be like ext3capture:basename=1g or 2m etc)") end + when 'bindmount' # this is for binding to container rather than overlays + if id_or_name !~ /\A\/\w[\w\.-\/]+:\/\w[\w\.-\/]+/ + self.errors.add(:singularity_overlays_specs, "contains invalid bindmount specification (must be like bindmount:/path/in/bourreau:/path/in/container) and free from special characters") + end else # Other errors self.errors.add(:singularity_overlays_specs, "contains invalid specification '#{kind}:#{id_or_name}'") diff --git a/BrainPortal/app/views/tool_configs/_form_fields.html.erb b/BrainPortal/app/views/tool_configs/_form_fields.html.erb index e3cd5b392..a03f65fb4 100644 --- a/BrainPortal/app/views/tool_configs/_form_fields.html.erb +++ b/BrainPortal/app/views/tool_configs/_form_fields.html.erb @@ -222,16 +222,19 @@ <% t.edit_cell(:singularity_overlays_specs, :header => "Singularity Overlays", :show_width => 2, :content => full_description(@tool_config.singularity_overlays_specs)) do |f| %> <%= f.text_area :singularity_overlays_specs, :rows => 6, :cols => 120 %>
- This field can contain one or several specifications for data overlays + This field can contain one or several specifications for data overlays and bindmounts to be included when the task is started with Singularity. - A specification can be either + An overlay specification can be either a full path (e.g. file:/a/b/data.squashfs), a path with a pattern (e.g. file:/a/b/data*.squashfs), a registered file identified by ID (e.g. userfile:123), a SquashFS Data Provider identified by its ID or name (e.g. dp:123, dp:DpNameHere) or an ext3 capture overlay basename (e.g. ext3capture:basename=SIZE where size is 12G or 12M). In the case of a Data Provider, the overlays will be the files that the provider uses. - Each overlay specification should be on a separate line. + Bindmount specification is like + bindmount:/bourreau/path/to/data:/containerized/path/to/data + + Each overlay or bind specification should be on a separate line. You can add comments, indicated with hash symbol #. For example, file:/a/b/atlas.squashfs # brain atlas
From bdb172394c49e0ada6c4ed0ca594833c6363ee85 Mon Sep 17 00:00:00 2001 From: Pierre Rioux Date: Thu, 13 Nov 2025 15:18:03 -0500 Subject: [PATCH 2/4] Adding bindmount configuration capability in TCs Work based on Sergiy's draft. Fixed bugs here and there, adjusted documentation, removed spurious changes. --- .../app/controllers/sessions_controller.rb | 2 +- BrainPortal/app/models/cluster_task.rb | 24 ++++++------ BrainPortal/app/models/tool_config.rb | 38 +++++++------------ .../views/tool_configs/_form_fields.html.erb | 34 +++++++++++------ 4 files changed, 47 insertions(+), 51 deletions(-) diff --git a/BrainPortal/app/controllers/sessions_controller.rb b/BrainPortal/app/controllers/sessions_controller.rb index 989a4ff8e..faf32c0d9 100644 --- a/BrainPortal/app/controllers/sessions_controller.rb +++ b/BrainPortal/app/controllers/sessions_controller.rb @@ -42,7 +42,7 @@ def new #:nodoc: # HEAD requests render nothing req_method = request.method.to_s.upcase if req_method == 'HEAD' - render :plain => "", :status => :ok + head :ok return end diff --git a/BrainPortal/app/models/cluster_task.rb b/BrainPortal/app/models/cluster_task.rb index ed6d80618..15bbfe447 100644 --- a/BrainPortal/app/models/cluster_task.rb +++ b/BrainPortal/app/models/cluster_task.rb @@ -2357,7 +2357,6 @@ def singularity_commands(command_script) # (3) The root of the GridShare area (all tasks workdirs) gridshare_dir = self.bourreau.cms_shared_dir # not mounted explicitely - # (6) Ext3 capture mounts, if any. # These will look like "-B .capt_abcd.ext3:/path/workdir/abcd:image-src=/" # While we are building these options, we're also creating @@ -2374,7 +2373,7 @@ def singularity_commands(command_script) # must be on a device different from the one for the work directory. capture_basenames = ext3capture_basenames.map { |basename,_| basename } - # (4) More -B (bind mounts) for all the relevant local data providers. + # (4a) More -B (bind mounts) for all the relevant local data providers. # This will be a string "-B path1 -B path2 -B path3" etc. # In the case of read-only input files, ro option is added esc_local_dp_mountpoints = local_dp_storage_paths.inject("") do |sing_opts,path| @@ -2383,16 +2382,12 @@ def singularity_commands(command_script) sing_opts end - # add tool config mounts, specified in 'overlay' - esc_local_dp_mountpoints += mountpoints.inject("") do |sing_opts,mount| - b_path, c_path = mount.split(":", 2) - sing_opts += " -B #{b_path.bash_escape}:#{c_path.bash_escape}:ro" + # (4b) Add tool config bindmounts, specified in 'overlay' + esc_local_bindmounts = bindmount_paths.inject("") do |sing_opts,(from_path,cont_path)| + sing_opts += " -B #{from_path.bash_escape}:#{cont_path.bash_escape}" sing_opts end - - - # (5) Overlays defined in the ToolConfig # Some of them might be patterns (e.g. /a/b/data*.squashfs) that need to # be resolved locally. @@ -2508,7 +2503,8 @@ def singularity_commands(command_script) # a) at its original cluster full path # b) at /DP_Cache (used only when shortening workdir) # 3) we mount the root of the gridshare area (for all tasks) -# 4) we mount each (if any) of the root directories for local data providers +# 4a) we mount each (if any) of the root directories for local data providers +# 4b) we mount each (if any) of the bindmounts configured in the ToolConfig # 5) we mount (if any) other fixed file system overlays # 6) we mount (if any) capture ext3 filesystems # 7) with -H we set the task's work directory as the singularity $HOME directory @@ -2518,7 +2514,8 @@ def singularity_commands(command_script) -B #{cache_dir.bash_escape} \\ -B #{cache_dir.bash_escape}:/DP_Cache \\ -B #{gridshare_dir.bash_escape} \\ - #{esc_local_dp_mountpoints} \\ + #{esc_local_dp_mountpoints} \\ + #{esc_local_bindmounts} \\ #{overlay_mounts} \\ -B #{task_workdir.bash_escape}:#{effect_workdir.bash_escape} \\ #{esc_capture_mounts} \\ @@ -2578,8 +2575,9 @@ def ext3capture_basenames end # Just invokes the same method on the task's ToolConfig. - def bindmounts - self.tool_config.bindmounts + # Returns an array of pairs, e.g. [ [ src, dest ], [ src, dest ] ] + def bindmount_paths + self.tool_config.bindmount_paths end # This method creates an empty +filename+ with +size+ bytes diff --git a/BrainPortal/app/models/tool_config.rb b/BrainPortal/app/models/tool_config.rb index 6b69b690a..9e70ec51e 100644 --- a/BrainPortal/app/models/tool_config.rb +++ b/BrainPortal/app/models/tool_config.rb @@ -384,9 +384,9 @@ def cbrain_task_class # dp:1234 # # CBRAIN db registered file # userfile:1234 - # # A ext3 capture filesystem, will NOT be returned here as an overlay + # # A ext3 capture filesystem, will NOT be returned here as an overlay (see method ext3capture_basenames() instead) # ext3capture:basename=12G - # # A readonly bind mount, will NOT be returned here as an overlay + # # A bind mount, will NOT be returned here as an overlay (see method bindmount_paths() instead) # bindmount:/local/basename:/container/basename def singularity_overlays_full_paths specs = parsed_overlay_specs @@ -413,7 +413,7 @@ def singularity_overlays_full_paths when 'ext3capture' [] # handled separately when 'bindmount' - [] # hanlded separatery + [] # handled separately else cb_error "Invalid '#{knd}:#{id_or_name}' overlay." end @@ -421,7 +421,7 @@ def singularity_overlays_full_paths end # Returns an array of the data providers that are - # specified in the attribute singularity_overlays_specs, + # specified in the attribute +singularity_overlays_specs+, # ignoring all other overlay specs for normal files. def data_providers_with_overlays return @_data_providers_with_overlays_ if @_data_providers_with_overlays_ @@ -432,29 +432,16 @@ def data_providers_with_overlays end.compact end - def additional_apptainer_mounts - specs = parsed_overlay_specs - return [] if specs.empty? - specs.map do |kind, id_or_name| - case kind - when 'dp' - dp = DataProvider.where_id_or_name(id_or_name).first - cb_error "Can't find DataProvider #{id_or_name} for fetching overlays" if ! dp - dp.additional_apptainer_mounts rescue nil - when 'file' - cb_error "Provide absolute path for overlay file '#{id_or_name}'." if (Pathname.new id_or_name).relative? - - end - end - end - - def bindmounts + # Returns an array of pairs extracted from the attribute + # +singularity_overlays_specs+ , ignoring all other overlay + # specs for normal files. + def bindmount_paths specs = parsed_overlay_specs return [] if specs.empty? specs .map { |pair| pair[1] if pair[0] == 'bindmount' } .compact - .map { |basename_basename | basename_and_basename.split(":") } + .map { |frompath_contpath| frompath_contpath.split(":",2) } end # Returns pairs [ [ basename, size], ...] as in [ [ 'work', '28g' ] @@ -504,7 +491,8 @@ def validate_container_rules #:nodoc: return errors.empty? end - # breaks down overlay spec onto a list of overlays + # Breaks down the singularity_overlays_specs attribute, and returns a list of pairs [ type, value ] e.g. + # [ [ 'userfile', '1234' ], [ 'file', '/hello/bye/data.sqs' ], [ 'bindmount', '/some/data/dir:/mount/this/here' ] ] def parsed_overlay_specs specs = self.singularity_overlays_specs return [] if specs.blank? @@ -575,8 +563,8 @@ def validate_overlays_specs #:nodoc: end when 'bindmount' # this is for binding to container rather than overlays - if id_or_name !~ /\A\/\w[\w\.-\/]+:\/\w[\w\.-\/]+/ - self.errors.add(:singularity_overlays_specs, "contains invalid bindmount specification (must be like bindmount:/path/in/bourreau:/path/in/container) and free from special characters") + if id_or_name !~ /\A\/\w[\w\.\-\/]+:\/\w[\w\.\-\/]+(:ro)?\z/ + self.errors.add(:singularity_overlays_specs, "contains invalid bindmount specification (must be like 'bindmount:/path/in/bourreau:/path/in/container' with or without a final ':ro') and free from special characters") end else # Other errors diff --git a/BrainPortal/app/views/tool_configs/_form_fields.html.erb b/BrainPortal/app/views/tool_configs/_form_fields.html.erb index a03f65fb4..4125aace1 100644 --- a/BrainPortal/app/views/tool_configs/_form_fields.html.erb +++ b/BrainPortal/app/views/tool_configs/_form_fields.html.erb @@ -224,18 +224,28 @@
This field can contain one or several specifications for data overlays and bindmounts to be included when the task is started with Singularity. - An overlay specification can be either - a full path (e.g. file:/a/b/data.squashfs), - a path with a pattern (e.g. file:/a/b/data*.squashfs), - a registered file identified by ID (e.g. userfile:123), - a SquashFS Data Provider identified by its ID or name (e.g. dp:123, dp:DpNameHere) - or an ext3 capture overlay basename (e.g. ext3capture:basename=SIZE where size is 12G or 12M). - In the case of a Data Provider, the overlays will be the files that the provider uses. - Bindmount specification is like - bindmount:/bourreau/path/to/data:/containerized/path/to/data - - Each overlay or bind specification should be on a separate line. - You can add comments, indicated with hash symbol #. +

+ Each overlay or bindmount specification should be on a separate line. +

+ An overlay specification can be either: +

+

    +
  • a full path (e.g. file:/a/b/data.squashfs),
  • +
  • a path with a pattern (e.g. file:/a/b/data*.squashfs),
  • +
  • a registered file identified by ID (e.g. userfile:123),
  • +
  • a SquashFS Data Provider identified by its ID or name (e.g. dp:123, dp:DpNameHere)
  • +
  • or an ext3 capture overlay basename (e.g. ext3capture:basename=SIZE where size is 12G or 12M).
  • +
+

+ In the case of a Data Provider, the overlays will be the SquashFS files that the provider uses for its storage. The provider of course must be local to the current execution server. +

+ A bindmount specification is one of: +

    +
  • bindmount:/bourreau/path/to/data:/containerized/path/to/data or
  • +
  • bindmount:/bourreau/path/to/data:/containerized/path/to/data:ro
  • +
+

+ You can add comments, indicated with a hash symbol #. For example, file:/a/b/atlas.squashfs # brain atlas

<% end %> From 06e7046015fc1b20c562e2913e49fe7d443df0e4 Mon Sep 17 00:00:00 2001 From: Pierre Rioux Date: Thu, 13 Nov 2025 16:41:20 -0500 Subject: [PATCH 3/4] Bindmount of plugins folder "container_mnt". If a plugins contain a folder named "container_mnt", and a boutiques descriptor has an entry in its custom field called "cbrain:plugins-container-bindmount" with a path as a value, then the plugins folder will be bindmounted into the apptainer container under the path specified in the descriptor. Implements issue #1569 on GitHub. --- BrainPortal/app/models/tool_config.rb | 26 ++++++++++++++++--- .../bourreaux/_bourreaux_display.html.erb | 4 +-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/BrainPortal/app/models/tool_config.rb b/BrainPortal/app/models/tool_config.rb index 9e70ec51e..fe304b620 100644 --- a/BrainPortal/app/models/tool_config.rb +++ b/BrainPortal/app/models/tool_config.rb @@ -436,12 +436,32 @@ def data_providers_with_overlays # +singularity_overlays_specs+ , ignoring all other overlay # specs for normal files. def bindmount_paths - specs = parsed_overlay_specs - return [] if specs.empty? - specs + specs = parsed_overlay_specs.presence || [] + + # Pairs of paths obtained from the ToolConfig's "overlay" configuration. + tc_paths = specs .map { |pair| pair[1] if pair[0] == 'bindmount' } .compact .map { |frompath_contpath| frompath_contpath.split(":",2) } + + # One additional bindmount path from the plugins directory where the tool + # comes from. Available ONLY if tool is configured with + # a boutiques descriptor. The following lines of code make + # a bunch of checks and as soon as a check fails, we just + # return with the array tc_paths computed above. + descriptor = self.boutiques_descriptor rescue nil + return tc_paths if descriptor.blank? + container_mountpoint = descriptor.custom["cbrain:plugins-container-bindmount"] + return tc_paths if container_mountpoint.blank? + desc_file = descriptor.from_file # "/path/to/RailsApp/cbrain-plugins/installed_plugins/boutiques_descriptors/toolname.json" + return tc_paths if desc_file.blank? + real_path = File.realpath(desc_file) # "/path/to/RailsApp/cbrain-plugins/plugin-name/boutiques_descriptors/toolname.json" + parent1 = Pathname.new(real_path).parent # "/path/to/RailsApp/cbrain-plugins/plugin-name/boutiques_descriptors" + return tc_paths if parent1.basename.to_s != "boutiques_descriptors" # check plugins convention + plugin_path = parent1.parent # "/path/to/RailsApp/cbrain-plugins/plugin-name" + plugin_containerized_dir = plugin_path + "container_mnt" # special plugins folder to mount + tc_paths << [ plugin_containerized_dir.to_s, container_mountpoint + ":ro" ] + return tc_paths end # Returns pairs [ [ basename, size], ...] as in [ [ 'work', '28g' ] diff --git a/BrainPortal/app/views/bourreaux/_bourreaux_display.html.erb b/BrainPortal/app/views/bourreaux/_bourreaux_display.html.erb index 34a4a3315..8d1724896 100644 --- a/BrainPortal/app/views/bourreaux/_bourreaux_display.html.erb +++ b/BrainPortal/app/views/bourreaux/_bourreaux_display.html.erb @@ -407,7 +407,7 @@ Stopping an Execution Server will also stop the Task Workers and Activity Workers running on it. Make sure they are not actively <%= html_colorize("processing","orange") %> something.

- <%= external_submit_button "2. Stop Bourreaux", "bourreau_form", + <%= external_submit_button "2. Stop Execution Server", "bourreau_form", :class => 'button', :name => 'operation', :value => 'stop_bourreaux', @@ -421,7 +421,7 @@ :class => 'button', :name => 'operation', :value => 'stop_tunnels', - :data => { :confirm => "Make sure Bourreaux, Task Workers and Activity workers are all stopped!" } + :data => { :confirm => "Make sure Execution Servers, Task Workers and Activity workers are all stopped!" } %> <% end %> From c7a24ece7bc1e1595ea831d735fce19326019710 Mon Sep 17 00:00:00 2001 From: Pierre Rioux Date: Fri, 14 Nov 2025 08:44:25 -0500 Subject: [PATCH 4/4] Added one more check. --- BrainPortal/app/models/tool_config.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/BrainPortal/app/models/tool_config.rb b/BrainPortal/app/models/tool_config.rb index fe304b620..cbe630459 100644 --- a/BrainPortal/app/models/tool_config.rb +++ b/BrainPortal/app/models/tool_config.rb @@ -460,6 +460,7 @@ def bindmount_paths return tc_paths if parent1.basename.to_s != "boutiques_descriptors" # check plugins convention plugin_path = parent1.parent # "/path/to/RailsApp/cbrain-plugins/plugin-name" plugin_containerized_dir = plugin_path + "container_mnt" # special plugins folder to mount + return tc_paths if ! File.directory?(plugin_containerized_dir.to_s) tc_paths << [ plugin_containerized_dir.to_s, container_mountpoint + ":ro" ] return tc_paths end