diff --git a/aif_gen/__init__.py b/aif_gen/__init__.py new file mode 100644 index 0000000..d63bc18 --- /dev/null +++ b/aif_gen/__init__.py @@ -0,0 +1 @@ +from . import config diff --git a/aif_gen/config/__init__.py b/aif_gen/config/__init__.py new file mode 100644 index 0000000..41d2e88 --- /dev/null +++ b/aif_gen/config/__init__.py @@ -0,0 +1 @@ +from . import generator diff --git a/aif_gen/config/generator/__init__.py b/aif_gen/config/generator/__init__.py new file mode 100644 index 0000000..4708fb0 --- /dev/null +++ b/aif_gen/config/generator/__init__.py @@ -0,0 +1,3 @@ +from . import subsections +from . import main +from . import utils diff --git a/aif_gen/config/generator/main.py b/aif_gen/config/generator/main.py new file mode 100755 index 0000000..eccfa72 --- /dev/null +++ b/aif_gen/config/generator/main.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 + +import os +import tkinter +import tkinter.filedialog +import tkinter.messagebox +## +import requests +from lxml import etree +## +from . import subsections + + +class Configurator(tkinter.Tk): + def __init__(self, version = '0.2.0', *args, **kwargs): + super().__init__(*args, **kwargs) + maxwidth, maxheight = self.winfo_screenwidth(), self.winfo_screenheight() + self.geometry('{0}x{1}+0+0'.format(maxwidth, maxheight)) + self.title('AIF-NG Configuration Generator') + self.xml_attrib = {('{http://www.w3.org/2001/XMLSchema-instance}' + 'schemaLocation'): ('http://aif-ng.io/ ' + 'http://aif-ng.io/aif.xsd'), + 'version': version} + self.xml_nsmap = {'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + None: 'http://aif-ng.io/'} + self.xml = etree.Element('aif', attrib = self.xml_attrib, nsmap = self.xml_nsmap) + self.xsd = None + self.savefilename = None + self.saveelems = [] + self._initMenuBar() + self._initXSD() + self.build() + + def _initMenuBar(self): + menubar = tkinter.Menu(self) + # File + filemenu = tkinter.Menu(menubar, tearoff = 0) + filemenu.add_command(label = 'New', command = self.file_new) + filemenu.add_command(label = 'Clear', command = self.file_clear) + filemenu.add_command(label = 'Save', command = self.file_save) + filemenu.add_command(label = 'Save as...', command = self.file_saveas) + filemenu.add_command(label = 'Load...', command = self.file_load) + filemenu.add_separator() + filemenu.add_command(label="Exit", command = self.exit) + menubar.add_cascade(label = 'File', menu = filemenu) + # Help + helpmenu = tkinter.Menu(menubar, tearoff = 0) + helpmenu.add_command(label = 'About') + menubar.add_cascade(label = 'Help', menu = helpmenu) + ## + self.config(menu = menubar) + return() + + def _initXSD(self, xsdpath = None): + # TODO: locally-cache XSD file? + if xsdpath: + xsdpath = os.path.abspath(os.path.expanduser(xsdpath)) + if not os.path.isfile(xsdpath): + raise ValueError(('An explicit XSD path was specified but ' + 'does not exist on the local filesystem')) + with open(xsdpath, 'rb') as fh: + raw_xsd = fh.read() + else: + xsi = self.xml.nsmap.get('xsi', 'http://www.w3.org/2001/XMLSchema-instance') + schemaLocation = '{{{0}}}schemaLocation'.format(xsi) + schemaURL = self.xml.attrib.get(schemaLocation, + 'https://aif-ng.io/aif.xsd?ref={0}'.format(self.xml.attrib['version'])) + split_url = schemaURL.split() + if len(split_url) == 2: # a properly defined schemaLocation + schemaURL = split_url[1] + else: + schemaURL = split_url[0] # a LAZY schemaLocation + req = requests.get(schemaURL) + if not req.ok: + raise RuntimeError('Could not download XSD') + raw_xsd = req.content + self.xsd = etree.XMLSchema(etree.XML(raw_xsd)) + return() + + def build(self): + self.saveelems.append(subsections.meta.Obj(self.xml, self)) + self.saveelems.append(subsections.storage.Obj(self.xml, self)) + # self.update() + return() + + def check(self): + if not self.xsd: + self._initXSD() + return(self.xsd.validate(self.xml)) + + def clearinput(self): + # ??? + # maybe https://stackoverflow.com/a/2260355/733214 + # or maybe https://stackoverflow.com/a/19477781/733214 ? + self.xml = etree.Element('aif', attrib = self.xml_attrib, nsmap = self.xml_nsmap) + for i in self.saveelems: + i.new() + return() + + def exit(self): + if not self.check(): + tkinter.messagebox.showwarning('Incompatible Configuration', + ('The configuration state as currently defined is not a valid ' + 'configuration file for AIF-NG. It will not work properly until ' + 'completed.')) + # Check for unsaved changes here? + self.destroy() + return() + + def file_load(self): + fname = tkinter.filedialog.askopenfilename(defaultextension = '.xml') + with open(fname, 'rb') as fh: + try: + self.xml = etree.fromstring(fh.read()) + except etree.XMLSyntaxError as e: + tkinter.messagebox.showerror('Invalid XML', + ('The imported configuration ({0}) is invalid XML: {1}').format(fname, + e)) + return() + if not self.check(): + tkinter.messagebox.showwarning('Incompatible Configuration', + ('The imported configuration ({0}) is not a valid configuration file ' + 'for AIF-NG. It will not work properly until completed.').format(fname)) + return() + + def file_clear(self): + self.clearinput() + return() + + def file_new(self): + self.clearinput() + self.savefilename = None + return() + + def file_save(self): + self.savefile() + return() + + def file_saveas(self): + self.savefile(forcefile = True) + + def savefile(self, forcefile = False): + if not self.check(): + # TODO: make this persistently configurable? + tkinter.messagebox.showwarning('Incompatible Configuration', + ('The configuration state as currently defined is not a valid ' + 'configuration file for AIF-NG. It will not work properly until ' + 'completed.')) + if not self.savefilename or forcefile: + savefilename = tkinter.filedialog.asksaveasfilename(defaultextension = '.xml') + if savefilename is None: # "Cancel" button + return() + self.savefilename = savefilename + for i in self.saveelems: + i.save() + with open(self.savefilename, 'wb') as fh: + fh.write(etree.tostring(self.xml, + encoding = 'utf-8', + xml_declaration = True, + pretty_print = True, + with_tail = True, + inclusive_ns_prefixes = True)) + return() + + +# if __name__ == '__main__': +# app = Configurator() +# app.mainloop() diff --git a/aif_gen/config/generator/subsections/__init__.py b/aif_gen/config/generator/subsections/__init__.py new file mode 100644 index 0000000..326fb36 --- /dev/null +++ b/aif_gen/config/generator/subsections/__init__.py @@ -0,0 +1,7 @@ +from . import meta +from . import storage +from . import network +from . import system +from . import pacman +from . import bootloader +from . import scripts diff --git a/aif_gen/config/generator/subsections/bootloader.py b/aif_gen/config/generator/subsections/bootloader.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/meta.py b/aif_gen/config/generator/subsections/meta.py new file mode 100644 index 0000000..f179150 --- /dev/null +++ b/aif_gen/config/generator/subsections/meta.py @@ -0,0 +1,40 @@ +import tkinter +## +import aif_gen.config.generator.utils as utils + + +class Obj(object): + def __init__(self, xmlroot, tkroot): + self.defaults = {'version': '0.2.0'} + self.xml = xmlroot + self.root = tkroot + self.frame = tkinter.LabelFrame(self.root, text = 'META', + bd = 1, relief = tkinter.RAISED, + font = ('Arial Bold', 15)) + # self.frame.grid(column = 0, row = 0) + self.frame.pack(side = 'top', fill = 'both', expand = True) + # TODO: Currently displays if ANY nested elements hover over. We don't want that. Eff it, fix later. + # utils.CreateToolTip(self.frame, 'This section controls information about AIF-NG itself.') + self.version() + + def version(self): + # Subsection header + frame = tkinter.LabelFrame(self.frame, text = 'VERSION', + bd = 1, relief = tkinter.RAISED, + font = ('Arial Bold', 12)) + # frame.grid(column = 0, row = 0) + frame.pack(side = 'top', fill = 'both', expand = True) + # Version entry + self.ver = tkinter.Entry(frame) + utils.CreateToolTip(self.ver, 'Must be a valid git reference (branch, tag, commit ID, etc.)') + self.ver.insert(0, self.defaults['version']) + self.ver.pack(side = 'top', fill = 'both', expand = True) + return() + + def new(self): + self.ver.delete(0, tkinter.END) + return() + + def save(self): + self.xml.attrib['version'] = self.ver.get() + return() diff --git a/aif_gen/config/generator/subsections/network.py b/aif_gen/config/generator/subsections/network.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/pacman.py b/aif_gen/config/generator/subsections/pacman.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/scripts.py b/aif_gen/config/generator/subsections/scripts.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/storage/__init__.py b/aif_gen/config/generator/subsections/storage/__init__.py new file mode 100644 index 0000000..4e00aff --- /dev/null +++ b/aif_gen/config/generator/subsections/storage/__init__.py @@ -0,0 +1,43 @@ +from . import block +from . import luks +from . import lvm +from . import mdadm +from . import filesystem +from . import mount + +import tkinter +## +import aif_gen.config.generator.utils as utils + + +class Obj(object): + def __init__(self, xmlroot, tkroot): + self.xml = xmlroot + self.root = tkroot + self.frame = tkinter.LabelFrame(self.root, text = 'STORAGE', + bd = 1, relief = tkinter.RAISED, + font = ('Arial Bold', 15)) + # self.frame.grid(column = 0, row = 0) + self.frame.pack(side = 'top', fill = 'both', expand = True) + self.vals = {} + self.block() + + def block(self): + frame = tkinter.LabelFrame(self.frame, text = 'BLOCK', + bd = 1, relief = tkinter.RAISED, + font = ('Arial Bold', 12)) + frame.pack(side = 'top', fill = 'both', expand = True) + # Version entry + self.vals['block'] = tkinter.Entry(frame) + utils.CreateToolTip(self.vals['block'], 'Path to a disk ("block") device to partition') + self.vals['block'].insert(0, '/dev/sda') + self.vals['block'].pack(side = 'top', fill = 'both', expand = True) + return() + + def new(self): + pass + return() + + def save(self): + pass + return() diff --git a/aif_gen/config/generator/subsections/storage/block.py b/aif_gen/config/generator/subsections/storage/block.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/storage/filesystem.py b/aif_gen/config/generator/subsections/storage/filesystem.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/storage/luks.py b/aif_gen/config/generator/subsections/storage/luks.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/storage/lvm.py b/aif_gen/config/generator/subsections/storage/lvm.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/storage/mdadm.py b/aif_gen/config/generator/subsections/storage/mdadm.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/storage/mount.py b/aif_gen/config/generator/subsections/storage/mount.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/subsections/system.py b/aif_gen/config/generator/subsections/system.py new file mode 100644 index 0000000..e69de29 diff --git a/aif_gen/config/generator/utils.py b/aif_gen/config/generator/utils.py new file mode 100644 index 0000000..ec3e16a --- /dev/null +++ b/aif_gen/config/generator/utils.py @@ -0,0 +1,50 @@ +import tkinter +import webbrowser + + +# TODO: http://effbot.org/zone/tkinter-text-hyperlink.htm ? + +class ToolTip(object): + # https://stackoverflow.com/a/56749167/733214 + def __init__(self, widget): + self.widget = widget + self.tipwindow = None + self.id = None + self.x = self.y = 0 + + def showtip(self, text): + self.text = text + if self.tipwindow or not self.text: + return() + x, y, cx, cy = self.widget.bbox("insert") + x = x + self.widget.winfo_rootx() + 57 + y = y + cy + self.widget.winfo_rooty() +27 + self.tipwindow = tw = tkinter.Toplevel(self.widget) + tw.wm_overrideredirect(1) + tw.wm_geometry('+{0}+{1}'.format(x, y)) + label = tkinter.Label(tw, text = self.text, justify = tkinter.LEFT, + background = '#ffffe0', relief = tkinter.SOLID, borderwidth = 1, + font = ('Tahoma', 8, 'normal')) + label.pack(ipadx = 1) + return() + + def hidetip(self): + tw = self.tipwindow + self.tipwindow = None + if tw: + tw.destroy() + return() + + +def CreateToolTip(widget, text): + toolTip = ToolTip(widget) + + def enter(event): + toolTip.showtip(text) + + def leave(event): + toolTip.hidetip() + + widget.bind('', enter) + widget.bind('', leave) + return()