Files
ubicloud/rhizome/kubernetes/lib/ubi_cni.rb
mohi-kalantari b36a49ea0d Remove handle_get function in CNI
This function is not called by kubelet or CRI and is only used by
some CNI tools. This commit removes the implementation and its tests.
2025-04-22 12:18:48 +02:00

257 lines
9.2 KiB
Ruby

# frozen_string_literal: true
require_relative "../../common/lib/util"
require_relative "../../common/lib/network"
require "fileutils"
require "ipaddr"
require "securerandom"
require "json"
require "logger"
class UbiCNI
MTU = 1400
IPAM_STORE_FILE = "/opt/cni/bin/ubicni-ipam-store"
def initialize(input_data, logger)
@input_data = input_data
@logger = logger
@cni_command = ENV["CNI_COMMAND"]
end
def run
log_environment
output = case @cni_command
when "ADD" then handle_add
when "DEL" then handle_del
else error_exit("Unsupported CNI command: #{@cni_command}")
end
puts output
end
def log_environment
@logger.info <<~LOG
-------------------------------------------------------
Handling new command: #{@cni_command}
ENV[CNI_CONTAINERID] #{ENV["CNI_CONTAINERID"]}
ENV[CNI_NETNS] #{ENV["CNI_NETNS"]}
ENV[CNI_IFNAME] #{ENV["CNI_IFNAME"]}
ENV[CNI_ARGS] #{ENV["CNI_ARGS"]}
ENV[CNI_PATH] #{ENV["CNI_PATH"]}
-------------------------------------------------------
LOG
end
def handle_add
check_required_env_vars(["CNI_CONTAINERID", "CNI_NETNS", "CNI_IFNAME"])
validate_input_ranges
subnet_ula_ipv6 = @input_data["ranges"]["subnet_ula_ipv6"]
subnet_ipv6 = @input_data["ranges"]["subnet_ipv6"]
subnet_ipv4 = @input_data["ranges"]["subnet_ipv4"]
container_id = ENV["CNI_CONTAINERID"]
cni_netns = ENV["CNI_NETNS"].sub("/var/run/netns/", "")
inner_ifname = ENV["CNI_IFNAME"]
@logger.info "Generating MAC addresses for container #{container_id}"
inner_mac = gen_mac
outer_mac = gen_mac
inner_link_local = mac_to_ipv6_link_local(inner_mac)
outer_link_local = mac_to_ipv6_link_local(outer_mac)
outer_ifname = "veth_#{container_id[0, 8]}"
@logger.info "Configuring DNS for network namespace #{cni_netns}"
setup_dns(cni_netns)
@logger.info "Setting up veth pair for container #{container_id}"
r "ip link add #{outer_ifname} addr #{outer_mac} type veth peer name #{inner_ifname} addr #{inner_mac} netns #{cni_netns}"
@logger.info "Allocating IP addresses for container #{container_id}"
ipv4_container_ip, ipv4_gateway_ip, container_ula_ipv6, container_ipv6 = allocate_ips_for_pod(container_id, subnet_ula_ipv6, subnet_ipv6, subnet_ipv4)
@logger.info "Setting up IPv6 configurations"
setup_ipv6(container_ipv6, inner_link_local, outer_link_local, cni_netns, inner_ifname, outer_ifname, setup_default_route: true)
setup_ipv6(container_ula_ipv6, inner_link_local, outer_link_local, cni_netns, inner_ifname, outer_ifname)
@logger.info "Setting up IPv4 configuration"
setup_ipv4(ipv4_container_ip, ipv4_gateway_ip, cni_netns, inner_ifname, outer_ifname)
response = build_add_response(inner_ifname, inner_mac, cni_netns, ipv4_container_ip, ipv4_gateway_ip, container_ula_ipv6, container_ipv6, outer_link_local)
@logger.info "ADD response: #{JSON.generate(response)}"
JSON.generate(response)
end
def build_add_response(inner_ifname, inner_mac, cni_netns, ipv4_container_ip, ipv4_gateway_ip, container_ula_ipv6, container_ipv6, outer_link_local)
{
cniVersion: "1.0.0",
interfaces: [{name: inner_ifname, mac: inner_mac, sandbox: "/var/run/netns/#{cni_netns}"}],
ips: [
{address: "#{ipv4_container_ip}/#{ipv4_container_ip.prefix}", gateway: ipv4_gateway_ip.to_s, interface: 0},
{address: "#{container_ula_ipv6}/#{container_ula_ipv6.prefix}", gateway: outer_link_local, interface: 0},
{address: "#{container_ipv6}/#{container_ipv6.prefix}", gateway: outer_link_local, interface: 0}
],
routes: [{dst: "0.0.0.0/0"}],
dns: {
nameservers: ["10.96.0.10"],
search: ["default.svc.cluster.local", "svc.cluster.local", "cluster.local"],
options: ["ndots:5"]
}
}
end
def setup_dns(cni_netns)
FileUtils.mkdir_p("/etc/netns/#{cni_netns}")
dns_config = <<~EOF
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
EOF
File.write("/etc/netns/#{cni_netns}/resolv.conf", dns_config)
end
def allocate_ips_for_pod(container_id, subnet_ula_ipv6, subnet_ipv6, subnet_ipv4)
result = nil
safe_write_to_file(IPAM_STORE_FILE) do |file|
ipam_store = begin
content = File.read(IPAM_STORE_FILE)
content.empty? ? {"allocated_ips" => {}} : JSON.parse(content)
rescue Errno::ENOENT
{"allocated_ips" => {}}
end
allocated_ips = ipam_store["allocated_ips"]
subnet_ula_ipv6_obj = IPAddr.new(subnet_ula_ipv6)
subnet_ipv6_obj = IPAddr.new(subnet_ipv6)
subnet_ipv4_obj = IPAddr.new(subnet_ipv4)
container_ula_ipv6 = find_random_available_ip(allocated_ips, subnet_ula_ipv6_obj)
container_ipv6 = find_random_available_ip(allocated_ips, subnet_ipv6_obj)
ipv4_container_ip = find_random_available_ip(allocated_ips, subnet_ipv4_obj)
ipv4_gateway_ip = find_random_available_ip(allocated_ips, subnet_ipv4_obj, reserved_ips: [ipv4_container_ip.to_s])
allocated_ips[container_id] = [
ipv4_container_ip.to_s,
ipv4_gateway_ip.to_s,
container_ula_ipv6.to_s,
container_ipv6.to_s
]
file.write(JSON.pretty_generate(ipam_store))
file.flush
file.fsync
result = [ipv4_container_ip, ipv4_gateway_ip, container_ula_ipv6, container_ipv6]
end
result
end
def setup_ipv6(container_ip, inner_link_local, outer_link_local, cni_netns, inner_ifname, outer_ifname, setup_default_route: false)
r "ip -6 -n #{cni_netns} addr replace #{container_ip}/#{container_ip.prefix} dev #{inner_ifname}"
r "ip -6 -n #{cni_netns} link set #{inner_ifname} mtu #{MTU} up"
if setup_default_route
r "ip -6 -n #{cni_netns} route replace default via #{outer_link_local} dev #{inner_ifname}"
end
r "ip -6 link set #{outer_ifname} mtu #{MTU} up"
r "ip -6 route replace #{container_ip}/#{container_ip.prefix} via #{inner_link_local} dev #{outer_ifname} mtu #{MTU}"
end
def setup_ipv4(container_ip, gateway_ip, cni_netns, inner_ifname, outer_ifname)
r "ip addr replace #{gateway_ip}/24 dev #{outer_ifname}"
r "ip link set #{outer_ifname} mtu #{MTU} up"
r "ip -n #{cni_netns} addr replace #{container_ip}/24 dev #{inner_ifname}"
r "ip -n #{cni_netns} link set #{inner_ifname} mtu #{MTU} up"
r "ip -n #{cni_netns} route replace default via #{gateway_ip}"
r "ip route replace #{container_ip}/#{container_ip.prefix} via #{gateway_ip} dev #{outer_ifname}"
r "echo 1 > /proc/sys/net/ipv4/conf/#{outer_ifname}/proxy_arp"
end
def handle_del
check_required_env_vars(["CNI_CONTAINERID"])
container_id = ENV["CNI_CONTAINERID"]
@logger.info "Releasing IPs for container #{container_id}"
safe_write_to_file(IPAM_STORE_FILE) do |file|
ipam_store = begin
content = File.read(IPAM_STORE_FILE)
content.empty? ? {"allocated_ips" => {}} : JSON.parse(content)
rescue Errno::ENOENT
{"allocated_ips" => {}}
end
ipam_store["allocated_ips"].delete(container_id)
file.write(JSON.pretty_generate(ipam_store))
file.flush
file.fsync
end
"{}"
end
def error_exit(message)
@logger.error message
puts JSON.generate({code: 100, msg: message})
exit 1
end
def find_random_available_ip(allocated_ips, subnet, reserved_ips: [])
unavailable_ips = allocated_ips.values.flatten.concat(reserved_ips)
if subnet.ipv4?
available_ips = generate_all_usable_ips(subnet)
available_ips.reject! { |ip| unavailable_ips.include?(ip.to_s) }
raise "No available IPs in subnet #{subnet}" if available_ips.empty?
available_ips.sample
else
max_retries = 100
max_retries.times do
ip = generate_random_ip(subnet)
unless unavailable_ips.include?(ip.to_s)
return ip
end
end
raise "Could not find an available IP after #{max_retries} retries"
end
end
def generate_random_ip(subnet)
subnet_size = calculate_subnet_size(subnet)
base = subnet.to_i & subnet.mask(subnet.prefix).to_i
# We subtract 3 from subnet_size:
# - 1 for the network address (offset 0)
# - 1 for the first usable IP (offset 1)
# - 1 for the broadcast address (offset = subnet_size - 1)
#
# Then we add 2 to the result of random_number so offsets start at 2.
random_offset = SecureRandom.random_number(subnet_size - 3) + 2
IPAddr.new(base + random_offset, Socket::AF_INET6)
end
def generate_all_usable_ips(subnet)
subnet_size = calculate_subnet_size(subnet)
base = subnet.to_i & subnet.mask(subnet.prefix).to_i
# Valid host IPs range from offset 2 to subnet_size - 2
(2...(subnet_size - 1)).map do |offset|
IPAddr.new(base + offset, Socket::AF_INET)
end
end
def calculate_subnet_size(subnet)
if subnet.ipv4?
2**(32 - subnet.prefix)
else
2**(128 - subnet.prefix)
end
end
def check_required_env_vars(vars)
vars.each do |var|
error_exit("Missing required environment variable: #{var}") unless ENV[var]
end
end
def validate_input_ranges
unless @input_data["ranges"]&.values_at("subnet_ula_ipv6", "subnet_ipv6", "subnet_ipv4")&.all?
error_exit("Missing required ranges in input data")
end
end
end