372 lines
14 KiB
Ruby
372 lines
14 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 'yaml'
|
|
require 'json'
|
|
require 'rest-client'
|
|
require 'astute/ext/hash'
|
|
require 'astute/cli/enviroment'
|
|
require 'astute/cli/yaml_validator'
|
|
|
|
module Astute
|
|
module Cli
|
|
|
|
class Enviroment
|
|
|
|
POWER_INFO_KEYS = ['power_type', 'power_user', 'power_pass', 'netboot_enabled']
|
|
ID_KEYS = ['id', 'uid']
|
|
COMMON_NODE_KEYS = ['name_servers']
|
|
KS_META_KEYS = ['mco_enable', 'mco_vhost', 'mco_pskey', 'mco_user', 'puppet_enable',
|
|
'install_log_2_syslog', 'mco_password', 'puppet_auto_setup', 'puppet_master',
|
|
'mco_auto_setup', 'auth_key', 'puppet_version', 'mco_connector', 'mco_host']
|
|
PROVISIONING_NET_KEYS = ['ip', 'power_address', 'mac', 'fqdn']
|
|
PROVISION_OPERATIONS = [:provision, :provision_and_deploy]
|
|
DEPLOY_OPERATIONS = [:deploy, :provision_and_deploy]
|
|
|
|
CIDR_REGEXP = '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|
|
|
2[0-4][0-9]|25[0-5])(\/(\d|[1-2]\d|3[0-2]))$'
|
|
|
|
def initialize(file, operation)
|
|
@config = YAML.load_file(file)
|
|
validate_enviroment(operation)
|
|
to_full_config(operation)
|
|
end
|
|
|
|
def [](key)
|
|
@config[key]
|
|
end
|
|
|
|
private
|
|
|
|
def to_full_config(operation)
|
|
@config['nodes'].each do |node|
|
|
|
|
# Common section
|
|
node['meta'] ||= {}
|
|
define_provisioning_network(node)
|
|
define_id_and_uid(node)
|
|
|
|
# Provision section
|
|
if PROVISION_OPERATIONS.include? operation
|
|
define_interfaces_and_interfaces_extra(node)
|
|
define_ks_spaces(node)
|
|
define_power_info(node)
|
|
define_ks_meta(node)
|
|
define_node_settings(node)
|
|
define_disks_section(node)
|
|
end
|
|
|
|
# Deploy section
|
|
if DEPLOY_OPERATIONS.include? operation
|
|
define_meta_interfaces(node)
|
|
define_fqdn(node)
|
|
define_network_data(node)
|
|
end
|
|
end
|
|
end
|
|
|
|
def validate_enviroment(operation)
|
|
validator = YamlValidator.new(operation)
|
|
errors = validator.validate(@config)
|
|
|
|
errors.each do |e|
|
|
if e.message.include?("is undefined")
|
|
Astute.logger.debug "WARNING: [#{e.path}] #{e.message}"
|
|
else
|
|
Astute.logger.error "ERROR: [#{e.path}] #{e.message}"
|
|
end
|
|
end
|
|
|
|
if errors.select {|e| !e.message.include?("is undefined") }.size > 0
|
|
raise Enviroment::ValidationError, "Environment validation failed"
|
|
end
|
|
|
|
if DEPLOY_OPERATIONS.include?(operation)
|
|
if @config['attributes']['quantum']
|
|
@config['nodes'].each do |node|
|
|
['public_br', 'internal_br'].each do |br|
|
|
if node[br].nil? || node[br].empty?
|
|
raise Enviroment::ValidationError, "Node #{node['name'] || node['hostname']}
|
|
required 'public_br' and 'internal_br' when quantum is 'true'"
|
|
end
|
|
end
|
|
end
|
|
|
|
errors = []
|
|
['quantum_parameters', 'quantum_access'].each do |param|
|
|
errors << param unless @config['attributes'].present?(param)
|
|
end
|
|
errors.each do |field|
|
|
msg = "#{field} is required when quantim is true"
|
|
raise Enviroment::ValidationError, msg
|
|
end
|
|
|
|
if !is_cidr_notation?(@config['attributes']['floating_network_range'])
|
|
msg = "'floating_network_range' is required CIDR notation when quantum is 'true'"
|
|
raise Enviroment::ValidationError, msg
|
|
end
|
|
|
|
if !is_cidr_notation?(@config['attributes']['floating_network_range'])
|
|
msg = "'floating_network_range' is required CIDR notation"
|
|
raise Enviroment::ValidationError, msg
|
|
end
|
|
else
|
|
if @config['attributes']['floating_network_range'].is_a?(Array)
|
|
msg = "'floating_network_range' is required array of IPs when quantum is 'false'"
|
|
raise Enviroment::ValidationError, msg
|
|
end
|
|
end
|
|
if !is_cidr_notation?(@config['attributes']['fixed_network_range'])
|
|
msg = "'fixed_network_range' is required CIDR notation"
|
|
raise Enviroment::ValidationError, msg
|
|
end
|
|
end
|
|
end
|
|
|
|
# Get data about discovered nodes using FuelWeb API
|
|
def find_node_api_data(node)
|
|
@api_data ||= begin
|
|
response = RestClient.get 'http://localhost:8000/api/nodes'
|
|
@api_data = JSON.parse(response).freeze
|
|
end
|
|
if node['mac']
|
|
api_node = @api_data.find{ |n| n['mac'].upcase == node['mac'].upcase }
|
|
return api_node if api_node
|
|
end
|
|
raise Enviroment::ValidationError, "Node #{node['name']} with mac address #{node['mac']}
|
|
not find among discovered nodes"
|
|
end
|
|
|
|
# Set uniq id and uid for node from Nailgun using FuelWeb API
|
|
def define_id_and_uid(node)
|
|
id = find_node_api_data(node)['id']
|
|
|
|
# This params set for node by Nailgun and should not be edit by user
|
|
node.merge!(
|
|
'id' => id,
|
|
'uid' => id
|
|
)
|
|
end
|
|
|
|
|
|
# Set meta/disks section for node. This data used in provision to calculate the percentage
|
|
# completion of the installation process.
|
|
# Example result for node['meta']
|
|
# "disks": [
|
|
# {
|
|
# "model": "VBOX HARDDISK",
|
|
# "disk": "disk/by-path/pci-0000:00:0d.0-scsi-0:0:0:0",
|
|
# "name": "sda",
|
|
# "size": 17179869184
|
|
# }...
|
|
# ]
|
|
def define_disks_section(node)
|
|
node['meta']['disks'] = find_node_api_data(node)['meta']['disks']
|
|
end
|
|
|
|
def define_parameters(node, config_group_name, keys, position=nil)
|
|
position ||= node
|
|
if @config[config_group_name]
|
|
config_group = @config[config_group_name]
|
|
keys.each do |key|
|
|
position.reverse_merge!(key => config_group[key])
|
|
end
|
|
end
|
|
|
|
absent_keys = position.absent_keys(keys)
|
|
if !absent_keys.empty?
|
|
raise Enviroment::ValidationError, "Please set #{config_group_name} block or
|
|
set params for #{node['name']} manually #{absent_keys.each {|k| p k}}"
|
|
end
|
|
@config.delete(config_group)
|
|
end
|
|
|
|
# Add common params from common_node_settings to every node. Already certain parameters will not be changed.
|
|
def define_node_settings(node)
|
|
define_parameters(node, 'common_node_settings', COMMON_NODE_KEYS)
|
|
end
|
|
|
|
# Add common params from common_power_info to every node. Already certain parameters will not be changed.
|
|
def define_power_info(node)
|
|
define_parameters(node, 'common_power_info', POWER_INFO_KEYS)
|
|
end
|
|
|
|
# Add common params from common_ks_meta to every node. Already certain parameters will not be changed.
|
|
def define_ks_meta(node)
|
|
define_parameters(node, 'common_ks_meta', KS_META_KEYS, node['ks_meta'])
|
|
end
|
|
|
|
# Add duplicates params to node: ip, power_address, mac, fqdn
|
|
def define_provisioning_network(node)
|
|
provision_eth = node['interfaces'].find {|eth| eth['use_for_provision'] } rescue nil
|
|
|
|
if provision_eth
|
|
if provision_eth.absent?('ip_address')
|
|
node['mac'] = provision_eth['mac_address']
|
|
api_node = find_node_api_data(node)
|
|
api_provision_eth = api_node['meta']['interfaces'].find { |n| n['mac'].to_s.upcase == provision_eth['mac_address'].to_s.upcase }
|
|
provision_eth['ip_address'] = api_provision_eth['ip']
|
|
provision_eth['netmask'] = api_provision_eth['netmask']
|
|
end
|
|
|
|
#define_parameters(node, 'use_for_provision', PROVISIONING_NET_KEYS)
|
|
|
|
node.reverse_merge!(
|
|
'ip' => provision_eth['ip_address'],
|
|
'power_address' => provision_eth['ip_address'],
|
|
'mac' => provision_eth['mac_address'],
|
|
'fqdn' => provision_eth['dns_name']
|
|
)
|
|
provision_eth.delete('use_for_provision')
|
|
end
|
|
|
|
|
|
absent_keys = node.absent_keys(PROVISIONING_NET_KEYS)
|
|
if !absent_keys.empty?
|
|
raise Enviroment::ValidationError, "Please set 'use_for_provision' parameter
|
|
for #{node['name']} or set manually #{absent_keys.each {|k| p k}}"
|
|
end
|
|
end
|
|
|
|
# Extend blocks interfaces and interfaces_extra to old formats:
|
|
# interfaces:
|
|
# eth0:
|
|
# ip_address: 10.20.0.188
|
|
# netmask: 255.255.255.0
|
|
# dns_name: controller-22.domain.tld
|
|
# static: '0'
|
|
# mac_address: 08:00:27:C2:06:DE
|
|
# interfaces_extra:
|
|
# eth0:
|
|
# onboot: 'yes'
|
|
# peerdns: 'no'
|
|
def define_interfaces_and_interfaces_extra(node)
|
|
return if [node['interfaces'], node['extra_interfaces']].all? {|i| i.is_a?(Hash)}
|
|
|
|
formated_interfaces = {}
|
|
interfaces_extra_interfaces = {}
|
|
node['interfaces'].each do |eth|
|
|
formated_interfaces[eth['name']] = eth
|
|
formated_interfaces[eth['name']].delete('name')
|
|
interfaces_extra_interfaces[eth['name']] = {
|
|
'onboot' => eth['onboot'],
|
|
'peerdns' => eth['onboot']
|
|
}
|
|
end
|
|
node['interfaces'] = formated_interfaces
|
|
node['extra_interfaces'] = interfaces_extra_interfaces
|
|
end
|
|
|
|
# Add duplicate param 'fqdn' to node if it is not specified
|
|
def define_fqdn(node)
|
|
node['fqdn'] ||= find_node_api_data(node)['meta']['system']['fqdn']
|
|
end
|
|
|
|
# Add meta/interfaces section for node:
|
|
# meta:
|
|
# interfaces:
|
|
# - name: eth0
|
|
# ip: 10.20.0.95
|
|
# netmask: 255.255.255.0
|
|
# mac: 08:00:27:C2:06:DE
|
|
# max_speed: 100
|
|
# current_speed: 100
|
|
def define_meta_interfaces(node)
|
|
node['meta']['interfaces'] = find_node_api_data(node)['meta']['interfaces']
|
|
end
|
|
|
|
# Add network_data section for node:
|
|
# network_data:
|
|
# - dev: eth1
|
|
# ip: 10.108.1.8
|
|
# name: public
|
|
# netmask: 255.255.255.0
|
|
# - dev: eth0
|
|
# ip: 10.108.0.8
|
|
# name:
|
|
# - management
|
|
# - storage
|
|
def define_network_data(node)
|
|
return if node['network_data'].is_a?(Array) && !node['network_data'].empty?
|
|
|
|
node['network_data'] = []
|
|
|
|
# If define_interfaces_and_interfaces_extra was call or format of config is full
|
|
if node['interfaces'].is_a?(Hash)
|
|
node['interfaces'].each do |key, value|
|
|
node['network_data'] << {
|
|
'dev' => key,
|
|
'ip' => value['ip_address'],
|
|
'name' => value['network_name'],
|
|
'netmask' => value['netmask']
|
|
}
|
|
end
|
|
else
|
|
node['interfaces'].each do |eth|
|
|
node['network_data'] << {
|
|
'dev' => eth['name'],
|
|
'ip' => eth['ip_address'],
|
|
'name' => eth['network_name'],
|
|
'netmask' => eth['netmask']
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
# Generate 'ks_spaces' param from 'ks_disks' param in section 'ks_meta'
|
|
# Example input for 'ks_disks' param:
|
|
# [{
|
|
# "type"=>"disk",
|
|
# "id"=>"disk/by-path/pci-0000:00:0d.0-scsi-0:0:0:0",
|
|
# "size"=>16384,
|
|
# "volumes"=>[
|
|
# {
|
|
# "type"=>"boot",
|
|
# "size"=>300
|
|
# },
|
|
# {
|
|
# "type"=>"pv",
|
|
# "size"=>16174,
|
|
# "vg"=>"os"
|
|
# }
|
|
# ]
|
|
# }]
|
|
# Example result for 'ks_spaces' param: "[{"type": "disk", "id": "disk/by-path/pci-0000:00:0d.0-scsi-0:0:0:0", "volumes": [{"type": "boot", "size": 300}, {"mount": "/boot", "type": "raid", "size": 200}, {"type": "lvm_meta", "name": "os", "size": 64}, {"size": 11264, "type": "pv", "vg": "os"}, {"type": "lvm_meta", "name": "image", "size": 64}, {"size": 4492, "type": "pv", "vg": "image"}], "size": 16384}]"
|
|
def define_ks_spaces(node)
|
|
if node['ks_meta'].present? 'ks_spaces'
|
|
node['ks_meta'].delete('ks_disks')
|
|
return
|
|
end
|
|
|
|
if node['ks_meta'].absent? 'ks_disks'
|
|
raise Enviroment::ValidationError, "Please set 'ks_disks' or 'ks_spaces' parameter
|
|
in section ks_meta for #{node['name']}"
|
|
end
|
|
|
|
node['ks_meta']['ks_spaces'] = '"' + node['ks_meta']['ks_disks'].to_json.gsub("\"", "\\\"") + '"'
|
|
node['ks_meta'].delete('ks_disks')
|
|
end
|
|
|
|
def is_cidr_notation?(value)
|
|
cidr = Regexp.new(CIDR_REGEXP)
|
|
!cidr.match(value).nil?
|
|
end
|
|
|
|
end # class end
|
|
|
|
class Enviroment::ValidationError < StandardError; end
|
|
|
|
end # module Cli
|
|
end
|