#!/usr/bin/python

# cabaret -- Filesystem configuration tool
# Copyright (C) 1997 Red Hat Software, Inc
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

import sys
if not '/usr/lib/rhs/python' in sys.path:
    sys.path[:0] = ['/usr/lib/rhs/python']

import os
import string

from ConfFstab import ConfFstab

from snack import *





# Module-scope variables

fslist = (
	('Linux', 'ext2'),
	('Network', 'nfs'),
	('CD-ROM', 'isofs'),
	('SMB Network', 'smbfs'),
	('MSDOS FAT-16', 'msdos'),
	('Window FAT-16', 'vfat'),
	('OS/2', 'hpfs'),
#	('Novell NCP', 'ncpfs'), netware fs we don't handle here yet
)

network_fslist = ('nfs', 'smbfs', 'ncpfs')






# Non-interactive helper functions

def format_fsline(fsline, width):
    devWidth = width - 41
    format_string = "%%-20.20s %%-%d.%ds  %%-10.10s" % (devWidth, devWidth)
    return format_string % (fsline.mountpoint, fsline.device, fsline.fstype)


def get_nfs_exports(host):
    # for now, assume that we have permission to mount anything exported...
    f = os.popen("/usr/sbin/showmount -e "+host+" 2>/dev/null |"+
		 " tail +2 | awk '{print $1}' | sort | uniq")
    remotedirs = f.readlines()
    f.close
    # strip newlines
    for index in range(len(remotedirs)):
	remotedirs[index] = remotedirs[index][:-1]
    return remotedirs


def fs_mounted(screen, mount):
    mounts = ConfFstab('/proc/mounts')
    return mounts.has_key(mount)


def mount_filesystem(mount, fsline):
    # attempt to mount the filesystem asyncronously; ignore errors
    return os.system(string.join(("/bin/mount -t", fsline.fstype,
	"-o", fsline.option_reflector.get_string(),
	fsline.device, mount,
	" >/dev/null 2>&1 &")))

def unmount_filesystem(mount):
    # attempt to unmount the filesystem asyncronously; ignore errors
    return os.system("/bin/umount "+mount+" >/dev/null 2>&1 &")

def format_filesystem(device, mount, type):
    # attempt to format the filesystem syncronously
    unmount_filesystem(mount)
    return os.system("/sbin/mkfs -t "+type+" "+device+" >/dev/null 2>&1")

def create_mountpoint(mount):
    try:
	return os.mkdir(mount, 0755)
    except:
	return None


def opt_off(v):
    """boolean for things that default to off"""
    if not v: return 0
    return 1

def opt_on(v):
    """boolean for things that default to on"""
    if v in ('1', 1, ''): return 1
    return 0

def bool_not(v):
    if v: return 0
    return 1








# Function for printing an error message

def show_error(screen, message):
    l = Label(message)
    b = Button("OK")
    g = GridForm(screen, "Error", 1, 2)
    g.add(l, 0, 0, anchorLeft=1)
    g.add(b, 0, 1, growx=1)
    g.form.addHotKey("F12")
    screen.pushHelpLine("                                F12=OK")
    g.run_once()
    screen.popHelpLine()










# Functions for editing fstab entries

