475 lines
18 KiB
Python
Executable File
475 lines
18 KiB
Python
Executable File
#!/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 common import timeout
|
|
import dhcp_checker.api
|
|
import dhcp_checker.utils
|
|
import logging
|
|
import operator
|
|
from optparse import OptionParser
|
|
import os
|
|
from settings import Settings
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import traceback
|
|
import urwid
|
|
import urwid.raw_display
|
|
import urwid.web_display
|
|
|
|
# set up logging
|
|
logging.basicConfig(filename='/var/log/fuelmenu.log',
|
|
format="%(asctime)s %(levelname)s %(message)s",
|
|
level=logging.DEBUG)
|
|
log = logging.getLogger('fuelmenu.loader')
|
|
|
|
|
|
class Loader(object):
|
|
|
|
def __init__(self, parent):
|
|
self.modlist = []
|
|
self.choices = []
|
|
self.child = None
|
|
self.children = []
|
|
self.childpage = None
|
|
self.parent = parent
|
|
|
|
def load_modules(self, module_dir):
|
|
if module_dir not in sys.path:
|
|
sys.path.append(module_dir)
|
|
|
|
modules = [os.path.splitext(f)[0] for f in os.listdir(module_dir)
|
|
if f.endswith('.py')]
|
|
|
|
for module in modules:
|
|
log.info('loading module %s' % module)
|
|
try:
|
|
imported = __import__(module)
|
|
pass
|
|
except ImportError as e:
|
|
log.error('module could not be imported: %s' % e)
|
|
continue
|
|
|
|
clsobj = getattr(imported, module, None)
|
|
modobj = clsobj(self.parent)
|
|
|
|
# add the module to the list
|
|
self.modlist.append(modobj)
|
|
# sort modules
|
|
self.modlist.sort(key=operator.attrgetter('priority'))
|
|
for module in self.modlist:
|
|
self.choices.append(module.name)
|
|
return (self.modlist, self.choices)
|
|
|
|
|
|
class FuelSetup(object):
|
|
|
|
def __init__(self):
|
|
self.footer = None
|
|
self.frame = None
|
|
self.screen = None
|
|
self.defaultsettingsfile = "%s/settings.yaml" \
|
|
% (os.path.dirname(__file__))
|
|
self.settingsfile = "/etc/fuel/astute.yaml"
|
|
self.managediface = "eth0"
|
|
#Set to true to move all settings to end
|
|
self.globalsave = True
|
|
self.version = self.getVersion("/etc/fuel/version.yaml")
|
|
self.main()
|
|
self.choices = []
|
|
|
|
def menu(self, title, choices):
|
|
body = [urwid.Text(title), urwid.Divider()]
|
|
for c in choices:
|
|
button = urwid.Button(c)
|
|
urwid.connect_signal(button, 'click', self.menu_chosen, c)
|
|
body.append(urwid.AttrMap(button, None, focus_map='reversed'))
|
|
return urwid.ListBox(urwid.SimpleListWalker(body))
|
|
#return urwid.ListBox(urwid.SimpleFocusListWalker(body))
|
|
|
|
def menu_chosen(self, button, choice):
|
|
size = self.screen.get_cols_rows()
|
|
self.screen.draw_screen(size, self.frame.render(size))
|
|
for item in self.menuitems.body.contents:
|
|
try:
|
|
if item.original_widget and \
|
|
item.original_widget.get_label() == choice:
|
|
item.set_attr_map({None: 'header'})
|
|
else:
|
|
item.set_attr_map({None: None})
|
|
except Exception as e:
|
|
log.info("%s" % item)
|
|
log.error("%s" % e)
|
|
self.setChildScreen(name=choice)
|
|
|
|
def setChildScreen(self, name=None):
|
|
if name is None:
|
|
self.child = self.children[0]
|
|
else:
|
|
self.child = self.children[int(self.choices.index(name))]
|
|
if not self.child.screen:
|
|
self.child.screen = self.child.screenUI()
|
|
self.childpage = self.child.screen
|
|
self.childfill = urwid.Filler(self.childpage, 'top', 40)
|
|
self.childbox = urwid.BoxAdapter(self.childfill, 40)
|
|
self.cols = urwid.Columns(
|
|
[
|
|
('fixed', 20, urwid.Pile([
|
|
urwid.AttrMap(self.menubox, 'bright'),
|
|
urwid.Divider(" ")])),
|
|
('weight', 3, urwid.Pile([
|
|
urwid.Divider(" "),
|
|
self.childbox,
|
|
urwid.Divider(" ")]))
|
|
], 1)
|
|
self.child.refresh()
|
|
self.listwalker[:] = [self.cols]
|
|
|
|
def refreshScreen(self):
|
|
size = self.screen.get_cols_rows()
|
|
self.screen.draw_screen(size, self.frame.render(size))
|
|
|
|
def refreshChildScreen(self, name):
|
|
child = self.children[int(self.choices.index(name))]
|
|
#Refresh child listwalker
|
|
child.listwalker[:] = child.listbox_content
|
|
|
|
#reassign childpage top level objects
|
|
self.childpage = urwid.ListBox(child.listwalker)
|
|
self.childfill = urwid.Filler(self.childpage, 'middle', 22)
|
|
self.childbox = urwid.BoxAdapter(self.childfill, 22)
|
|
self.cols = urwid.Columns(
|
|
[
|
|
('fixed', 20, urwid.Pile([
|
|
urwid.AttrMap(self.menubox, 'bright'),
|
|
urwid.Divider(" ")])),
|
|
('weight', 3, urwid.Pile([
|
|
urwid.Divider(" "),
|
|
self.childbox,
|
|
urwid.Divider(" ")]))
|
|
], 1)
|
|
#Refresh top level listwalker
|
|
#self.listwalker[:] = [self.cols]
|
|
|
|
def getVersion(self, versionfile):
|
|
try:
|
|
versiondata = Settings().read(versionfile)
|
|
return versiondata['VERSION']['release']
|
|
except (IOError, KeyError):
|
|
log.error("Unable to set Fuel version from %s" % versionfile)
|
|
return ""
|
|
|
|
def main(self):
|
|
#Disable kernel print messages. They make our UI ugly
|
|
noout = open('/dev/null', 'w')
|
|
subprocess.call(["sysctl", "-w", "kernel.printk=4 1 1 7"],
|
|
stdout=noout, stderr=noout)
|
|
|
|
text_header = (u"Fuel %s setup "
|
|
u"Use Up/Down/Left/Right to navigate. F8 exits."
|
|
% self.version)
|
|
text_footer = (u"Status messages go here.")
|
|
|
|
#Top and bottom lines of frame
|
|
self.header = urwid.AttrWrap(urwid.Text(text_header), 'header')
|
|
self.footer = urwid.AttrWrap(urwid.Text(text_footer), 'footer')
|
|
|
|
#Prepare submodules
|
|
loader = Loader(self)
|
|
moduledir = "%s/modules" % (os.path.dirname(__file__))
|
|
self.children, self.choices = loader.load_modules(module_dir=moduledir)
|
|
|
|
if len(self.children) == 0:
|
|
import sys
|
|
sys.exit(1)
|
|
#Build list of choices excluding visible
|
|
self.visiblechoices = []
|
|
for child, choice in zip(self.children, self.choices):
|
|
if child.visible:
|
|
self.visiblechoices.append(choice)
|
|
|
|
self.menuitems = self.menu(u'Menu', self.visiblechoices)
|
|
menufill = urwid.Filler(self.menuitems, 'top', 40)
|
|
self.menubox = urwid.BoxAdapter(menufill, 40)
|
|
|
|
self.child = self.children[0]
|
|
self.childpage = self.child.screenUI()
|
|
self.childfill = urwid.Filler(self.childpage, 'top', 22)
|
|
self.childbox = urwid.BoxAdapter(self.childfill, 22)
|
|
self.cols = urwid.Columns(
|
|
[
|
|
('fixed', 20, urwid.Pile([
|
|
urwid.AttrMap(self.menubox, 'bright'),
|
|
urwid.Divider(" ")])),
|
|
('weight', 3, urwid.Pile([
|
|
urwid.Divider(" "),
|
|
self.childbox,
|
|
urwid.Divider(" ")]))
|
|
], 1)
|
|
self.listwalker = urwid.SimpleListWalker([self.cols])
|
|
#self.listwalker = urwid.TreeWalker([self.cols])
|
|
self.listbox = urwid.ListBox(self.listwalker)
|
|
#listbox = urwid.ListBox(urwid.SimpleListWalker(listbox_content))
|
|
|
|
self.frame = urwid.Frame(urwid.AttrWrap(self.listbox, 'body'),
|
|
header=self.header, footer=self.footer)
|
|
|
|
palette = \
|
|
[
|
|
('body', 'black', 'light gray', 'standout'),
|
|
('reverse', 'light gray', 'black'),
|
|
('header', 'white', 'dark red', 'bold'),
|
|
('important', 'dark blue', 'light gray',
|
|
('standout', 'underline')),
|
|
('editfc', 'white', 'dark blue', 'bold'),
|
|
('editbx', 'light gray', 'dark blue'),
|
|
('editcp', 'black', 'light gray', 'standout'),
|
|
('bright', 'dark gray', 'light gray', ('bold', 'standout')),
|
|
('buttn', 'black', 'dark cyan'),
|
|
('buttnf', 'white', 'dark blue', 'bold'),
|
|
('light gray', 'white', 'light gray', 'bold'),
|
|
('red', 'dark red', 'light gray', 'bold'),
|
|
('black', 'black', 'black', 'bold'),
|
|
]
|
|
|
|
# use appropriate Screen class
|
|
if urwid.web_display.is_web_request():
|
|
self.screen = urwid.web_display.Screen()
|
|
else:
|
|
self.screen = urwid.raw_display.Screen()
|
|
|
|
def unhandled(key):
|
|
if key == 'f8':
|
|
raise urwid.ExitMainLoop()
|
|
if key == 'shift tab':
|
|
self.child.walker.tab_prev()
|
|
if key == 'tab':
|
|
self.child.walker.tab_next()
|
|
|
|
self.mainloop = urwid.MainLoop(self.frame, palette, self.screen,
|
|
unhandled_input=unhandled)
|
|
#Initialize each module completely before any events are handled
|
|
for child in reversed(self.children):
|
|
self.setChildScreen(name=child.name)
|
|
#Prepare DNS for resolution
|
|
dnsobj = self.children[int(self.choices.index("DNS & Hostname"))]
|
|
dnsobj.setEtcResolv()
|
|
|
|
signal.signal(signal.SIGUSR1, self.handle_sigusr1)
|
|
|
|
self.mainloop.run()
|
|
|
|
def exit_program(self, button):
|
|
#return kernel logging to normal
|
|
noout = open('/dev/null', 'w')
|
|
subprocess.call(["sysctl", "-w", "kernel.printk=7 4 1 7"],
|
|
stdout=noout, stderr=noout)
|
|
#Fix /etc/hosts and /etc/resolv.conf before quitting
|
|
dnsobj = self.children[int(self.choices.index("DNS & Hostname"))]
|
|
dnsobj.fixEtcHosts()
|
|
dnsobj.setEtcResolv('127.0.0.1')
|
|
|
|
raise urwid.ExitMainLoop()
|
|
|
|
def handle_sigusr1(self, signum, stack):
|
|
log.info("Received signal: %s" % signum)
|
|
try:
|
|
savetimeout = 60
|
|
success, modulename = timeout.run_with_timeout(
|
|
self.global_save, timeout=savetimeout,
|
|
default=(False, "timeout"))
|
|
if success:
|
|
log.info("Save successful!")
|
|
else:
|
|
log.error("Save failed on module %s" % modulename)
|
|
|
|
except (KeyboardInterrupt, timeout.TimeoutError):
|
|
log.exception("Save on signal timed out. Save not complete.")
|
|
except Exception:
|
|
log.exception("Save failed for unknown reason:")
|
|
self.exit_program(None)
|
|
|
|
def global_save(self):
|
|
#Runs save function for every module
|
|
for module, modulename in zip(self.children, self.choices):
|
|
#Run invisible modules. They may not have screen methods
|
|
if not module.visible:
|
|
try:
|
|
module.apply(None)
|
|
except Exception as e:
|
|
log.error("Unable to save module %s: %s" % (modulename, e))
|
|
continue
|
|
else:
|
|
try:
|
|
log.info("Checking and applying module: %s"
|
|
% modulename)
|
|
self.footer.set_text("Checking and applying module: %s"
|
|
% modulename)
|
|
self.refreshScreen()
|
|
module.refresh()
|
|
if module.apply(None):
|
|
log.info("Saving module: %s" % modulename)
|
|
else:
|
|
return False, modulename
|
|
except AttributeError as e:
|
|
log.debug("Module %s does not have save function: %s"
|
|
% (modulename, e))
|
|
return True, None
|
|
|
|
|
|
def setup():
|
|
urwid.web_display.set_preferences("Fuel Setup")
|
|
# try to handle short web requests quickly
|
|
if urwid.web_display.handle_short_request():
|
|
return
|
|
FuelSetup()
|
|
|
|
|
|
def save_only(iface, settingsfile='/etc/fuel/astute.yaml'):
|
|
import common.network as network
|
|
from common import pwgen
|
|
import netifaces
|
|
|
|
#Calculate and set Static/DHCP pool fields
|
|
#Max IPs = net size - 2 (master node + bcast)
|
|
try:
|
|
ip = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr']
|
|
netmask = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['netmask']
|
|
mac = netifaces.ifaddresses(iface)[netifaces.AF_LINK][0]['addr']
|
|
except Exception:
|
|
print("Interface %s is missing either IP address or netmask"
|
|
% (iface))
|
|
sys.exit(1)
|
|
net_ip_list = network.getNetwork(ip, netmask)
|
|
try:
|
|
dhcp_pool = net_ip_list[1:]
|
|
dynamic_start = str(dhcp_pool[0])
|
|
dynamic_end = str(dhcp_pool[-1])
|
|
except Exception:
|
|
print("Unable to define DHCP pools")
|
|
sys.exit(1)
|
|
try:
|
|
hostname, sep, domain = os.uname()[1].partition('.')
|
|
except Exception:
|
|
print("Unable to calculate hostname and domain")
|
|
sys.exit(1)
|
|
try:
|
|
dhcptimeout = 5
|
|
default = []
|
|
with timeout.run_with_timeout(dhcp_checker.utils.IfaceState, [iface],
|
|
timeout=dhcptimeout) as iface:
|
|
dhcp_server_data = timeout.run_with_timeout(
|
|
dhcp_checker.api.check_dhcp_on_eth,
|
|
[iface, dhcptimeout], timeout=dhcptimeout,
|
|
default=default)
|
|
except (KeyboardInterrupt, timeout.TimeoutError):
|
|
log.debug("DHCP scan timed out")
|
|
log.warning(traceback.format_exc())
|
|
dhcp_server_data = default
|
|
|
|
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))
|
|
print("ERROR: %s foreign DHCP server(s) found: %s" %
|
|
(num_dhcp, dhcp_server_data))
|
|
if network.duplicateIPExists(ip, iface):
|
|
log.error("Duplicate host found with IP {0}".format(ip))
|
|
print("ERROR: Duplicate host found with IP {0}".format(ip))
|
|
|
|
newsettings = Settings().read(settingsfile)
|
|
settings = \
|
|
{
|
|
"ADMIN_NETWORK/interface": iface,
|
|
"ADMIN_NETWORK/ipaddress": ip,
|
|
"ADMIN_NETWORK/netmask": netmask,
|
|
"ADMIN_NETWORK/mac": mac,
|
|
"ADMIN_NETWORK/dhcp_pool_start": dynamic_start,
|
|
"ADMIN_NETWORK/dhcp_pool_end": dynamic_end,
|
|
"ADMIN_NETWORK/dhcp_gateway": ip,
|
|
"HOSTNAME": hostname,
|
|
"DNS_DOMAIN": domain,
|
|
"DNS_SEARCH": domain,
|
|
"astute/user": "naily",
|
|
"astute/password": pwgen.password(),
|
|
"cobbler/user": "cobbler",
|
|
"cobbler/password": pwgen.password(),
|
|
"keystone/admin_token": pwgen.password(),
|
|
"keystone/ostf_user": "ostf",
|
|
"keystone/ostf_password": pwgen.password(),
|
|
"keystone/nailgun_user": "nailgun",
|
|
"keystone/nailgun_password": pwgen.password(),
|
|
"keystone/monitord_user": "monitord",
|
|
"keystone/monitord_password": pwgen.password(),
|
|
"mcollective/user": "mcollective",
|
|
"mcollective/password": pwgen.password(),
|
|
"postgres/keystone_dbname": "keystone",
|
|
"postgres/keystone_user": "keystone",
|
|
"postgres/keystone_password": pwgen.password(),
|
|
"postgres/nailgun_dbname": "nailgun",
|
|
"postgres/nailgun_user": "nailgun",
|
|
"postgres/nailgun_password": pwgen.password(),
|
|
"postgres/ostf_dbname": "ostf",
|
|
"postgres/ostf_user": "ostf",
|
|
"postgres/ostf_password": pwgen.password(),
|
|
"FUEL_ACCESS/user": "admin",
|
|
"FUEL_ACCESS/password": "admin",
|
|
}
|
|
for setting in settings.keys():
|
|
if "/" in setting:
|
|
part1, part2 = setting.split("/")
|
|
if part1 not in newsettings.keys():
|
|
newsettings[part1] = {}
|
|
#Keep old values for passwords if already set
|
|
if "password" in setting:
|
|
newsettings[part1].setdefault(part2, settings[setting])
|
|
else:
|
|
newsettings[part1][part2] = settings[setting]
|
|
else:
|
|
if "password" in setting:
|
|
newsettings.setdefault(setting, settings[setting])
|
|
else:
|
|
newsettings[setting] = settings[setting]
|
|
|
|
#Write astute.yaml
|
|
Settings().write(newsettings, defaultsfile=None,
|
|
outfn=settingsfile)
|
|
|
|
|
|
def main(*args, **kwargs):
|
|
if urwid.VERSION < (1, 1, 0):
|
|
print("This program requires urwid 1.1.0 or greater.")
|
|
|
|
parser = OptionParser()
|
|
parser.add_option("-s", "--save-only", dest="save_only",
|
|
action="store_true",
|
|
help="Save default values and exit.")
|
|
|
|
parser.add_option("-i", "--iface", dest="iface", metavar="IFACE",
|
|
default="eth0", help="Set IFACE as primary.")
|
|
|
|
options, args = parser.parse_args()
|
|
|
|
if options.save_only:
|
|
save_only(options.iface)
|
|
else:
|
|
setup()
|
|
|
|
if '__main__' == __name__ or urwid.web_display.is_web_request():
|
|
setup()
|