Class: OsCtld::ContainerControl::Frontend

Inherits:
Object
  • Object
show all
Defined in:
lib/osctld/container_control/frontend.rb

Overview

Frontend is run from osctld in daemon mode, when it is running as root

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command_class, ct) ⇒ Frontend

Returns a new instance of Frontend.

Parameters:



12
13
14
15
# File 'lib/osctld/container_control/frontend.rb', line 12

def initialize(command_class, ct)
  @command_class = command_class
  @ct = ct
end

Instance Attribute Details

#command_classClass (readonly)

Returns:

  • (Class)


5
6
7
# File 'lib/osctld/container_control/frontend.rb', line 5

def command_class
  @command_class
end

#ctContainer (readonly)

Returns:



8
9
10
# File 'lib/osctld/container_control/frontend.rb', line 8

def ct
  @ct
end

Instance Method Details

#exec_runner(opts = {}) ⇒ ContainerControl::Result (protected)

Fork&exec to the container user and invoke the runner.

#exec_runner forks from osctld and then execs into osctld-ct-runner. The runner then switches to the container’s user and enters its cgroups. This runner is safe to use when you need to attach to the container, e.g. with LXC::Container#attach.

It is however more costly than #fork_runner as it makes the Ruby runtime to start all over again. Use #fork_runner when you don’t need to attach to the container.

Parameters:

  • opts (Hash) (defaults to: {})

Options Hash (opts):

  • :args (Array)

    command arguments

  • :kwargs (Hash)

    command arguments

  • :reset_subtree_control (Boolean, nil)
  • :stdin (IO, nil)
  • :stdout (IO, nil)
  • :stderr (IO, nil)

Returns:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/osctld/container_control/frontend.rb', line 46

def exec_runner(opts = {})
  # Used to send command to the runner
  cmd_r, cmd_w = IO.pipe

  # Used to read return value
  ret_r, ret_w = IO.pipe

  # File descriptors to capture output/feed input
  stdin = opts[:stdin]
  stdout = opts.fetch(:stdout, $stdout)
  stderr = opts.fetch(:stderr, $stderr)

  # User configuration
  sysuser = ct.user.sysusername
  ugid = ct.user.ugid
  homedir = ct.user.homedir
  cgroup_path = ct.entry_cgroup_path
  prlimits = ct.prlimits.export
  syslogns_pid = ct.init_pid
  syslogns_tag = syslogns_pid.nil? && ct.syslogns_tag

  # Runner configuration
  runner_opts = {
    name: command_class.name,

    pool: ct.pool.name,
    id: ct.id,
    lxc_home: ct.lxc_home,
    user_home: ct.user.homedir,
    log_file: ct.log_path,

    args: opts.fetch(:args, []),
    kwargs: opts.fetch(:kwargs, {}),

    return: ret_w.fileno,
    stdin: stdin && stdin.fileno,
    stdout: stdout.fileno,
    stderr: stderr.fileno
  }

  CGroup.mkpath_all(cgroup_path.split('/'), chown: ugid)

  # On cgroup v2, we must reset subtree control for lxc-execute to work.
  # The subtree control is configured by osctld when creating the entry_cgroup_path,
  # which is a bit unfortunate in this case.
  if opts.fetch(:reset_subtree_control, false) && CGroup.v2?
    CGroup.reset_subtree_control(ct.abs_cgroup_path(nil))
  end

  pid = SwitchUser.fork(
    keep_fds: [
      cmd_r,
      ret_w,
      stdin,
      stdout,
      stderr
    ].compact
  ) do
    # Closed by SwitchUser.fork
    # cmd_w.close
    # ret_r.close

    $stdin.reopen(cmd_r)

    [cmd_r, ret_w, stdin, stdout, stderr].compact.each do |io|
      io.close_on_exec = false
    end

    SwitchUser.apply_prlimits(Process.pid, prlimits)
    SwitchUser.switch_to(
      sysuser,
      ugid,
      homedir,
      cgroup_path,
      syslogns_pid:,
      syslogns_tag:
    )
    Process.exec(::OsCtld.bin('osctld-ct-runner'))
    exit
  end

  stdin.close if stdin
  stdout.close if stdout != $stdout
  stderr.close if stderr != $stderr

  cmd_w.write(runner_opts.to_json)
  cmd_w.close

  ret_w.close

  begin
    ret = JSON.parse(ret_r.readline, symbolize_names: true)
    Process.wait(pid)
    ContainerControl::Result.from_runner(ret)
  rescue EOFError
    Process.wait(pid)
    ContainerControl::Result.new(
      false,
      message: 'user runner failed',
      user_runner: true
    )
  end
end

#execute(*args, **kwargs) ⇒ Object

Implement this method

Parameters:

  • args (Array)

    command arguments

  • kwargs (Array)

    command arguments

Raises:

  • (NotImplementedError)


20
21
22
# File 'lib/osctld/container_control/frontend.rb', line 20

def execute(*args, **kwargs)
  raise NotImplementedError
end

#fork_runner(opts = {}) ⇒ ContainerControl::Result (protected)

Fork to the container user and invoke the runner.

#fork_runner can be used only when we do not need to enter the container itself. It does not attach to its cgroups, because a forked osctld can have a large memory footprint, which we do not want to charge to the container. It can be used only to interact with LXC from the outside.

Parameters:

  • opts (Hash) (defaults to: {})

Options Hash (opts):

  • :args (Array)

    command arguments

  • :kwargs (Hash)

    command arguments

  • :stdin (IO, nil)
  • :stdout (IO, nil)
  • :stderr (IO, nil)
  • :switch_to_system (Boolean)

Returns:



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/osctld/container_control/frontend.rb', line 166

def fork_runner(opts = {})
  r, w = IO.pipe

  stdin = opts[:stdin]
  stdout = opts.fetch(:stdout, $stdout)
  stderr = opts.fetch(:stderr, $stderr)

  runner_opts = {
    id: ct.id,
    lxc_home: ct.lxc_home,
    user_home: ct.user.homedir,
    log_file: ct.log_path,
    stdin:,
    stdout:,
    stderr:
  }

  ctid = ct.ident
  args = opts.fetch(:args, [])
  kwargs = opts.fetch(:kwargs, {})
  sysuser = ct.user.sysusername
  ugid = ct.user.ugid
  homedir = ct.user.homedir

  pid = SwitchUser.fork(keep_fds: [w, stdin, stdout, stderr].compact) do
    # Closed by SwitchUser.fork
    # r.close

    Process.setproctitle(
      "osctld: #{ctid} " \
      "runner:#{command_class.name.split('::').last.downcase}"
    )

    if opts.fetch(:switch_to_system, true)
      SwitchUser.switch_to_system(sysuser, ugid, ugid, homedir)
    end

    runner = command_class::Runner.new(**runner_opts)
    ret = runner.execute(*args, **kwargs)
    w.write("#{ret.to_json}\n")

    exit
  end

  w.close

  begin
    ret = JSON.parse(r.readline, symbolize_names: true)
    Process.wait(pid)
    ContainerControl::Result.from_runner(ret)
  rescue EOFError
    Process.wait(pid)
    ContainerControl::Result.new(
      false,
      message: 'user runner failed',
      user_runner: true
    )
  end
end