def edit_fsline_helper(screen, mount):
    opts = screen.fstab[mount].options
    fstype = screen.fstab[mount].fstype

    auto = Checkbox("Mount automatically at boot", opt_on(opts.auto))
    rw = Checkbox("Writable", opt_on(opts.rw))
    user = Checkbox("Users can mount filesystem", opt_off(opts.user))
    nosuid = Checkbox("Ignore setuid executables", bool_not(opt_on(opts.suid)))
    nodev = Checkbox("Ignore device files", bool_not(opt_on(opts.dev)))
    noexc = Checkbox("Ignore executables", bool_not(opt_on(opts.exc)))
    sync = Checkbox("Update metadata syncronously", opt_off(opts.sync))
    atime = Checkbox("Update atime", opt_on(opts.atime))
    if fstype == 'nfs':
	bb = ButtonBar(screen, (("NFS Options", "nfs", "F2"),
				("Paths", "paths", "F3"),
				("Type", "type", "F4"),
				("Cancel", 0, "F9"),
				("Done", 1, "F12")))
	screen.pushHelpLine('        F2=NFS Options    F3=Paths    F4=Type   F9=Cancel    F12=Done')

	intr = Checkbox("Allow signals to interrupt", opt_off(opts.intr))
	hard = Checkbox("Retry requests indefinitely",
			bool_not(opt_off(opts.soft)))
	bg = Checkbox("Retry in background if mount fails",
			bool_not(opt_on(opts.fg)))
	rsizel = Label("Size of read buffer:")
	rsize = Entry(width = 10, scroll = 1, text=opts.rsize)
	wsizel = Label("Size of write buffer:")
	wsize = Entry(width = 10, scroll = 1, text=opts.wsize)
	nb = Button("Done")
    else:
	bb = ButtonBar(screen, (("Paths", "paths", "F3"),
				("Type", "type", "F4"),
				("Cancel", 0, "F9"),
				("Done", 1, "F12")))
	screen.pushHelpLine('                 F3=Paths    F4=Type    F9=Cancel    F12=Done')
    g = GridForm(screen, "Options for "+mount, 1, 9)
    g.add(auto, 0, 0, anchorLeft = 1)
    g.add(rw, 0, 1, anchorLeft = 1)
    g.add(user, 0, 2, anchorLeft = 1)
    g.add(nosuid, 0, 3, anchorLeft = 1)
    g.add(nodev, 0, 4, anchorLeft = 1)
    g.add(noexc, 0, 5, anchorLeft = 1)
    g.add(sync, 0, 6, anchorLeft = 1)
    g.add(atime, 0, 7, anchorLeft = 1)
    g.add(bb, 0, 8, (0, 1, 0, 0), growx = 1)

    # A gridform for changing paths
    newmountl = Label("Mountpoint:")
    newmount = Entry(width=30, scroll=1, text=mount)
    newdevl = Label("Device:")
    newdev = Entry(width=30, scroll=1, text=screen.fstab[mount].device)
    pb = Button("Done")
    pg = GridForm(screen, "Change paths for "+mount, 1, 5)
    pg.add(newmountl, 0, 0, anchorLeft=1)
    pg.add(newmount, 0, 1, anchorLeft=1)
    pg.add(newdevl, 0, 2, (0, 1, 0, 0), anchorLeft=1)
    pg.add(newdev, 0, 3, (0, 0, 0, 1), anchorLeft=1)
    pg.add(pb, 0, 4, growx=1)
    pg.form.addHotKey("F12")

    # A gridform for changing fstype
    type = Listbox(height = min(len(fslist), screen.height - 18),
		   width = 20, returnExit = 1)
    for (name, thisfstype) in fslist:
	type.append(name+" ("+thisfstype+")", thisfstype)
    type.setCurrent(fstype)
    typel = Label("Or type one here:")
    typee = Entry(text=fstype, width = 20, scroll = 0, returnExit = 1)
    tb = ButtonBar(screen, (("Set", 1, "F12"), ("Cancel", 0, "F9")))
    tg = GridForm(screen, "Choose Filesystem Type", 1, 4)
    tg.add(type, 0, 0, anchorLeft = 1)
    tg.add(typel, 0, 1, (0, 1, 0, 0), anchorLeft=1)
    tg.add(typee, 0, 2, (0, 0, 0, 1), anchorLeft = 1)
    tg.add(tb, 0, 3, growx = 1)

    if fstype == 'nfs':
	ng = GridForm(screen, "NFS Options for "+mount, 1, 8)
	ng.add(intr, 0, 0, anchorLeft = 1)
	ng.add(hard, 0, 1, anchorLeft = 1)
	ng.add(bg, 0, 2, anchorLeft = 1)
	ng.add(rsizel, 0, 3, (0, 1, 0, 0), anchorLeft = 1)
	ng.add(rsize, 0, 4, anchorLeft = 1)
	ng.add(wsizel, 0, 5, (0, 1, 0, 0), anchorLeft = 1)
	ng.add(wsize, 0, 6, anchorLeft = 1)
	ng.add(nb, 0, 7, (0, 1, 0, 0))
	ng.form.addHotKey("F12")

    while 1:
	result = bb.buttonPressed(g.run())
	if not result:
	    screen.popHelpLine()
	    screen.popWindow()
	    return None
	elif result == 'nfs':
	    # can only happen for an NFS filesystem
	    screen.pushHelpLine('                                    F12=Done')
	    ng.run_popup()
	    screen.popHelpLine()
	elif result == 'paths':
	    screen.pushHelpLine('                                    F12=Done')
	    pg.run_popup()
	    screen.popHelpLine()
        elif result == 'type':
	    screen.pushHelpLine('                             F12=Set   F9=Cancel')
	    tresult = tg.run_popup()
	    if tresult == typee:
		screen.fstab[mount].fstype = typee.value()
	    elif tresult == type or tb.buttonPressed(tresult):
		screen.fstab[mount].fstype = type.current()
	    screen.popHelpLine()
	else:
	    # must be done
	    break

    screen.popHelpLine()
    screen.popWindow()


    opts.auto = auto.value()
    opts.rw = rw.value()
    opts.user = user.value()
    opts.suid = bool_not(nosuid.value())
    opts.dev = bool_not(nodev.value())
    opts.exc = bool_not(noexc.value())
    opts.sync = sync.value()
    opts.atime = atime.value()
    if fstype == 'nfs':
	opts.intr = intr.value()
	opts.soft = bool_not(hard.value())
	opts.fg = bool_not(bg.value())
	opts.rsize = rsize.value()
	opts.wsize = wsize.value()

    screen.fstab[mount].device = newdev.value()

    if mount != newmount.value():
	screen.fstab.move(mount, newmount.value())

    return newmount.value()

