add api template for pipeline_invocation
[arvados.git] / app / models / node.rb
1 class Node < ActiveRecord::Base
2   include AssignUuid
3   include KindAndEtag
4   include CommonApiTemplate
5   serialize :info, Hash
6   before_validation :ensure_ping_secret
7   after_update :dnsmasq_update
8
9   MAX_SLOTS = 64
10
11   @@confdir = if Rails.configuration.respond_to? :dnsmasq_conf_dir
12                 Rails.configuration.dnsmasq_conf_dir
13               elsif File.exists? '/etc/dnsmasq.d/.'
14                 '/etc/dnsmasq.d'
15               else
16                 nil
17               end
18   @@domain = Rails.configuration.compute_node_domain rescue `hostname --domain`.strip
19
20   api_accessible :superuser, :extend => :common do |t|
21     t.add :hostname
22     t.add :domain
23     t.add :ip_address
24     t.add :first_ping_at
25     t.add :last_ping_at
26     t.add :info
27     t.add :status
28   end
29
30   def info
31     @info ||= Hash.new
32     super
33   end
34
35   def domain
36     super || @@domain
37   end
38
39   def status
40     if !self.last_ping_at
41       if Time.now - self.created_at > 5.minutes
42         'startup-fail'
43       else
44         'pending'
45       end
46     elsif Time.now - self.last_ping_at > 1.hours
47       'missing'
48     else
49       'running'
50     end
51   end
52
53   def ping(o)
54     raise "must have :ip and :ping_secret" unless o[:ip] and o[:ping_secret]
55
56     if o[:ping_secret] != self.info[:ping_secret]
57       logger.info "Ping: secret mismatch: received \"#{o[:ping_secret]}\" != \"#{self.info[:ping_secret]}\""
58       return nil
59     end
60     self.last_ping_at = Time.now
61
62     # Record IP address
63     if self.ip_address.nil?
64       logger.info "#{self.uuid} ip_address= #{o[:ip]}"
65       self.ip_address = o[:ip]
66       self.first_ping_at = Time.now
67     end
68
69     # Record instance ID if not already known
70     self.info[:ec2_instance_id] ||= o[:ec2_instance_id]
71
72     # Assign hostname
73     if self.slot_number.nil?
74       try_slot = 0
75       begin
76         self.slot_number = try_slot
77         begin
78           self.save!
79           break
80         rescue ActiveRecord::RecordNotUnique
81           try_slot += 1
82         end
83         raise "No available node slots" if try_slot == MAX_SLOTS
84       end while true
85       self.hostname = self.class.hostname_for_slot(self.slot_number)
86     end
87
88     save
89   end
90
91   def start!(ping_url_method)
92     ping_url = ping_url_method.call({ uuid: self.uuid, ping_secret: self.info[:ping_secret] })
93     cmd = ["ec2-run-instances",
94            "--user-data '#{ping_url}'",
95            "-t c1.xlarge -n 1 -g orvos-compute",
96            "ami-68ca6901"
97           ].join(' ')
98     self.info[:ec2_start_command] = cmd
99     logger.info "#{self.uuid} ec2_start_command= #{cmd.inspect}"
100     result = `#{cmd} 2>&1`
101     self.info[:ec2_start_result] = result
102     logger.info "#{self.uuid} ec2_start_result= #{result.inspect}"
103     result.match(/INSTANCE\s*(i-[0-9a-f]+)/) do |m|
104       self.info[:ec2_instance_id] = m[1]
105     end
106     self.save!
107   end
108
109   protected
110
111   def ensure_ping_secret
112     self.info[:ping_secret] ||= rand(2**256).to_s(36)
113   end
114
115   def dnsmasq_update
116     if self.hostname_changed? or self.ip_address_changed?
117       if self.hostname and self.ip_address
118         self.class.dnsmasq_update(self.hostname, self.ip_address)
119       end
120     end
121   end
122
123   def self.dnsmasq_update(hostname, ip_address)
124     return unless @@confdir
125     ptr_domain = ip_address.
126       split('.').reverse.join('.').concat('.in-addr.arpa')
127     hostfile = File.join @@confdir, hostname
128     File.open hostfile, 'w' do |f|
129       f.puts "address=/#{hostname}/#{ip_address}"
130       f.puts "address=/#{hostname}.#{@@domain}/#{ip_address}" if @@domain
131       f.puts "ptr-record=#{ptr_domain},#{hostname}"
132     end
133     File.open(File.join(@@confdir, 'restart.txt'), 'w') do |f|
134       # this should trigger a dnsmasq restart
135     end
136   end
137
138   def self.hostname_for_slot(slot_number)
139     "compute#{slot_number}"
140   end
141
142   # At startup, make sure all DNS entries exist.  Otherwise, slurmctld
143   # will refuse to start.
144   if @@confdir and
145       !File.exists? (File.join(@@confdir, hostname_for_slot(MAX_SLOTS-1)))
146     (0..MAX_SLOTS-1).each do |slot_number|
147       hostname = hostname_for_slot(slot_number)
148       hostfile = File.join @@confdir, hostname
149       if !File.exists? hostfile
150         dnsmasq_update(hostname, '127.40.4.0')
151       end
152     end
153   end
154 end