=begin __ ___ _ _ _____ ____ __ __ \ \ / / |__ (_) |_ ___| ___| _ ___ ___ / ___| \/ | \ \ /\ / /| '_ \| | __/ _ \ |_ | | | / __|/ _ \ | | |\/| | \ V V / | | | | | || __/ _|| |_| \__ \ __/ |___| | | | \_/\_/ |_| |_|_|\__\___|_| \__,_|___/\___|\____|_| |_| Container Manager Copyright (C) 2015 David Prandzioch This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. =end module ContainerManagerAdapter # Container adapter for linux-vserver. Encapsulates all vserver specific command line # wrapping class Vserver # Lists all available containers # # @return [Array] def containers container_list = [] self.container_names.each do |item| container_list << container(item) end container_list end # Starts a container with the given name # # @param name [String] The container name # # @raise [RuntimeError] # # @return [String] CLI output def start(name) res = Open3.capture3($vserver_cmd_start.gsub('[name]', name)) if res[2].exitstatus == 0 && state(name) == 'RUNNING' $logger.info("container " + name + " successfully started") return res[0].strip end $logger.warn("container " + name + " could not be started") raise RuntimeError, res[1].strip end # Stops a container with the given name # # @param name [String] The container name # # @raise [RuntimeError] # # @return [String] CLI output def stop(name) if state(name) == 'STOPPED' $logger.warn("container " + name + " could not be stopped, because it is not running") raise RuntimeError, 'container is not running' end res = Open3.capture3($vserver_cmd_stop.gsub('[name]', name)) if res[2].exitstatus == 0 && state(name) == 'STOPPED' $logger.info("container " + name + " successfully stopped") return res[0].strip end $logger.warn("container " + name + " could not be stopped") raise RuntimeError, res[1].strip end # Kills a container with the given name # # @param name [String] The container name # # @raise [RuntimeError] # # @return [String] CLI output def kill(name) return stop(name) end # Deletes a container with the given name # # @param name [String] The container name # # @raise [RuntimeError] # # @return [String] CLI output def delete(name) res = Open3.capture3($vserver_cmd_destroy.gsub('[name]', name)) if res[1].empty? && res[2].exitstatus == 0 $logger.info("container " + name + " successfully deleted") return res[0].strip end $logger.warn("container" + name + " could not be deleted") raise RuntimeError, res[1].strip end # Returns information for a single container # # @param name [String] The container name # # @return [Hash] Hash with information about the container def container(name) data = {} data[:name] = name data[:state] = translate_state(state(name)) data[:ip_address] = ip_addr(name) data[:cpu_usage] = 0 data[:cpu_cores] = assigned_cpu_cores(name).count data[:memory_limit_bytes] = memory_limit(name).to_i data[:memory_usage_bytes] = memory_usage(name).to_i data[:disk_space_gb] = 0 data[:disk_usage_gb] = 0 data[:container_type] = 'vserver' data end # Creates a container with the given parameters # # @param name [String] The container name # @param ip_address [String] A valid IPv4 address # @param disk_size_gb [Integer] The disk size in GB # @param memory_limit_mb [Integer] The memory limit in MB # @param cpu_core_count [Integer] Amount of Vcores to assign. 0 for no limit # # @raise [RuntimeError] # # @return [String] CLI output def create_container(name, ip_address, disk_size_gb, memory_limit_mb, cpu_core_count) output = '' begin if cpu_core_count != 0 cpuset = generate_cpu_set(cpu_core_count, ResourceManager.new('linux')) end new_context = highest_context() + 1 cmd = $vserver_cmd_create.gsub('[name]', name).gsub('[ip_address]', ip_address).gsub('[context]', new_context.to_s) create_result = Open3.capture3(cmd) output += create_result[0] output += create_result[1] if create_result[2].exitstatus != 0 raise RuntimeError, 'command did not exit with status 0' end # memory limit memory_limit_bytes = memory_limit_mb.to_i * 1024 * 1024 page_size = `#{$page_size_cmd}`.to_i memory_limit_pages = memory_limit_bytes / page_size write_config_file(name, '/rlimits/rss.hard', memory_limit_pages.to_s) if cpu_core_count != 0 # cpu core limit write_config_file(name, '/cgroup/cpuset.cpus', cpuset) end $logger.info("creation of container " + name + " successful") return output.strip rescue => e $logger.warn("container " + name + " could not be created, rolling back...") # rollback delete(name) if exist?(name) output += e.message raise RuntimeError, output.strip end end # Checks if a container with the given name exists # # @param name [String] The container name # # @return [Boolean] Existing/not existing def exist?(name) self.container_names.include?(name) end # Returns the amount of free cpu cores # # @param resman [ResourceManager] A ResourceManager instance # # @return [Integer] The amount of free cpu cores def free_cpu_core_count(resman) self.free_cpu_cores(resman).count end protected # Writes the config file of a vserver # # @param name [String] The container name # @param path [String] Filepath # @param content [String] File contents # # @return [Boolean] Result of File.write def write_config_file(name, path, content) config_path = "#{$vserver_config_dir.gsub('[name]', name)}#{path}" `mkdir -p #{File.dirname(config_path)}` File.write(config_path, content) end # Reads the config file of a vserver # # @param name [String] The container name # # @raise [StandardError] # # @return [String] Contents of the configuration file def read_config_file(name, path) config_path = "#{$vserver_config_dir.gsub('[name]', name)}#{path}" if File.exists?(config_path) == false return nil end File.read(config_path).strip end # Creates a map of the vserver contexts # # @return [Hash] def context_map map = {} containers.each() do |container| map[container[:name]] = context(container[:name]) end map end # Reads the context for a specific container # # @param name [String] The container name # # @raise [RuntimeError] # # @return [Integer] The context ID def context(name) file = "#{$vserver_config_dir.gsub('[name]', name)}/context" if false == File.readable?(file) raise RuntimeError, "file #{file} is not readable" end context = File.read(file) context.to_i end # Returns the stats for a container # # @param name [String] The container name # # @return [Array] Stats def vserver_stat(name) res = `#{$vserver_cmd_stat.gsub('[name]', name)}` res.split(' ').map(&:strip) end # Returns the current memory usage # # @param name [String] The container name # # @return [Integer] Memory usage def memory_usage(name) stat = vserver_stat(name) if stat[2] == nil return nil end (stat[2][0..-2].to_f * 1024.0 * 1024.0).to_i end # Finds the highest context # # @return [Integer] The highest context def highest_context contextmap = context_map() if contextmap.empty? return 1 end contextmap.max_by{|k, v| v}.last end # Returns the ip address for a container # # @return [String] The IP address def ip_addr(name) read_config_file(name, '/interfaces/0/ip') end # Returns the state for a container # # @param name [String] The container name # # @return [String] Container status, STOPPED or RUNNING def state(name) output = `#{$vserver_cmd_stat.gsub('[name]', name)}` if output.empty? return 'STOPPED' else return 'RUNNING' end end # Returns the memory limit for a container # # @param name [String] The container name # # @return [Integer|NilClass] Memory limit in bytes def memory_limit(name) ctx = context(name) page_size = `#{$page_size_cmd}`.to_i memory_limit_result = `#{$vserver_cmd_get_memory_limit.gsub('[context]', ctx.to_s)}` parts = memory_limit_result.split(' ').map(&:strip) if parts.empty? return nil else return (parts.last.to_i * page_size) end end # Gets the globally assigned cpu cores # # @return [Array] def used_cpu_cores used_cores = [] container_names.each do |c| self.assigned_cpu_cores(c).each do |cpu| used_cores.push(cpu) end end used_cores end # Gets the cpu cores assigned to a container # # @param name [String] The container name # # @return [Array] with cpu core ids def assigned_cpu_cores(name) value = self.read_config_file(name, '/cgroup/cpuset.cpus') used_cores = [] if value == nil return used_cores end value.split(',').each do |cpu| if cpu.include?('-') range = Range.new(*cpu.split('-').map(&:to_i)) range.each do |i| used_cores.push(i) end else used_cores.push(cpu.to_i) end end used_cores end # Finds free cpu cores # # @param resman [ResourceManager] A ResourceManager instance # # @return [Array] Free cpu cores def free_cpu_cores(resman) raise ArgumentError, 'no valid ResourceManager instance' unless resman.is_a?(ResourceManager) free_cores = [] used_cores = self.used_cpu_cores() (0..(resman.total_cpu_cores-1)).each do |i| if used_cores.include?(i) == false free_cores.push(i) end end free_cores end # Generates a cpu set for a container # # @param count [Integer] Number of cpu cores to use # @param resman [ResourceManager] A ResourceManager instance # # @raise [RuntimeError] # # @return [String] cpuset string def generate_cpu_set(count, resman) count = count.to_i free_cores = self.free_cpu_cores(resman) if count > free_cores.count raise RuntimeError, 'not enough cpu cores left' end cores_to_use = [] count.times do cores_to_use.push(free_cores.shift) end cores_to_use.join(',') end def container_names `#{$vserver_cmd_ls}`.lines.map(&:strip).uniq end end end