def edit_fsline(screen, mount, delete_on_cancel = 0):
    """returns None on cancel"""

    newmount = edit_fsline_helper(screen, mount)
    if newmount: return newmount

    # must have cancelled
    if delete_on_cancel:
	del screen.fstab[mount]
    return None







# Functions for adding fstab entries.

def get_mount_device(screen):
    """returns None on cancel"""
    ml = Label("Mountpoint:")
    m = Entry(width = 20)
    dl = Label("Device:")
    d = Entry(width = 20)
    bb = ButtonBar(screen, (("Continue", 1, "F12"), ("Cancel", 0, "F9")))
    screen.pushHelpLine('                          F12=Continue   F9=Cancel')
    g = GridForm(screen, "Choose Paths", 1, 5)
    g.add(ml, 0, 0, anchorLeft = 1)
    g.add(m, 0, 1, (0, 0, 0, 1))
    g.add(dl, 0, 2, anchorLeft = 1)
    g.add(d, 0, 3, (0, 0, 0, 1))
    g.add(bb, 0, 4, growx = 1)
    result = g.run_once()
    screen.popHelpLine()
    if bb.buttonPressed(result):
	return (1, m.value(), d.value())
    else:
	return (None, None, None)

def get_mount_device_nfs(screen):
    """returns None on cancel"""
    sl = Label("Remote system:")
    s = Entry(width = 20, returnExit = 1)
    bb = ButtonBar(screen, (("Continue", 1, "F12"), ("Cancel", 0, "F9")))
    screen.pushHelpLine('                          F12=Continue   F9=Cancel')
    g = GridForm(screen, "Choose Remote System", 1, 3)
    g.add(sl, 0, 0, anchorLeft = 1)
    g.add(s, 0, 1, (0, 0, 0, 1))
    g.add(bb, 0, 2, growx = 1)
    result = g.run_once()
    screen.popHelpLine()
    if not bb.buttonPressed(result) and not result == s:
	return (None, None, None)

    remotedirs = get_nfs_exports(s.value())

    rl = Label("Remote path:")
    if len(remotedirs):
	rp = Listbox(height = min(len(remotedirs), screen.height - 19),
		     width = 30)
	for dir in remotedirs:
	    rp.append(dir, dir)
    else:
	rp = Entry(width = 30)
    ll = Label("Local path:")
    lp = Entry(width = 30)

    bb = ButtonBar(screen, (("Continue", 1, "F12"), ("Cancel", 0, "F9")))
    screen.pushHelpLine('                          F12=Continue   F9=Cancel')
    g = GridForm(screen, "Choose Paths", 1, 5)
    g.add(rl, 0, 0, anchorLeft = 1)
    g.add(rp, 0, 1, (0, 0, 0, 1))
    g.add(ll, 0, 2, anchorLeft = 1)
    g.add(lp, 0, 3, (0, 0, 0, 1))
    g.add(bb, 0, 4, growx = 1)
    result = g.run_once()
    screen.popHelpLine()
    if not bb.buttonPressed(result):
	return (None, None, None)

    if rp.__dict__.has_key("value"):
	remotePath = rp.value()
    else:
	remotePath = rp.current()

    return (1, lp.value(), s.value()+":"+remotePath)


