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