=begin __ ___ _ _ _____ ____ __ __ \ \ / / |__ (_) |_ ___| ___| _ ___ ___ / ___| \/ | \ \ /\ / /| '_ \| | __/ _ \ |_ | | | / __|/ _ \ | | |\/| | \ V V / | | | | | || __/ _|| |_| \__ \ __/ |___| | | | \_/\_/ |_| |_|_|\__\___|_| \__,_|___/\___|\____|_| |_| Container Manager Copyright (c) 2015 David Prandzioch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =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 # @param template [String] Name of the template to use # # @raise [RuntimeError] # # @return [String] CLI output def create_container(name, ip_address, disk_size_gb, memory_limit_mb, cpu_core_count, template) output = '' begin if cpu_core_count != 0 cpuset = generate_cpu_set(cpu_core_count, ResourceManager.new('linux')) end if false == $vserver_cmd_create.has_key?(template) raise ArgumentError, "template does not exist" end new_context = highest_context() + 1 cmd = $vserver_cmd_create[template].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 if memory_limit_mb.to_i != 0 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) end 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 # Returns a list of supported templates # # @return [Hash] List of supported templates def supported_templates $vserver_cmd_create.keys 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