def get_mount_device_network(screen):
    """returns None on cancel"""
    sl = Label("Remote system:")
    s = Entry(width = 20)
    rl = Label("Remote path:")
    r = Entry(width = 20)
    ll = Label("Local path:")
    l = Entry(width=20)
    bb = ButtonBar(screen, (("Continue", 1, "F12"), ("Cancel", 0, "F9")))
    screen.pushHelpLine('                          F12=Continue   F9=Cancel')
    g = GridForm(screen, "Choose Device and Mountpoint", 1, 7)
    g.add(sl, 0, 0, anchorLeft = 1)
    g.add(s, 0, 1, (0, 0, 0, 1))
    g.add(rl, 0, 2, anchorLeft = 1)
    g.add(r, 0, 3, (0, 0, 0, 1))
    g.add(ll, 0, 4, anchorLeft = 1)
    g.add(l, 0, 5, (0, 0, 0, 1))
    g.add(bb, 0, 6, growx = 1)
    result = g.run_once()
    screen.popHelpLine()
    if bb.buttonPressed(result):
	return (1, l.value(), s.value()+":"+r.value())
    else:
	return (None, None, None)

def add_fsline(screen):
    """returns None on cancel"""
    tl = Label("Choose a filesystem:")
    l = Listbox(height = min(len(fslist), screen.height - 18),
		width = 20, returnExit = 1)
    for (name, fstype) in fslist:
	l.append(name+" ("+fstype+")", fstype)
    te = Label("Or type one here:")
    e = Entry(width = 20, scroll = 0, returnExit = 1)
    bb = ButtonBar(screen, (("Add", 1, "F12"), ("Cancel", 0, "F9")))
    screen.pushHelpLine('                              F12=Add   F9=Cancel')
    g = GridForm(screen, "Choose Filesystem Type", 1, 5)
    g.add(tl, 0, 0, anchorLeft = 1)
    g.add(l, 0, 1, (0, 0, 0, 1))
    g.add(te, 0, 2, anchorLeft = 1)
    g.add(e, 0, 3, (0, 0, 0, 1))
    g.add(bb, 0, 4, growx = 1)
    result = g.run_once()
    screen.popHelpLine()
    button =  bb.buttonPressed(result)
    if button == 0:
	return None
    elif button == 1:
	if e.value(): fstype = e.value()
	else: fstype = l.current()
    elif result == e:
	fstype = e.value()
    elif result == l:
	fstype = l.current()
    else:
	raise NameError, "add_fsline: result unknown: "+str(result)

    if fstype == 'nfs':
	(cont, mount, device) = get_mount_device_nfs(screen)
    elif fstype in network_fslist:
	(cont, mount, device) = get_mount_device_network(screen)
    else:
	(cont, mount, device) = get_mount_device(screen)

    if not cont: return None

    # do not let users create existing mountpoints
    if screen.fstab.has_key(mount):
	show_error(screen, "Mountpoint already exists")
	return None

    # We add this so that edit_fsline has something to edit;
    # we tell edit_fsline to delete_on_cancel so that it goes away
    # again if they cancel.
    screen.fstab.add(device, mount, fstype)

    mount = edit_fsline(screen, mount, delete_on_cancel = 1)
    if mount: return mount
    else: return None








