# encoding: UTF-8 # # Cookbook Name:: openstack-object-storage # Resource:: ring_script # # Copyright 2012, Rackspace US, 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. # # Author: Ron Pedde # require 'pp' # rubocop:disable PerlBackrefs, CyclomaticComplexity, MethodLength def generate_script # need to load and parse the existing rings. ports = { 'object' => '6000', 'container' => '6001', 'account' => '6002' } must_rebalance = false ring_path = @new_resource.ring_path ring_data = { raw: {}, parsed: {}, in_use: {} } disk_data = {} dirty_cluster_reasons = [] ['account', 'container', 'object'].each do |which| ring_data[:raw][which] = nil if ::File.exist?("#{ring_path}/#{which}.builder") IO.popen("su swift -c 'swift-ring-builder #{ring_path}/#{which}.builder'") do |pipe| ring_data[:raw][which] = pipe.readlines # Chef::Log.debug("#{ which.capitalize } Ring data: #{ring_data[:raw][which]}") ring_data[:parsed][which] = parse_ring_output(ring_data[:raw][which]) node.set['openstack']['object-storage']['state']['ring'][which] = ring_data[:parsed][which] end else Chef::Log.info("#{which.capitalize} ring builder files do not exist!") end # collect all the ring data, and note what disks are in use. All I really # need is a hash of device and id ring_data[:in_use][which] ||= {} if ring_data[:parsed][which][:hosts] ring_data[:parsed][which][:hosts].each do |ip, dev| dev.each do |dev_id, devhash| ring_data[:in_use][which].store(devhash[:device], devhash[:id]) end end end Chef::Log.debug("#{which.capitalize} Ring - In use: #{PP.pp(ring_data[:in_use][which], '')}") # figure out what's present in the cluster disk_data[which] = {} role = node['openstack']['object-storage']["#{which}_server_chef_role"] disk_state, _, _ = Chef::Search::Query.new.search(:node, "chef_environment:#{node.chef_environment} AND roles:#{role}") # for a running track of available disks disk_data[:available] ||= {} disk_data[:available][which] ||= {} disk_state.each do |swiftnode| if swiftnode['openstack']['object-storage']['state'] && swiftnode['openstack']['object-storage']['state']['devs'] swiftnode['openstack']['object-storage']['state']['devs'].each do |k, v| disk_data[which][v[:ip]] = disk_data[which][v[:ip]] || {} disk_data[which][v[:ip]][k] = {} v.keys.each { |x| disk_data[which][v[:ip]][k].store(x, v[x]) } if swiftnode['openstack']['object-storage'].key?("#{which}-zone") disk_data[which][v[:ip]][k]['zone'] = swiftnode['openstack']['object-storage']["#{which}-zone"] elsif swiftnode['openstack']['object-storage'].key?('zone') disk_data[which][v[:ip]][k]['zone'] = swiftnode['openstack']['object-storage']['zone'] else fail "Node #{swiftnode[:hostname]} has no zone assigned" end disk_data[:available][which][v[:mountpoint]] = v[:ip] unless v[:mounted] dirty_cluster_reasons << "Disk #{v[:name]} (#{v[:uuid]}) is not mounted on host #{v[:ip]} (#{swiftnode[:hostname]})" end end end end Chef::Log.debug("#{which.capitalize} Ring - Avail: #{PP.pp(disk_data[:available][which], '')}") end # Have the raw data, now bump it together and drop the script s = "#!/bin/bash\n\n# This script is automatically generated.\n" s << "# Running it will likely blow up your system if you don't review it carefully.\n" s << "# You have been warned.\n\n" unless node['openstack']['object-storage']['auto_rebuild_rings'] s << "if [ \"$1\" != \"--force\" ]; then\n" s << " echo \"Auto rebuild rings is disabled, so you must use --force to generate rings\"\n" s << " exit 0\n" s << "fi\n\n" end # Chef::Log.debug("#{PP.pp(disk_data, dump='')}") new_disks = {} missing_disks = {} new_servers = [] ['account', 'container', 'object'].each do |which| # remove available disks that are already in the ring new_disks[which] = disk_data[:available][which].reject { |k, v| ring_data[:in_use][which].key?(k) } # find all in-ring disks that are not in the cluster missing_disks[which] = ring_data[:in_use][which].reject { |k, v| disk_data[:available][which].key?(k) } Chef::Log.debug("#{which.capitalize} Ring - Missing: #{PP.pp(missing_disks[which], '')}") Chef::Log.debug("#{which.capitalize} Ring - New: #{PP.pp(new_disks[which], '')}") s << "\n# -- #{which.capitalize} Servers --\n\n" disk_data[which].keys.sort.each do |ip| s << "# #{ip}\n" disk_data[which][ip].keys.sort.each do |k| v = disk_data[which][ip][k] s << '# ' + v.keys.sort.select { |x| ['ip', 'device', 'uuid'].include?(x) }.map { |x| v[x] }.join(', ') if new_disks[which].key?(v['mountpoint']) s << ' (NEW!)' new_servers << ip unless new_servers.include?(ip) end s << "\n" end end # for all those servers, check if they are already in the ring. If not, # then we need to add them to the ring. For those that *were* in the # ring, and are no longer in the ring, we need to delete those. s << "\n" # add the new disks disk_data[which].keys.sort.each do |ip| disk_data[which][ip].keys.sort.each do |uuid| v = disk_data[which][ip][uuid] if new_disks[which].key?(v['mountpoint']) s << "swift-ring-builder #{ring_path}/#{which}.builder add z#{v['zone']}-#{v['ip']}:#{ports[which]}/#{v['mountpoint']} #{v['size']}\n" must_rebalance = true end end end # remove the disks -- sort to ensure consistent order missing_disks[which].keys.sort.each do |mountpoint| diskinfo = ring_data[:parsed][which][:hosts].select { |k, v| v.key?(mountpoint) }.map { |_, v| v[mountpoint] }[0] Chef::Log.debug("Missing diskinfo: #{PP.pp(diskinfo, '')}") description = Hash[diskinfo.select { |k, v| [:zone, :ip, :device].include?(k) }].map { |k, v| "#{k}: #{v}" }.join(', ') s << "# #{description}\n" s << "swift-ring-builder #{ring_path}/#{which}.builder remove d#{missing_disks[which][mountpoint]}\n" must_rebalance = true end s << "\n" if must_rebalance s << "swift-ring-builder #{ring_path}/#{which}.builder rebalance\n\n\n" else s << "# #{which.capitalize} ring has no outstanding changes!\n\n" end # we'll only rebalance if we meet the minimums for new adds if node['openstack']['object-storage'].key?('wait_for') if node['openstack']['object-storage']['wait_for'] > new_servers.count Chef::Log.debug('New servers, but not enough to force a rebalance') must_rebalance = false end end end [s, must_rebalance] end # Parse the raw output of swift-ring-builder def parse_ring_output(ring_data) output = { state: {} } ring_data.each do |line| if line =~ /build version ([0-9]+)/ output[:state][:build_version] = $1 elsif line =~ /^Devices:\s+id\s+region\s+zone\s+/ next elsif line =~ /^Devices:\s+id\s+zone\s+/ next elsif line =~ /^\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+)\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+)\s+(\S+)\s+([0-9.]+)\s+(\d+)\s+([-0-9.]+)\s*$/ output[:hosts] ||= {} output[:hosts][$4] ||= {} output[:hosts][$4][$8] ||= {} output[:hosts][$4][$8][:id] = $1 output[:hosts][$4][$8][:region] = $2 output[:hosts][$4][$8][:zone] = $3 output[:hosts][$4][$8][:ip] = $4 output[:hosts][$4][$8][:port] = $5 output[:hosts][$4][$8][:replication_ip] = $6 output[:hosts][$4][$8][:replication_port] = $7 output[:hosts][$4][$8][:device] = $8 output[:hosts][$4][$8][:weight] = $9 output[:hosts][$4][$8][:partitions] = $10 output[:hosts][$4][$8][:balance] = $11 elsif line =~ /^\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+)\s+(\S+)\s+([0-9.]+)\s+(\d+)\s+([-0-9.]+)\s*$/ output[:hosts] ||= {} output[:hosts][$4] ||= {} output[:hosts][$4][$6] ||= {} output[:hosts][$4][$6][:id] = $1 output[:hosts][$4][$6][:region] = $2 output[:hosts][$4][$6][:zone] = $3 output[:hosts][$4][$6][:ip] = $4 output[:hosts][$4][$6][:port] = $5 output[:hosts][$4][$6][:device] = $6 output[:hosts][$4][$6][:weight] = $7 output[:hosts][$4][$6][:partitions] = $8 output[:hosts][$4][$6][:balance] = $9 elsif line =~ /^\s+(\d+)\s+(\d+)\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+)\s+(\S+)\s+([0-9.]+)\s+(\d+)\s+([-0-9.]+)\s*$/ output[:hosts] ||= {} output[:hosts][$3] ||= {} output[:hosts][$3][$5] ||= {} output[:hosts][$3][$5][:id] = $1 output[:hosts][$3][$5][:zone] = $2 output[:hosts][$3][$5][:ip] = $3 output[:hosts][$3][$5][:port] = $4 output[:hosts][$3][$5][:device] = $5 output[:hosts][$3][$5][:weight] = $6 output[:hosts][$3][$5][:partitions] = $7 output[:hosts][$3][$5][:balance] = $8 elsif line =~ /(\d+) partitions, (\d+\.\d+) replicas, (\d+) regions, (\d+) zones, (\d+) devices, (\d+\.\d+) balance$/ output[:state][:partitions] = $1 output[:state][:replicas] = $2 output[:state][:regions] = $3 output[:state][:zones] = $4 output[:state][:devices] = $5 output[:state][:balance] = $6 elsif line =~ /(\d+) partitions, (\d+) replicas, (\d+) zones, (\d+) devices, (\d+\.\d+) balance$/ output[:state][:partitions] = $1 output[:state][:replicas] = $2 output[:state][:zones] = $3 output[:state][:devices] = $4 output[:state][:balance] = $5 elsif line =~ /^The minimum number of hours before a partition can be reassigned is (\d+)$/ output[:state][:min_part_hours] = $1 else fail "Cannot parse ring builder output for #{line}" end end output end action :ensure_exists do Chef::Log.debug("Ensuring #{new_resource.name}") new_resource.updated_by_last_action(false) s, must_update = generate_script script_file = File new_resource.name do owner new_resource.owner group new_resource.group mode new_resource.mode content s end script_file.run_action(:create) new_resource.updated_by_last_action(must_update) end