Weekend appengine project: symlinkd.com

A few weekends ago I got an itch to play around with Appengine, and was trying to think of something suitably simple.

One idea that had been banging around in my head for awhile was a simpler take on dynamic DNS (ala dyndns). I've had a dyndns account forever, but it annoyed me that they periodically deactivated my account until I logged in and 're-confirmed' that it was active.

This was especially irritating to me because they problem they're solving (storing an IP address and updating the value) isn't really a difficult one. Essentially, since I could easily obtain the originating address from an incoming HTTP request, all I really needed was to request a unique address, record the originating host, and have a way to retrieve that information on demand.

Further, it occurred to me that URL shorteners (like tinyURL) have some overlap here: they also have a unique address (or 'handle') that refers to some recorded value and a method for redirecting you there. So, I decided to make a single app that does both: a 'tinyURL' that's easy to programmatically update with the originating IP of the computer making the update request.

Moreover, I decided that since each 'handle' might change frequently, it might be nice to provide a persistent history of when and how it changed. After implementing this, I realized that, as a cool side effect, it makes it possible to use a link handle like a categorized bookmark list. Naturally, then, I'd need to make each handle subscribable to better suit this use case.

Thus, with a few hundred lines of python/html and a dab of javascript, symlinkd was created. It was mostly built in one evening in front of the TV, though I fiddled with it for a few evenings afterward. It mostly does what I want it to do, though I'll likely continue to poke at it.

Overall, I'm happy that I can now use curl to update my home IP address (rather than dyndns's binary blob client), and that I have my own personal URL shortener ;)

Random thoughts and things I learned:

  • I don't think I like datastore, and I'm quite sure I don't really like GQL
  • the minimalistic 'webapp' framework created for appengine is pretty serviceable
  • getting user auth for free is a nice side effect of using appengine
  • adding pubsubhubbub support to a content feed is really, really easy
  • I like the Atom protocol for publishing syndicated content, especially when I'm generating the feed template manually.
  • you can't use subdomains with appengine if you want to use your own domain (instead of .appspot.com)

Permanent Link

Simple 'cloud' backup with Amazon S3

I've had an Amazon S3 account set up for some time, and kept meaning to do something with it. Tonight I had a few free minutes and decided to take a look around to see what kind of tools are available for using it.

Coalescing a few things I found on the web (plus a man page or two), here's how to simply set up backups using s3fs

(these steps assume that you're using ubuntu, that you've already set up fuse, and that you already have an S3 account)

First, install a a few prerequisites for s3fs

sudo apt-get install build-essential libcurl4-openssl-dev libxml2-dev libfuse-dev 

Second, download the s3 source using the link above and untar it

user@example.com:/tmp $ curl http://s3fs.googlecode.com/files/s3fs-r177-source.tar.gz|tar -xzf -
user@example.com:/tmp $ cd s3fs

Third, compile it and put the resulting binary somewhere sensible

user@example.com:/tmp/s3fs $ make && mv s3fs /usr/local/sbin

Next, create /etc/passwd-s3fs, containing your AWS keypair

echo [ACCESS_KEY]:[SECRET_KEY] >/etc/passwd-s3fs

Now, you can execute 's3fs ' to mount an S3 bucket to the given path. Rather than doing this manually though, we'd like to have it done automatically at boot.

Happily, the 'mount' binary deals with unrecognized filesystem types by searching for a separate mount program at /sbin/mount.TYPE

Thus, to have your s3 bucket (called 'my-backup') mounted at startup, create a symlink from /usr/local/sbin/s3fs to /sbin/mount.s3fs

ln -sf /usr/local/sbin/s3fs /sbin/mount.s3fs

and add an appropriate line to fstab

echo "my-backup /backup s3fs use_cache=/tmp 0 0" >>/etc/fstab

Now, issuing 'mount /backup' should automatically mount your S3 bucket, and it should automatically be mounted at each boot.

Once this is working correctly, having offsite backups is as easy as a cronjob and rsync. Or, create a samba/nfs share at the same mount point, and have an easy way to provide backups to all the computers on your home network.

Permanent Link

Riff player update, and random things I learned about wxpython

Over the holidays I decided to revisit my riffplayer project, and see if I could rework it a bit, using wxpython for the GUI, and gstreamer to handle media playback. The advantage to this approach is that I get native widgets and dialogs, and I'm no longer dependent on openGL acceleration for video playback.