# Function for deleting fstab entries

def delete_fsline(screen, mount):
    """Ask about unmounting filesystem, but only if it is mounted.
    Offer user a chance to cancel."""
    # yes, we really want Textboxes not Labels here; keeps the help lined
    # up and won't mess up the screen for really long mounts.
    if fs_mounted(screen, mount):
	t = Textbox(60, 1, "Unmount and delete filesystem "+mount+"?")
	bb = ButtonBar(screen, (("Unmount", 2, "F12"),
				("Delete", 1, "F3"),
				("Cancel", 0, "F9")))
	screen.pushHelpLine('         F12=Unmount      F3=Delete     F9=Cancel')
    else:
	t = Textbox(60, 1, "Delete filesystem "+mount+"?")
	bb = ButtonBar(screen, (("Delete", 1, "F3"),
				("Cancel", 0, "F9")))
	screen.pushHelpLine('              F3=Delete             F9=Cancel')

    g = GridForm(screen, "Delete?", 1, 2)
    g.add(t, 0, 0, (0, 0, 0, 2))
    g.add(bb, 0, 1, growx = 1)
    result = g.run_once()
    screen.popHelpLine()
    action = bb.buttonPressed(result)

    if not action: return 0

    if action == 2:
	unmount_filesystem(mount)

    del screen.fstab[mount]
    return 1









# Functions for changing the status of fstab entries

def format_filesystem_fsline(screen, mount, fsline):
    """Make SURE that the user really wants to format the filesystem,
    and let them choose the type."""

    radio_list = []
    for (label, fstype) in fslist:
	if fstype == fsline.fstype: default = 1
	else: default = 0
	radio_list.append(label+" ("+fstype+")", fstype, default)
    rb = RadioBar(screen, radio_list)

    bb = ButtonBar(screen, (("OK", 1, "F12"),
			    ("Cancel", 0, "F9")))
    screen.pushHelpLine('                             F12=OK    F9=Cancel')

    g = GridForm(screen, "Choose filesystem type", 1, 2)
    g.add(rb, 0, 0)
    g.add(bb, 0, 1, growx=1)
    result = g.run_once()
    screen.popHelpLine()

    if not bb.buttonPressed(result):
	# cancel
	return None

    format_filesystem(fsline.device, mount, rb.getSelection())

def status_fsline(screen, mount, fsline):
    """Offer status-changing options that DO NOT affect the fstab file"""
    if fs_mounted(screen, mount):
	mounted = "mounted"
	bb = ButtonBar(screen, (("Unmount", 1, "F12"),
				("Cancel", 0, "F9")))
	screen.pushHelpLine('                       F12=Unmount         F9=Cancel')
    else:
	bb = ButtonBar(screen, (("Mount", 2, "F12"),
				("Format", 3, "F6"),
				("Cancel", 0, "F9")))
	mounted = "unmounted"
	screen.pushHelpLine('                  F12=Mount    F6=Format     F9=Cancel')

    l = Label(mount+" is "+mounted)


    g = GridForm(screen, "Status", 1, 2)
    g.add(l, 0, 0, (0, 0, 0, 2), anchorLeft = 1)
    g.add(bb, 0, 1, growx = 1)
    result = g.run_once()
    screen.popHelpLine()
    action = bb.buttonPressed(result)

    if not action: return 0

    elif action == 1:
	return unmount_filesystem(mount)

    elif action == 2:
	return mount_filesystem(mount, fsline)

    elif action == 3:
	return format_filesystem_fsline(screen, mount, fsline)

    return 1










# Function for saving fstab entries

def ask_save_changes(screen, quit = 0):
    if quit:
	t = Textbox(40, 1, "Save changes before quitting?")
	onfail = "Discard"
    else:
	t = Textbox(40, 1, "Save changes?")
	onfail = "Cancel"
    bb = ButtonBar(screen, (("Save", 1, "F12"), (onfail, 0, "F9")))
    screen.pushHelpLine('                         F12=Save          F9='+onfail)
    g = GridForm(screen, "Save?", 1, 2)
    g.add(t, 0, 0, (0, 0, 0, 2))
    g.add(bb, 0, 1, growx = 1)
    result = g.run_once()
    screen.popHelpLine()
    return bb.buttonPressed(result)








