Merge branch '11332-fix-crunchscript' refs #11332
[arvados.git] / services / api / app / models / container_request.rb
1 require 'whitelist_update'
2
3 class ContainerRequest < ArvadosModel
4   include HasUuid
5   include KindAndEtag
6   include CommonApiTemplate
7   include WhitelistUpdate
8
9   serialize :properties, Hash
10   serialize :environment, Hash
11   serialize :mounts, Hash
12   serialize :runtime_constraints, Hash
13   serialize :command, Array
14   serialize :scheduling_parameters, Hash
15
16   before_validation :fill_field_defaults, :if => :new_record?
17   before_validation :validate_runtime_constraints
18   before_validation :validate_scheduling_parameters
19   before_validation :set_container
20   validates :command, :container_image, :output_path, :cwd, :presence => true
21   validate :validate_state_change
22   validate :validate_change
23   after_save :update_priority
24   after_save :finalize_if_needed
25   before_create :set_requesting_container_uuid
26
27   api_accessible :user, extend: :common do |t|
28     t.add :command
29     t.add :container_count
30     t.add :container_count_max
31     t.add :container_image
32     t.add :container_uuid
33     t.add :cwd
34     t.add :description
35     t.add :environment
36     t.add :expires_at
37     t.add :filters
38     t.add :log_uuid
39     t.add :mounts
40     t.add :name
41     t.add :output_name
42     t.add :output_path
43     t.add :output_uuid
44     t.add :priority
45     t.add :properties
46     t.add :requesting_container_uuid
47     t.add :runtime_constraints
48     t.add :scheduling_parameters
49     t.add :state
50     t.add :use_existing
51   end
52
53   # Supported states for a container request
54   States =
55     [
56      (Uncommitted = 'Uncommitted'),
57      (Committed = 'Committed'),
58      (Final = 'Final'),
59     ]
60
61   State_transitions = {
62     nil => [Uncommitted, Committed],
63     Uncommitted => [Committed],
64     Committed => [Final]
65   }
66
67   def state_transitions
68     State_transitions
69   end
70
71   def skip_uuid_read_permission_check
72     # XXX temporary until permissions are sorted out.
73     %w(modified_by_client_uuid container_uuid requesting_container_uuid)
74   end
75
76   def finalize_if_needed
77     if state == Committed && Container.find_by_uuid(container_uuid).final?
78       reload
79       act_as_system_user do
80         finalize!
81       end
82     end
83   end
84
85   # Finalize the container request after the container has
86   # finished/cancelled.
87   def finalize!
88     out_coll = nil
89     log_coll = nil
90     c = Container.find_by_uuid(container_uuid)
91     ['output', 'log'].each do |out_type|
92       pdh = c.send(out_type)
93       next if pdh.nil?
94       if self.output_name and out_type == 'output'
95         coll_name = self.output_name
96       else
97         coll_name = "Container #{out_type} for request #{uuid}"
98       end
99       manifest = Collection.unscoped do
100         Collection.where(portable_data_hash: pdh).first.manifest_text
101       end
102
103       coll = Collection.new(owner_uuid: owner_uuid,
104                             manifest_text: manifest,
105                             portable_data_hash: pdh,
106                             name: coll_name,
107                             properties: {
108                               'type' => out_type,
109                               'container_request' => uuid,
110                             })
111       coll.save_with_unique_name!
112       if out_type == 'output'
113         out_coll = coll.uuid
114       else
115         log_coll = coll.uuid
116       end
117     end
118     update_attributes!(state: Final, output_uuid: out_coll, log_uuid: log_coll)
119   end
120
121   def self.full_text_searchable_columns
122     super - ["mounts"]
123   end
124
125   protected
126
127   def fill_field_defaults
128     self.state ||= Uncommitted
129     self.environment ||= {}
130     self.runtime_constraints ||= {}
131     self.mounts ||= {}
132     self.cwd ||= "."
133     self.container_count_max ||= Rails.configuration.container_count_max
134     self.scheduling_parameters ||= {}
135   end
136
137   def set_container
138     if (container_uuid_changed? and
139         not current_user.andand.is_admin and
140         not container_uuid.nil?)
141       errors.add :container_uuid, "can only be updated to nil."
142       return false
143     end
144     if state_changed? and state == Committed and container_uuid.nil?
145       self.container_uuid = Container.resolve(self).uuid
146     end
147     if self.container_uuid != self.container_uuid_was
148       if self.container_count_changed?
149         errors.add :container_count, "cannot be updated directly."
150         return false
151       else
152         self.container_count += 1
153       end
154     end
155   end
156
157   def validate_runtime_constraints
158     case self.state
159     when Committed
160       [['vcpus', true],
161        ['ram', true],
162        ['keep_cache_ram', false]].each do |k, required|
163         if !required && !runtime_constraints.include?(k)
164           next
165         end
166         v = runtime_constraints[k]
167         unless (v.is_a?(Integer) && v > 0)
168           errors.add(:runtime_constraints,
169                      "[#{k}]=#{v.inspect} must be a positive integer")
170         end
171       end
172     end
173   end
174
175   def validate_scheduling_parameters
176     if self.state == Committed
177       if scheduling_parameters.include? 'partitions' and
178          (!scheduling_parameters['partitions'].is_a?(Array) ||
179           scheduling_parameters['partitions'].reject{|x| !x.is_a?(String)}.size !=
180             scheduling_parameters['partitions'].size)
181             errors.add :scheduling_parameters, "partitions must be an array of strings"
182       end
183     end
184   end
185
186   def validate_change
187     permitted = [:owner_uuid]
188
189     case self.state
190     when Uncommitted
191       # Permit updating most fields
192       permitted.push :command, :container_count_max,
193                      :container_image, :cwd, :description, :environment,
194                      :filters, :mounts, :name, :output_path, :priority,
195                      :properties, :requesting_container_uuid, :runtime_constraints,
196                      :state, :container_uuid, :use_existing, :scheduling_parameters,
197                      :output_name
198
199     when Committed
200       if container_uuid.nil?
201         errors.add :container_uuid, "has not been resolved to a container."
202       end
203
204       if priority.nil?
205         errors.add :priority, "cannot be nil"
206       end
207
208       # Can update priority, container count, name and description
209       permitted.push :priority, :container_count, :container_count_max, :container_uuid,
210                      :name, :description
211
212       if self.state_changed?
213         # Allow create-and-commit in a single operation.
214         permitted.push :command, :container_image, :cwd, :description, :environment,
215                        :filters, :mounts, :name, :output_path, :properties,
216                        :requesting_container_uuid, :runtime_constraints,
217                        :state, :container_uuid, :use_existing, :scheduling_parameters,
218                        :output_name
219       end
220
221     when Final
222       if not current_user.andand.is_admin and not (self.name_changed? || self.description_changed?)
223         errors.add :state, "of container request can only be set to Final by system."
224       end
225
226       if self.state_changed? || self.name_changed? || self.description_changed? || self.output_uuid_changed? || self.log_uuid_changed?
227           permitted.push :state, :name, :description, :output_uuid, :log_uuid
228       else
229         errors.add :state, "does not allow updates"
230       end
231
232     else
233       errors.add :state, "invalid value"
234     end
235
236     check_update_whitelist permitted
237   end
238
239   def update_priority
240     if self.state_changed? or
241         self.priority_changed? or
242         self.container_uuid_changed?
243       act_as_system_user do
244         Container.
245           where('uuid in (?)',
246                 [self.container_uuid_was, self.container_uuid].compact).
247           map(&:update_priority!)
248       end
249     end
250   end
251
252   def set_requesting_container_uuid
253     return !new_record? if self.requesting_container_uuid   # already set
254
255     token_uuid = current_api_client_authorization.andand.uuid
256     container = Container.where('auth_uuid=?', token_uuid).order('created_at desc').first
257     self.requesting_container_uuid = container.uuid if container
258     true
259   end
260 end