Skip to content

Commit

Permalink
Rewrite wrapper script
Browse files Browse the repository at this point in the history
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
laurelmay committed Dec 25, 2017
1 parent be4a155 commit e298121
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 69 deletions.
1 change: 1 addition & 0 deletions ansible.cfg
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
5 changes: 5 additions & 0 deletions roles/oem/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
path: /etc/skel/Desktop
state: directory
mode: 0750
- name: Ensure log directory exists
file:
path: /opt/vmtools/logs
state: directory
mode: 0755
- name: Copy welcome shortcut to skeleton desktop directory
copy:
src: welcome-to-vm.desktop
Expand Down
50 changes: 0 additions & 50 deletions roles/task-shortcuts/files/uug-ansible-wrapper

This file was deleted.

287 changes: 287 additions & 0 deletions roles/task-shortcuts/files/uug-ansible-wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
#!/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
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()
9 changes: 4 additions & 5 deletions roles/task-shortcuts/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,25 @@
file_mode: 0644
when: icon_mode == 'user'
- set_fact: bin_path="{{ base_path }}/vmtools/bin"
- set_fact: uug_ansible_wrapper="{{ bin_path }}/uug-ansible-wrapper"
- set_fact: uug_ansible_wrapper="{{ bin_path }}/uug-ansible-wrapper.py"

- name: Create VMTools bin
file:
path: "{{ bin_path }}"
state: directory
- name: Install Ansible wrapper script
copy:
src: uug-ansible-wrapper
src: uug-ansible-wrapper.py
dest: '{{ uug_ansible_wrapper }}'
mode: 0755
- name: Install JMU Tux icon
copy:
src: jmu-tux.svg
dest: "{{ base_path }}/jmu-tux.svg"
mode: 0644
- name: Copy shortcuts to skeleton desktop directory
- name: Copy shortcut to desktop file directory
template:
src: desktop-template.desktop.j2
dest: "{{ icon_path }}/{{ item.tags }}.desktop"
dest: "{{ icon_path }}/jmucs_config.desktop"
mode: "{{ file_mode }}"
with_items: '{{ supported_courses }}'

Loading

0 comments on commit e298121

Please sign in to comment.