As part of the effort to support linux users connecting to our IPSEC/L2TP based VPN, I developed my first ever gnome panel applet written in Python (another first for me). I have a functional bash script that can be used to connect to/disconnect from the VPN, but I wanted an easier way for users to interact with the script. As a result, I developed the applet to display connection status, prompt for connection credentials and to manage the connection process so users don’t have to open a terminal and work at the command line.

The vpn shell script indicates status through a /var/lock/subsys/VPN file. The applet below constantly polls status by checking for the existence of this file (see the status_timeout method). Once the user has clicked on the applet icon, and entered the appropriate credentials, if necessary, the vpn shell script is invoked accordingly using the Popen object from the subprocess module.

This is (roughly) the python script implementing the vpn gnome panel applet:


#!/usr/bin/env python
import pygtk
pygtk.require('2.0')

import gtk
import gnomeapplet
import sys
import gobject
import subprocess
import os
import time
import logging

class VPN_Applet(gnomeapplet.Applet):

    applet = None
    image_file = "/usr/local/share/vpn/applet/images/vpn.png"
    image_file_hover = "/usr/local/share/vpn/applet/images/vpn_hover.png"
    proc_file = "/var/lock/subsys/VPN"
    username = ""
    passwd = ""
    title = "VPN Applet"
    cancel = False
    logging.basicConfig(level=logging.DEBUG)

    def __init__(self, applet, iid):
        logging.debug("__init__")

        self.applet = applet
        size = self.applet.get_size() - 2
        pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(self.image_file, size, size)
        image = gtk.Image()
        image.set_from_pixbuf(pixbuf)
        tooltips = gtk.Tooltips()
        tooltips.set_tip(image, "Click to connect to VPN\nStatus: Disconnected")
        # Status update timeout (check every 3 seconds)
        gobject.timeout_add(3000, self.status_timeout, image, tooltips)
        self.applet.add(image)
        self.applet.connect("button-press-event", self.button_press)
        self.applet.connect("change-size", self.change_size, image)
        self.applet.connect("change-background", self.change_background)
        self.applet.connect("enter-notify-event", self.enter_notify, image)
        self.applet.connect("leave-notify-event", self.leave_notify, image)
        self.applet.show_all()

    def check_connected(self):
        if (os.path.exists(self.proc_file)):
            return True
        else:
            return False

    def status_timeout(self, image, tooltips):
        logging.debug("status_timeout")
        if (self.check_connected()):
            txt = "disconnect from"
            status = "Connected"
        else:
            txt = "connect to"
            status = "Disconnected"
        tooltips.set_tip(image, "Click to " + txt + " VPN\nStatus: " + status)
        return True

    def do_image(self, image_file, image):
        logging.debug("do_image")

        size = self.applet.get_size() - 2
        pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(image_file, size, size)
        image.set_from_pixbuf(pixbuf)        

    def change_size(self, applet, new_size, image):
        logging.debug("change_size")

        self.do_image(self.image_file, image)

    def change_background(self, applet, type, color, pixmap):
        logging.debug("change_background")

        applet.set_style(None)
        applet.modify_style(gtk.RcStyle())
        if (type == gnomeapplet.COLOR_BACKGROUND):
            applet.modify_bg(gtk.STATE_NORMAL, color)
        elif (type == gnomeapplet.PIXMAP_BACKGROUND):
            applet.get_style().bg_pixmap[gtk.STATE_NORMAL] = pixmap

    def enter_notify(self, applet, event, image):
        logging.debug("enter_notify")

        self.do_image(self.image_file_hover, image)

    def leave_notify(self, applet, event, image):
        logging.debug("leave_notify")

        self.do_image(self.image_file, image)

    def button_press(self, button, event):
        logging.debug("button_press")

        if (event.button == 1):
            logging.debug("create dialog")
            self.setup_dialog()
            if (not(self.cancel)):
                if (not(self.check_connected())):
                    # Prompt for username/password
                    # Continue prompting until they are non-empty or user cancels
                    while ((self.username == "" or self.passwd == "") and not(self.cancel)):
                        self.username_prompt()
                    # If the user doesn't want to cancel, complete the connection
                    if (not(self.cancel)):
                        logging.debug("gksudo vpn start")
                        args = "-n '" + self.username + "' -p '" + self.passwd + "'"
                        proc = subprocess.Popen("gksudo -D '" + self.title + "' \"vpn " + args + " start\"",
                                                stdout=subprocess.PIPE,
                                                stderr=subprocess.STDOUT,
                                                shell=True)
                        self.show_progress_bar(proc)
                    # Clear username/password here
                    self.username=""
                    self.passwd=""
                else:
                    logging.debug("gksudo vpn stop")
                    proc = subprocess.Popen("gksudo -D \"" + self.title + "\" \"vpn stop\"",
                                            stdout=subprocess.PIPE,
                                            stderr=subprocess.STDOUT,
                                            shell=True)
                    self.show_progress_bar(proc)

    def setup_dialog(self):
        logging.debug("setup_dialog")

        vpn_dialog = gtk.Dialog(title=self.title,
                                flags=(gtk.DIALOG_MODAL | gtk.DIALOG_NO_SEPARATOR),
                                buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
                                         gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
        # Test here for vpn connection status and prompt accordingly
        if (self.check_connected()):
            txt = "disconnect from"
        else:
            txt = "connect to"
        vpn_dialog.connect("response", self.dialog_response)
        dialog_label = gtk.Label("Would you like to " + txt + " the Virtual Private Network?")
        dialog_label.set_line_wrap(True)
        vpn_dialog.vbox.pack_start(dialog_label, padding=10)
        vpn_dialog.show_all()
        vpn_dialog.run()

    def dialog_response(self, dialog, response_id):
        logging.debug("dialog_response")

        dialog.destroy()
        if (response_id == gtk.RESPONSE_ACCEPT):
            self.cancel = False
        else:
            self.cancel = True

    def show_progress_bar(self, proc):
        logging.debug("show_progress_bar")

        if (self.check_connected()):
            txt = "Disconnecting from "
        else:
            txt = "Connecting to "
        progress_dialog = gtk.Dialog(title=self.title,
                                     flags=(gtk.DIALOG_MODAL | gtk.DIALOG_NO_SEPARATOR))
        progress_dialog.set_deletable(False)
        label = gtk.Label(txt + "VPN")
        progress_dialog.vbox.pack_start(label, padding=10)
        progress_bar = gtk.ProgressBar()
        progress_bar.set_pulse_step(0.1)
        progress_dialog.vbox.pack_start(progress_bar, padding=10)
        gobject.timeout_add(250, self.progress_bar_timeout, progress_dialog,
                            proc, progress_bar, label)
        progress_dialog.show_all()
        progress_dialog.run()

    def progress_bar_timeout(self, progress_dialog, proc, progress_bar, label):
        logging.debug("progress_bar_timeout")

        progress_bar.pulse()
        if (proc.poll() != None):
            if (self.check_connected()):
                txt = "Connected to "
            else:
                txt = "Disconnected from "
            label.set_text(txt + "VPN.  Click the 'OK' button below to continue.")
            label.set_line_wrap(True)
            progress_dialog.vbox.remove(progress_bar)
            scrolled_window = gtk.ScrolledWindow()
            text_view = gtk.TextView()
            text_view.set_editable(False)
            text_view.set_cursor_visible(False)
            text_view.set_size_request(400, 200)
            buffer = text_view.get_buffer()
            buffer.set_text(proc.stdout.read())
            scrolled_window.add(text_view)
            progress_dialog.vbox.pack_start(scrolled_window)
            progress_dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
            progress_dialog.connect("response", self.dialog_response)
            progress_dialog.show_all()
            return False
        else:
            return True

    def username_changed(self, username):
        logging.debug("username_changed")
        self.username = username.get_text()

    def passwd_changed(self, passwd):
        logging.debug("passwd_changed")
        self.passwd = passwd.get_text()

    def username_prompt_activate(self, entry, dialog):
        logging.debug("username_prompt_activate")
        dialog.response(gtk.RESPONSE_ACCEPT)

    def username_prompt(self):
        logging.debug("username_prompt")

        username_prompt = gtk.Dialog(title=self.title,
                                  flags=(gtk.DIALOG_MODAL | gtk.DIALOG_NO_SEPARATOR),
                                  buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
                                          gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
        username = gtk.Entry(32)
        username.connect("changed", self.username_changed)
        username.connect("activate", self.username_prompt_activate, username_prompt)
        passwd = gtk.Entry(32)
        passwd.set_visibility(False)
        passwd.connect("changed", self.passwd_changed)
        passwd.connect("activate", self.username_prompt_activate, username_prompt)
        prompt = gtk.Label("Please enter your username and password:")
        prompt.set_line_wrap(True)
        table = gtk.Table(3, 2)
        table.attach(prompt, 0, 2, 0, 1, xpadding=5, ypadding=5)
        username_label = gtk.Label("Username (required):")
        table.attach(username_label, 0, 1, 1, 2, xpadding=5, ypadding=5)
        table.attach(username, 1, 2, 1, 2, xpadding=5, ypadding=5)
        passwd_label = gtk.Label("Password (required):")
        table.attach(passwd_label, 0, 1, 2, 3, xpadding=5, ypadding=5)
        table.attach(passwd, 1, 2, 2, 3, xpadding=5, ypadding=5)
        username_prompt.vbox.pack_start(table)
        username_prompt.connect("response", self.dialog_response)
        username_prompt.set_deletable(False)
        username_prompt.show_all()
        username_prompt.run()

gobject.type_register(VPN_Applet)

def vpn_factory(applet, iid):
    VPN_Applet(applet, iid)
    return gtk.TRUE

# run it in a gtk window
if len(sys.argv) > 1 and sys.argv[1] == "run-in-window":
    main_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
    main_window.set_title("VPN Applet")
    main_window.connect("destroy", gtk.mainquit)
    main_window.set_default_size(36, 36)
    app = gnomeapplet.Applet()
    vpn_factory(app, None)
    app.reparent(main_window)
    main_window.show_all()
    gtk.main()
    sys.exit()

if __name__ == '__main__':
    gnomeapplet.bonobo_factory("OAFIID:GNOME_VPN_Applet_Factory",
                               VPN_Applet.__gtype__,
                               "VPN Applet", "0.5", vpn_factory)

While there is plenty of Python and PyGTK documentation and examples, there is very little documentation on developing gnome applets using Python. Hopefully the code above will be helpful, in addition to the following links:

GNOME Documentation Library – PanelApplet
PyGTK 2.0 Reference Manual
PyGTK 2.0 Tutorial
Gnome applets with Python
Python Tutorial
subprocess – Subprocess Management

Tags: , , , , , , , , , ,

Leave a Reply

Powered by Laughing Squid