#!/usr/bin/env python # Copyright 2013 Mirantis, 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. from ctypes import cdll from fuelmenu.common import dialog from fuelmenu.common.modulehelper import ModuleHelper from fuelmenu.common import network from fuelmenu.common import replace import fuelmenu.common.urwidwrapper as widget from fuelmenu.common import utils import logging import netaddr import os import re import socket import urwid import urwid.raw_display import urwid.web_display res_init = cdll.LoadLibrary('libc.so.6').__res_init log = logging.getLogger('fuelmenu.mirrors') blank = urwid.Divider() class dnsandhostname(urwid.WidgetWrap): def __init__(self, parent): self.name = "DNS & Hostname" self.priority = 50 self.visible = True self.netsettings = dict() self.getNetwork() self.gateway = self.get_default_gateway_linux() self.extdhcp = True self.parent = parent # UI Text self.header_content = ["DNS and hostname setup", "Note: Leave " "External DNS blank if you do not have " "Internet access."] self.fields = ["HOSTNAME", "DNS_DOMAIN", "DNS_SEARCH", "DNS_UPSTREAM", "blank", "TEST_DNS"] hostname, sep, domain = os.uname()[1].partition('.') self.defaults = \ { "HOSTNAME": {"label": "Hostname", "tooltip": "Hostname to use for Fuel master node", "value": hostname}, "DNS_UPSTREAM": {"label": "External DNS", "tooltip": "DNS server(s) (comma separated) \ to handle DNS requests (example 8.8.8.8,8.8.4.4)", "value": "8.8.8.8"}, "DNS_DOMAIN": {"label": "Domain", "tooltip": "Domain suffix to user for all \ nodes in your cluster", "value": domain}, "DNS_SEARCH": {"label": "Search Domain", "tooltip": "Domains to search when looking up \ DNS (space separated)", "value": domain}, "TEST_DNS": {"label": "Hostname to test DNS:", "value": "www.google.com", "tooltip": "DNS record to resolve to see if DNS \ is accessible"} } self.load() self.screen = None def fixEtcHosts(self): # replace ip for env variable HOSTNAME in /etc/hosts if self.netsettings[self.parent.managediface]["addr"] != "": managediface_ip = self.netsettings[ self.parent.managediface]["addr"] else: managediface_ip = "127.0.0.1" found = False with open("/etc/hosts") as fh: for line in fh: if re.match("%s.*%s" % (managediface_ip, socket.gethostname()), line): found = True break if not found: expr = ".*%s.*" % socket.gethostname() replace.replaceInFile("/etc/hosts", expr, "%s %s %s" % ( managediface_ip, socket.gethostname(), socket.gethostname().split('.')[0])) def check(self, args): """Validate that all fields have valid values through sanity checks.""" self.parent.footer.set_text("Checking data...") self.parent.refreshScreen() # Get field information responses = dict() for index, fieldname in enumerate(self.fields): if fieldname == "blank": pass else: responses[fieldname] = self.edits[index].get_edit_text() # Validate each field errors = [] # hostname must be under 60 chars if len(responses["HOSTNAME"]) >= 60: errors.append("Hostname must be under 60 chars.") # hostname must not be empty if len(responses["HOSTNAME"]) == 0: errors.append("Hostname must not be empty.") # hostname needs to have valid chars if re.search('[^a-z0-9-]', responses["HOSTNAME"]): errors.append( "Hostname must contain only alphanumeric and hyphen.") # domain must be under 180 chars if len(responses["DNS_DOMAIN"]) >= 180: errors.append("Domain must be under 180 chars.") # domain must not be empty if len(responses["DNS_DOMAIN"]) == 0: errors.append("Domain must not be empty.") # domain needs to have valid chars if re.match('[^a-z0-9-.]', responses["DNS_DOMAIN"]): errors.append( "Domain must contain only alphanumeric, period and hyphen.") # ensure external DNS is valid if len(responses["DNS_UPSTREAM"]) == 0: # We will allow empty if user doesn't need external networking # and present a strongly worded warning msg = "If you continue without DNS, you may not be able to access"\ + " external data necessary for installation needed for " \ + "some OpenStack Releases." dialog.display_dialog( self, widget.TextLabel(msg), "Empty DNS Warning") else: upstream_nameservers = responses["DNS_UPSTREAM"].split(',') # external DNS must contain only numbers, periods, and commas # Needs more serious ip address checking if re.match('[^0-9.,]', responses["DNS_UPSTREAM"]): errors.append( "External DNS must contain only IP addresses and commas.") # Ensure local IPs are not in upstream list host_ips = network.list_host_ip_addresses() for nameserver in upstream_nameservers: if nameserver in host_ips: errors.append("Host IPs cannot be in upstream DNS.") break if len(upstream_nameservers) > 3: errors.append( "Unable to specify more than 3 External DNS addresses.") # ensure test DNS name isn't empty if len(responses["TEST_DNS"]) == 0: errors.append("Test DNS must not be empty.") # Validate first IP address for nameserver in upstream_nameservers: if not netaddr.valid_ipv4(nameserver): errors.append("Not a valid IP address for DNS server:" " {0}".format(nameserver)) # Try to resolve with first address if not self.checkDNS(upstream_nameservers[0]): # Warn user that DNS resolution failed, but continue msg = "Unable to resolve %s.\n\n" % responses['TEST_DNS']\ + "Possible causes for DNS failure include:\n"\ + "* Invalid DNS server\n"\ + "* Invalid gateway\n"\ + "* Other networking issue\n\n"\ + "Fuel Setup can save this configuration, but "\ + "you may want to correct your settings." dialog.display_dialog(self, widget.TextLabel(msg), "DNS Failure Warning") self.parent.refreshScreen() if len(errors) > 0: log.error("Errors: %s %s" % (len(errors), errors)) ModuleHelper.display_failed_check_dialog(self, errors) return False else: self.parent.footer.set_text("No errors found.") return responses def apply(self, args): self.fixEtcHosts() responses = self.check(args) if responses is False: log.error("Check failed. Not applying") log.error("%s" % (responses)) return False self.save(responses) # Update network details so we write correct IP address self.getNetwork() # Apply hostname cmd = ["/usr/bin/hostnamectl", "set-hostname", responses["HOSTNAME"]] err_code, _, errout = utils.execute(cmd) if err_code != 0: log.error("Hostname change failed with an error: " "\"{0}\"".format(errout)) self.parent.footer.set_text("Unable to apply changes. Check logs " "for more details.") return False # remove old hostname from /etc/hosts f = open("/etc/hosts", "r") lines = f.readlines() f.close() with open("/etc/hosts", "w") as etchosts: for line in lines: if "localhost" in line: etchosts.write(line) elif responses["HOSTNAME"] in line \ or self.parent.settings["HOSTNAME"] \ or self.netsettings[self.parent.managediface]['addr'] \ in line: continue else: etchosts.write(line) etchosts.close() # append hostname and ip address to /etc/hosts with open("/etc/hosts", "a") as etchosts: if self.netsettings[self.parent.managediface]["addr"] != "": managediface_ip = self.netsettings[ self.parent.managediface]["addr"] else: managediface_ip = "127.0.0.1" etchosts.write( "%s %s.%s %s\n" % (managediface_ip, responses["HOSTNAME"], responses['DNS_DOMAIN'], responses["HOSTNAME"])) etchosts.close() def make_resolv_conf(filename): if self.netsettings[self.parent.managediface]["addr"] != "": managediface_ip = self.netsettings[ self.parent.managediface]["addr"] else: managediface_ip = "127.0.0.1" with open(filename, 'w') as f: f.write("search {0}\n".format(responses['DNS_SEARCH'])) f.write("domain {0}\n".format(responses['DNS_DOMAIN'])) if utils.is_post_deployment(): f.write("nameserver {0}\n".format(managediface_ip)) for upstream_dns in responses['DNS_UPSTREAM'].split(','): f.write("nameserver {0}\n".format(upstream_dns)) # Create a temporary resolv.conf so DNS works before the cobbler # container is up and running. # TODO(asheplyakov): puppet does a similar thing, perhaps we can # use the corresponding template instead of duplicating it here. make_resolv_conf('/etc/resolv.conf') # Reread resolv.conf res_init() return True def cancel(self, button): ModuleHelper.cancel(self, button) def resolv_conf_settings(self): # Parse /etc/resolv.conf if it contains data settings = {} search, domain, nameservers = self.getDNS() if search: settings["DNS_SEARCH"] = search if domain: settings["DNS_DOMAIN"] = domain if nameservers: settings["DNS_UPSTREAM"] = nameservers return settings def load(self): # Precedence of DNS information: # Class defaults, fuelmenu default YAML, astute.yaml, uname, # /etc/resolv.conf oldsettings = self.parent.settings # Read hostname from uname try: hostname, sep, domain = os.uname()[1].partition('.') oldsettings["HOSTNAME"] = hostname oldsettings["DNS_DOMAIN"] = domain oldsettings["DNS_SEARCH"] = domain except Exception: log.warning("Unable to look up system hostname") oldsettings.update(self.resolv_conf_settings()) ModuleHelper.load_to_defaults(oldsettings, self.defaults, ignoredparams=['TEST_DNS']) def getDNS(self, resolver="/etc/resolv.conf"): nameservers = [] domain = None searches = None try: with open(resolver, 'r') as f: for line in f: line = line.strip() if line.startswith("search "): searches = ' '.join(line.split(' ')[1:]) if line.startswith("domain "): domain = line.split(' ')[1] if line.startswith("nameserver "): nameservers.append(line.split(' ')[1]) except EnvironmentError: log.warn("Unable to open /etc/resolv.conf") # Always remove local IPs from nameserver list host_ips = network.list_host_ip_addresses() for nameserver in nameservers: if nameserver in host_ips: nameservers.remove(nameserver) return searches, domain, ",".join(nameservers) def save(self, responses): newsettings = ModuleHelper.make_settings_from_responses(responses) self.parent.settings.merge(newsettings) # Update self.defaults for index, fieldname in enumerate(self.fields): if fieldname != "blank": self.defaults[fieldname]['value'] = newsettings[fieldname] def checkDNS(self, server): # Note: Python's internal resolver caches negative answers. # Therefore, we should call dig externally to be sure. command = ["dig", "+short", "+time=3", "+retries=1", self.defaults["TEST_DNS"]['value'], "@{0}".format(server)] code, _, _ = utils.execute(command) return code == 0 def getNetwork(self): ModuleHelper.getNetwork(self) def getDHCP(self, iface): return ModuleHelper.getDHCP(iface) def get_default_gateway_linux(self): return ModuleHelper.get_default_gateway_linux() def refresh(self): if self.parent.dns_might_have_changed: settings = self.resolv_conf_settings() for index, fieldname in enumerate(self.fields): if fieldname in settings: self.edits[index].set_edit_text(settings[fieldname]) self.parent.dns_might_have_changed = False def screenUI(self): return ModuleHelper.screenUI(self, self.header_content, self.fields, self.defaults, show_all_buttons=True)