From 90fac7b94cebb1fdc8ee639150979207cd03e77c Mon Sep 17 00:00:00 2001 From: Konrad Hanff Date: Tue, 1 Jun 2021 14:07:35 +0200 Subject: [PATCH] Copy execution environment to Poseidon on create and update When creating or updating an execution environment, an API call to Poseidon is made with the necessary information to create the corresponding Nomad job. If runner management is configured, his will display a warning (currently in the same color as if it were a success) in the UI, if the API call fails. The environment is saved even if it fails. If runner management is not configured, this warning will not be created. --- app/controllers/concerns/common_behavior.rb | 17 +++++++--- .../execution_environments_controller.rb | 19 +++++++++-- app/models/execution_environment.rb | 33 +++++++++++++++++++ .../execution_environments/_form.html.slim | 5 ++- .../execution_environments/show.html.slim | 2 +- config/locales/de.yml | 5 ++- config/locales/en.yml | 5 ++- ..._add_cpu_limit_to_execution_environment.rb | 7 ++++ ..._exposed_ports_in_execution_environment.rb | 17 ++++++++++ db/schema.rb | 3 +- 10 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 db/migrate/20210601095654_add_cpu_limit_to_execution_environment.rb create mode 100644 db/migrate/20210602071834_clean_exposed_ports_in_execution_environment.rb diff --git a/app/controllers/concerns/common_behavior.rb b/app/controllers/concerns/common_behavior.rb index 4c36cf63..8d63246d 100644 --- a/app/controllers/concerns/common_behavior.rb +++ b/app/controllers/concerns/common_behavior.rb @@ -5,10 +5,13 @@ module CommonBehavior @object = options[:object] respond_to do |format| if @object.save - yield if block_given? + notice = t('shared.object_created', model: @object.class.model_name.human) + if block_given? + result = yield + notice = result if result.present? + end path = options[:path].try(:call) || @object - respond_with_valid_object(format, notice: t('shared.object_created', model: @object.class.model_name.human), -path: path, status: :created) + respond_with_valid_object(format, notice: notice, path: path, status: :created) else respond_with_invalid_object(format, template: :new) end @@ -42,9 +45,13 @@ path: path, status: :created) @object = options[:object] respond_to do |format| if @object.update(options[:params]) + notice = t('shared.object_updated', model: @object.class.model_name.human) + if block_given? + result = yield + notice = result if result.present? + end path = options[:path] || @object - respond_with_valid_object(format, notice: t('shared.object_updated', model: @object.class.model_name.human), -path: path, status: :ok) + respond_with_valid_object(format, notice: notice, path: path, status: :ok) else respond_with_invalid_object(format, template: :edit) end diff --git a/app/controllers/execution_environments_controller.rb b/app/controllers/execution_environments_controller.rb index 4449fa41..95132b09 100644 --- a/app/controllers/execution_environments_controller.rb +++ b/app/controllers/execution_environments_controller.rb @@ -3,6 +3,8 @@ class ExecutionEnvironmentsController < ApplicationController include CommonBehavior + RUNNER_MANAGEMENT_PRESENT = CodeOcean::Config.new(:code_ocean).read[:runner_management].present? + before_action :set_docker_images, only: %i[create edit new update] before_action :set_execution_environment, only: MEMBER_ACTIONS + %i[execute_command shell statistics] before_action :set_testing_framework_adapters, only: %i[create edit new update] @@ -15,7 +17,9 @@ class ExecutionEnvironmentsController < ApplicationController def create @execution_environment = ExecutionEnvironment.new(execution_environment_params) authorize! - create_and_respond(object: @execution_environment) + create_and_respond(object: @execution_environment) do + copy_execution_environment_to_poseidon + end end def destroy @@ -105,7 +109,7 @@ class ExecutionEnvironmentsController < ApplicationController def execution_environment_params if params[:execution_environment].present? - params[:execution_environment].permit(:docker_image, :exposed_ports, :editor_mode, :file_extension, :file_type_id, :help, :indent_size, :memory_limit, :name, :network_enabled, :permitted_execution_time, :pool_size, :run_command, :test_command, :testing_framework).merge( + params[:execution_environment].permit(:docker_image, :exposed_ports, :editor_mode, :file_extension, :file_type_id, :help, :indent_size, :memory_limit, :cpu_limit, :name, :network_enabled, :permitted_execution_time, :pool_size, :run_command, :test_command, :testing_framework).merge( user_id: current_user.id, user_type: current_user.class.name ) end @@ -155,6 +159,15 @@ class ExecutionEnvironmentsController < ApplicationController end def update - update_and_respond(object: @execution_environment, params: execution_environment_params) + update_and_respond(object: @execution_environment, params: execution_environment_params) do + copy_execution_environment_to_poseidon + end end + + def copy_execution_environment_to_poseidon + unless RUNNER_MANAGEMENT_PRESENT && @execution_environment.copy_to_poseidon + t('execution_environments.form.errors.not_synced_to_poseidon') + end + end + private :copy_execution_environment_to_poseidon end diff --git a/app/models/execution_environment.rb b/app/models/execution_environment.rb index e4d39a93..3db35c35 100644 --- a/app/models/execution_environment.rb +++ b/app/models/execution_environment.rb @@ -7,6 +7,10 @@ class ExecutionEnvironment < ApplicationRecord include DefaultValues VALIDATION_COMMAND = 'whoami' + RUNNER_MANAGEMENT_PRESENT = CodeOcean::Config.new(:code_ocean).read[:runner_management].present? + BASE_URL = CodeOcean::Config.new(:code_ocean).read[:runner_management][:url] if RUNNER_MANAGEMENT_PRESENT + HEADERS = {'Content-Type' => 'application/json'}.freeze + DEFAULT_CPU_LIMIT = 20 after_initialize :set_default_values @@ -26,6 +30,8 @@ class ExecutionEnvironment < ApplicationRecord validates :permitted_execution_time, numericality: {only_integer: true}, presence: true validates :pool_size, numericality: {only_integer: true}, presence: true validates :run_command, presence: true + validates :cpu_limit, presence: true, numericality: {greater_than: 0, only_integer: true} + validates :exposed_ports, format: {with: /\A(([[:digit:]]{1,5},)*([[:digit:]]{1,5}))?\z/} def set_default_values set_default_values_if_present(permitted_execution_time: 60, pool_size: 0) @@ -36,6 +42,33 @@ class ExecutionEnvironment < ApplicationRecord name end + def copy_to_poseidon + return false unless RUNNER_MANAGEMENT_PRESENT + + url = "#{BASE_URL}/execution-environments/#{id}" + response = Faraday.put(url, to_json, HEADERS) + return true if [201, 204].include? response.status + + Rails.logger.warn("Could not create execution environment in Poseidon, got response: #{response.as_json}") + false + end + + def to_json(*_args) + { + id: id, + image: docker_image, + prewarmingPoolSize: pool_size, + cpuLimit: cpu_limit, + memoryLimit: memory_limit, + networkAccess: network_enabled, + exposedPorts: exposed_ports_list, + }.to_json + end + + def exposed_ports_list + (exposed_ports || '').split(',').map(&:to_i) + end + def valid_test_setup? if test_command? ^ testing_framework? errors.add(:test_command, diff --git a/app/views/execution_environments/_form.html.slim b/app/views/execution_environments/_form.html.slim index 13ee252e..9447d1c6 100644 --- a/app/views/execution_environments/_form.html.slim +++ b/app/views/execution_environments/_form.html.slim @@ -15,11 +15,14 @@ .help-block.form-text == t('.hints.docker_image') .form-group = f.label(:exposed_ports) - = f.text_field(:exposed_ports, class: 'form-control', placeholder: '3000, 4000') + = f.text_field(:exposed_ports, class: 'form-control', placeholder: '3000,4000', pattern: '^((\d{1,5},)*(\d{1,5}))?$') .help-block.form-text == t('.hints.exposed_ports') .form-group = f.label(:memory_limit) = f.number_field(:memory_limit, class: 'form-control', min: DockerClient::MINIMUM_MEMORY_LIMIT, value: f.object.memory_limit || DockerClient::DEFAULT_MEMORY_LIMIT) + .form-group + = f.label(:cpu_limit) + = f.number_field(:cpu_limit, class: 'form-control', min: 1, step: 1, value: ExecutionEnvironment::DEFAULT_CPU_LIMIT) .form-check.mb-3 label.form-check-label = f.check_box(:network_enabled, class: 'form-check-input') diff --git a/app/views/execution_environments/show.html.slim b/app/views/execution_environments/show.html.slim index 21133a71..6d9fb32e 100644 --- a/app/views/execution_environments/show.html.slim +++ b/app/views/execution_environments/show.html.slim @@ -5,7 +5,7 @@ h1 = row(label: 'execution_environment.name', value: @execution_environment.name) = row(label: 'execution_environment.user', value: link_to_if(policy(@execution_environment.author).show?, @execution_environment.author, @execution_environment.author)) = row(label: 'execution_environment.file_type', value: @execution_environment.file_type.present? ? link_to(@execution_environment.file_type, @execution_environment.file_type) : nil) -- [:docker_image, :exposed_ports, :memory_limit, :network_enabled, :permitted_execution_time, :pool_size].each do |attribute| +- [:docker_image, :exposed_ports, :memory_limit, :cpu_limit, :network_enabled, :permitted_execution_time, :pool_size].each do |attribute| = row(label: "execution_environment.#{attribute}", value: @execution_environment.send(attribute)) - [:run_command, :test_command].each do |attribute| = row(label: "execution_environment.#{attribute}") do diff --git a/config/locales/de.yml b/config/locales/de.yml index 29cf2eb4..344f1d58 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -14,6 +14,7 @@ de: file_type_id: Standard-Dateityp help: Hilfetext memory_limit: Speicher-Limit (in MB) + cpu_limit: CPU-Limit (in MHz) network_enabled: Netzwerkzugriff name: Name permitted_execution_time: Erlaubte Ausführungszeit (in Sekunden) @@ -281,7 +282,9 @@ de: hints: command: filename wird automatisch durch den richtigen Dateinamen ersetzt. docker_image: 'Wählen Sie ein Docker-Image aus der Liste oder fügen Sie ein neues hinzu, welches über DockerHub verfügbar ist.' - exposed_ports: Während der Ausführung sind diese Ports für den Nutzer zugänglich. + exposed_ports: Während der Ausführung sind diese Ports für den Nutzer zugänglich. Die Portnummern müssen mit Komma, aber ohne Leerzeichen voneinander getrennt sein. + errors: + not_synced_to_poseidon: Die Ausführungsumgebung wurde erstellt, aber aufgrund eines Fehlers nicht zu Poseidon synchronisiert. index: shell: Shell shell: diff --git a/config/locales/en.yml b/config/locales/en.yml index 41c37dfc..183a1186 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -14,6 +14,7 @@ en: file_type_id: Default File Type help: Help Text memory_limit: Memory Limit (in MB) + cpu_limit: CPU Limit (in MHz) name: Name network_enabled: Network Enabled permitted_execution_time: Permitted Execution Time (in Seconds) @@ -281,7 +282,9 @@ en: hints: command: filename is automatically replaced with the correct filename. docker_image: Pick a Docker image listed above or add a new one which is available via DockerHub. - exposed_ports: During code execution these ports are accessible for the user. + exposed_ports: During code execution these ports are accessible for the user. Port numbers must be separated by a comma but no space. + errors: + not_synced_to_poseidon: The ExecutionEnvironment was created but not synced to Poseidon due to an error. index: shell: Shell shell: diff --git a/db/migrate/20210601095654_add_cpu_limit_to_execution_environment.rb b/db/migrate/20210601095654_add_cpu_limit_to_execution_environment.rb new file mode 100644 index 00000000..c74aa69a --- /dev/null +++ b/db/migrate/20210601095654_add_cpu_limit_to_execution_environment.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddCpuLimitToExecutionEnvironment < ActiveRecord::Migration[6.1] + def change + add_column :execution_environments, :cpu_limit, :integer, default: 20 + end +end diff --git a/db/migrate/20210602071834_clean_exposed_ports_in_execution_environment.rb b/db/migrate/20210602071834_clean_exposed_ports_in_execution_environment.rb new file mode 100644 index 00000000..32db9950 --- /dev/null +++ b/db/migrate/20210602071834_clean_exposed_ports_in_execution_environment.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CleanExposedPortsInExecutionEnvironment < ActiveRecord::Migration[6.1] + def change + ExecutionEnvironment.all.each do |execution_environment| + continue if execution_environment.exposed_ports.nil? + + cleaned = execution_environment.exposed_ports.gsub(/[[:space:]]/, '') + list = cleaned.split(',').map(&:to_i).uniq + if list.empty? + execution_environment.update(exposed_ports: nil) + else + execution_environment.update(exposed_ports: list.join(',')) + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fb4dbc1d..beefc5b1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_05_19_134938) do +ActiveRecord::Schema.define(version: 2021_06_02_071834) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -112,6 +112,7 @@ ActiveRecord::Schema.define(version: 2021_05_19_134938) do t.integer "file_type_id" t.integer "memory_limit" t.boolean "network_enabled" + t.integer "cpu_limit" end create_table "exercise_collection_items", id: :serial, force: :cascade do |t|