diff --git a/app/assets/javascripts/autograder.js b/app/assets/javascripts/autograder.js
new file mode 100644
index 000000000..951be1ac1
--- /dev/null
+++ b/app/assets/javascripts/autograder.js
@@ -0,0 +1,42 @@
+;
+(function() {
+ $(document).ready(function() {
+ function access_key_callback() {
+ const checked = $(this).prop('checked');
+ const $access_key_field = $('#autograder_access_key');
+ const $access_key_id_field = $('#autograder_access_key_id');
+ $access_key_field.prop('disabled', !checked);
+ $access_key_id_field.prop('disabled', !checked);
+ if (!checked) {
+ $access_key_field.val('', checked);
+ $access_key_id_field.val('', checked);
+ }
+ }
+
+ $('#autograder_use_access_key').on('change', access_key_callback);
+ access_key_callback.call($('#autograder_use_access_key'));
+
+ function initializeEC2Dropdown() {
+ if ($.fn.tooltip) {
+ $('.browser-default[data-tooltip]').tooltip({
+ enterDelay: 300,
+ exitDelay: 200,
+ position: 'top'
+ });
+ }
+
+ $('#autograder_instance_type option').hover(
+ function() { $(this).addClass('highlighted-option'); },
+ function() { $(this).removeClass('highlighted-option'); }
+ );
+
+ $('#autograder_instance_type').on('change.ec2-instance', function() {
+ const selectedInstance = $(this).val();
+ console.log('Selected EC2 instance type:', selectedInstance);
+ });
+ }
+
+ // Initialize the EC2 dropdown functionality
+ initializeEC2Dropdown();
+ });
+})();
\ No newline at end of file
diff --git a/app/controllers/autograders_controller.rb b/app/controllers/autograders_controller.rb
index a71787f3f..30c7b1a73 100755
--- a/app/controllers/autograders_controller.rb
+++ b/app/controllers/autograders_controller.rb
@@ -16,6 +16,9 @@ def create
a.autograde_timeout = 180
a.autograde_image = "autograding_image"
a.release_score = true
+ a.access_key_id = ""
+ a.access_key = ""
+ a.instance_type = "t2.micro"
end
if @autograder.save
flash[:success] = "Autograder created."
@@ -38,7 +41,14 @@ def edit
action_auth_level :update, :instructor
def update
- if @autograder.update(autograder_params) && @assessment.update(assessment_params)
+ # Clear secrets if use_access_key is disabled
+ params_to_update = autograder_params
+ unless params_to_update[:use_access_key]
+ params_to_update[:access_key] = nil
+ params_to_update[:access_key_id] = nil
+ end
+
+ if @autograder.update(params_to_update) && @assessment.update(assessment_params)
flash[:success] = "Autograder saved."
begin
upload
@@ -112,7 +122,10 @@ def set_autograder
end
def autograder_params
- params[:autograder].permit(:autograde_timeout, :autograde_image, :release_score)
+ params.require(:autograder).permit(
+ :autograde_timeout, :autograde_image, :release_score,
+ :use_access_key, :access_key, :access_key_id, :instance_type
+ )
end
def assessment_params
diff --git a/app/controllers/scoreboards_controller.rb b/app/controllers/scoreboards_controller.rb
index c760a62f4..d037510de 100755
--- a/app/controllers/scoreboards_controller.rb
+++ b/app/controllers/scoreboards_controller.rb
@@ -270,13 +270,13 @@ def createScoreboardEntry(scores, autoresult)
!@scoreboard ||
!@scoreboard.colspec ||
@scoreboard.colspec.blank?
-
+
# First we need to get the total score
total = 0.0
@assessment.problems.each do |problem|
total += scores[problem.name].to_f
end
-
+
# Now build the array of scores
entry = []
entry << total.round(1).to_s
@@ -285,32 +285,57 @@ def createScoreboardEntry(scores, autoresult)
end
return entry
end
-
+
# At this point we have an autograded assessment with a
# customized scoreboard. Extract the scoreboard entry
# from the scoreboard array object in the JSON autoresult.
-
- parsed = ActiveSupport::JSON.decode(autoresult)
-
- # ensure that the parsed result is a hash with scoreboard field, where scoreboard is an array
- if !parsed || !parsed.is_a?(Hash) || !parsed["scoreboard"] ||
- !parsed["scoreboard"].is_a?(Array)
- # If there is no autoresult for this student (typically
- # because their code did not compile or it segfaulted and
- # the instructor's autograder did not catch it) then
- # raise an error, will be handled by caller
- if @cud.instructor?
- (flash.now[:error] = "Error parsing scoreboard for autograded assessment: " \
- "scoreboard result is not an array. Please ensure that the autograder returns " \
- "scoreboard results as an array.")
- end
- Rails.logger.error("Scoreboard error in #{@course.name}/#{@assessment.name}: " \
+
+ begin
+ parsed = ActiveSupport::JSON.decode(autoresult)
+
+ # ensure that the parsed result is a hash with scoreboard field, where scoreboard is an array
+ if !parsed || !parsed.is_a?(Hash) || !parsed["scoreboard"] ||
+ !parsed["scoreboard"].is_a?(Array)
+ # If there is no autoresult for this student (typically
+ # because their code did not compile or it segfaulted and
+ # the instructor's autograder did not catch it) then
+ # raise an error, will be handled by caller
+ if @cud.instructor?
+ (flash.now[:error] = "Error parsing scoreboard for autograded assessment: " \
+ "scoreboard result is not an array. Please ensure that the autograder returns " \
+ "scoreboard results as an array.")
+ end
+ Rails.logger.error("Scoreboard error in #{@course.name}/#{@assessment.name}: " \
"Scoreboard result is not an array")
- raise StandardError
+ raise StandardError
+ end
+
+ # ADDED: Ensure the scoreboard entries align with colspec expectations
+ if @scoreboard && !@scoreboard.colspec.blank?
+ begin
+ colspec_parsed = ActiveSupport::JSON.decode(@scoreboard.colspec)
+ if colspec_parsed && colspec_parsed["scoreboard"] && colspec_parsed["scoreboard"].is_a?(Array)
+ expected_length = colspec_parsed["scoreboard"].length
+ # Ensure the entry has enough elements, pad with nil if needed
+ if parsed["scoreboard"].length < expected_length
+ Rails.logger.warn("Scoreboard entry too short (#{parsed["scoreboard"].length}) for user, expected #{expected_length}")
+ # Pad the array to match expected length
+ parsed["scoreboard"] = parsed["scoreboard"].fill(nil, parsed["scoreboard"].length, expected_length - parsed["scoreboard"].length)
+ end
+ end
+ rescue => e
+ Rails.logger.error("Error validating scoreboard entry against colspec: #{e.message}")
+ end
+ end
+
+ return parsed["scoreboard"]
+ rescue => e
+ # Log the error and re-raise to be handled by the caller
+ Rails.logger.error("Error parsing scoreboard JSON: #{e.message}")
+ raise
end
-
- parsed["scoreboard"]
end
+
#
# scoreboardOrderSubmissions - This function provides a "<=>"
diff --git a/app/helpers/assessment_autograde_core.rb b/app/helpers/assessment_autograde_core.rb
index 41c100ac8..5541594ac 100644
--- a/app/helpers/assessment_autograde_core.rb
+++ b/app/helpers/assessment_autograde_core.rb
@@ -157,16 +157,31 @@ def get_job_status(job_id)
# Returns the Tango response
#
def tango_add_job(course, assessment, upload_file_list, callback_url, job_name, output_file)
- job_properties = { "image" => @autograde_prop.autograde_image,
- "files" => upload_file_list.map do |f|
- { "localFile" => f["remoteFile"],
- "destFile" => Pathname.new(f["destFile"]).basename.to_s }
- end,
- "output_file" => output_file,
- "timeout" => @autograde_prop.autograde_timeout,
- "callback_url" => callback_url,
- "jobName" => job_name,
- "disable_network" => assessment.disable_network }.to_json
+ job_properties = {
+ "image" => @autograde_prop.autograde_image,
+ "files" => upload_file_list.map do |f|
+ { "localFile" => f["remoteFile"],
+ "destFile" => Pathname.new(f["destFile"]).basename.to_s }
+ end,
+ "output_file" => output_file,
+ "timeout" => @autograde_prop.autograde_timeout,
+ "callback_url" => callback_url,
+ "jobName" => job_name,
+ "disable_network" => assessment.disable_network
+ }
+
+ if Rails.configuration.x.ec2_ssh
+ job_properties["ec2Vmms"] = true
+ job_properties["instanceType"] = @autograde_prop.instance_type.presence || "t3.micro"
+
+ if @autograde_prop.use_access_key?
+ job_properties["accessKey"] = @autograde_prop.access_key
+ job_properties["accessKeyId"] = @autograde_prop.access_key_id
+ end
+ end
+
+ job_properties = job_properties.to_json
+
begin
response = TangoClient.addjob("#{course.name}-#{assessment.name}", job_properties)
rescue TangoClient::TangoException => e
diff --git a/app/models/autograder.rb b/app/models/autograder.rb
index 36e65d9ce..204e95e9a 100755
--- a/app/models/autograder.rb
+++ b/app/models/autograder.rb
@@ -5,14 +5,26 @@
class Autograder < ApplicationRecord
belongs_to :assessment
+ # Encryption for AWS credentials
+ has_encrypted :access_key
+ has_encrypted :access_key_id
+
trim_field :autograde_image
- # extremely short timeout values cause the backend to throw system errors
+ # Validations
validates :autograde_timeout,
numericality: { greater_than_or_equal_to: 10, less_than_or_equal_to: 900 }
validates :autograde_image, :autograde_timeout, presence: true
validates :autograde_image, length: { maximum: 64 }
+ with_options if: :use_access_key do
+ validates :access_key_id, presence: true, format: {
+ with: /\A[A-Z0-9]{16,24}\z/,
+ message: "looks invalid"
+ }
+ validates :access_key, presence: true
+ end
+
after_commit -> { assessment.dump_yaml }
SERIALIZABLE = Set.new %w[autograde_image autograde_timeout release_score]
diff --git a/app/views/autograders/_basic_settings.html.erb b/app/views/autograders/_basic_settings.html.erb
new file mode 100644
index 000000000..0ee1ac71a
--- /dev/null
+++ b/app/views/autograders/_basic_settings.html.erb
@@ -0,0 +1,36 @@
+<%= f.text_field :autograde_image, display_name: "VM Image",
+ help_text: "VM image for autograding (e.g. rhel.img). #{link_to 'Click here', tango_status_course_jobs_path} to view the list of VM images and pools currently being used".html_safe +
+ (Rails.configuration.x.docker_image_upload_enabled.presence ? ", or #{link_to 'click here', course_dockers_path} to upload a new docker image.".html_safe : "."),
+ required: true, maxlength: 64 %>
+
+<%= f.number_field :autograde_timeout, display_name: "Timeout",
+ help_text: "Timeout for autograding jobs (secs). Must be between 10s and 900s, inclusive.", min: 10, max: 900 %>
+<%= f.check_box :release_score,
+ display_name: "Release Scores?",
+ help_text: "Check to release autograded scores to students immediately after autograding (strongly recommended)." %>
+
+<% help_tar_text = "Tar file exists, upload a file to override." %>
+<% help_makefile_text = "Makefile exists, upload a file to override." %>
+<%= f.file_field :makefile, label_text: "Autograder Makefile", action: :upload, file_exists_text: help_makefile_text, class: "form-file-field", file_exists: @makefile_exists %>
+<%= f.file_field :tar, label_text: "Autograder Tar", action: :upload, file_exists_text: help_tar_text, class: "form-file-field", file_exists: @tar_exists %>
+
+ Both of the above files will be renamed upon upload.
+
+
+<%= f.fields_for :assessment, @assessment do |af| %>
+ <%= af.check_box :disable_network, help_text: "Disable network access for autograding containers." %>
+<% end %>
+
+<%= f.submit "Save Settings" %>
+
+<%= link_to "Delete Autograder", course_assessment_autograder_path(@course, @assessment),
+ method: :delete, class: "btn danger",
+ data: { confirm: "Are you sure you want to delete the Autograder for this assesssment?" } %>
+
+<% unless @makefile_exists.nil? %>
+ <%= link_to "Download Makefile", download_file_course_assessment_autograder_path(file_path: @makefile_exists, file_key: 'makefile'), class: "btn" %>
+<% end %>
+
+<% unless @tar_exists.nil? %>
+ <%= link_to "Download Tar", download_file_course_assessment_autograder_path(file_path: @tar_exists, file_key: 'tar'), class: "btn" %>
+<% end %>
diff --git a/app/views/autograders/_ec2_settings.html.erb b/app/views/autograders/_ec2_settings.html.erb
new file mode 100755
index 000000000..a651a40bf
--- /dev/null
+++ b/app/views/autograders/_ec2_settings.html.erb
@@ -0,0 +1,100 @@
+<% content_for :javascripts do %>
+ <%= javascript_include_tag "autograder" %>
+<% end %>
+
+EC2 Settings
+<%= f.check_box :use_access_key,
+ display_name: "Enable Access Key",
+ help_text: "(Optional) Use your own provided access key to authenticate to different EC2 instances than the default one on Tango" %>
+<%= f.password_field :access_key,
+ display_name: "Access Key",
+ value: nil,
+ autocomplete: "new-password",
+ placeholder: "Leave blank to keep existing secret" %>
+<%= f.text_field :access_key_id,
+ display_name: "Access Key ID",
+ value: nil,
+ autocomplete: "off",
+ placeholder: "Leave blank to keep existing secret" %>
+
+<%
+ # Group EC2 instance options by category for better organization
+ ec2_instance_options = [
+ # General Purpose - T2 (burstable)
+ ['T2 - General Purpose (Burstable)', [
+ ['t2.nano - 1 vCPU, 0.5 GiB RAM (minimal, lowest cost)', 't2.nano'],
+ ['t2.micro - 1 vCPU, 1 GiB RAM (lowest cost, suitable for small jobs)', 't2.micro'],
+ ['t2.small - 1 vCPU, 2 GiB RAM (low cost, better for memory-intensive tasks)', 't2.small'],
+ ['t2.medium - 2 vCPU, 4 GiB RAM (balanced, good for most autograding tasks)', 't2.medium'],
+ ['t2.large - 2 vCPU, 8 GiB RAM (better for memory-heavy workloads)', 't2.large'],
+ ['t2.xlarge - 4 vCPU, 16 GiB RAM (good for parallel processing)', 't2.xlarge'],
+ ['t2.2xlarge - 8 vCPU, 32 GiB RAM (high performance, burstable)', 't2.2xlarge']
+ ]],
+ # General Purpose - T3 (newer generation)
+ ['T3 - General Purpose (Newer Generation)', [
+ ['t3.micro - 2 vCPU, 1 GiB RAM (better performance than t2.micro)', 't3.micro'],
+ ['t3.small - 2 vCPU, 2 GiB RAM (improved over t2.small)', 't3.small'],
+ ['t3.medium - 2 vCPU, 4 GiB RAM (better CPU performance than t2)', 't3.medium'],
+ ['t3.large - 2 vCPU, 8 GiB RAM (improved networking)', 't3.large']
+ ]],
+ # Compute Optimized
+ ['C - Compute Optimized', [
+ ['c5.large - 2 vCPU, 4 GiB RAM (compute-optimized, faster CPU)', 'c5.large'],
+ ['c5.xlarge - 4 vCPU, 8 GiB RAM (high compute performance)', 'c5.xlarge'],
+ ['c5.2xlarge - 8 vCPU, 16 GiB RAM (very high compute performance)', 'c5.2xlarge']
+ ]],
+ # Memory Optimized
+ ['R - Memory Optimized', [
+ ['r5.large - 2 vCPU, 16 GiB RAM (memory-optimized, for large datasets)', 'r5.large'],
+ ['r5.xlarge - 4 vCPU, 32 GiB RAM (high memory capacity)', 'r5.xlarge'],
+ ['r5.2xlarge - 8 vCPU, 64 GiB RAM (very high memory capacity)', 'r5.2xlarge']
+ ]]
+ ]
+%>
+
+<%
+ selected_instance_type = @autograder.instance_type
+ valid_instance_types = ec2_instance_options.flat_map { |_group, instances| instances.map(&:last) }
+ if selected_instance_type.present? && !valid_instance_types.include?(selected_instance_type)
+ raise ArgumentError, "Invalid EC2 instance type: #{selected_instance_type}"
+ end
+%>
+
+EC2 Instance Type
+
+<%= f.select :instance_type,
+ grouped_options_for_select(ec2_instance_options, @autograder.instance_type),
+ { include_blank: false, label: "EC2 Instance Type" },
+ { class: 'browser-default',
+ data: { tooltip: "Select an EC2 instance type based on your autograding needs. Larger instances cost more but run faster." },
+ display_name: "EC2 Instance Type" } %>
+
+ Choose an instance type based on your autograding requirements:
+
+ - T2 instances: Burstable performance instances, good for varying workloads
+
+ - t2.micro: Default, lowest cost, suitable for simple autograding
+ - t2.medium/large: Better for more complex tests with moderate requirements
+
+
+ - T3 instances: Newer generation with better baseline performance than T2
+ - C5 (Compute-optimized): Best for CPU-bound tasks like compilation, testing algorithms
+ - R5 (Memory-optimized): Best for memory-intensive operations with large datasets
+
+
+
Recommendations:
+
+ - Start with t2.micro for basic assignments with minimal resource requirements
+ - Use t2.medium or t3.medium for most standard programming assignments
+ - Choose c5.xlarge for computationally intensive tasks (e.g., compiler projects)
+ - Select r5.large for memory-intensive workloads (e.g., large datasets, ML)
+
+
+
Note: Larger instances incur higher AWS costs. View EC2 pricing
+
+
+<%= f.submit "Save Settings" %>
+
+<%= link_to "Delete Autograder", course_assessment_autograder_path(@course, @assessment),
+ method: :delete, class: "btn danger",
+ data: { confirm: "Are you sure you want to delete the Autograder for this assesssment?" } %>
diff --git a/app/views/autograders/_form.html.erb b/app/views/autograders/_form.html.erb
index 1a7c985e1..07e2f1e3d 100755
--- a/app/views/autograders/_form.html.erb
+++ b/app/views/autograders/_form.html.erb
@@ -1,40 +1,34 @@
-<%= form_for @autograder, url: course_assessment_autograder_path(@course, @assessment, @autograder),
- builder: FormBuilderWithDateTimeInput,
- html: { multipart: true } do |f| %>
- <%= f.text_field :autograde_image, display_name: "VM Image",
- help_text: "VM image for autograding (e.g. rhel.img). #{link_to 'Click here', tango_status_course_jobs_path} to view the list of VM images and pools currently being used".html_safe +
- (Rails.configuration.x.docker_image_upload_enabled.presence ? ", or #{link_to 'click here', course_dockers_path} to upload a new docker image.".html_safe : "."),
- required: true, maxlength: 64 %>
+
+
+
+ <% list = ["basic"] %>
+ <% if Rails.configuration.x.ec2_ssh.presence %>
+ <% list.append("ec2") %>
+ <% end %>
+ <% list.each do |tab_name| %>
+ <%= if tab_name == params[:active_tab]
+ tag.li(link_to(tab_name.titleize, "#tab_#{tab_name}"), class: "active tab")
+ else
+ tag.li(link_to(tab_name.titleize, "#tab_#{tab_name}"), class: :tab)
+ end %>
+ <% end %>
+
+
+
- <%= f.number_field :autograde_timeout, display_name: "Timeout",
- help_text: "Timeout for autograding jobs (secs). Must be between 10s and 900s, inclusive.", min: 10, max: 900 %>
- <%= f.check_box :release_score,
- display_name: "Release Scores?",
- help_text: "Check to release autograded scores to students immediately after autograding (strongly recommended)." %>
-
- <% help_tar_text = "Tar file exists, upload a file to override." %>
- <% help_makefile_text = "Makefile exists, upload a file to override." %>
- <%= f.file_field :makefile, label_text: "Autograder Makefile", action: :upload, file_exists_text: help_makefile_text, class: "form-file-field", file_exists: @makefile_exists %>
- <%= f.file_field :tar, label_text: "Autograder Tar", action: :upload, file_exists_text: help_tar_text, class: "form-file-field", file_exists: @tar_exists %>
-
- Both of the above files will be renamed upon upload.
-
-
- <%= f.fields_for :assessment, @assessment do |af| %>
- <%= af.check_box :disable_network, help_text: "Disable network access for autograding containers." %>
- <% end %>
-
- <%= f.submit "Save Settings" %>
-
- <%= link_to "Delete Autograder", course_assessment_autograder_path(@course, @assessment),
- method: :delete, class: "btn danger",
- data: { confirm: "Are you sure you want to delete the Autograder for this assesssment?" } %>
-
- <% unless @makefile_exists.nil? %>
- <%= link_to "Download Makefile", download_file_course_assessment_autograder_path(file_path: @makefile_exists, file_key: 'makefile'), class: "btn" %>
- <% end %>
-
- <% unless @tar_exists.nil? %>
- <%= link_to "Download Tar", download_file_course_assessment_autograder_path(file_path: @tar_exists, file_key: 'tar'), class: "btn" %>
- <% end %>
-<% end %>
+
+
+ <%= form_for @autograder, url: course_assessment_autograder_path(@course, @assessment, @autograder),
+ builder: FormBuilderWithDateTimeInput,
+ html: { multipart: true } do |f| %>
+
+ <%= render "basic_settings", f: %>
+
+ <% if Rails.configuration.x.ec2_ssh.presence %>
+
+ <%= render "ec2_settings", f: %>
+
+ <% end %>
+ <% end %>
+
+
diff --git a/app/views/scoreboards/show.html.erb b/app/views/scoreboards/show.html.erb
index 2215a7526..f3fe53ce6 100644
--- a/app/views/scoreboards/show.html.erb
+++ b/app/views/scoreboards/show.html.erb
@@ -62,11 +62,14 @@
<%= grade[:time] %> |
<% if @colspec %>
<% @colspec.each_with_index do |c, i| %>
- <%# this is a hack for 15-122's image lab. It displays b64 encoded results in image tags %>
<% if c["img"] %>
-  |
+ <% if grade[:entry].is_a?(Array) && grade[:entry].length > i && !grade[:entry][i].nil? %>
+  |
+ <% else %>
+ <%= render 'error_icon' %>
+ <% end %>
<% else %>
- <% if grade[:entry].is_a?(Array) && grade[:entry].length > i %>
+ <% if grade[:entry].is_a?(Array) && grade[:entry].length > i && !grade[:entry][i].nil? %>
<%= grade[:entry][i] %> |
<% else %>
<%= render 'error_icon' %>
@@ -74,10 +77,13 @@
<% end %>
<% end %>
<% else %>
- <%# this should be guaranteed to be an array, but for redundancy, check that entry is array %>
<% if grade[:entry].is_a?(Array) %>
<% grade[:entry].each do |column| %>
- <%= column %> |
+ <% if !column.nil? %>
+ <%= column %> |
+ <% else %>
+ <%= render 'error_icon' %>
+ <% end %>
<% end %>
<% else %>
<%= render 'error_icon' %>
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 33ab24e31..f38aed721 100755
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -74,6 +74,9 @@
# Feature flag for docker image upload
config.x.docker_image_upload_enabled = true
+ # Feature flag for EC2 autograder
+ config.x.ec2_ssh = true
+
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
diff --git a/config/environments/production.rb.template b/config/environments/production.rb.template
index 0d624c445..25d342ee3 100755
--- a/config/environments/production.rb.template
+++ b/config/environments/production.rb.template
@@ -88,6 +88,9 @@ Autolab3::Application.configure do
# Feature flag for docker image upload
config.x.docker_image_upload_enabled = false
+ # Feature flag for EC2 autograder
+ config.x.ec2_ssh = false
+
# ID for Heap Analytics
config.x.analytics_id = nil
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
index 25a3998fc..e35743789 100644
--- a/config/initializers/filter_parameter_logging.rb
+++ b/config/initializers/filter_parameter_logging.rb
@@ -1,4 +1,4 @@
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
-Rails.application.config.filter_parameters += [:password, :session, :warden, :secret, :salt, :cookie, :csrf, :restful_key, :lockbox_master_key, :lti_tool_private_key]
+Rails.application.config.filter_parameters += [:password, :session, :warden, :secret, :salt, :cookie, :csrf, :restful_key, :lockbox_master_key, :lti_tool_private_key, :access_key, :access_key_id]
diff --git a/db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb b/db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb
new file mode 100644
index 000000000..2f6052824
--- /dev/null
+++ b/db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb
@@ -0,0 +1,7 @@
+class AddEc2SshFieldsToAutograders < ActiveRecord::Migration[6.1]
+ def change
+ add_column :autograders, :instance_type, :string, default: ""
+ add_column :autograders, :access_key, :string, default: ""
+ add_column :autograders, :access_key_id, :string, default: ""
+ end
+end
diff --git a/db/migrate/20241211042124_add_use_access_key_to_autograder.rb b/db/migrate/20241211042124_add_use_access_key_to_autograder.rb
new file mode 100644
index 000000000..8d84eaf2d
--- /dev/null
+++ b/db/migrate/20241211042124_add_use_access_key_to_autograder.rb
@@ -0,0 +1,5 @@
+class AddUseAccessKeyToAutograder < ActiveRecord::Migration[6.1]
+ def change
+ add_column :autograders, :use_access_key, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20251021034813_encrypt_autograder_credentials.rb b/db/migrate/20251021034813_encrypt_autograder_credentials.rb
new file mode 100644
index 000000000..8f36ff341
--- /dev/null
+++ b/db/migrate/20251021034813_encrypt_autograder_credentials.rb
@@ -0,0 +1,44 @@
+class EncryptAutograderCredentials < ActiveRecord::Migration[6.1]
+ def up
+ # Add encrypted columns for access keys
+ add_column :autograders, :access_key_ciphertext, :text
+ add_column :autograders, :access_key_id_ciphertext, :text
+
+ # Migrate existing plaintext data to encrypted columns
+ Autograder.reset_column_information
+ Autograder.find_each do |autograder|
+ if autograder.read_attribute(:access_key).present?
+ autograder.access_key = autograder.read_attribute(:access_key)
+ end
+ if autograder.read_attribute(:access_key_id).present?
+ autograder.access_key_id = autograder.read_attribute(:access_key_id)
+ end
+ autograder.save(validate: false) # Skip validation during migration
+ end
+
+ # Remove plaintext columns after migration
+ remove_column :autograders, :access_key
+ remove_column :autograders, :access_key_id
+ end
+
+ def down
+ # Add back plaintext columns
+ add_column :autograders, :access_key, :string, default: ""
+ add_column :autograders, :access_key_id, :string, default: ""
+
+ # Migrate encrypted data back to plaintext columns
+ Autograder.reset_column_information
+ Autograder.find_each do |autograder|
+ if autograder.access_key_ciphertext.present?
+ autograder.update_column(:access_key, autograder.access_key || "")
+ end
+ if autograder.access_key_id_ciphertext.present?
+ autograder.update_column(:access_key_id, autograder.access_key_id || "")
+ end
+ end
+
+ # Remove encrypted columns
+ remove_column :autograders, :access_key_ciphertext
+ remove_column :autograders, :access_key_id_ciphertext
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b212ea0a3..9ab23010f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,9 +10,9 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2024_04_06_174050) do
+ActiveRecord::Schema.define(version: 2025_10_21_034813) do
- create_table "active_storage_attachments", force: :cascade do |t|
+ create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
@@ -22,7 +22,7 @@
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
- create_table "active_storage_blobs", force: :cascade do |t|
+ create_table "active_storage_blobs", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
@@ -34,13 +34,13 @@
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
- create_table "active_storage_variant_records", force: :cascade do |t|
+ create_table "active_storage_variant_records", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
- create_table "annotations", force: :cascade do |t|
+ create_table "annotations", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "submission_id"
t.string "filename"
t.integer "position"
@@ -56,11 +56,11 @@
t.boolean "global_comment", default: false
end
- create_table "announcements", force: :cascade do |t|
+ create_table "announcements", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "title"
t.text "description"
- t.datetime "start_date"
- t.datetime "end_date"
+ t.timestamp "start_date"
+ t.timestamp "end_date"
t.integer "course_user_datum_id"
t.integer "course_id"
t.datetime "created_at"
@@ -69,7 +69,7 @@
t.boolean "system", default: false, null: false
end
- create_table "assessment_user_data", force: :cascade do |t|
+ create_table "assessment_user_data", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "course_user_datum_id", null: false
t.integer "assessment_id", null: false
t.integer "latest_submission_id"
@@ -85,10 +85,10 @@
t.index ["latest_submission_id"], name: "index_assessment_user_data_on_latest_submission_id", unique: true
end
- create_table "assessments", force: :cascade do |t|
- t.datetime "due_at"
- t.datetime "end_at"
- t.datetime "start_at"
+ create_table "assessments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
+ t.timestamp "due_at"
+ t.timestamp "end_at"
+ t.timestamp "start_at"
t.string "name"
t.text "description"
t.datetime "created_at"
@@ -121,7 +121,7 @@
t.boolean "disable_network", default: false
end
- create_table "attachments", force: :cascade do |t|
+ create_table "attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "filename"
t.string "mime_type"
t.string "name"
@@ -136,7 +136,7 @@
t.index ["slug"], name: "index_attachments_on_slug", unique: true
end
- create_table "authentications", force: :cascade do |t|
+ create_table "authentications", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "provider", null: false
t.string "uid", null: false
t.integer "user_id"
@@ -144,14 +144,20 @@
t.datetime "updated_at"
end
- create_table "autograders", force: :cascade do |t|
+ create_table "autograders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "assessment_id"
t.integer "autograde_timeout"
t.string "autograde_image"
t.boolean "release_score"
+ t.string "instance_type", default: ""
+ t.boolean "use_access_key", default: false
+ t.string "ami", default: ""
+ t.string "security_group", default: ""
+ t.text "access_key_ciphertext"
+ t.text "access_key_id_ciphertext"
end
- create_table "course_user_data", force: :cascade do |t|
+ create_table "course_user_data", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "lecture"
t.string "section", default: ""
t.string "grade_policy", default: ""
@@ -167,7 +173,7 @@
t.string "course_number", default: ""
end
- create_table "courses", force: :cascade do |t|
+ create_table "courses", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name"
t.string "semester"
t.integer "late_slack"
@@ -187,14 +193,14 @@
t.boolean "disable_on_end", default: false
end
- create_table "extensions", force: :cascade do |t|
+ create_table "extensions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "course_user_datum_id"
t.integer "assessment_id"
t.integer "days"
t.boolean "infinite", default: false, null: false
end
- create_table "friendly_id_slugs", charset: "utf8mb3", force: :cascade do |t|
+ create_table "friendly_id_slugs", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "slug", null: false
t.integer "sluggable_id", null: false
t.string "sluggable_type", limit: 50
@@ -205,7 +211,7 @@
t.index ["sluggable_type", "sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_type_and_sluggable_id"
end
- create_table "github_integrations", force: :cascade do |t|
+ create_table "github_integrations", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "oauth_state"
t.text "access_token_ciphertext"
t.integer "user_id"
@@ -215,37 +221,37 @@
t.index ["user_id"], name: "index_github_integrations_on_user_id", unique: true
end
- create_table "groups", force: :cascade do |t|
+ create_table "groups", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
- create_table "lti_course_data", force: :cascade do |t|
+ create_table "lti_course_data", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "context_id"
t.integer "course_id"
t.datetime "last_synced"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
t.string "membership_url"
t.string "platform"
t.boolean "auto_sync", default: false
t.boolean "drop_missing_students", default: false
end
- create_table "module_data", force: :cascade do |t|
+ create_table "module_data", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "field_id"
t.integer "data_id"
t.binary "data"
end
- create_table "module_fields", force: :cascade do |t|
+ create_table "module_fields", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "user_module_id"
t.string "name"
t.string "data_type"
end
- create_table "oauth_access_grants", force: :cascade do |t|
+ create_table "oauth_access_grants", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "resource_owner_id", null: false
t.integer "application_id", null: false
t.string "token", null: false
@@ -254,10 +260,11 @@
t.datetime "created_at", null: false
t.datetime "revoked_at"
t.string "scopes"
+ t.index ["application_id"], name: "fk_rails_b4b53e07b8"
t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true
end
- create_table "oauth_access_tokens", force: :cascade do |t|
+ create_table "oauth_access_tokens", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "resource_owner_id"
t.integer "application_id"
t.string "token", null: false
@@ -267,12 +274,13 @@
t.datetime "created_at", null: false
t.string "scopes"
t.string "previous_refresh_token", default: "", null: false
+ t.index ["application_id"], name: "fk_rails_732cb83ab7"
t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true
t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id"
t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true
end
- create_table "oauth_applications", force: :cascade do |t|
+ create_table "oauth_applications", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "uid", null: false
t.string "secret", null: false
@@ -284,7 +292,7 @@
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
end
- create_table "oauth_device_flow_requests", force: :cascade do |t|
+ create_table "oauth_device_flow_requests", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "application_id", null: false
t.string "scopes", default: "", null: false
t.string "device_code", null: false
@@ -294,11 +302,12 @@
t.datetime "resolved_at"
t.integer "resource_owner_id"
t.string "access_code"
+ t.index ["application_id"], name: "fk_rails_4035c6e0ed"
t.index ["device_code"], name: "index_oauth_device_flow_requests_on_device_code", unique: true
t.index ["user_code"], name: "index_oauth_device_flow_requests_on_user_code", unique: true
end
- create_table "problems", force: :cascade do |t|
+ create_table "problems", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name"
t.text "description"
t.integer "assessment_id"
@@ -310,7 +319,7 @@
t.index ["assessment_id", "name"], name: "problem_uniq", unique: true
end
- create_table "risk_conditions", force: :cascade do |t|
+ create_table "risk_conditions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "condition_type"
t.text "parameters"
t.integer "version"
@@ -319,9 +328,9 @@
t.integer "course_id"
end
- create_table "scheduler", force: :cascade do |t|
+ create_table "scheduler", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "action"
- t.datetime "next"
+ t.timestamp "next"
t.integer "interval"
t.integer "course_id"
t.datetime "created_at"
@@ -330,20 +339,20 @@
t.boolean "disabled", default: false
end
- create_table "score_adjustments", force: :cascade do |t|
+ create_table "score_adjustments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "kind", null: false
t.float "value", null: false
t.string "type", default: "Tweak", null: false
end
- create_table "scoreboards", force: :cascade do |t|
+ create_table "scoreboards", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "assessment_id"
t.text "banner"
t.text "colspec"
t.boolean "include_instructors", default: false
end
- create_table "scores", force: :cascade do |t|
+ create_table "scores", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "submission_id"
t.float "score"
t.text "feedback", size: :medium
@@ -356,7 +365,7 @@
t.index ["submission_id"], name: "index_scores_on_submission_id"
end
- create_table "submissions", force: :cascade do |t|
+ create_table "submissions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.integer "version"
t.integer "course_user_datum_id"
t.integer "assessment_id"
@@ -382,7 +391,7 @@
t.index ["course_user_datum_id"], name: "index_submissions_on_course_user_datum_id"
end
- create_table "users", force: :cascade do |t|
+ create_table "users", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "first_name", default: "", null: false
t.string "last_name", default: "", null: false
@@ -411,20 +420,20 @@
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
- create_table "watchlist_configurations", force: :cascade do |t|
+ create_table "watchlist_configurations", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.json "category_blocklist"
t.json "assessment_blocklist"
- t.integer "course_id"
+ t.bigint "course_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.boolean "allow_ca", default: false
t.index ["course_id"], name: "index_watchlist_configurations_on_course_id"
end
- create_table "watchlist_instances", force: :cascade do |t|
- t.integer "course_user_datum_id"
- t.integer "course_id"
- t.integer "risk_condition_id"
+ create_table "watchlist_instances", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
+ t.bigint "course_user_datum_id"
+ t.bigint "course_id"
+ t.bigint "risk_condition_id"
t.integer "status", default: 0
t.boolean "archived", default: false
t.datetime "created_at", null: false
@@ -437,4 +446,8 @@
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "github_integrations", "users"
+ add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id"
+ add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id"
+ add_foreign_key "oauth_device_flow_requests", "oauth_applications", column: "application_id"
end