Obligatory screenshot of current progress riff player screenshot

After several hours, and 500 lines of python or so, I can say that I'm reasonably happy with both wxpython and gstreamer so far. With both though, there were a few things that I either had to figure out by trial and error, or piece together from random blogs. For the benefit of anybody who might run across this page, here are a few things I learned.

1.) Don't bother with IDs. The wxpython wiki's tutorial and examples usually show widgets being created with explicit integer ids, and then being later referenced by these same ids. Working this way requires you to worry about making sure each id is unique, and then having to look up the id later when you get ready to bind events to widgets. Instead, just use -1 as the id, and use the instance of the widget directly instead of using the id.

2.) Use built-in event hooks for updating the gui and background tasks. Rather than using a separate thread to update state in your application (an approach shown in one of the tutorials), use the built-in event hooks: wx.EVT_UPDATE_UI and wx.EVT_IDLE to update your gui or perform background tasks. Just bind these as you would any other events

An example, (from my media player):

class RiffPlayerFrame(wx.Frame):

  def __init__(self, parent, id, title):
    """Initialize the main riff player frame."""
    wx.Frame.__init__(self, parent, wx.ID_ANY, title, size = (700, 500))
    self.Bind(wx.EVT_CLOSE, self.Destroy)

   .... (omitted)

    self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI)
    self.Bind(wx.EVT_IDLE, self.OnIdle)

  def OnIdle(self, event):
    """Event handler for the Idle task.
    
    Used to update non-gui background processes
    """
    if not int(time.time()) % 5:
      if not self._sync_done:
        self._ApplyOffset()
        self._sync_done = True
    else:
      self._sync_done = False

  def OnUpdateUI(self, event):
    """Event handler for the EVT_UPDATE_UI psuedo-signal."""
    self.offset_timer.SetLabel('Offset: %s' % self.offset)
    if self.synced:
      self.sync_button.SetValue(True)
      self.riff_slider.Disable()
    else:
      self.sync_button.SetValue(False)
      self.riff_slider.Enable()
    try:
      vid_duration_nano, _ = self.video.query_duration(gst.FORMAT_TIME)
      vid_position_nano, _ = self.video.query_position(gst.FORMAT_TIME)
      riff_duration_nano, _ = self.riff.query_position(gst.FORMAT_TIME)
      riff_position_nano, _ = self.riff.query_position(gst.FORMAT_TIME)
    except Exception, e:
      # will be raised if stream isn't rolled for playback
      return
    self.video_timer.SetLabel(self._FormatTimestamp(vid_position_nano))
    self.video_slider.SetValue(vid_position_nano/1000000000)
    self.riff_timer.SetLabel(self._FormatTimestamp(riff_position_nano))
    self.riff_slider.SetValue(riff_position_nano/1000000000)

  1. When using gstreamer to play multiple files simultaneously, you can cram multiple playbins into a single pipeline. After fighting with adders and demuxers for several hours, I came across an example that used this approach.

An example:

  def _InitGstreamer(self):
    # set up gstreamer pipeline
    self.player = gst.Pipeline('player')

    self.riff = gst.element_factory_make('playbin', 'riff-pbin')
    self.riff.set_property('volume', 5.0)
    self.video = gst.element_factory_make('playbin', 'video-pbin')
    self.video.set_property('volume', 5.0)

    self.video_sink = gst.element_factory_make('autovideosink', 'video-sink')
    self.video.set_property('video-sink', self.video_sink)

    self.player.add(self.riff, self.video)
    bus = self.player.get_bus()
    bus.add_signal_watch()
    bus.enable_sync_message_emission()
    bus.connect('message', self.OnMessage)
    bus.connect('sync-message::element', self.OnSyncMessage)

Permanent Link

Riff Player

People who know me probably know that I'm a fan of the 90's tv show Mystery Science Theater 3000, which consisted of a thinly-plotted excuse to make fun of bad movies.

The show has been off the air for several year, but several of the principles have kept the spirit of the show alive with various projects. One of these projects, called RiffTrax, involves the same crew creating commentary tracks making fun of modern movies. The idea here is that you sync up the commentary track with the dvd, and hilarity will (presumably) ensue. To help with the syncing part, they produce a free 'Riff Player' application that will attempt to keep the commentary in sync with the video. The problem is, it's Windows only, and it only works for actual DVDs, not bare video files. Since they didn't seem motivated to fix this, I decided to roll my own.

