# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you 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. import CsHelper import logging import os from netaddr import * from random import randint from CsGuestNetwork import CsGuestNetwork from cs.CsDatabag import CsDataBag from cs.CsFile import CsFile LEASES = "/var/lib/misc/dnsmasq.leases" DHCP_HOSTS = "/etc/dhcphosts.txt" DHCP_OPTS = "/etc/dhcpopts.txt" CLOUD_CONF = "/etc/dnsmasq.d/cloud.conf" class CsDhcp(CsDataBag): """ Manage dhcp entries """ def process(self): self.hosts = {} self.changed = [] self.devinfo = CsHelper.get_device_info() self.preseed() self.dhcp_hosts = CsFile(DHCP_HOSTS) self.dhcp_opts = CsFile(DHCP_OPTS) self.conf = CsFile(CLOUD_CONF) self.dhcp_leases = CsFile(LEASES) self.dhcp_hosts.repopulate() self.dhcp_opts.repopulate() for item in self.dbag: if item == "id": continue if not self.dbag[item]['remove']: self.add(self.dbag[item]) self.configure_server() restart_dnsmasq = False need_delete_leases = False if self.conf.commit(): restart_dnsmasq = True need_delete_leases = True if self.dhcp_hosts.commit(): need_delete_leases = True if self.dhcp_leases.commit(): need_delete_leases = True self.dhcp_opts.commit() if need_delete_leases: self.delete_leases() self.write_hosts() if not self.cl.is_redundant() or self.cl.is_primary(): if restart_dnsmasq: CsHelper.service("dnsmasq", "restart") else: CsHelper.start_if_stopped("dnsmasq") CsHelper.service("dnsmasq", "reload") def configure_server(self): # self.conf.addeq("dhcp-hostsfile=%s" % DHCP_HOSTS) idx = 0 listen_address = ["127.0.0.1"] for i in self.devinfo: if not i['dnsmasq']: continue device = i['dev'] ip = i['ip'].split('/')[0] gn = CsGuestNetwork(device, self.config) # Gateway gateway = '' if self.config.is_vpc(): gateway = gn.get_gateway() else: gateway = i['gateway'] sline = "dhcp-range=set:interface-%s-%s" % (device, idx) if self.cl.is_redundant(): line = "dhcp-range=set:interface-%s-%s,%s,static" % (device, idx, gateway) else: line = "dhcp-range=set:interface-%s-%s,%s,static" % (device, idx, ip) self.conf.search(sline, line) sline = "dhcp-option=tag:interface-%s-%s,15" % (device, idx) line = "dhcp-option=tag:interface-%s-%s,15,%s" % (device, idx, gn.get_domain()) self.conf.search(sline, line) # DNS search order if gn.get_dns() and device: sline = "dhcp-option=tag:interface-%s-%s,6" % (device, idx) dns_list = [x for x in gn.get_dns() if x] if self.config.is_dhcp() and not self.config.use_extdns(): guest_ip = self.config.address().get_guest_ip() if guest_ip and guest_ip in dns_list and ip not in dns_list: ## Replace the default guest IP in VR with the ip in additional IP ranges, if shared network has multiple IP ranges. dns_list.remove(guest_ip) dns_list.insert(0, ip) line = "dhcp-option=tag:interface-%s-%s,6,%s" % (device, idx, ','.join(dns_list)) self.conf.search(sline, line) if gateway != '0.0.0.0': sline = "dhcp-option=tag:interface-%s-%s,3," % (device, idx) line = "dhcp-option=tag:interface-%s-%s,3,%s" % (device, idx, gateway) self.conf.search(sline, line) # Netmask netmask = '' if self.config.is_vpc(): netmask = gn.get_netmask() else: netmask = str(i['network'].netmask) sline = "dhcp-option=tag:interface-%s-%s,1," % (device, idx) line = "dhcp-option=tag:interface-%s-%s,1,%s" % (device, idx, netmask) self.conf.search(sline, line) # Listen Address if self.cl.is_redundant(): listen_address.append(gateway) else: listen_address.append(ip) # Add localized "data-server" records in /etc/hosts for VPC routers if self.config.is_vpc() or self.config.is_router(): self.add_host(gateway, "%s data-server" % CsHelper.get_hostname()) elif self.config.is_dhcp(): self.add_host(ip, "%s data-server" % CsHelper.get_hostname()) idx += 1 # Listen Address sline = "listen-address=" line = "listen-address=%s" % (','.join(listen_address)) self.conf.search(sline, line) def delete_leases(self): macs_dhcphosts = [] try: logging.info("Attempting to delete entries from dnsmasq.leases file for VMs which are not on dhcphosts file") for host in open(DHCP_HOSTS): macs_dhcphosts.append(host.split(',')[0]) removed = 0 for leaseline in open(LEASES): lease = leaseline.split(' ') mac = lease[1] ip = lease[2] if mac not in macs_dhcphosts: cmd = "dhcp_release $(ip route get %s | grep eth | head -1 | awk '{print $3}') %s %s" % (ip, ip, mac) logging.info(cmd) CsHelper.execute(cmd) removed = removed + 1 self.del_host(ip) logging.info("Deleted %s entries from dnsmasq.leases file" % str(removed)) except Exception as e: logging.error("Caught error while trying to delete entries from dnsmasq.leases file: %s" % e) def preseed(self): self.add_host("127.0.0.1", "localhost") self.add_host("127.0.1.1", "%s" % CsHelper.get_hostname()) self.add_host("::1", "localhost ip6-localhost ip6-loopback") self.add_host("ff02::1", "ip6-allnodes") self.add_host("ff02::2", "ip6-allrouters") def write_hosts(self): file = CsFile("/etc/hosts") file.repopulate() for ip in self.hosts: file.add("%s\t%s" % (ip, self.hosts[ip])) if file.is_changed(): file.commit() logging.info("Updated hosts file") else: logging.debug("Hosts file unchanged") def add(self, entry): self.add_host(entry['ipv4_address'], entry['host_name']) # Lease time set to "infinite" since we properly control all DHCP/DNS config via CloudStack. # Infinite time helps avoid some edge cases which could cause DHCPNAK being sent to VMs since # (RHEL) system lose routes when they receive DHCPNAK. # When VM is expunged, its active lease and DHCP/DNS config is properly removed from related files in VR, # so the infinite duration of lease does not cause any issues or garbage. lease = 'infinite' if entry['default_entry']: self.dhcp_hosts.add("%s,%s,%s,%s" % (entry['mac_address'], entry['ipv4_address'], entry['host_name'], lease)) self.dhcp_leases.search(entry['mac_address'], "0 %s %s %s *" % (entry['mac_address'], entry['ipv4_address'], entry['host_name'])) else: tag = entry['ipv4_address'].replace(".", "_") self.dhcp_hosts.add("%s,set:%s,%s,%s,%s" % (entry['mac_address'], tag, entry['ipv4_address'], entry['host_name'], lease)) self.dhcp_opts.add("%s,%s" % (tag, 3)) self.dhcp_opts.add("%s,%s" % (tag, 6)) self.dhcp_opts.add("%s,%s" % (tag, 15)) self.dhcp_leases.search(entry['mac_address'], "0 %s %s %s *" % (entry['mac_address'], entry['ipv4_address'], entry['host_name'])) i = IPAddress(entry['ipv4_address']) # Calculate the device for v in self.devinfo: if i > v['network'].network and i < v['network'].broadcast: v['dnsmasq'] = True # Virtual Router v['gateway'] = entry['default_gateway'] def add_host(self, ip, hosts): self.hosts[ip] = hosts def del_host(self, ip): if ip in self.hosts: self.hosts.pop(ip)