0e93c8b6c8
With hundreds of nodes Cobbler sync cannot fit default 30 secods
timeout. Cobbler performance is going to be investigated in the next
release. By now lets just increase the timeout.
Change-Id: Ief8ff93fc808549e8d729040512a266b0c09383d
Closes-Bug: #1608700
(cherry picked from commit f030161d19
)
345 lines
12 KiB
Ruby
345 lines
12 KiB
Ruby
# 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
|