class MainForm(SnackScreen):

    def draw_main_window(self):

	# should we throw an exception if the window is too small?

	# resize this string to fit the display
	spaceWidth = self.width - 49
	format_string = "%%s %%-%d.%ds  %%s" % (spaceWidth, spaceWidth)
	self.drawRootText(0, 0, format_string % (
		"cabaret 0.5", "", "Copyright (C) 1997 Red Hat Software"))


	fsbox = Listbox(height = self.height - 12, width = self.width - 8,
			returnExit = 1)
	# consider sorting these later; fstab.keys() returns them in
	# the order they are in the fstab file.
	for mountpoint in self.fstab.keys():
	     fsbox.append(format_fsline(self.fstab[mountpoint], self.width),
			  mountpoint)

	bb = ButtonBar(self, (("Edit", "edit", "F1"),
				("Add", "add", "F2"),
				("Delete", "delete", "F3"),
				("Status", "status", "F4"),
				("Save", "save", "F5"),
				("Quit", "quit", "F12")))

	# consider trying to resize this string?  Difficult to make the
	# strings always come out exactly beneath the buttons, though.
	self.pushHelpLine(
          '     F1=Edit     F2=Add     F3=Delete     F4=Status    F5=Save    F12=Quit')

	g = GridForm(self, "Edit Filesystem Configuration", 1, 2)
	g.add(fsbox, 0, 0, (0, 0, 0, 1))
	g.add(bb, 0, 1, growx = 1)

	return ((g, fsbox, bb))


    def run(self):

	save_changes = 0
	self.fstab = ConfFstab()

	(g, fsbox, bb) = self.draw_main_window()

	while 1:
	    result = g.run()
	    button = bb.buttonPressed(result)
	    if button:
		if button == "edit":
		    mount = edit_fsline(self, fsbox.current())
		    if mount:
			# may have changed what should be in the display;
			# since that includes the key, best to redraw.
			create_mountpoint(mount)
			self.popWindow
			(g, fsbox, bb) = self.draw_main_window()
			fsbox.setCurrent(mount)
			save_changes = 1
		elif button == "add":
		    mount = add_fsline(self)
		    if mount:
			create_mountpoint(mount)
	 		fsbox.append(format_fsline(self.fstab[mount],
						   self.width),
				     mount)
			fsbox.setCurrent(mount)
			save_changes = 1
		elif button == "delete":
		    if delete_fsline(self, fsbox.current()):
			fsbox.delete(fsbox.current())
			save_changes = 1
		elif button == "status":
		    if status_fsline(self, fsbox.current(), self.fstab[fsbox.current()]):
			save_changes = 1
		elif button == "save":
		    if ask_save_changes(self):
			self.fstab.write()
			save_changes = 0
		elif button == "quit":
		    break;
	    elif result == fsbox:
		mount = edit_fsline(self, fsbox.current())
		if mount:
		    # may have changed what should be in the display;
		    # since that includes the key, best to redraw.
		    self.popWindow
		    (g, fsbox, bb) = self.draw_main_window()
		    fsbox.setCurrent(mount)
		    save_changes = 1
	    elif result == "RESIZE":
		# Some day, snack will be capable of this, and we'll be ready.
		self.popWindow
		(g, fsbox, bb) = self.draw_main_window()
	    else:
		raise NameError, "Unknown button"

	if save_changes and ask_save_changes(self, quit=1):
	    self.fstab.write()



if __name__ == '__main__':
    m = MainForm()
    try:
	m.run()
	m.popWindow()
	m.finish()
	m.fstab.canonize()
	for line in m.fstab.lines:
	    print line
    except:
	m.finish()
	print "error in cabaret:"
	raise sys.exc_type, sys.exc_value, sys.exc_traceback