My version is written in python, using the pyglet library to render the video and draw the interface. Because pyglet use OpenGL to do all it's rendering, it runs on Windows, Mac, and Linux. It's already usable enough to satisfy most of what I want, including the ability to save and load sync points from a local sqlite database (using SHA1 hashes of the file data to hopefully prevent overlaps), and has separate volume controls for the riff and video tracks. To make it easier to collaboratively maintain a database of common sync points, I'm creating an appengine app that will eventually be checked for sync-data automatically.

Obligatory screenshot:

Permanent Link

TinyUrl clone

Yesterday, while listening to lugradio, I heard Aq mention someone creating a tinyurl clone that allowed you to specify your own link "handle" to reference the url (tinyurl uses a random alphanumeric id). That got me thinking about the usefulness of having my own personal url aliasing service. I use del.icio.us to manage my bookmarks, but I can definitely see the use-case for named, shortcut bookmarks for things of particular interest.

My second thought was how trivial this would be to implement in django. Result: 44 lines of code and 75 lines of html later, and I have my own tinyurl service. You can see the working implementation here (disclaimer: don't assume it's remotely bug-free, secure, or well implemented.)

the model

from django.db import models

class Link(models.Model):
    handle = models.CharField(maxlength=255,unique=True)
    url = models.CharField(maxlength=255)

    def __str__(self):
        return self.handle

    class Admin:
        pass

The views

from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponseNotFound
from django.shortcuts import render_to_response, get_object_or_404
from django.newforms import form_for_model
from django.utils import simplejson
from jonallie.tinylink.models import Link

def index(request):   
 LinkForm = form_for_model(Link)
    message = ""
    if request.method == 'POST':
        form = LinkForm(request.POST)
        if form.is_valid():
            form.save()
            message = "Form saved: access your new link at http://jonallie.com/tinylink/go/%s" % form.data['handle']
            form = LinkForm()
        else:
            message = "Form NOT saved. Check errors below"
    else:
        form = LinkForm()
    return render_to_response('tinylink/index.html',{'form':form,'message':message})
            
def redirect(request,link_handle):   
    link = get_object_or_404(Link, handle=link_handle)
    return HttpResponseRedirect(link.url)
    
def check_handle(request,link_handle):  
    link = get_object_or_404(Link,handle=link_handle)
    return HttpResponse(simplejson.dumps({'handle':link.handle}), mimetype='application/javascript')

Permanent Link

Password expiration notification in python

I've seen several scripts to notify users of impending AD password expiration, but nearly all of them are vbs. Here's a script to accomplish the same goal using python-ldap.

#!/usr/bin/env python
"""
    password_notify.py:
        provides notification of active directory password expiration via email

"""

import sys
import time
import smtplib
import ldap
import syslog

ldaphost = 'HOST'
ldapbase = 'SEARCHBASE'
ldapfilter = '(&(objectCategory=person)(objectClass=user)(!(pwdLastSet=0)))'
ldapuser = 'USER'
ldappass = 'PASS'
mailhost = 'MAILHOST'
from_address = 'donotreply@YOURDOMAIN.COM'
expiry_days = 90
notify_days = 5 # days before expiration to start notification



msg_template = """\
To: %s
From: %s
Subject: Password Expiration Notification

This message is to notify you that your directory password will expire
%s . Please change it before that time using one of the following methods: 

1.) Reboot or logoff/logon your PC and follow the password change prompts.

2.) If you're a Webmail user, you can change your password from within webmail by choosing 
'Options' from the menu on the left, then clicking the 'Change Password' button 
near the bottom of the page. In the popup window that appears, type 'DOMAIN' (no quotes) 
into the field labled 'Domain', and populate the rest of the fields with your username,
old password, and new password twice.

3.) If you're currently logged into your PC _as yourself_, you can change your password
by pressing Ctrl-Alt-Del, and clicking the 'Change Password...' button

[ password guidelines here ]

You'll continue to receive this message each day until your password expires,
or until you reset it using one of the procedures above.

Thanks,

The Helpdesk
"""
def exit_with_error(errorstr, errno=2):
    """Print error to screen and exit"""
    print errorstr
    sys.exit(2)

def wintime_to_epoch(wintime):
    """Convert windows time stamps to epoch seconds"""
    return (int(wintime)-116444736000000000)/10000000 

