fuel-menu/fuelmenu/modules/dnsandhostname.py

384 lines
15 KiB
Python

#!/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)