10290: Add scheduling_parameters map to containers and container_requests, and move...
[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 :mounts
39     t.add :name
40     t.add :output_path
41     t.add :priority
42     t.add :properties
43     t.add :requesting_container_uuid
44     t.add :runtime_constraints
45     t.add :state
46     t.add :use_existing
47     t.add :scheduling_parameters
48   end
49
50   # Supported states for a container request
51   States =
52     [
53      (Uncommitted = 'Uncommitted'),
54      (Committed = 'Committed'),
55      (Final = 'Final'),
56     ]
57
58   State_transitions = {
59     nil => [Uncommitted, Committed],
60     Uncommitted => [Committed],
61     Committed => [Final]
62   }
63
64   def state_transitions
65     State_transitions
66   end
67
68   def skip_uuid_read_permission_check
69     # XXX temporary until permissions are sorted out.
70     %w(modified_by_client_uuid container_uuid requesting_container_uuid)
71   end
72
73   def finalize_if_needed
74     if state == Committed && Container.find_by_uuid(container_uuid).final?
75       reload
76       act_as_system_user do
77         finalize!
78       end
79     end
80   end
81
82   # Finalize the container request after the container has
83   # finished/cancelled.
84   def finalize!
85     update_attributes!(state: Final)
86     c = Container.find_by_uuid(container_uuid)
87     ['output', 'log'].each do |out_type|
88       pdh = c.send(out_type)
89       next if pdh.nil?
90       manifest = Collection.where(portable_data_hash: pdh).first.manifest_text
91       Collection.create!(owner_uuid: owner_uuid,
92                          manifest_text: manifest,
93                          portable_data_hash: pdh,
94                          name: "Container #{out_type} for request #{uuid}",
95                          properties: {
96                            'type' => out_type,
97                            'container_request' => uuid,
98                          })
99     end
100   end
101
102   protected
103
104   def fill_field_defaults
105     self.state ||= Uncommitted
106     self.environment ||= {}
107     self.runtime_constraints ||= {}
108     self.mounts ||= {}
109     self.cwd ||= "."
110     self.container_count_max ||= Rails.configuration.container_count_max
111     self.scheduling_parameters ||= {}
112   end
113
114   # Create a new container (or find an existing one) to satisfy this
115   # request.
116   def resolve
117     c_mounts = mounts_for_container
118     c_runtime_constraints = runtime_constraints_for_container
119     c_container_image = container_image_for_container
120     c = act_as_system_user do
121       c_attrs = {command: self.command,
122                  cwd: self.cwd,
123                  environment: self.environment,
124                  output_path: self.output_path,
125                  container_image: c_container_image,
126                  mounts: c_mounts,
127                  runtime_constraints: c_runtime_constraints}
128
129       reusable = self.use_existing ? Container.find_reusable(c_attrs) : nil
130       if not reusable.nil?
131         reusable
132       else
133         c_attrs[:scheduling_parameters] = self.scheduling_parameters
134         Container.create!(c_attrs)
135       end
136     end
137     self.container_uuid = c.uuid
138   end
139
140   # Return a runtime_constraints hash that complies with
141   # self.runtime_constraints but is suitable for saving in a container
142   # record, i.e., has specific values instead of ranges.
143   #
144   # Doing this as a step separate from other resolutions, like "git
145   # revision range to commit hash", makes sense only when there is no
146   # opportunity to reuse an existing container (e.g., container reuse
147   # is not implemented yet, or we have already found that no existing
148   # containers are suitable).
149   def runtime_constraints_for_container
150     rc = {}
151     runtime_constraints.each do |k, v|
152       if v.is_a? Array
153         rc[k] = v[0]
154       else
155         rc[k] = v
156       end
157     end
158     rc
159   end
160
161   # Return a mounts hash suitable for a Container, i.e., with every
162   # readonly collection UUID resolved to a PDH.
163   def mounts_for_container
164     c_mounts = {}
165     mounts.each do |k, mount|
166       mount = mount.dup
167       c_mounts[k] = mount
168       if mount['kind'] != 'collection'
169         next
170       end
171       if (uuid = mount.delete 'uuid')
172         c = Collection.
173           readable_by(current_user).
174           where(uuid: uuid).
175           select(:portable_data_hash).
176           first
177         if !c
178           raise ArvadosModel::UnresolvableContainerError.new "cannot mount collection #{uuid.inspect}: not found"
179         end
180         if mount['portable_data_hash'].nil?
181           # PDH not supplied by client
182           mount['portable_data_hash'] = c.portable_data_hash
183         elsif mount['portable_data_hash'] != c.portable_data_hash
184           # UUID and PDH supplied by client, but they don't agree
185           raise ArgumentError.new "cannot mount collection #{uuid.inspect}: current portable_data_hash #{c.portable_data_hash.inspect} does not match #{c['portable_data_hash'].inspect} in request"
186         end
187       end
188     end
189     return c_mounts
190   end
191
192   # Return a container_image PDH suitable for a Container.
193   def container_image_for_container
194     coll = Collection.for_latest_docker_image(container_image)
195     if !coll
196       raise ArvadosModel::UnresolvableContainerError.new "docker image #{container_image.inspect} not found"
197     end
198     return coll.portable_data_hash
199   end
200
201   def set_container
202     if (container_uuid_changed? and
203         not current_user.andand.is_admin and
204         not container_uuid.nil?)
205       errors.add :container_uuid, "can only be updated to nil."
206       return false
207     end
208     if state_changed? and state == Committed and container_uuid.nil?
209       resolve
210     end
211     if self.container_uuid != self.container_uuid_was
212       if self.container_count_changed?
213         errors.add :container_count, "cannot be updated directly."
214         return false
215       else
216         self.container_count += 1
217       end
218     end
219   end
220
221   def validate_runtime_constraints
222     case self.state
223     when Committed
224       ['vcpus', 'ram'].each do |k|
225         if not (runtime_constraints.include? k and
226                 runtime_constraints[k].is_a? Integer and
227                 runtime_constraints[k] > 0)
228           errors.add :runtime_constraints, "#{k} must be a positive integer"
229         end
230       end
231
232       if runtime_constraints.include? 'keep_cache_ram' and
233          (!runtime_constraints['keep_cache_ram'].is_a?(Integer) or
234           runtime_constraints['keep_cache_ram'] <= 0)
235             errors.add :runtime_constraints, "keep_cache_ram must be a positive integer"
236       elsif !runtime_constraints.include? 'keep_cache_ram'
237         runtime_constraints['keep_cache_ram'] = Rails.configuration.container_default_keep_cache_ram
238       end
239     end
240   end
241
242   def validate_scheduling_parameters
243     if self.state == Committed
244       if scheduling_parameters.include? 'partitions' and
245          (!scheduling_parameters['partitions'].is_a?(Array) ||
246           scheduling_parameters['partitions'].reject{|x| !x.is_a?(String)}.size !=
247             scheduling_parameters['partitions'].size)
248             errors.add :scheduling_parameters, "partitions must be an array of strings"
249       end
250     end
251   end
252
253   def validate_change
254     permitted = [:owner_uuid]
255
256     case self.state
257     when Uncommitted
258       # Permit updating most fields
259       permitted.push :command, :container_count_max,
260                      :container_image, :cwd, :description, :environment,
261                      :filters, :mounts, :name, :output_path, :priority,
262                      :properties, :requesting_container_uuid, :runtime_constraints,
263                      :state, :container_uuid, :use_existing, :scheduling_parameters
264
265     when Committed
266       if container_uuid.nil?
267         errors.add :container_uuid, "has not been resolved to a container."
268       end
269
270       if priority.nil?
271         errors.add :priority, "cannot be nil"
272       end
273
274       # Can update priority, container count, name and description
275       permitted.push :priority, :container_count, :container_count_max, :container_uuid, :name, :description
276
277       if self.state_changed?
278         # Allow create-and-commit in a single operation.
279         permitted.push :command, :container_image, :cwd, :description, :environment,
280                        :filters, :mounts, :name, :output_path, :properties,
281                        :requesting_container_uuid, :runtime_constraints,
282                        :state, :container_uuid, :scheduling_parameters
283       end
284
285     when Final
286       if not current_user.andand.is_admin and not (self.name_changed? || self.description_changed?)
287         errors.add :state, "of container request can only be set to Final by system."
288       end
289
290       if self.state_changed? || self.name_changed? || self.description_changed?
291           permitted.push :state, :name, :description
292       else
293         errors.add :state, "does not allow updates"
294       end
295
296     else
297       errors.add :state, "invalid value"
298     end
299
300     check_update_whitelist permitted
301   end
302
303   def update_priority
304     if self.state_changed? or
305         self.priority_changed? or
306         self.container_uuid_changed?
307       act_as_system_user do
308         Container.
309           where('uuid in (?)',
310                 [self.container_uuid_was, self.container_uuid].compact).
311           map(&:update_priority!)
312       end
313     end
314   end
315
316   def set_requesting_container_uuid
317     return !new_record? if self.requesting_container_uuid   # already set
318
319     token_uuid = current_api_client_authorization.andand.uuid
320     container = Container.where('auth_uuid=?', token_uuid).order('created_at desc').first
321     self.requesting_container_uuid = container.uuid if container
322     true
323   end
324 end