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