-- -- lua lxc module -- -- Copyright © 2012 Oracle. -- -- Authors: -- Dwight Engen -- -- This library is free software; you can redistribute it and/or -- modify it under the terms of the GNU Lesser General Public -- License as published by the Free Software Foundation; either -- version 2.1 of the License, or (at your option) any later version. -- -- This library is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -- Lesser General Public License for more details. -- -- You should have received a copy of the GNU Lesser General Public -- License along with this library; if not, write to the Free Software -- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -- local core = require("lxc.core") local lfs = require("lfs") local table = require("table") local string = require("string") local io = require("io") module("lxc", package.seeall) local lxc_path local cgroup_path local log_level = 3 -- lua 5.1 compat if table.unpack == nil then table.unpack = unpack end -- the following two functions can be useful for debugging function printf(...) local function wrapper(...) io.write(string.format(...)) end local status, result = pcall(wrapper, ...) if not status then error(result, 2) end end function log(level, ...) if (log_level >= level) then printf(os.date("%Y-%m-%d %T ")) printf(...) end end function string:split(delim, max_cols) local cols = {} local start = 1 local nextc repeat nextc = string.find(self, delim, start) if (nextc and #cols ~= max_cols - 1) then table.insert(cols, string.sub(self, start, nextc-1)) start = nextc + #delim else table.insert(cols, string.sub(self, start, string.len(self))) nextc = nil end until nextc == nil or start > #self return cols end function dirname(path) local f,output f = io.popen("dirname " .. path) output = f:read('*all') f:close() return output:sub(1,-2) end function basename(path, suffix) local f,output f = io.popen("basename " .. path .. " " .. (suffix or "")) output = f:read('*all') f:close() return output:sub(1,-2) end function cgroup_path_get() local f,line,cgroup_path f = io.open("/proc/mounts", "r") if (f) then while true do local c line = f:read() if line == nil then break end c = line:split(" ", 6) if (c[1] == "cgroup") then cgroup_path = dirname(c[2]) break end end f:close() end if (not cgroup_path) then cgroup_path = "/sys/fs/cgroup" end return cgroup_path end -- container class container = {} container_mt = {} container_mt.__index = container function container:new(lname, config) local lcore local lnetcfg = {} local lstats = {} if lname then if config then lcore = core.container_new(lname, config) else lcore = core.container_new(lname) end end return setmetatable({ctname = lname, core = lcore, netcfg = lnetcfg, stats = lstats}, container_mt) end -- methods interfacing to core functionality function container:config_file_name() return self.core:config_file_name() end function container:defined() return self.core:defined() end function container:init_pid() return self.core:init_pid() end function container:name() return self.core:name() end function container:start() return self.core:start() end function container:stop() return self.core:stop() end function container:shutdown(timeout) return self.core:shutdown(timeout) end function container:wait(state, timeout) return self.core:wait(state, timeout) end function container:freeze() return self.core:freeze() end function container:unfreeze() return self.core:unfreeze() end function container:running() return self.core:running() end function container:state() return self.core:state() end function container:create(template, ...) return self.core:create(template, ...) end function container:destroy() return self.core:destroy() end function container:get_config_path() return self.core:get_config_path() end function container:set_config_path(path) return self.core:set_config_path(path) end function container:append_config_item(key, value) return self.core:set_config_item(key, value) end function container:clear_config_item(key) return self.core:clear_config_item(key) end function container:get_config_item(key) local value local vals = {} value = self.core:get_config_item(key) -- check if it is a single item if (not value or not string.find(value, "\n")) then return value end -- it must be a list type item, make a table of it vals = value:split("\n", 1000) -- make it a "mixed" table, ie both dictionary and list for ease of use for _,v in ipairs(vals) do vals[v] = true end return vals end function container:set_config_item(key, value) return self.core:set_config_item(key, value) end function container:get_keys(base) local ktab = {} local keys if (base) then keys = self.core:get_keys(base) base = base .. "." else keys = self.core:get_keys() base = "" end if (keys == nil) then return nil end keys = keys:split("\n", 1000) for _,v in ipairs(keys) do local config_item = base .. v ktab[v] = self.core:get_config_item(config_item) end return ktab end function container:load_config(alt_path) if (alt_path) then return self.core:load_config(alt_path) else return self.core:load_config() end end function container:save_config(alt_path) if (alt_path) then return self.core:save_config(alt_path) else return self.core:save_config() end end -- methods for stats collection from various cgroup files -- read integers at given coordinates from a cgroup file function container:stat_get_ints(controller, item, coords) local f = io.open(cgroup_path.."/"..controller.."/lxc/"..self.ctname.."/"..item, "r") local lines = {} local result = {} if (not f) then for k,c in ipairs(coords) do table.insert(result, 0) end else for line in f:lines() do table.insert(lines, line) end f:close() for k,c in ipairs(coords) do local col col = lines[c[1]]:split(" ", 80) local val = tonumber(col[c[2]]) table.insert(result, val) end end return table.unpack(result) end -- read an integer from a cgroup file function container:stat_get_int(controller, item) local f = io.open(cgroup_path.."/"..controller.."/lxc/"..self.ctname.."/"..item, "r") if (not f) then return 0 end local line = f:read() f:close() -- if line is nil (on an error like Operation not supported because -- CONFIG_MEMCG_SWAP_ENABLED isn't enabled) return 0 return tonumber(line) or 0 end function container:stat_match_get_int(controller, item, match, column) local val local f = io.open(cgroup_path.."/"..controller.."/lxc/"..self.ctname.."/"..item, "r") if (not f) then return 0 end for line in f:lines() do printf("matching line:%s with match:%s\n", line, match) if (string.find(line, match)) then local col col = line:split(" ", 80) val = tonumber(col[column]) or 0 printf("found line!! val:%d\n", val) end end f:close() return val end function container:stats_get(total) local stat = {} stat.mem_used = self:stat_get_int("memory", "memory.usage_in_bytes") stat.mem_limit = self:stat_get_int("memory", "memory.limit_in_bytes") stat.memsw_used = self:stat_get_int("memory", "memory.memsw.usage_in_bytes") stat.memsw_limit = self:stat_get_int("memory", "memory.memsw.limit_in_bytes") stat.cpu_use_nanos = self:stat_get_int("cpuacct", "cpuacct.usage") stat.cpu_use_user, stat.cpu_use_sys = self:stat_get_ints("cpuacct", "cpuacct.stat", {{1, 2}, {2, 2}}) stat.blkio = self:stat_match_get_int("blkio", "blkio.throttle.io_service_bytes", "Total", 2) if (total) then total.mem_used = total.mem_used + stat.mem_used total.mem_limit = total.mem_limit + stat.mem_limit total.memsw_used = total.memsw_used + stat.memsw_used total.memsw_limit = total.memsw_limit + stat.memsw_limit total.cpu_use_nanos = total.cpu_use_nanos + stat.cpu_use_nanos total.cpu_use_user = total.cpu_use_user + stat.cpu_use_user total.cpu_use_sys = total.cpu_use_sys + stat.cpu_use_sys total.blkio = total.blkio + stat.blkio end return stat end local M = { container = container } function M.stats_clear(stat) stat.mem_used = 0 stat.mem_limit = 0 stat.memsw_used = 0 stat.memsw_limit = 0 stat.cpu_use_nanos = 0 stat.cpu_use_user = 0 stat.cpu_use_sys = 0 stat.blkio = 0 end -- return configured containers found in LXC_PATH directory function M.containers_configured(names_only) local containers = {} for dir in lfs.dir(lxc_path) do if (dir ~= "." and dir ~= "..") then local cfgfile = lxc_path .. "/" .. dir .. "/config" local cfgattr = lfs.attributes(cfgfile) if (cfgattr and cfgattr.mode == "file") then if (names_only) then -- note, this is a "mixed" table, ie both dictionary and list containers[dir] = true table.insert(containers, dir) else local ct = container:new(dir) -- note, this is a "mixed" table, ie both dictionary and list containers[dir] = ct table.insert(containers, dir) end end end end table.sort(containers, function (a,b) return (a < b) end) return containers end -- return running containers found in cgroup fs function M.containers_running(names_only) local containers = {} local attr -- the lxc directory won't exist if no containers has ever been started attr = lfs.attributes(cgroup_path .. "/cpu/lxc") if (not attr) then return containers end for file in lfs.dir(cgroup_path .. "/cpu/lxc") do if (file ~= "." and file ~= "..") then local pathfile = cgroup_path .. "/cpu/lxc/" .. file local attr = lfs.attributes(pathfile) if (attr.mode == "directory") then if (names_only) then -- note, this is a "mixed" table, ie both dictionary and list containers[file] = true table.insert(containers, file) else local ct = container:new(file) -- note, this is a "mixed" table, ie both dictionary and list containers[file] = ct table.insert(containers, file) end end end end table.sort(containers, function (a,b) return (a < b) end) return containers end lxc_path = core.default_config_path_get() cgroup_path = cgroup_path_get() return M