Class: OsCtld::Monitor::Process

Inherits:
Object
  • Object
show all
Includes:
OsCtl::Lib::Utils::Log
Defined in:
lib/osctld/monitor/process.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pool, user, group, stdout, containers:) ⇒ Process

Returns a new instance of Process.



57
58
59
60
61
62
63
64
# File 'lib/osctld/monitor/process.rb', line 57

def initialize(pool, user, group, stdout, containers:)
  @pool = pool
  @user = user
  @group = group
  @stdout = stdout
  @containers = containers
  @last_line = nil
end

Class Method Details

.cgroup_path(ct) ⇒ Object



53
54
55
# File 'lib/osctld/monitor/process.rb', line 53

def self.cgroup_path(ct)
  File.join(ct.group.full_cgroup_path(ct.user), 'monitor')
end

.spawn(ct) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/osctld/monitor/process.rb', line 7

def self.spawn(ct)
  cg_path = cgroup_path(ct)
  out_r, out_w = IO.pipe

  CGroup.mkpath_all(cg_path.split('/'))

  pid = Process.fork do
    $stdout.reopen(out_w)
    out_r.close

    SwitchUser.switch_to(
      ct.user.sysusername,
      ct.user.ugid,
      ct.user.homedir,
      cg_path
    )

    Process.exec('lxc-monitor', '-P', ct.lxc_home, '-n', '.*')
  end

  out_w.close
  [pid, out_r]
end

.stop_monitord(ct) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/osctld/monitor/process.rb', line 31

def self.stop_monitord(ct)
  pid = Process.fork do
    SwitchUser.switch_to(
      ct.user.sysusername,
      ct.user.ugid,
      ct.user.homedir,
      cgroup_path(ct)
    )

    Process.exec('lxc-monitor', '-P', ct.lxc_home, '--quit')
  end

  _, status = Process.wait2(pid)

  if status.exitstatus == 0
    log(:info, :monitor, 'Stopped lxc-monitord')

  else
    log(:info, :monitor, 'Failed to stop lxc-monitord')
  end
end

Instance Method Details

#cancel_exited_run(ct) ⇒ Object (protected)



188
189
190
191
192
193
194
# File 'lib/osctld/monitor/process.rb', line 188

def cancel_exited_run(ct)
  ct.run_conf&.nfs_cancellation&.abort
rescue StandardError => e
  # A failed cancellation must be visible without killing the shared
  # pool/user/group monitor and losing future state changes.
  log(:warn, :monitor, "Unable to cancel NFS for exited container #{ct.ident}: #{e.message}")
end

#check_exiting_runs ⇒ Object (protected)



99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/osctld/monitor/process.rb', line 99

def check_exiting_runs
  @containers.call.each do |ct|
    next unless ct.pool == @pool && ct.user == @user && ct.group == @group

    run_conf = ct.run_conf
    next unless run_conf

    run_conf.nfs_cancellation.capture(run_conf.init_pid)
    run_conf.nfs_cancellation.abort_if_exiting
  rescue StandardError => e
    log(:warn, :monitor, "Unable to check exiting container #{ct.ident}: #{e.message}")
  end
end

#monitor ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/osctld/monitor/process.rb', line 66

def monitor
  exit_checks = Queue.new
  exit_thread = Thread.new do
    check_exiting_runs until exit_checks.pop(timeout: 1)
  end

  until @stdout.eof?
    line = @stdout.readline
    next if line == @last_line

    @last_line = line

    state = parse(line)
    next unless state

    update_state(state)
  end

  true
rescue IOError
  log(:info, :monitor, "Monitoring of #{@pool.name}:#{@user.name}:#{@group.name} failed")
  false
ensure
  begin
    exit_checks << true if exit_checks
    exit_thread&.join
  ensure
    @stdout.close unless @stdout.closed?
  end
end

#parse(line) ⇒ Object (protected)



113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/osctld/monitor/process.rb', line 113

def parse(line)
  if /'([^']+)' changed state to \[([^\]]+)\]/ =~ line
    log(:info, :monitor, "Container #{@pool.name}:#{::Regexp.last_match(1)} entered state #{::Regexp.last_match(2)}")
    return { pool: @pool.name, ctid: ::Regexp.last_match(1), state: ::Regexp.last_match(2).downcase.to_sym }

  elsif /'([^']+)' exited with status \[(\d+)\]/ =~ line
    log(:info, :monitor, "Container #{@pool.name}:#{::Regexp.last_match(1)} exited with #{::Regexp.last_match(2)}")

  else
    log(:warn, :monitor, "Line from lxc-monitor not recognized: '#{line}'")
  end

  nil
end

#update_state(change) ⇒ Object (protected)



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/osctld/monitor/process.rb', line 128

def update_state(change)
  ct = DB::Containers.find(change[:ctid], change[:pool])

  unless ct
    log(:warn, :monitor, "Container #{change[:pool]}:#{change[:ctid]} not found")
    return
  end

  return if ct.state == :error

  # PID1 has exited by STOPPING, but the LXC monitor can still block while
  # releasing NFS mounts. Cancel the retained run before publishing a
  # terminal state that lets a new start discard its namespace handles.
  if %i[stopping stopped aborted].include?(change[:state])
    cancel_exited_run(ct)
  end

  # When transitioning to `running`, send the event only after init_pid was set
  # below, so that when {Commands::Container::Start} finishes waiting and returns,
  # the init_pid is not nil.
  if change[:state] != :running
    Eventd.report(:state, pool: ct.pool.name, id: ct.id, state: change[:state])
  end

  ct.state = change[:state]
  init_pid = nil

  case ct.state
  when :running
    begin
      init_pid = wait_for_init_pid(ct)
      if init_pid
        init_pid = ct.set_init_pid(init_pid)
        ct.ensure_run_conf.nfs_cancellation.capture(init_pid) if init_pid
      end
    rescue ContainerControl::Error, SystemCallError, IOError => e
      init_pid = nil
      log(:warn, :monitor, "Unable to get state of container #{ct.ident}: #{e.message}")
    end

    Eventd.report(:state, pool: ct.pool.name, id: ct.id, state: change[:state])

    if init_pid
      Eventd.report(:ct_init_pid, pool: ct.pool.name, id: ct.id, init_pid:)
    end

    Hook.run(ct, :post_start, init_pid: ct.init_pid)

  when :aborting
    # It has happened that ct.run_conf was nil, circumstances unknown
    ct.ensure_run_conf.aborted = true

  when :stopping
    Hook.run(ct, :on_stop)

  when :stopped, :aborted
    ct.mounts.prune
  end
end

#wait_for_init_pid(ct) ⇒ Object (protected)



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/osctld/monitor/process.rb', line 196

def wait_for_init_pid(ct)
  deadline = Time.now + 10
  st = nil

  loop do
    st = ContainerControl::Commands::State.run!(ct)
    return st.init_pid if st.init_pid
    break if Time.now >= deadline

    sleep(0.1)
  end

  log(:warn, :monitor, "Container #{ct.ident} is running without an init PID, last LXC state was #{st&.state || 'unknown'}")
  nil
end