From b83dc5519f3bb218e4e3443462b48ec27eda7230 Mon Sep 17 00:00:00 2001 From: Jeff Ohrstrom Date: Tue, 21 Mar 2023 15:13:26 -0400 Subject: [PATCH] Auto scripts (#2661) add the auto_scripts smart attribute and go ahead and add cluster config too This also fixed next_id not working right and form_item_id needs to be a symbol. --- apps/dashboard/app/lib/smart_attributes.rb | 1 + .../attributes/auto_scripts.rb | 51 +++++++++++++++ apps/dashboard/app/models/script.rb | 64 +++++++++++++++---- 3 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 apps/dashboard/app/lib/smart_attributes/attributes/auto_scripts.rb diff --git a/apps/dashboard/app/lib/smart_attributes.rb b/apps/dashboard/app/lib/smart_attributes.rb index e04c0e77d8..9f50d7f634 100644 --- a/apps/dashboard/app/lib/smart_attributes.rb +++ b/apps/dashboard/app/lib/smart_attributes.rb @@ -9,6 +9,7 @@ module SmartAttributes require "smart_attributes/attributes/auto_primary_group" require "smart_attributes/attributes/auto_queues" require "smart_attributes/attributes/auto_qos" + require "smart_attributes/attributes/auto_scripts" require "smart_attributes/attributes/bc_account" require "smart_attributes/attributes/bc_email_on_started" require "smart_attributes/attributes/bc_num_hours" diff --git a/apps/dashboard/app/lib/smart_attributes/attributes/auto_scripts.rb b/apps/dashboard/app/lib/smart_attributes/attributes/auto_scripts.rb new file mode 100644 index 0000000000..a1f2a05dbf --- /dev/null +++ b/apps/dashboard/app/lib/smart_attributes/attributes/auto_scripts.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module SmartAttributes + class AttributeFactory + + AUTO_SCRIPT_EXTENSIONS = ['sh', 'csh', 'bash', 'slurm', 'sbatch', 'qsub'].freeze + + # Build this attribute object. Must specify a valid directory in opts + # + # @param opts [Hash] attribute's options + # @return [Attributes::AutoScripts] the attribute object + def self.build_auto_scripts(opts = {}) + dir = Pathname.new(opts[:directory].to_s) + options = script_options_from_directory(dir) + + static_opts = { + options: options + }.merge(opts.without(:options).to_h) + + Attributes::AutoScripts.new('auto_scripts', static_opts) + end + + def self.script_options_from_directory(dir) + return [] unless dir.directory? && dir.readable? + + Dir.glob("#{dir}/*.{#{AUTO_SCRIPT_EXTENSIONS.join(',')}}").map do |file| + [File.basename(file), file] + end + end + end + + module Attributes + class AutoScripts < Attribute + def widget + 'select' + end + + def label(*) + (opts[:label] || 'Script').to_s + end + + # Submission hash describing how to submit this attribute + # @param fmt [String, nil] formatting of hash + # @return [Hash] submission hash + def submit(*) + content = File.read(value) + { script: { content: content } } + end + end + end +end diff --git a/apps/dashboard/app/models/script.rb b/apps/dashboard/app/models/script.rb index e653fda24d..c172d57f4d 100644 --- a/apps/dashboard/app/models/script.rb +++ b/apps/dashboard/app/models/script.rb @@ -3,12 +3,9 @@ class Script include ActiveModel::Model - attr_reader :title - - attr_reader :id + attr_reader :title, :id, :project_dir, :smart_attributes class << self - def scripts_dir(project_dir) @scripts_dir ||= Pathname.new("#{project_dir}/.ondemand/scripts").tap do |path| path.mkpath unless path.exist? @@ -41,11 +38,22 @@ def next_id(project_dir) all(project_dir) .map(&:id) .map(&:to_i) - .max || 0 + 1 + .prepend(0) + .max + 1 + end + + def batch_clusters + Rails.cache.fetch('script_batch_clusters', expires_in: 4.hours) do + Configuration.job_clusters.reject do |c| + reject_cluster?(c) + end.map(&:id).map(&:to_s) + end end - end - attr_reader :project_dir, :smart_attributes + def reject_cluster? + c.kubernetes? || c.linux_host? || c.systemd? + end + end def initialize(opts = {}) opts = opts.to_h.with_indifferent_access @@ -53,12 +61,19 @@ def initialize(opts = {}) @project_dir = opts[:project_dir] || raise(StandardError, 'You must set the project directory') @id = opts[:id] @title = opts[:title].to_s - @smart_attributes = build_smart_attributes(opts[:form] || []) + sm_opts = { + form: opts[:form] || [], + attributes: opts[:attributes] || {} + } + add_cluster_to_form(**sm_opts, clusters: Script.batch_clusters) + + @smart_attributes = build_smart_attributes(**sm_opts) end - def build_smart_attributes(form_list) - form_list.map do |form_item_id| - SmartAttributes::AttributeFactory.build(form_item_id, {}) + def build_smart_attributes(form: [], attributes: {}) + form.map do |form_item_id| + attrs = attributes[form_item_id.to_sym].to_h.symbolize_keys + SmartAttributes::AttributeFactory.build(form_item_id, attrs) end end @@ -73,7 +88,7 @@ def to_h end end end - alias_method :inspect, :to_h + alias inspect to_h # Delegate methods to smart_attributes' getter # @@ -112,4 +127,29 @@ def save end private + + def add_cluster_to_form(form: [], attributes: {}, clusters: []) + form.prepend('cluster') unless form.include?('cluster') + + attributes[:cluster] = if clusters.size > 1 + select_clusters(clusters) + else + fixed_cluster(clusters) + end + end + + def select_clusters(clusters) + { + widget: 'select', + label: 'Cluster', + options: clusters + } + end + + def fixed_cluster(clusters) + { + value: clusters.first.id.to_s, + fixed: true + } + end end