chef/cookbooks/swift/providers/ringfile.rb
#
# Copyright 2011, Dell
#
# 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: andi abes
#
##
# This LWRP will read the current state of a current ring, by executing
# swift-ring-builder and parsing its output. It would then compare the
# desired set of disks to the disks present.
# It currently does not change parameters (zone assignment or weight).
# to achieve that, you'd have to remove and readd the disk.
if __FILE__ == $0
def action(sym)
end
class Chef
class Log
def self.debug(s)
puts s
end
end
end
end
##
# some internal data structs to hold ring info read from existing files
class RingInfo
attr_accessor :partitions, :replicas, :zones, :device_num, :devices, :min_part_hours
class RingDeviceInfo
attr_accessor :id, :region, :zone, :ip, :port, :name, :weight, :partitions
def initialize
Chef::Log.debug "new device"
self
end
def to_s
s = "@#{@id}:#{@zone}[#{@ip}:#{@port}]/#{@name}"
end
end
def initialize
@devices = {}
self
end
def self.dev_key ip,port,name
"#{ip}:#{port.to_s}-#{name}"
end
def add_device d
Chef::Log.debug "added device @ip #{d.ip}:#{d.port}"
key = RingInfo.dev_key d.ip,d.port ,d.name
@devices[key] = d
end
def to_s
s = ""
#s <<"r:" << @replicas <<"z:" << @zones
devices.each { |d|
s << "\n " << d.to_s
}
end
end
def load_current_resource
name = @new_resource.name
name = "/etc/swift/#{name}"
@current_resource = Chef::Resource::SwiftRingfile.new(name)
@ring_test = nil
Chef::Log.info("parsing ring-file for #{name}")
IO.popen("swift-ring-builder #{name}") { |pipe|
ring_txt = pipe.readlines
Chef::Log.debug("raw ring info:#{ring_txt}")
@ring_test = scan_ring_desc ring_txt
Chef::Log.debug("at end of load, current ring is: #{@ring_test.to_s}")
}
compute_deltas
end
def parse_gen_info_line(line, ringinfo)
Chef::Log.debug("parsing gen info: " + line)
line =~ /^(\d+) partitions, ([0-9.]+) replicas, (\d+) regions, (\d+) zones, (\d+) devices,.*$/
if $~.nil?
raise "failed to parse gen info: #{line}"
end
ringinfo.partitions = $1
ringinfo.replicas = $2
_regions = $3
ringinfo.zones = $4
ringinfo.device_num = $5
end
def parse_dev_info_line(line, ringinfo)
Chef::Log.debug("parsing dev info: " + line)
# Line looks like this:
# id region zone ip address:port replication ip:port name weight partitions balance meta
# 0 1 0 192.168.125.14:6200 192.168.125.14:6200 2d4dc9923ed244dc9cac8f283ca79748 99.00 0 -100.00
# 0 1 0 192.168.125.11:6002 192.168.125.11:6002 f5e3892aad224b2896c1f865a799e96a 0.00 65536 999.99 DEL \n
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.]+).*$/
if $~.nil?
raise "failed to parse dev info: #{line}"
end
dev = RingInfo::RingDeviceInfo.new
dev.id = $1
dev.region = $2
dev.zone = $3
dev.ip = $4
dev.port = $5
_replication_ip = $6
_replication_port = $7
dev.name = $8
dev.weight = $9
dev.partitions = $10
ringinfo.add_device dev
end
def scan_ring_desc(input)
ringinfo = RingInfo.new
# if the current state is :ignore, this is the next state
ignore_next_state = ""
# regexp to ignore lines until this match (this line will be ignored too)
ignore_until = nil
state = :init
input.each do |line|
case state
when :init
state = :gen_info
when :ignore
Chef::Log.debug("ignoring line: " + line)
state = ignore_next_state if ignore_until.nil? || line =~ ignore_until
when :gen_info
parse_gen_info_line(line, ringinfo)
state = :ignore
ignore_next_state = :dev_info
ignore_until = /^Devices: /
when :dev_info
parse_dev_info_line(line, ringinfo)
else
raise "Internal error: unknown state \"#{state}\""
end
end
ringinfo
end
###
# compute disks to be added or removed (and update the dirty flag)
def compute_deltas
req_disks = @new_resource.disks
keyed_req = {} # for easy lookup, make a map of the requested disks
cur = @ring_test
name = @new_resource.name
@to_add = []
@to_rem = []
## figure out which disks need adding
req_disks.each {|disk|
key = RingInfo.dev_key disk[:ip],disk[:port],disk[:dev_name]
@to_add << disk unless cur and cur.devices[key] # add unless present
keyed_req[key] = disk
}
### figure out which disks need removing
cur.devices.each {|key, d|
@to_rem << d unless keyed_req[key] # remove unless still requested
} if cur
Chef::Log.info("disks, to add #{@to_add.length} , to remove: #{@to_rem.length}")
Chef::Log.debug("disks, to add #{@to_add.join(";")} , to remove: #{@to_rem.join(";")}")
end
action :apply do
name = @new_resource.name
cur = @ring_test
Chef::Log.info("current content of: #{name} #{(cur.nil? ? "-not there" : cur.to_s)}")
## make sure file exists
create_ring
# if we're changing the ring, make sure that file timestamps differ somewhat
if @to_add.length > 0 or @to_rem.length > 0
sleep 0.1
end
@to_add.each do |d|
execute "add disk #{d[:ip]}:#{d[:port]}/#{d[:dev_name]} to #{name}" do
user node[:swift][:user]
group node[:swift][:group]
command "swift-ring-builder #{name} add z#{d[:zone]}-#{d[:ip]}:#{d[:port]}/#{d[:dev_name]} #{d[:weight]}"
cwd "/etc/swift"
end
end
@to_rem.each do |d|
execute "remove disk #{d.id} from #{name}" do
user node[:swift][:user]
group node[:swift][:group]
command "swift-ring-builder #{name} remove d#{d.id} "
cwd "/etc/swift"
end
end
end
action :rebalance do
name = @current_resource.name
dirty = false
ring_data_mtime = ::File.new(name).mtime if ::File.exist?(name)
ring_data_mtime ||= File.new(name).mtime if ::File.exist?(name)
ring_data_mtime ||= 0
ring_name = name.sub(/^(.*)\..*$/, '\1.ring.gz')
ring_file_mtime = (::File.exist?(ring_name) ? ::File.mtime(ring_name) : -1)
dirty = true if (ring_data_mtime.to_i > ring_file_mtime.to_i)
Chef::Log.info("current status for: #{name} is #{dirty ? "dirty" : "not-dirty"} #{ring_name} #{ring_data_mtime.to_i}/#{ring_file_mtime.to_i}")
execute "rebalance ring for #{name}" do
user node[:swift][:user]
group node[:swift][:group]
command "swift-ring-builder #{name} rebalance"
cwd "/etc/swift"
returns [0,1] # returns 1 if it didn't do anything, 2 on
end if dirty
# if no rebalance was needed, but the the ring file is not there, make sure to make it.
if !::File.exist?(ring_name) then
dirty = true
execute "writeout ring for #{name}" do
user node[:swift][:user]
group node[:swift][:group]
command "swift-ring-builder #{name} write_ring"
cwd "/etc/swift"
returns [0,1] ## returns 1 if it didn't do anything, 2 on error.
end
end
@new_resource.updated_by_last_action(dirty)
end
def create_ring
name = @new_resource.name
mh = @new_resource.min_part_hours ? @new_resource.min_part_hours : 1
parts = @new_resource.partitions ? @new_resource.partitions : 18
replicas = @new_resource.replicas ? @new_resource.replicas : 3
execute "create #{name} ring" do
user node[:swift][:user]
group node[:swift][:group]
command "swift-ring-builder #{name} create #{parts} #{replicas} #{mh}"
creates "/etc/swift/#{name}"
cwd "/etc/swift"
end
end
if __FILE__ == $0
test_str_juno = <<TEST
/etc/swift/account.builder, build version 1
65536 partitions, 3.000000 replicas, 1 regions, 1 zones, 1 devices, 0.00 balance
The minimum number of hours before a partition can be reassigned is 24
Devices: id region zone ip address port replication ip replication port name weight partitions balance meta
0 1 0 192.168.125.10 6202 192.168.125.10 6202 04cc15ee94ff41169b5c30c927e82bd7 99.00 196608 0.00
TEST
test_str_liberty = <<TEST
/etc/swift/object.builder, build version 4
65536 partitions, 3.000000 replicas, 1 regions, 2 zones, 4 devices, 100.00 balance, 0.00 dispersion
The minimum number of hours before a partition can be reassigned is 24
The overload factor is 0.00% (0.000000)
Devices: id region zone ip address port replication ip replication port name weight partitions balance meta
0 1 0 192.168.125.14 6200 192.168.125.14 6200 2d4dc9923ed244dc9cac8f283ca79748 99.00 0 -100.00
1 1 1 192.168.125.15 6200 192.168.125.15 6200 98efd70297d547f2b90b697362e10b2c 99.00 0 -100.00
2 1 0 192.168.125.15 6200 192.168.125.15 6200 bd0e9f3cc6ab4a67abf99d3789f8daaa 99.00 0 -100.00
3 1 1 192.168.125.13 6200 192.168.125.13 6200 69cb18d1fac847269b6183242db5c731 99.00 0 -100.00
TEST
puts "=== Juno ==="
r = scan_ring_desc test_str_juno.lines
puts "no r for you \n\n\n" if r.nil?
puts r.to_s
puts ""
puts "=== Liberty ==="
r = scan_ring_desc test_str_liberty.lines
puts "no r for you \n\n\n" if r.nil?
puts r.to_s
end