def epoch_to_wintime(epoch_secs):
    """Convert unix epoc seconds to windows time units"""
    return (int(epoch_secs)*10000000)+116444736000000000
    
cur_win_time = epoch_to_wintime(time.time())

# explicitly open a syslog connection
syslog.openlog("password_notify.py")

# open a connection to the mail server
smail = smtplib.SMTP(mailhost)

# attempt to establish a connection to the ldap server
try:
    conn = ldap.open(ldaphost)
    conn.simple_bind_s(ldapuser, ldappass)
except:
    syslog.syslog("Error opening connection to %s" % ldaphost)
    exit_with_error("Ldap connection error") 

# grab the list of "person" objects from AD
try:
    users = conn.search_st(ldapbase, ldap.SCOPE_SUBTREE, ldapfilter)
except: 
    syslog.syslog("Error retreiving userlist using filter: %s" % ldapfilter)
    exit_with_error("LDAP search error")
    

for user in users:
    try:
        name = user[1]['cn'][0]
        mail = user[1]['mail'][0]
        last_set = long(user[1]['pwdLastSet'][0])
        expires_win = last_set + expiry_days*24*60*60*10000000
        notify_win = expires_win - notify_days*24*60*60*10000000
    except:
        continue 
    if cur_win_time > notify_win and cur_win_time < expires_win:
        syslog.syslog("Sending notification to: %s" % mail)
        smail.sendmail(from_address,mail,msg_template % (mail, from_address,time.ctime(wintime_to_epoch(expires_win))))

sys.exit(0)

Permanent Link

Better net-bridge script

I recently set up an openvpn server on Ubuntu LTS using bridged networking and was really dissatisfied with the sample scripts for bringing the bridge adapter up/down. The script below is what I came up with instead:

    # startup script to establish bridged network
    # based on sambple scripts from openvpn
    
    function exit_with_error {
        echo $*
        exit 2
    }
    
    # bridged interface
    br="br0"
    
    # tap interface
    tap="tap0"
    
    # physical address
    eth="eth0"
    eth_addr="172.16.1.28"
    eth_mask="255.255.0.0"
    eth_cast="172.16.255.255"
    eth_gw="172.16.32.2"
    
    case "$1" in
        "start")
            echo "Starting bridged networking"
            
            # create tunnel
            /usr/sbin/openvpn --mktun --dev $tap
            [ "$?" != "0" ] && exit_with_error "Failed to create tunnel"
            
            /usr/sbin/brctl addbr $br
            /usr/sbin/brctl addif $br $eth
            /usr/sbin/brctl addif $br $tap
            [ "$?" != "0" ] && exit_with_error "Failed to create bridge"
            
            /sbin/ifconfig $tap 0.0.0.0 promisc up || exit_with_error "Failed to configure $tap"
            /sbin/ifconfig $eth 0.0.0.0 promisc up || exit_with_error "Falied to configure $eth"
            /sbin/ifconfig $br $eth_addr netmask $eth_mask broadcast $eth_cast || exit_with_error "Failed to configure $br"
            #/sbin/ifconfig $br up
            
            # add default route
            /sbin/route add -net default gw $eth_gw
    
        # create iptables rules
        /sbin/iptables -A INPUT -i $br -j ACCEPT
        /sbin/iptables -A INPUT -i $tap -j ACCEPT
        /sbin/iptables -A FORWARD -i $br -j ACCEPT
        /sbin/iptables -A FORWARD -i $tap -j ACCEPT
            ;;
            
        "stop")
            echo "Stopping bridged networking"
            
            # bring down bridge
            /sbin/ifconfig $br down
            
            # tear down bridge
            /usr/sbin/brctl delbr $br
            /usr/sbin/openvpn --rmtun --dev $tap
            
            # reset address of eth0
            /sbin/ifconfig $eth $eth_addr netmask $eth_mask broadcast $eth_cast
            #/sbin/ifconfig $eth up
            /sbin/route add -net default gw $eth_gw
    
        # flush iptables rules
        /sbin/iptables -F INPUT
        /sbin/iptables -F FORWARD
            ;;
            
        *)
            echo "Usage: $0 {start|stop}" >&2
            exit 1
            ;;
    esac
    
    exit 0

Permanent Link

Stupid Webmin Tricks

I've been working alot with webmin in the past view weeks, and learning a ton along the way. I'm sure that most of this is already well documented (like the rest of webmin) but I didn't know it, and that makes it important. :-)

