# Copyright 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. require 'xmlrpc/client' module Astute module Provision class CobblerError < RuntimeError; end class Cobbler attr_reader :remote def initialize(o={}) Astute.logger.debug("Cobbler options:\n#{o.pretty_inspect}") if (match = /^http:\/\/([^:]+?):?(\d+)?(\/.+)/.match(o['url'])) host = match[1] port = match[2] || '80' path = match[3] else host = o['host'] || 'localhost' port = o['port'] || '80' path = o['path'] || '/cobbler_api' end @username = o['username'] || 'cobbler' @password = o['password'] || 'cobbler' Astute.logger.debug("Connecting to cobbler with: host: #{host} port: #{port} path: #{path}") @remote = XMLRPC::Client.new(host, path, port) @remote.timeout = 120 Astute.logger.debug("Cobbler initialize with username: #{@username}, password: #{@password}") end def token remote.call('login', @username, @password) end def item_from_hash(what, name, data, opts = {}) options = { :item_preremove => true, }.merge!(opts) cobsh = Cobsh.new(data.merge({'what' => what, 'name' => name})) cobblerized = cobsh.cobblerized Astute.logger.debug("Creating/editing item from hash:\n#{cobsh.pretty_inspect}") remove_item(what, name) if options[:item_preremove] # get existent item id or create new one item_id = get_item_id(what, name) # defining all item options cobblerized.each do |opt, value| next if opt == 'interfaces' Astute.logger.debug("Setting #{what} #{name} opt: #{opt}=#{value}") remote.call('modify_item', what, item_id, opt, value, token) end # defining system interfaces if what == 'system' && cobblerized.has_key?('interfaces') Astute.logger.debug("Defining system interfaces #{name} #{cobblerized['interfaces']}") remote.call('modify_system', item_id, 'modify_interface', cobblerized['interfaces'], token) end # save item into cobbler database Astute.logger.debug("Saving #{what} #{name}") remote.call('save_item', what, item_id, token) end def remove_item(what, name, recursive=true) remote.call('remove_item', what, name, token, recursive) if item_exists(what, name) end def remove_system(name) remove_item('system', name) end def item_exists(what, name) remote.call('has_item', what, name) end def items_by_criteria(what, criteria) remote.call('find_items', what, criteria) end def system_by_mac(mac) items_by_criteria('system', {"mac_address" => mac})[0] end def system_exists?(name) item_exists('system', name) end def get_item_id(what, name) if item_exists(what, name) item_id = remote.call('get_item_handle', what, name, token) else item_id = remote.call('new_item', what, token) remote.call('modify_item', what, item_id, 'name', name, token) end item_id end def sync remote.call('sync', token) rescue Net::ReadTimeout, XMLRPC::FaultException => e retries ||= 0 retries += 1 raise e if retries > 2 Astute.logger.warn("Cobbler problem. Try to repeat: #{retries} attempt") sleep 10 retry end def power(name, action) options = {"systems" => [name], "power" => action} remote.call('background_power_system', options, token) end def power_on(name) power(name, 'on') end def power_off(name) power(name, 'off') end def power_reboot(name) power(name, 'reboot') end def event_status(event_id) remote.call('get_task_status', event_id) end def netboot(name, state) state = ['on', 'yes', true, 'true', 1, '1'].include?(state) if system_exists?(name) system_id = get_item_id('system', name) else raise CobblerError, "System #{name} not found." end remote.call('modify_system', system_id, 'netboot_enabled', state, token) remote.call('save_system', system_id, token, 'edit') end end class Cobsh < ::Hash ALIASES = { 'ks_meta' => ['ksmeta'], 'mac_address' => ['mac'], 'ip_address' => ['ip'], } # these fields can be get from the cobbler code # you can just import cobbler.item_distro.FIELDS # or cobbler.item_system.FIELDS FIELDS = { 'system' => { 'fields' => [ 'name', 'owners', 'profile', 'image', 'status', 'kernel_options', 'kernel_options_post', 'ks_meta', 'enable_gpxe', 'proxy', 'netboot_enabled', 'kickstart', 'comment', 'server', 'virt_path', 'virt_type', 'virt_cpus', 'virt_file_size', 'virt_disk_driver', 'virt_ram', 'virt_auto_boot', 'power_type', 'power_address', 'power_user', 'power_pass', 'power_id', 'hostname', 'gateway', 'name_servers', 'name_servers_search', 'ipv6_default_device', 'ipv6_autoconfiguration', 'mgmt_classes', 'mgmt_parameters', 'boot_files', 'fetchable_files', 'template_files', 'redhat_management_key', 'redhat_management_server', 'repos_enabled', 'ldap_enabled', 'ldap_type', 'monit_enabled', ], 'interfaces_fields' => [ 'mac_address', 'mtu', 'ip_address', 'interface_type', 'interface_master', 'bonding_opts', 'bridge_opts', 'management', 'static', 'netmask', 'dhcp_tag', 'dns_name', 'static_routes', 'virt_bridge', 'ipv6_address', 'ipv6_secondaries', 'ipv6_mtu', 'ipv6_static_routes', 'ipv6_default_gateway' ], 'special' => ['interfaces', 'interfaces_extra'] }, 'profile' => { 'fields' => [ 'name', 'owners', 'distro', 'parent', 'enable_gpxe', 'enable_menu', 'kickstart', 'kernel_options', 'kernel_options_post', 'ks_meta', 'proxy', 'repos', 'comment', 'virt_auto_boot', 'virt_cpus', 'virt_file_size', 'virt_disk_driver', 'virt_ram', 'virt_type', 'virt_path', 'virt_bridge', 'dhcp_tag', 'server', 'name_servers', 'name_servers_search', 'mgmt_classes', 'mgmt_parameters', 'boot_files', 'fetchable_files', 'template_files', 'redhat_management_key', 'redhat_management_server' ] }, 'distro' => { 'fields' => ['name', 'owners', 'kernel', 'initrd', 'kernel_options', 'kernel_options_post', 'ks_meta', 'arch', 'breed', 'os_version', 'comment', 'mgmt_classes', 'boot_files', 'fetchable_files', 'template_files', 'redhat_management_key', 'redhat_management_server'] } } def initialize(h) Astute.logger.debug("Cobsh is initialized with:\n#{h.pretty_inspect}") raise CobblerError, "Cobbler hash must have 'name' key" unless h.has_key? 'name' raise CobblerError, "Cobbler hash must have 'what' key" unless h.has_key? 'what' raise CobblerError, "Unsupported 'what' value" unless FIELDS.has_key? h['what'] h.each{|k, v| store(k, v)} end def cobblerized Astute.logger.debug("Cobblerizing hash:\n#{pretty_inspect}") ch = {} ks_meta = '' kernel_options = '' each do |k, v| k = aliased(k) if ch.has_key?(k) && ch[k] == v next elsif ch.has_key?(k) raise CobblerError, "Wrong cobbler data: #{k} is duplicated" end # skiping not valid item options unless valid_field?(k) Astute.logger.warn("Key #{k} is not valid. Will be skipped.") next end ks_meta = serialize_cobbler_parameter(v) if 'ks_meta' == k kernel_options = serialize_cobbler_parameter(v) if 'kernel_options' == k # special handling for system interface fields # which are the only objects in cobbler that will ever work this way if k == 'interfaces' ch.store('interfaces', cobblerized_interfaces) next end # here we convert interfaces_extra options into ks_meta format if k == 'interfaces_extra' ks_meta << cobblerized_interfaces_extra next end ch.store(k, v) end # each do |k, v| ch.store('ks_meta', ks_meta.strip) unless ks_meta.strip.empty? ch.store('kernel_options', kernel_options.strip) unless kernel_options.strip.empty? ch end def serialize_cobbler_parameter(param) serialized_param = '' if param.kind_of?(Hash) param.each do |ks_meta_key, ks_meta_value| serialized_param << " #{ks_meta_key}=#{serialize_cobbler_value(ks_meta_value)}" end elsif param.kind_of?(String) param else raise CobblerError, "Wrong param format. It must be Hash or String: '#{param}'" end serialized_param end def serialize_cobbler_value(value) if value.kind_of?(Hash) || value.kind_of?(Array) return "\"#{value.to_json.gsub('"', '\"')}\"" end value end def aliased(k) # converting 'foo-bar' keys into 'foo_bar' keys k1 = k.gsub(/-/,'_') # converting orig keys into alias keys # example: 'ksmeta' into 'ks_meta' k2 = ALIASES.each_key.select{|ak| ALIASES[ak].include?(k1)}[0] || k1 Astute.logger.debug("Key #{k} aliased with #{k2}") if k != k2 k2 end def valid_field?(k) (FIELDS[fetch('what')]['fields'].include?(k) or (FIELDS[fetch('what')]['special'] or []).include?(k)) end def valid_interface_field?(k) (FIELDS[fetch('what')]['interfaces_fields'] or []).include?(k) end def cobblerized_interfaces interfaces = {} fetch('interfaces').each do |iname, ihash| ihash.each do |iopt, ivalue| iopt = aliased(iopt) if interfaces.has_key?("#{iopt}-#{iname}") raise CobblerError, "Wrong interface cobbler data: #{iopt} is duplicated" end unless valid_interface_field?(iopt) Astute.logger.debug("Interface key #{iopt} is not valid. Skipping") next end Astute.logger.debug("Defining interfaces[#{iopt}-#{iname}] = #{ivalue}") interfaces["#{iopt}-#{iname}"] = ivalue end end interfaces end def cobblerized_interfaces_extra # here we just want to convert interfaces_extra into ks_meta interfaces_extra_str = "" fetch('interfaces_extra').each do |iname, iextra| iextra.each do |k, v| Astute.logger.debug("Adding into ks_meta interface_extra_#{iname}_#{k}=#{v}") interfaces_extra_str << " interface_extra_#{iname}_#{k}=#{v}" end end interfaces_extra_str end end end end