fuel-astute/lib/astute/cobbler.rb

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