first trick: custom, "unattended" installs. Webmin's main setup script calls two external (and by default non-existent) scripts during the install process. By creating these two scripts, and placing them in the webmin install directory, you can "pre-set" various options; any variable pre-set in this way won't be prompted for during install. This is great for maintaining a standard configuration across multiple systems.

The first script, setup-pre.sh is called at the beginning of the setup script, and is primarily useful for pre-setting variables.

My commented example:

    ####################################### 5-2005 ##
    #
    # setup-pre.sh:
    #
    #  called by setup.sh. sets up pre-defined
    #  variables that will be used as defaults
    #  by webmin setup rather than prompting
    #  the user. This allows for partially or
    #  completely automated installs. Variables
    #  declared here _won't_ be prompted for at
    #  install.
    # 
    # author: jon allie  [ jon (at)jonallie(dot)com 
    ##################################################
    
    
    ## $config_dir: directory for webmin to store it's config files
    #   will be created by the installer if it doesn't exist.
    config_dir=/opt/freeware/webmin/config
    
    ## $var_dir: directory for webmin to store it's log files
    #    will be created by the installer if it doesn't exist.
    var_dir=/opt/freeware/webmin/log
    
    ## $perl:  full path to the perl executable.
    perl=/usr/bin/perl
    
    ## $os_type: os type, see os_list.txt for possible values
    os_type=aix
    
    ## $os_version: version number of os_type specified above
    os_version=5.2
    
    ## $real_os_type
    real_os_type=IBM AIX
    
    ## $real_os_version
    real_os_version=5.2

    ## $port: port for webmin server to listen on.
    port=10000

    ## $login: webmin username
    login=admin

    ## $password: plain text password. don't use this
    #password=

    ## $crypt: pre-encrypted password. set this to 'x' to enable unix authentication
    crypt=x

    ## $ssl: whether or not to use ssl. 1 for yes, 0 for no
    # the Net::SSLeay library MUST be installed for 
    # this to function. 
    ssl=1
    
    ## $atboot: whether or not to start webmin at boot.
    atboot=1

setup-post.sh is, of course, called at the end of the setup process, and can be used for just about anything. Here, I'm disabling the (stupidly default) options to remember passwords and to display the system hostname on the login page.

    ####################################### 5-2005 ##
    #
    # setup-post.sh:
    #
    #  called by setup.sh. this script is called
    # as a final step before starting webmin
    # at the end of install. Used to set final
    # options.
    #
    # author: jon allie [ jon(at)jonallie(dot)com ]
    ##################################################
    
    ### set shorter name for location of global config file
    gconfig=$config_dir/config

    ## exit if global config isn't writable
    if [ ! -w $gconfig ]; then
        exit 0
    fi

    ### set extra security options in global config

    ## disable the option to remember passwords
    echo "noremember=1" >> $gconfig

    ## disable display of hostname on login page
    echo "nohostname=1" >> $gconfig

Permanent Link

Random Idea: The Pirate VCS

After reading about Sourceforge being forced to remove the backupHDDVD project, I had a thought: Why isn't there a 'Pirate Bay' for developers? a distributed, untrackable version control system that could be used for developing projects of questionable legality, sortof a bazaaar + bittorret 'mashup' (yea, I'm starting to hate that word too)

Permanent Link

MySQL Backup script with encryption

