529 lines
23 KiB
Python
529 lines
23 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.
|
|
|
|
import logging
|
|
import os
|
|
|
|
from fuelclient.cli import error
|
|
from fuelclient import objects
|
|
import netaddr
|
|
import urwid
|
|
import urwid.raw_display
|
|
import urwid.web_display
|
|
import yaml
|
|
|
|
from fuelmenu.common import dialog
|
|
from fuelmenu.common import errors as f_errors
|
|
from fuelmenu.common import modulehelper
|
|
from fuelmenu.common import network
|
|
from fuelmenu.common import puppet
|
|
import fuelmenu.common.urwidwrapper as widget
|
|
from fuelmenu.common import utils
|
|
from fuelmenu import consts
|
|
log = logging.getLogger('fuelmenu.pxe_setup')
|
|
blank = urwid.Divider()
|
|
|
|
|
|
class CobblerConfig(urwid.WidgetWrap):
|
|
def __init__(self, parent):
|
|
self.name = "PXE Setup"
|
|
self.visible = True
|
|
self.netsettings = dict()
|
|
self.parent = parent
|
|
self.getNetwork()
|
|
self.gateway = self.get_default_gateway_linux()
|
|
self.activeiface = self.parent.managediface
|
|
|
|
# UI text
|
|
text1 = "Settings for PXE booting of slave nodes."
|
|
text2 = "Select the interface where PXE will run:"
|
|
# Placeholder for network settings text
|
|
self.net_choices = widget.ChoicesGroup(sorted(self.netsettings.keys()),
|
|
default_value=self.activeiface,
|
|
fn=self.radioSelect)
|
|
self.net_text1 = widget.TextLabel("")
|
|
self.net_text2 = widget.TextLabel("")
|
|
self.net_text3 = widget.TextLabel("")
|
|
self.net_text4 = widget.TextLabel("")
|
|
self.header_content = [text1, text2, self.net_choices, self.net_text1,
|
|
self.net_text2, self.net_text3, self.net_text4]
|
|
self.fields = ["dynamic_label", "ADMIN_NETWORK/dhcp_pool_start",
|
|
"ADMIN_NETWORK/dhcp_pool_end",
|
|
"ADMIN_NETWORK/dhcp_gateway"]
|
|
|
|
self.defaults = \
|
|
{
|
|
"ADMIN_NETWORK/dhcp_pool_start": {"label": "DHCP Pool Start",
|
|
"tooltip": "Used for \
|
|
defining IPs for hosts and instance public addresses",
|
|
"value": "10.0.0.3"},
|
|
"ADMIN_NETWORK/dhcp_pool_end": {"label": "DHCP Pool End",
|
|
"tooltip": "Used for defining \
|
|
IPs for hosts and instance public addresses",
|
|
"value": "10.0.0.254"},
|
|
"ADMIN_NETWORK/dhcp_gateway": {"label": "DHCP Gateway",
|
|
"tooltip": "Default gateway \
|
|
to advertise via DHCP to nodes",
|
|
"value": "10.0.0.2"},
|
|
"dynamic_label": {"label": "DHCP pool for node discovery:",
|
|
"tooltip": "",
|
|
"type": modulehelper.WidgetType.LABEL},
|
|
}
|
|
|
|
self.load()
|
|
self.extdhcp = True
|
|
self.screen = None
|
|
self.apply_dialog_message = {
|
|
'title': "Apply failed in module {0}".format(self.name),
|
|
"message": "Error applying changes. Check logs for details."
|
|
}
|
|
|
|
def check(self, args):
|
|
"""Validates all fields have valid values and some sanity checks."""
|
|
self.parent.footer.set_text("Checking data...")
|
|
self.parent.refreshScreen()
|
|
|
|
# Refresh networking to make sure IP matches
|
|
self.getNetwork()
|
|
|
|
# Get field information
|
|
responses = dict()
|
|
|
|
for index, fieldname in enumerate(self.fields):
|
|
if fieldname != "blank" and "label" not in fieldname:
|
|
responses[fieldname] = self.edits[index].get_edit_text()
|
|
|
|
# Validate each field
|
|
errors = []
|
|
|
|
# Set internal_{ipaddress,netmask,interface}
|
|
responses["ADMIN_NETWORK/interface"] = self.activeiface
|
|
responses["ADMIN_NETWORK/netmask"] = self.netsettings[
|
|
self.activeiface]["netmask"]
|
|
responses["ADMIN_NETWORK/mac"] = self.netsettings[
|
|
self.activeiface]["mac"]
|
|
responses["ADMIN_NETWORK/ipaddress"] = self.netsettings[
|
|
self.activeiface]["addr"]
|
|
|
|
# ensure management interface is valid
|
|
if responses["ADMIN_NETWORK/interface"] not in self.netsettings.keys():
|
|
errors.append("Management interface not valid")
|
|
else:
|
|
self.parent.footer.set_text("Scanning for DHCP servers. \
|
|
Please wait...")
|
|
self.parent.refreshScreen()
|
|
|
|
try:
|
|
dhcptimeout = 5
|
|
dhcp_server_data = network.search_external_dhcp(
|
|
self.activeiface, dhcptimeout)
|
|
except network.NetworkException:
|
|
log.warning('DHCP scan failed.')
|
|
dhcp_server_data = []
|
|
|
|
num_dhcp = len(dhcp_server_data)
|
|
if num_dhcp == 0:
|
|
log.debug("No DHCP servers found")
|
|
else:
|
|
# Problem exists, but permit user to continue
|
|
log.error("%s foreign DHCP server(s) found: %s" %
|
|
(num_dhcp, dhcp_server_data))
|
|
|
|
# Build dialog elements
|
|
dhcp_info = []
|
|
dhcp_info.append(urwid.Padding(
|
|
urwid.Text(("header", "!!! WARNING !!!")),
|
|
"center"))
|
|
dhcp_info.append(widget.TextLabel("You have selected an \
|
|
interface that contains one or more DHCP servers. This will impact \
|
|
provisioning. You should disable these DHCP servers before you continue, or \
|
|
else deployment will likely fail."))
|
|
dhcp_info.append(widget.TextLabel(""))
|
|
for index, dhcp_server in enumerate(dhcp_server_data):
|
|
dhcp_info.append(widget.TextLabel("DHCP Server #%s:" %
|
|
(index + 1)))
|
|
dhcp_info.append(widget.TextLabel("IP address: %-10s" %
|
|
dhcp_server['server_ip']))
|
|
dhcp_info.append(widget.TextLabel("MAC address: %-10s" %
|
|
dhcp_server['mac']))
|
|
dhcp_info.append(widget.TextLabel(""))
|
|
dialog.display_dialog(self, urwid.Pile(dhcp_info),
|
|
"DHCP Servers Found on %s"
|
|
% self.activeiface)
|
|
# Ensure pool start and end are on the same subnet as mgmt_if
|
|
# Ensure mgmt_if has an IP first
|
|
if len(self.netsettings[responses[
|
|
"ADMIN_NETWORK/interface"]]["addr"]) == 0:
|
|
errors.append("Go to Interfaces to configure management \
|
|
interface first.")
|
|
else:
|
|
# Ensure ADMIN_NETWORK/interface is not running DHCP
|
|
if self.netsettings[responses[
|
|
"ADMIN_NETWORK/interface"]]["bootproto"] == "dhcp":
|
|
errors.append("%s is running DHCP. Change it to static "
|
|
"first." % self.activeiface)
|
|
# Ensure DHCP Pool Start and DHCP Pool are valid IPs
|
|
try:
|
|
if netaddr.valid_ipv4(responses[
|
|
"ADMIN_NETWORK/dhcp_pool_start"]):
|
|
dhcp_start = netaddr.IPAddress(
|
|
responses["ADMIN_NETWORK/dhcp_pool_start"])
|
|
if not dhcp_start:
|
|
raise f_errors.BadIPException(
|
|
"Not a valid IP address")
|
|
else:
|
|
raise f_errors.BadIPException("Not a valid IP address")
|
|
except Exception:
|
|
errors.append("Invalid IP address for DHCP Pool Start")
|
|
try:
|
|
if netaddr.valid_ipv4(responses[
|
|
"ADMIN_NETWORK/dhcp_gateway"]):
|
|
dhcp_gateway = netaddr.IPAddress(
|
|
responses["ADMIN_NETWORK/dhcp_gateway"])
|
|
if not dhcp_gateway:
|
|
raise f_errors.BadIPException(
|
|
"Not a valid IP address")
|
|
else:
|
|
raise f_errors.BadIPException(
|
|
"Not a valid IP address")
|
|
except Exception:
|
|
errors.append("Invalid IP address for DHCP Gateway")
|
|
|
|
try:
|
|
if netaddr.valid_ipv4(responses[
|
|
"ADMIN_NETWORK/dhcp_pool_end"]):
|
|
dhcp_end = netaddr.IPAddress(
|
|
responses["ADMIN_NETWORK/dhcp_pool_end"])
|
|
if not dhcp_end:
|
|
raise f_errors.BadIPException(
|
|
"Not a valid IP address")
|
|
else:
|
|
raise f_errors.BadIPException(
|
|
"Not a valid IP address")
|
|
except Exception:
|
|
errors.append("Invalid IP address for DHCP Pool end")
|
|
|
|
# Ensure pool start and end are in the same
|
|
# subnet of each other
|
|
netmask = self.netsettings[responses[
|
|
"ADMIN_NETWORK/interface"
|
|
]]["netmask"]
|
|
if not network.inSameSubnet(
|
|
responses["ADMIN_NETWORK/dhcp_pool_start"],
|
|
responses["ADMIN_NETWORK/dhcp_pool_end"], netmask):
|
|
errors.append("DHCP Pool start and end are not in the "
|
|
"same subnet.")
|
|
|
|
# Ensure pool start and end are in the right netmask
|
|
mgmt_if_ipaddr = self.netsettings[responses[
|
|
"ADMIN_NETWORK/interface"]]["addr"]
|
|
if network.inSameSubnet(responses[
|
|
"ADMIN_NETWORK/dhcp_pool_start"],
|
|
mgmt_if_ipaddr, netmask) is False:
|
|
errors.append("DHCP Pool start does not match management"
|
|
" network.")
|
|
if network.inSameSubnet(responses[
|
|
"ADMIN_NETWORK/dhcp_pool_end"],
|
|
mgmt_if_ipaddr, netmask) is False:
|
|
errors.append("DHCP Pool end does not match management "
|
|
"network.")
|
|
|
|
if network.inSameSubnet(responses[
|
|
"ADMIN_NETWORK/dhcp_gateway"],
|
|
mgmt_if_ipaddr, netmask) is False:
|
|
errors.append("DHCP Gateway does not match management "
|
|
"network.")
|
|
|
|
self.parent.footer.set_text("Scanning for duplicate IP address"
|
|
"es. Please wait...")
|
|
# Bind arping to mgmt_if_ipaddr if it assigned
|
|
assigned_ips = [v.get('addr') for v in
|
|
self.netsettings.itervalues()]
|
|
arping_bind = mgmt_if_ipaddr in assigned_ips
|
|
if network.duplicateIPExists(mgmt_if_ipaddr, self.activeiface,
|
|
arping_bind):
|
|
errors.append("Duplicate host found with IP {0}.".format(
|
|
mgmt_if_ipaddr))
|
|
|
|
# Extra checks for post-deployment changes
|
|
if utils.is_post_deployment():
|
|
settings = self.parent.settings
|
|
|
|
# Admin interface cannot change
|
|
if self.activeiface != settings["ADMIN_NETWORK"]["interface"]:
|
|
errors.append("Cannot change admin interface after deployment")
|
|
|
|
# PXE network range must contain previous PXE network range
|
|
old_range = network.range(
|
|
settings["ADMIN_NETWORK"]["dhcp_pool_start"],
|
|
settings["ADMIN_NETWORK"]["dhcp_pool_end"])
|
|
new_range = network.range(
|
|
responses["ADMIN_NETWORK/dhcp_pool_start"],
|
|
responses["ADMIN_NETWORK/dhcp_pool_end"])
|
|
if old_range[0] not in new_range:
|
|
errors.append("DHCP range must contain previous values.")
|
|
if old_range[-1] not in new_range:
|
|
errors.append("DHCP range can only be increased after "
|
|
"deployment.")
|
|
|
|
if len(errors) > 0:
|
|
log.error("Errors: %s %s" % (len(errors), errors))
|
|
modulehelper.ModuleHelper.display_failed_check_dialog(self, errors)
|
|
return False
|
|
else:
|
|
self.parent.footer.set_text("No errors found.")
|
|
return responses
|
|
|
|
def apply(self, args):
|
|
responses = self.check(args)
|
|
if responses is False:
|
|
log.error("Check failed. Not applying")
|
|
log.error("%s" % (responses))
|
|
return False
|
|
self.save(responses)
|
|
if utils.is_post_deployment():
|
|
self.parent.apply_tasks.add(self.update_dhcp)
|
|
return True
|
|
|
|
def update_dhcp(self):
|
|
settings = self.parent.settings.get("ADMIN_NETWORK")
|
|
if not self._update_nailgun(settings):
|
|
return False
|
|
if os.path.exists(consts.HIERA_NET_SETTINGS):
|
|
result, msg = self._update_hiera_dnsmasq(settings)
|
|
else:
|
|
result = self._update_dnsmasq(settings)
|
|
if not result:
|
|
modulehelper.ModuleHelper.display_dialog(
|
|
self, error_msg=self.apply_dialog_message["message"],
|
|
title=self.apply_dialog_message["title"])
|
|
return False
|
|
cobbler_sync = ["cobbler", "sync"]
|
|
code, out, err = utils.execute(cobbler_sync)
|
|
if code != 0:
|
|
log.error(err)
|
|
modulehelper.ModuleHelper.display_dialog(
|
|
self, error_msg=self.apply_dialog_message["message"],
|
|
title=self.apply_dialog_message["title"])
|
|
return False
|
|
return True
|
|
|
|
def _update_nailgun(self, settings):
|
|
msg = "Apply changes to Nailgun"
|
|
log.info(msg)
|
|
self.parent.footer.set_text(msg)
|
|
self.parent.refreshScreen()
|
|
|
|
# TODO(mzhnichkov) this manifest apply twice(here and in feature
|
|
# groups). Need to combine this calls
|
|
result, msg = puppet.puppetApplyManifest(consts.PUPPET_NAILGUN)
|
|
if not result:
|
|
modulehelper.ModuleHelper.display_dialog(
|
|
self, error_msg=self.apply_dialog_message["message"],
|
|
title=self.apply_dialog_message["title"])
|
|
return False
|
|
|
|
data = {
|
|
"gateway": settings["dhcp_gateway"],
|
|
"ip_ranges": [
|
|
[settings["dhcp_pool_start"], settings["dhcp_pool_end"]]
|
|
]
|
|
}
|
|
try:
|
|
objects.NetworkGroup(consts.ADMIN_NETWORK_ID).set(data)
|
|
except error.HTTPError as e:
|
|
log.error(e.message)
|
|
modulehelper.ModuleHelper.display_dialog(
|
|
self, error_msg=self.apply_dialog_message["message"],
|
|
title=self.apply_dialog_message["title"])
|
|
return False
|
|
return True
|
|
|
|
def _update_hiera_dnsmasq(self, settings):
|
|
"""Update Hiera and dnsmasq
|
|
|
|
PXE related configuration should be written in separate
|
|
configuration file when you create additional admin
|
|
network(this behavior was introduced in nailgun's
|
|
DnsmasqUpdateTask class)
|
|
"""
|
|
|
|
msg = "Apply changes to Hiera and Dnsmasq"
|
|
log.info(msg)
|
|
self.parent.footer.set_text(msg)
|
|
self.parent.refreshScreen()
|
|
with open(consts.HIERA_NET_SETTINGS, "r") as hiera_settings:
|
|
networks = yaml.safe_load(hiera_settings)
|
|
net = netaddr.IPNetwork(
|
|
"{0}/{1}".format(settings["ipaddress"],
|
|
settings["netmask"]))
|
|
for admin_net in networks["admin_networks"]:
|
|
if str(net.cidr) == admin_net["cidr"]:
|
|
admin_net["ip_ranges"] = [
|
|
[settings["dhcp_pool_start"],
|
|
settings["dhcp_pool_end"]]
|
|
]
|
|
admin_net["gateway"] = settings["dhcp_gateway"]
|
|
with open(consts.HIERA_NET_SETTINGS, "w") as hiera_settings:
|
|
yaml.safe_dump(networks, hiera_settings)
|
|
return puppet.puppetApplyManifest(consts.PUPPET_DHCP_RANGES)
|
|
|
|
def _update_dnsmasq(self, settings):
|
|
puppet_classes = [{
|
|
"type": "resource",
|
|
"class": "fuel::dnsmasq::dhcp_range",
|
|
"name": "default",
|
|
"params": {
|
|
"dhcp_start_address": settings["dhcp_pool_start"],
|
|
"dhcp_end_address": settings["dhcp_pool_end"],
|
|
"dhcp_netmask": settings["netmask"],
|
|
"dhcp_gateway": settings["dhcp_gateway"],
|
|
"next_server": settings["ipaddress"]
|
|
}
|
|
}]
|
|
log.debug("Start puppet with data {0}".format(puppet_classes))
|
|
return puppet.puppetApply(puppet_classes)
|
|
|
|
def cancel(self, button):
|
|
modulehelper.ModuleHelper.cancel(self, button)
|
|
self.setNetworkDetails()
|
|
|
|
def load(self):
|
|
settings = self.parent.settings
|
|
modulehelper.ModuleHelper.load_to_defaults(settings, self.defaults)
|
|
|
|
iface = settings.get("ADMIN_NETWORK", {}).get("interface")
|
|
if iface in self.netsettings.keys():
|
|
self.activeiface = iface
|
|
|
|
def save(self, responses):
|
|
newsettings = modulehelper.ModuleHelper.make_settings_from_responses(
|
|
responses)
|
|
|
|
# Need to calculate and netmask
|
|
newsettings['ADMIN_NETWORK']['netmask'] = \
|
|
self.netsettings[newsettings['ADMIN_NETWORK']['interface']][
|
|
"netmask"]
|
|
|
|
# Update self.defaults
|
|
for index, fieldname in enumerate(self.fields):
|
|
if fieldname != "blank" and "label" not in fieldname:
|
|
self.defaults[fieldname]['value'] = responses[fieldname]
|
|
|
|
self.parent.settings.merge(newsettings)
|
|
self.parent.footer.set_text("Changes saved successfully.")
|
|
|
|
def getNetwork(self):
|
|
modulehelper.ModuleHelper.getNetwork(self)
|
|
|
|
def getDHCP(self, iface):
|
|
return modulehelper.ModuleHelper.getDHCP(iface)
|
|
|
|
def get_default_gateway_linux(self):
|
|
return modulehelper.ModuleHelper.get_default_gateway_linux()
|
|
|
|
def radioSelect(self, current, state, user_data=None):
|
|
"""Update network details and display information."""
|
|
# Urwid returns the previously selected radio button.
|
|
# The previous object has True state, which is wrong.
|
|
# Somewhere in rb group a RadioButton is set to True.
|
|
for rb in current.group:
|
|
if rb.get_label() == current.get_label():
|
|
continue
|
|
if rb.base_widget.state is True:
|
|
self.activeiface = rb.base_widget.get_label()
|
|
self.parent.managediface = self.activeiface
|
|
break
|
|
self.gateway = self.get_default_gateway_linux()
|
|
self.getNetwork()
|
|
self.setNetworkDetails()
|
|
return
|
|
|
|
def setNetworkDetails(self):
|
|
self.net_text1.set_text("Interface: %-13s Link: %s" % (
|
|
self.activeiface, self.netsettings[self.activeiface]['link'].
|
|
upper()))
|
|
self.net_text2.set_text("IP: %-15s MAC: %s" % (self.netsettings[
|
|
self.activeiface]['addr'],
|
|
self.netsettings[self.activeiface]['mac']))
|
|
self.net_text3.set_text("Netmask: %-15s Gateway: %s" % (
|
|
self.netsettings[self.activeiface]['netmask'],
|
|
self.gateway))
|
|
if self.netsettings[self.activeiface]['link'].upper() == "UP":
|
|
if self.netsettings[self.activeiface]['bootproto'] == "dhcp":
|
|
self.net_text4.set_text("WARNING: Cannot use interface running"
|
|
" DHCP.\nReconfigure as static in "
|
|
"Network Setup screen.")
|
|
else:
|
|
self.net_text4.set_text("")
|
|
else:
|
|
self.net_text4.set_text("WARNING: This interface is DOWN. "
|
|
"Configure it first.")
|
|
|
|
# If DHCP pool start and matches activeiface network, don't update
|
|
# This means if you change your pool values, go to another page, then
|
|
# go back, it will not reset your changes. But what is more likely is
|
|
# you will change the network settings for admin interface and then
|
|
# come back to this page to update your DHCP settings. If the
|
|
# inSameSubnet test fails, just recalculate and set new values.
|
|
for index, key in enumerate(self.fields):
|
|
if key == "ADMIN_NETWORK/dhcp_pool_start":
|
|
dhcp_start = self.edits[index].get_edit_text()
|
|
break
|
|
if network.inSameSubnet(dhcp_start,
|
|
self.netsettings[self.activeiface]['addr'],
|
|
self.netsettings[self.activeiface]['netmask']):
|
|
log.debug("Valid network settings configured. Skipping "
|
|
"generation.")
|
|
return
|
|
else:
|
|
log.debug("Existing network settings missing or invalid. "
|
|
"Updating...")
|
|
|
|
# Calculate and set Static/DHCP pool fields
|
|
# Max IPs = net size - 2 (master node + bcast)
|
|
# Add gateway so we exclude it
|
|
net_ip_list = network.getNetwork(
|
|
self.netsettings[self.activeiface]['addr'],
|
|
self.netsettings[self.activeiface]['netmask'],
|
|
self.gateway)
|
|
try:
|
|
dhcp_pool = net_ip_list[1:]
|
|
dynamic_start = str(dhcp_pool[0])
|
|
dynamic_end = str(dhcp_pool[-1])
|
|
if self.net_text4.get_text() == "":
|
|
self.net_text4.set_text("This network configuration can "
|
|
"support %s nodes." % len(dhcp_pool))
|
|
except Exception:
|
|
# We don't have valid values, so mark all fields empty
|
|
dynamic_start = ""
|
|
dynamic_end = ""
|
|
for index, key in enumerate(self.fields):
|
|
if key == "ADMIN_NETWORK/dhcp_pool_start":
|
|
self.edits[index].set_edit_text(dynamic_start)
|
|
elif key == "ADMIN_NETWORK/dhcp_pool_end":
|
|
self.edits[index].set_edit_text(dynamic_end)
|
|
elif key == "ADMIN_NETWORK/dhcp_gateway":
|
|
self.edits[index].set_edit_text(self.netsettings[
|
|
self.activeiface]['addr'])
|
|
|
|
def refresh(self):
|
|
self.getNetwork()
|
|
self.setNetworkDetails()
|
|
|
|
def screenUI(self):
|
|
return modulehelper.ModuleHelper.screenUI(self, self.header_content,
|
|
self.fields, self.defaults)
|