9898: add unlock method also on the container model.
[arvados.git] / services / api / app / models / container.rb
1 require 'whitelist_update'
2
3 class Container < ArvadosModel
4   include HasUuid
5   include KindAndEtag
6   include CommonApiTemplate
7   include WhitelistUpdate
8
9   serialize :environment, Hash
10   serialize :mounts, Hash
11   serialize :runtime_constraints, Hash
12   serialize :command, Array
13
14   before_validation :fill_field_defaults, :if => :new_record?
15   before_validation :set_timestamps
16   validates :command, :container_image, :output_path, :cwd, :priority, :presence => true
17   validate :validate_state_change
18   validate :validate_change
19   validate :validate_lock
20   after_validation :assign_auth
21   after_save :handle_completed
22
23   has_many :container_requests, :foreign_key => :container_uuid, :class_name => 'ContainerRequest', :primary_key => :uuid
24   belongs_to :auth, :class_name => 'ApiClientAuthorization', :foreign_key => :auth_uuid, :primary_key => :uuid
25
26   api_accessible :user, extend: :common do |t|
27     t.add :command
28     t.add :container_image
29     t.add :cwd
30     t.add :environment
31     t.add :exit_code
32     t.add :finished_at
33     t.add :locked_by_uuid
34     t.add :log
35     t.add :mounts
36     t.add :output
37     t.add :output_path
38     t.add :priority
39     t.add :progress
40     t.add :runtime_constraints
41     t.add :started_at
42     t.add :state
43     t.add :auth_uuid
44   end
45
46   # Supported states for a container
47   States =
48     [
49      (Queued = 'Queued'),
50      (Locked = 'Locked'),
51      (Running = 'Running'),
52      (Complete = 'Complete'),
53      (Cancelled = 'Cancelled')
54     ]
55
56   State_transitions = {
57     nil => [Queued],
58     Queued => [Locked, Cancelled],
59     Locked => [Queued, Running, Cancelled],
60     Running => [Complete, Cancelled]
61   }
62
63   def state_transitions
64     State_transitions
65   end
66
67   def update_priority!
68     if [Queued, Locked, Running].include? self.state
69       # Update the priority of this container to the maximum priority of any of
70       # its committed container requests and save the record.
71       self.priority = ContainerRequest.
72         where(container_uuid: uuid,
73               state: ContainerRequest::Committed).
74         maximum('priority')
75       self.save!
76     end
77   end
78
79   def lock
80     if self.state == Locked
81       raise AlreadyLockedError
82     end
83     with_lock do
84       self.state = Locked
85       self.save!
86     end
87   end
88
89   def unlock
90     if self.state == Queued
91       raise InvalidStateTransitionError
92     end
93     with_lock do
94       self.state = Queued
95       self.save!
96     end
97   end
98
99   def self.readable_by(*users_list)
100     if users_list.select { |u| u.is_admin }.any?
101       return self
102     end
103     user_uuids = users_list.map { |u| u.uuid }
104     uuid_list = user_uuids + users_list.flat_map { |u| u.groups_i_can(:read) }
105     uuid_list.uniq!
106     permitted = "(SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (:uuids))"
107     joins(:container_requests).
108       where("container_requests.uuid IN #{permitted} OR "+
109             "container_requests.owner_uuid IN (:uuids)",
110             uuids: uuid_list)
111   end
112
113   protected
114
115   def fill_field_defaults
116     self.state ||= Queued
117     self.environment ||= {}
118     self.runtime_constraints ||= {}
119     self.mounts ||= {}
120     self.cwd ||= "."
121     self.priority ||= 1
122   end
123
124   def permission_to_create
125     current_user.andand.is_admin
126   end
127
128   def permission_to_update
129     current_user.andand.is_admin
130   end
131
132   def set_timestamps
133     if self.state_changed? and self.state == Running
134       self.started_at ||= db_current_time
135     end
136
137     if self.state_changed? and [Complete, Cancelled].include? self.state
138       self.finished_at ||= db_current_time
139     end
140   end
141
142   def validate_change
143     permitted = [:state]
144
145     if self.new_record?
146       permitted.push(:owner_uuid, :command, :container_image, :cwd,
147                      :environment, :mounts, :output_path, :priority,
148                      :runtime_constraints)
149     end
150
151     case self.state
152     when Queued, Locked
153       permitted.push :priority
154
155     when Running
156       permitted.push :priority, :progress
157       if self.state_changed?
158         permitted.push :started_at
159       end
160
161     when Complete
162       if self.state_was == Running
163         permitted.push :finished_at, :output, :log, :exit_code
164       end
165
166     when Cancelled
167       case self.state_was
168       when Running
169         permitted.push :finished_at, :output, :log
170       when Queued, Locked
171         permitted.push :finished_at
172       end
173
174     else
175       # The state_transitions check will add an error message for this
176       return false
177     end
178
179     check_update_whitelist permitted
180   end
181
182   def validate_lock
183     # If the Container is already locked by someone other than the
184     # current api_client_auth, disallow all changes -- except
185     # priority, which needs to change to reflect max(priority) of
186     # relevant ContainerRequests.
187     if locked_by_uuid_was
188       if locked_by_uuid_was != Thread.current[:api_client_authorization].uuid
189         check_update_whitelist [:priority]
190       end
191     end
192
193     if [Locked, Running].include? self.state
194       # If the Container was already locked, locked_by_uuid must not
195       # changes. Otherwise, the current auth gets the lock.
196       need_lock = locked_by_uuid_was || Thread.current[:api_client_authorization].uuid
197     else
198       need_lock = nil
199     end
200
201     # The caller can provide a new value for locked_by_uuid, but only
202     # if it's exactly what we expect. This allows a caller to perform
203     # an update like {"state":"Unlocked","locked_by_uuid":null}.
204     if self.locked_by_uuid_changed?
205       if self.locked_by_uuid != need_lock
206         return errors.add :locked_by_uuid, "can only change to #{need_lock}"
207       end
208     end
209     self.locked_by_uuid = need_lock
210   end
211
212   def assign_auth
213     if self.auth_uuid_changed?
214       return errors.add :auth_uuid, 'is readonly'
215     end
216     if not [Locked, Running].include? self.state
217       # don't need one
218       self.auth.andand.update_attributes(expires_at: db_current_time)
219       self.auth = nil
220       return
221     elsif self.auth
222       # already have one
223       return
224     end
225     cr = ContainerRequest.
226       where('container_uuid=? and priority>0', self.uuid).
227       order('priority desc').
228       first
229     if !cr
230       return errors.add :auth_uuid, "cannot be assigned because priority <= 0"
231     end
232     self.auth = ApiClientAuthorization.
233       create!(user_id: User.find_by_uuid(cr.modified_by_user_uuid).id,
234               api_client_id: 0)
235   end
236
237   def handle_completed
238     # This container is finished so finalize any associated container requests
239     # that are associated with this container.
240     if self.state_changed? and [Complete, Cancelled].include? self.state
241       act_as_system_user do
242         # Notify container requests associated with this container
243         ContainerRequest.where(container_uuid: uuid,
244                                :state => ContainerRequest::Committed).each do |cr|
245           cr.container_completed!
246         end
247
248         # Try to cancel any outstanding container requests made by this container.
249         ContainerRequest.where(requesting_container_uuid: uuid,
250                                :state => ContainerRequest::Committed).each do |cr|
251           cr.priority = 0
252           cr.save
253         end
254       end
255     end
256   end
257
258 end