I recently rewrote the script that handles our mysql backups, and thought someone else might find it useful. It includes support for optional bzip compression of the dump files, and optional encryption using openssl. It's available for download here or you can see it inline after the jump

        #!/bin/bash
        ##################################################################
        # sqlbackup.sh:
        #   rolling sqlbackups using mysqldump
        #
        #
        #   This script will dump mysql databases to a specified directory
        #   with optional encrytion.
        #
        #
        ###################################################################
    
        # program options
        backup_dir=/backup
        backup_age=28
        use_encryption=0
        use_bzip=0
        nice_level=19
        mysql_user=root
        mysql_passfile=/etc/keys/.mysqlpass
        openssl_passfile=/etc/keys/.opensslpass
        date=`date +%Y%m%d`
        exten=".sql"
        logfile=/var/log/mysqlbackup.log
        mysqldump_options=" --all-databases --single-transaction --quote-names "
    
        # program paths
        mysqldump=/usr/bin/mysqldump
        openssl=/usr/bin/openssl
        bzip2=/usr/bin/bzip2
    
    
        # usage summary
        function usage {
        echo "
        sqlbackup.sh:
            creates backups of sql databases using the mysqldump command,
            including optional encryption, bzip2 compression, and 
            variable backup aging
    
        Usage: sqlbackup.sh [OPTIONS]
    
            Options:
    
            -h              : display this help
    
            -b <dir>        : select directory to store backups. This directory 
                            will not be created.
    
            -e              : enable encryption using openssl's des3 encryption. 
    
            -k <file>       : specify file containing password to be used for encryption
                            This defaults to /etc/keys/.opensslpass
                    
            -u <user>       : username to use for connecting to MySQL
    
            -p </user></file><file>       : specifies a file containing a password to be used for
                            connecting to MySQL
        
            -z              : enable bzip2 compression of backups
    
            -a <days>       : backup age in days. Backups older than this will be deleted
    
            -n <level>      : Specifies a nice level. This level will apply to all operations of the 
                            backup. Defaults to 19 (very nice)
                    
            -l </level></days></file><file>       : logfile path. Alternate path to a file for logging. Defaults
                            to '/var/log/mysqlbackup.log'
                    
        "
        }      
    
        # echo a string to screen and log
        function echo_and_log {
            logstring="[ `date` ] : $*"
            echo $logstring
            echo $logstring >> $logfile
        }
    
    
        # print error to screen and exit
        function exit_with_error {
            echo_and_log "Error: $*"
            echo_and_log "Exiting with code 1"
            exit 1
        }
    
        # funtion to delete old backups
        function clean_backups {
        find $backup_dir -type f -mtime +${backup_age} -exec rm -f {} \;
        [ "$?" == "0" ] || echo_and_log "Error encountered when cleaning old backups from $backup_dir"
        }
    
    
        # process command line options
        while getopts ":hb:ek:u:p:za:n:l:" opt 
        do
            case $opt in
                b) backup_dir=$OPTARG ;;
                e) use_encryption=1 ;;
                k) openssl_passfile=$OPTARG ;;
                u) mysql_user=$OPTARG ;;
                p) mysql_passfile=$OPTARG ;;
                z) use_bzip=1 ;;
                a) backup_age=$OPTARG ;;
                n) nice_level=$OPTARG ;;
                l) logfile=$OPTARG ;;
                h) usage 
                    exit 0 ;;
                \?) echo "unknown option"
                    usage 
                    exit 1 ;;
                *)  usage
                    exit 1 ;;
            esac
        done
    
        # set umask
        umask 0077
    
        # start processing
        echo_and_log "Starting MySQL backup: encryption = $use_encryption, bzip = $use_bzip, backup directory = $backup_dir"
        
        # error checking and validation
        [ -d $backup_dir ] || exit_with_error "Backup directory '$backup_dir' does not exist"
        [ -n "$logfile" ] || exit_with_error "No log file specified."
        [ -f $mysql_passfile ] || exit_with_error "MySQL password file '$mysql_passfile' does not exist"
        [ -x $mysqldump ] || exit_with_error "MySQLDump program at '$mysqldump' does not exist or is not executable"
        # validate compression options
        if [ "$use_bzip" == "1" ]
        then
            [ -x $bzip2 ] || exit_with_error "Bzip2 program at '$bzip2' does not exist or is not executable"
            bzip2_command=" | nice -n $nice_level $bzip2 "
            exten="${exten}.bz2"
        fi
    
        # validate encryption options
        if [ "$use_encryption" == "1" ]
        then 
            [ -x $openssl ] || exit_with_error "OpenSSL program at '$openssl' does not exist or is not executable"
            [ -f $openssl_passfile ] || exit_with_error "OpenSSL password file '$openssl_passfile' does not exist"
            openssl_command=" | nice -n $nice_level $openssl des3 -salt -pass file:${openssl_passfile} "
            exten="${exten}.cryp"
        fi
    
        # call funtion to clean backup directory
        clean_backups
    
        # execute dump command
        command="$mysqldump $mysqldump_options --user=${mysql_user} --password=$(<$mysql_passfile) $bzip2_command $openssl_command > $backup_dir/mysqlbackup_${date}${exten}"
        eval $command
        [ "$?" == "0" ] || exit_with_error "MySQLDump command returned non-zero error code. Backup not completed successfully"
    
        # finish
        echo_and_log "Backup complete"
        exit 0
    
</file></dir>

Permanent Link