-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rewrite the Ansible wrapper script in Python to allow the user to select which courses they need to have their machine configured for. Enable logging by specifying a path in ansible.cfg. Remove previous wrappers and installed desktop shortcuts in favor of the new wrapper
- Loading branch information
Showing
8 changed files
with
303 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
[defaults] | ||
nocows=1 | ||
log_path=/opt/vmtools/logs/last_run.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
#!/usr/bin/env python3 | ||
""" | ||
This tools creates a simple GUI for running ansible-pull with a | ||
predetermined set of tags. It displays the output from the ansible-pull | ||
command in a VTE within the GUI. It allows the user to override some things | ||
in a configuration file (~/.config/vm_config). The branch to pull can be | ||
overriden by setting FORCE_BRANCH and the URL to pull from can be overriden | ||
with FORCE_GIT_URL | ||
""" | ||
|
||
import os | ||
import platform | ||
import subprocess | ||
import sys | ||
import gi | ||
gi.require_version('Gtk', '3.0') | ||
gi.require_version('Vte', '2.91') | ||
from gi.repository import Gtk, Vte | ||
from gi.repository import GLib | ||
|
||
# Common should always run, but put it in the list | ||
tags = ["common"] | ||
# Map of course names to the Ansible tags | ||
courses = {'CS 101': 'cs101', 'CS 149': 'cs149', 'CS 159': 'cs159', | ||
'CS 261': 'cs261', 'CS 354': 'cs354'} | ||
user_config_path = os.environ['HOME'] + "/.config/vm_config" | ||
user_config = {} | ||
release = None | ||
url = None | ||
|
||
|
||
def main(): | ||
parse_user_config() | ||
global url | ||
url = get_remote_url() | ||
try: | ||
global release | ||
release = get_distro_release_name() | ||
except ValueError: | ||
unable_to_detect_branch() | ||
|
||
win = CheckboxWindow() | ||
win.connect("delete-event", Gtk.main_quit) | ||
win.show_all() | ||
Gtk.main() | ||
|
||
|
||
class CheckboxWindow(Gtk.Window): | ||
"""The main window for the program. Includes a series of checkboxes for | ||
courses as well as a VTE to show the output of the Ansible command""" | ||
|
||
def __init__(self): | ||
Gtk.Window.__init__(self, title="JMU CS VM Configuration") | ||
|
||
# Attempt to use tux as the icon. If it fails, that's okay | ||
try: | ||
self.set_icon_from_file("/opt/jmu-tux.svg") | ||
except: # noqa | ||
pass | ||
|
||
self.set_border_width(10) | ||
|
||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) | ||
self.add(vbox) | ||
|
||
label = Gtk.Label("Select the courses you need configured on the VM") | ||
label.set_alignment(0.0, 0.0) | ||
vbox.pack_start(label, False, False, 0) | ||
|
||
courses_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) | ||
# This button doesn't do anything. Common is always run | ||
refresh = Gtk.CheckButton("Refresh base configuration") | ||
refresh.set_active(True) | ||
refresh.set_sensitive(False) | ||
courses_box.pack_start(refresh, False, False, 0) | ||
# Add a checkbox for every course; sorting is necessary because | ||
# dictionaries do not guarantee that order is preserved | ||
for (course, tag) in sorted(courses.items()): | ||
checkbox = Gtk.CheckButton(course) | ||
checkbox.connect("toggled", self.on_button_toggled, tag) | ||
courses_box.pack_start(checkbox, False, False, 0) | ||
vbox.pack_start(courses_box, False, False, 0) | ||
|
||
# Add informational labels to the bottom of the window | ||
label_box = Gtk.Box(spacing=6) | ||
url_label = Gtk.Label("URL: " + url) | ||
label_box.pack_start(url_label, False, False, 6) | ||
branch_label = Gtk.Label("Branch: " + release) | ||
label_box.pack_start(branch_label, False, False, 6) | ||
vbox.pack_end(label_box, False, False, 0) | ||
|
||
# Add run and cancel buttons | ||
button_box = Gtk.Box(spacing=6) | ||
self.run_button = Gtk.Button.new_with_label("Run") | ||
self.run_button.connect("clicked", self.on_run_clicked) | ||
button_box.pack_start(self.run_button, True, True, 0) | ||
self.cancel_button = Gtk.Button.new_with_mnemonic("_Cancel") | ||
self.cancel_button.connect("clicked", Gtk.main_quit) | ||
button_box.pack_end(self.cancel_button, True, True, 0) | ||
vbox.pack_end(button_box, False, True, 0) | ||
|
||
# Add the terminal to the window | ||
self.terminal = Vte.Terminal() | ||
self.terminal.connect("child-exited", self.sub_command_exited) | ||
vbox.pack_end(self.terminal, True, True, 0) | ||
|
||
def on_button_toggled(self, button, name): | ||
"""Adds the name of the button that triggered this call to the list of | ||
tags that will be passed to ansible-pull""" | ||
if button.get_active(): | ||
tags.append(name) | ||
else: | ||
tags.remove(name) | ||
|
||
def sub_command_exited(self, widget, exit_status): | ||
"""Displays a dialog informing the user whether the gksudo and | ||
ansible-pull commands completely successfully or not""" | ||
self.cancel_button.set_sensitive(True) | ||
self.run_button.set_sensitive(True) | ||
if exit_status == 0: | ||
dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, | ||
Gtk.ButtonsType.OK, "Complete") | ||
dialog.format_secondary_text("Your machine has been configured" | ||
"for: " + | ||
",".join(tags) + | ||
"\nPress OK to exit.") | ||
dialog.run() | ||
dialog.destroy() | ||
Gtk.main_quit() | ||
# 65280 should be the exit code if the gksudo dialog is dismissed | ||
elif exit_status == 65280: | ||
dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, | ||
Gtk.ButtonsType.OK, | ||
"Unable to authenticate") | ||
gksudo_err_msg = "Either an incorrect password was entered or " \ | ||
"the password dialog was closed. Please try again" | ||
dialog.format_secondary_text(gksudo_err_msg) | ||
dialog.run() | ||
dialog.destroy() | ||
else: | ||
dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, | ||
Gtk.ButtonsType.OK, "Error") | ||
ansible_err_msg = "There was an error while running the " \ | ||
"configuration tasks. Please try again.\n" \ | ||
"If this issue continues to occur, copy the" \ | ||
"newest log from /opt/vmtools/logs and" \ | ||
"<a href='" + url + "'> create an issue</a>" | ||
dialog.set_markup(ansible_err_msg) | ||
dialog.run() | ||
dialog.destroy() | ||
|
||
def on_run_clicked(self, button): | ||
"""Begins the process of running the command in the VTE and disables | ||
the run and cancel buttons so that they cannot be used while the | ||
command is running""" | ||
if not validate_branch(release, url): | ||
invalid_branch(self, release, url) | ||
|
||
gksudo_msg = "<b>Enter your password to configure your VM</b>\n" \ | ||
"To configure your virtual machine, administrator" \ | ||
" privileges are required." | ||
|
||
self.cancel_button.set_sensitive(False) | ||
self.run_button.set_sensitive(False) | ||
|
||
self.terminal.spawn_sync(Vte.PtyFlags.DEFAULT, | ||
os.environ['HOME'], | ||
["/usr/bin/gksudo", "--message", | ||
gksudo_msg, "--", | ||
"ansible-pull", "-U", url, | ||
"-C", release, | ||
"--purge", "-i", "hosts", | ||
"-t", ",".join(tags)], | ||
[], | ||
GLib.SpawnFlags.DO_NOT_REAP_CHILD, | ||
None, | ||
None, | ||
) | ||
|
||
|
||
def parse_user_config(): | ||
"""Loads the user's configuration into the user_config global variable""" | ||
try: | ||
with open(user_config_path, "r") as config: | ||
for line in config: | ||
(key, val) = line.split("=") | ||
user_config[key] = val.strip() | ||
except FileNotFoundError: | ||
pass | ||
|
||
|
||
def parse_os_release(): | ||
"""Returns the data in /etc/os-release in a dictionary. If the file does | ||
not exist, an empty dictionary is returned""" | ||
config = {} | ||
try: | ||
with open("/etc/os_release", "r") as os_release: | ||
for line in os_release: | ||
(key, val) = line.split("=") | ||
config[key] = val.strip() | ||
except FileNotFoundError: | ||
pass | ||
|
||
return config | ||
|
||
|
||
def get_distro_release_name(): | ||
"""Attempts to get the release name of the currently-running OS. This | ||
will initially use platform.linux_distribution(), but that may not | ||
work on some distros or versions of Python. It falls back to reading | ||
/etc/os-release and then regardless of whether or not a release has | ||
been found, if FORCE_BRANCH exists in ~/.config/vm_config that will be | ||
returned. If nothing is found, a ValueError is raised.""" | ||
release = "" | ||
linux_distribution = platform.linux_distribution()[2] | ||
release = linux_distribution | ||
if release == "" or release is None: | ||
os_release_config = parse_os_release() | ||
release = os_release_config.get('VERSION_CODENAME') or None | ||
|
||
if 'FORCE_BRANCH' in user_config: | ||
release = user_config['FORCE_BRANCH'] | ||
|
||
if release == "" or release == " " or release is None: | ||
raise ValueError("Version could not be detected") | ||
|
||
return release | ||
|
||
|
||
def get_remote_url(): | ||
"""Checks if the user has specified a FORCE_GIT_URL in their config file. | ||
If so, that is returned. Otherwise, the default jmunixusers URL is | ||
returned""" | ||
if 'FORCE_GIT_URL' in user_config: | ||
return user_config['FORCE_GIT_URL'] | ||
return "https://github.com/jmunixusers/cs-vm-build" | ||
|
||
|
||
def validate_branch(branch, remote): | ||
"""Checks the branch passed in against the branches available on remote. | ||
Returns true if branch exists on remote. This may be subject to false | ||
postivies, but that should not be an issue""" | ||
output = subprocess.run(["git", "ls-remote", remote], | ||
stdout=subprocess.PIPE) | ||
|
||
branches = output.stdout.decode("utf-8") | ||
|
||
return branch in branches | ||
|
||
|
||
def invalid_branch(parent, release, url): | ||
"""Displays a dialog if the branch choses does not exist on the remote""" | ||
dialog = Gtk.MessageDialog(parent, 0, Gtk.MessageType.ERROR, | ||
Gtk.ButtonsType.CANCEL, "Invalid release") | ||
bad_branch_msg = "The release chosen does not exist at the project URL." \ | ||
" Please check the settings listed below and try again." \ | ||
"\nRelease" + release + "\nURL: " + url + \ | ||
"\nIf you're using a current release of Linux Mint, you" \ | ||
" may submit" \ | ||
"<a href='" + url + "'>an issue</a> requesting support" \ | ||
"for the release list above." | ||
dialog.set_markup(bad_branch_msg) | ||
dialog.run() | ||
dialog.destroy() | ||
Gtk.main_quit() | ||
|
||
|
||
def unable_to_detect_branch(): | ||
"""Displays a dialog to ask the user if they would like to use the master | ||
branch. If the user clicks yes, release is set to master. If the user | ||
says no, the script exits""" | ||
dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.ERROR, | ||
Gtk.ButtonsType.YES_NO, "OS Detection error") | ||
master_prompt = "The version of your OS could not be determined. " \ | ||
"Would you like to use the master branch?" \ | ||
"This is very dangerous" | ||
dialog.format_secondary_text(master_prompt) | ||
response = dialog.run() | ||
dialog.destroy() | ||
if response != Gtk.ResponseType.YES: | ||
sys.exit(1) | ||
else: | ||
global release | ||
release = "master" | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.