21700: Install Bundler system-wide in Rails postinst
[arvados.git] / services / api / app / models / node.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'tempfile'
6
7 class Node < ArvadosModel
8   include HasUuid
9   include KindAndEtag
10   include CommonApiTemplate
11
12   # Posgresql JSONB columns should NOT be declared as serialized, Rails 5
13   # already know how to properly treat them.
14   attribute :properties, :jsonbHash, default: {}
15   attribute :info, :jsonbHash, default: {}
16
17   before_validation :ensure_ping_secret
18   after_update :dns_server_update
19
20   # Only a controller can figure out whether or not the current API tokens
21   # have access to the associated Job.  They're expected to set
22   # job_readable=true if the Job UUID can be included in the API response.
23   belongs_to :job,
24              foreign_key: 'job_uuid',
25              primary_key: 'uuid',
26              optional: true
27   attr_accessor :job_readable
28
29   UNUSED_NODE_IP = '127.40.4.0'
30   MAX_VMS = 3
31
32   api_accessible :user, :extend => :common do |t|
33     t.add :hostname
34     t.add :domain
35     t.add :ip_address
36     t.add :last_ping_at
37     t.add :slot_number
38     t.add :status
39     t.add :api_job_uuid, as: :job_uuid
40     t.add :crunch_worker_state
41     t.add :properties
42   end
43   api_accessible :superuser, :extend => :user do |t|
44     t.add :first_ping_at
45     t.add :info
46     t.add lambda { |x| Rails.configuration.Containers.SLURM.Managed.ComputeNodeNameservers.keys }, :as => :nameservers
47   end
48
49   after_initialize do
50     @bypass_arvados_authorization = false
51   end
52
53   def domain
54     super || Rails.configuration.Containers.SLURM.Managed.ComputeNodeDomain
55   end
56
57   def api_job_uuid
58     job_readable ? job_uuid : nil
59   end
60
61   def crunch_worker_state
62     return 'down' if slot_number.nil?
63     case self.info.andand['slurm_state']
64     when 'alloc', 'comp', 'mix', 'drng'
65       'busy'
66     when 'idle'
67       'idle'
68     else
69       'down'
70     end
71   end
72
73   def status
74     if !self.last_ping_at
75       if db_current_time - self.created_at > 5.minutes
76         'startup-fail'
77       else
78         'pending'
79       end
80     elsif db_current_time - self.last_ping_at > 1.hours
81       'missing'
82     else
83       'running'
84     end
85   end
86
87   def ping(o)
88     raise "must have :ip and :ping_secret" unless o[:ip] and o[:ping_secret]
89
90     if o[:ping_secret] != self.info['ping_secret']
91       logger.info "Ping: secret mismatch: received \"#{o[:ping_secret]}\" != \"#{self.info['ping_secret']}\""
92       raise ArvadosModel::UnauthorizedError.new("Incorrect ping_secret")
93     end
94
95     current_time = db_current_time
96     self.last_ping_at = current_time
97
98     @bypass_arvados_authorization = true
99
100     # Record IP address
101     if self.ip_address.nil?
102       logger.info "#{self.uuid} ip_address= #{o[:ip]}"
103       self.ip_address = o[:ip]
104       self.first_ping_at = current_time
105     end
106
107     # Record instance ID if not already known
108     if o[:ec2_instance_id]
109       if !self.info['ec2_instance_id']
110         self.info['ec2_instance_id'] = o[:ec2_instance_id]
111       elsif self.info['ec2_instance_id'] != o[:ec2_instance_id]
112         logger.debug "Multiple nodes have credentials for #{self.uuid}"
113         raise "#{self.uuid} is already running at #{self.info['ec2_instance_id']} so rejecting ping from #{o[:ec2_instance_id]}"
114       end
115     end
116
117     assign_slot
118
119     # Record other basic stats
120     ['total_cpu_cores', 'total_ram_mb', 'total_scratch_mb'].each do |key|
121       if value = (o[key] or o[key.to_sym])
122         self.properties[key] = value.to_i
123       else
124         self.properties.delete(key)
125       end
126     end
127
128     save!
129   end
130
131   def assign_slot
132     return if self.slot_number.andand > 0
133     while true
134       self.slot_number = self.class.available_slot_number
135       if self.slot_number.nil?
136         raise "No available node slots"
137       end
138       begin
139         save!
140         return assign_hostname
141       rescue ActiveRecord::RecordNotUnique
142         # try again
143       end
144     end
145   end
146
147   protected
148
149   def assign_hostname
150     if self.hostname.nil? and Rails.configuration.Containers.SLURM.Managed.AssignNodeHostname
151       self.hostname = self.class.hostname_for_slot(self.slot_number)
152     end
153   end
154
155   def self.available_slot_number
156     # Join the sequence 1..max with the nodes table. Return the first
157     # (i.e., smallest) value that doesn't match the slot_number of any
158     # existing node.
159     connection.exec_query('SELECT n FROM generate_series(1, $1) AS slot(n)
160                           LEFT JOIN nodes ON n=slot_number
161                           WHERE slot_number IS NULL
162                           LIMIT 1',
163                           # query label:
164                           'Node.available_slot_number',
165                           # bind vars:
166                           [MAX_VMS],
167                          ).rows.first.andand.first
168   end
169
170   def ensure_ping_secret
171     self.info['ping_secret'] ||= rand(2**256).to_s(36)
172   end
173
174   def dns_server_update
175     if saved_change_to_ip_address? && ip_address
176       Node.where('id != ? and ip_address = ?',
177                  id, ip_address).each do |stale_node|
178         # One or more(!) stale node records have the same IP address
179         # as the new node. Clear the ip_address field on the stale
180         # nodes. Otherwise, we (via SLURM) might inadvertently connect
181         # to the new node using the old node's hostname.
182         stale_node.update!(ip_address: nil)
183       end
184     end
185     if hostname_before_last_save && saved_change_to_hostname?
186       self.class.dns_server_update(hostname_before_last_save, UNUSED_NODE_IP)
187     end
188     if hostname && (saved_change_to_hostname? || saved_change_to_ip_address?)
189       self.class.dns_server_update(hostname, ip_address || UNUSED_NODE_IP)
190     end
191   end
192
193   def self.dns_server_update hostname, ip_address
194     ok = true
195
196     ptr_domain = ip_address.
197       split('.').reverse.join('.').concat('.in-addr.arpa')
198
199     template_vars = {
200       hostname: hostname,
201       uuid_prefix: Rails.configuration.ClusterID,
202       ip_address: ip_address,
203       ptr_domain: ptr_domain,
204     }
205
206     if (!Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir.to_s.empty? and
207         !Rails.configuration.Containers.SLURM.Managed.DNSServerConfTemplate.to_s.empty?)
208       tmpfile = nil
209       begin
210         begin
211           template = IO.read(Rails.configuration.Containers.SLURM.Managed.DNSServerConfTemplate)
212         rescue IOError, SystemCallError => e
213           logger.error "Reading #{Rails.configuration.Containers.SLURM.Managed.DNSServerConfTemplate}: #{e.message}"
214           raise
215         end
216
217         hostfile = File.join Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir, "#{hostname}.conf"
218         Tempfile.open(["#{hostname}-", ".conf.tmp"],
219                                  Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir) do |f|
220           tmpfile = f.path
221           f.puts template % template_vars
222         end
223         File.rename tmpfile, hostfile
224       rescue IOError, SystemCallError => e
225         logger.error "Writing #{hostfile}: #{e.message}"
226         ok = false
227       ensure
228         if tmpfile and File.file? tmpfile
229           # Cleanup remaining temporary file.
230           File.unlink tmpfile
231         end
232       end
233     end
234
235     if !Rails.configuration.Containers.SLURM.Managed.DNSServerUpdateCommand.empty?
236       cmd = Rails.configuration.Containers.SLURM.Managed.DNSServerUpdateCommand % template_vars
237       if not system cmd
238         logger.error "dns_server_update_command #{cmd.inspect} failed: #{$?}"
239         ok = false
240       end
241     end
242
243     if (!Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir.to_s.empty? and
244         !Rails.configuration.Containers.SLURM.Managed.DNSServerReloadCommand.to_s.empty?)
245       restartfile = File.join(Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir, 'restart.txt')
246       begin
247         File.open(restartfile, 'w') do |f|
248           # Typically, this is used to trigger a dns server restart
249           f.puts Rails.configuration.Containers.SLURM.Managed.DNSServerReloadCommand
250         end
251       rescue IOError, SystemCallError => e
252         logger.error "Unable to write #{restartfile}: #{e.message}"
253         ok = false
254       end
255     end
256
257     ok
258   end
259
260   def self.hostname_for_slot(slot_number)
261     config = Rails.configuration.Containers.SLURM.Managed.AssignNodeHostname
262
263     return nil if !config
264
265     sprintf(config, {:slot_number => slot_number})
266   end
267
268   # At startup, make sure all DNS entries exist.  Otherwise, slurmctld
269   # will refuse to start.
270   if (!Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir.to_s.empty? and
271       !Rails.configuration.Containers.SLURM.Managed.DNSServerConfTemplate.to_s.empty? and
272       !Rails.configuration.Containers.SLURM.Managed.AssignNodeHostname.empty?)
273
274     (0..MAX_VMS-1).each do |slot_number|
275       hostname = hostname_for_slot(slot_number)
276       hostfile = File.join Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir, "#{hostname}.conf"
277       if !File.exist? hostfile
278         n = Node.where(:slot_number => slot_number).first
279         if n.nil? or n.ip_address.nil?
280           dns_server_update(hostname, UNUSED_NODE_IP)
281         else
282           dns_server_update(hostname, n.ip_address)
283         end
284       end
285     end
286   end
287
288   def permission_to_update
289     @bypass_arvados_authorization or super
290   end
291
292   def permission_to_create
293     current_user and current_user.is_admin
294   end
295 end