| # 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. | |
| from . import CsHelper | |
| import logging | |
| import os | |
| from netaddr import * | |
| from random import randint | |
| import json | |
| import fcntl | |
| import shutil | |
| import tempfile | |
| from .CsGuestNetwork import CsGuestNetwork | |
| from cs.CsDatabag import CsDataBag | |
| from cs.CsFile import CsFile | |
| from cs.CsAddress import CsIP | |
| 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.add("bind-interfaces", 0) | |
| 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) | |
| sline = "dhcp-option=%s,26" % device | |
| line = "dhcp-option=%s,26,%s" % (device, i['mtu']) | |
| 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: | |
| logging.info("Releasing DHCP lease for IP: %s, mac: %s", ip, mac) | |
| 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) | |
| if self.ensure_lease_removed(ip): | |
| logging.info("Lease for %s still existed after dhcp_release; removed manually", ip) | |
| 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 lease_exists(self, ip): | |
| if not os.path.exists(LEASES): | |
| return False | |
| with open(LEASES, "r") as fp: | |
| for line in fp: | |
| fields = line.split() | |
| if len(fields) >= 3 and fields[2] == ip: | |
| return True | |
| return False | |
| def remove_lease(self, ip): | |
| if not os.path.exists(LEASES): | |
| return False | |
| removed = False | |
| with open(LEASES, "r+") as fp: | |
| fcntl.flock(fp.fileno(), fcntl.LOCK_EX) | |
| lines = fp.readlines() | |
| fd, tmp_path = tempfile.mkstemp( | |
| prefix="dnsmasq.leases.", | |
| dir=os.path.dirname(LEASES) | |
| ) | |
| try: | |
| with os.fdopen(fd, "w") as tmp: | |
| for line in lines: | |
| fields = line.split() | |
| if len(fields) >= 3 and fields[2] == ip: | |
| removed = True | |
| continue | |
| tmp.write(line) | |
| if removed: | |
| shutil.move(tmp_path, LEASES) | |
| # reload dnsmasq | |
| try: | |
| CsHelper.service("dnsmasq", "reload") | |
| except Exception: | |
| pass | |
| else: | |
| os.remove(tmp_path) | |
| finally: | |
| fcntl.flock(fp.fileno(), fcntl.LOCK_UN) | |
| return removed | |
| def ensure_lease_removed(self, ip): | |
| if self.lease_exists(ip): | |
| return self.remove_lease(ip) | |
| return False | |
| 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 v['network'].broadcast 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) |