Sample backup script for monitoring configuration changes

For any company managing a large number of Iguana instances, producing a centrally-managed picture of site configurations provides a great deal of value. The following sample script would be a helpful part of that solution.

  • This script is written for Mac OS X and Linux, but you could easily port it into a Windows environment.
  • This script is deployed via a ‘From Translator’ component configured to run every 5 minutes or so. We suggest making this channel (and script) part of your standard Iguana installation practice.

Here is a copy-paste version of the main script:

mon = require "monitor_config"

function main()

   mon.checkRepo()
end

Here is a copy-paste version of the ‘monitor_config’ module that you will need to add to your shared library:

mon = {}

-- This script should work on Mac OS X or Linux.
-- It makes use of these commands:
--   rm, cp, tar
-- To make the same script work on Windows you'd need to change those commands
-- to use equivalent tools - possible zip on windows.
-- The script:
--  Unpacks the fossil repository if the modification time has changed.
--  Renames the directory GUIDs of the translator projects to use a combination of channel names + translator type prefix.
--  Gets the other and shared directories
--  and the IguanaConfiguration.xml file 
--  and makes a tarball 

-- For this back up to be useful you'd need to then tranfer the tarball to a central server say by HTTPS and then do something
-- useful - like unpack it into a central repository which can be used to track changes.

local CONFIG_FILE = "backup.json"
local SCRATCH_DIR = "scratch"
local PACKAGE_DIR = "package"
local TAR_BALL = 'package.tgz'

local Config={
   lasttime=0      
}

function mon.checkRepo()
   local ConfigFile = iguana.workingDir()..'vcs_repo.sqlite'
   local ConfigLastModified = os.fs.stat(ConfigFile).mtime
   trace(ConfigLastModified)
   trace(Config.lasttime)
   if Config.lasttime < ConfigLastModified then
      iguana.logInfo("Configuration changed making "..TAR_BALL)
      mon.backupRepo()
      if os.fs.access(TAR_BALL) then
         iguana.logInfo(TAR_BALL..' successfully created');
         iguana.setChannelStatus{text='Backed up at '..os.ts.date('%c',os.ts.gmtime())}
      else
         iguana.logError('Unable to create '..TAR_BALL);
         iguana.setChannelStatus{color='red'}
      end
      Config.lasttime = ConfigLastModified
      if not iguana.isTest() then
         mon.saveLastBackup()
      end
   end
end

function MkEmptyDir(Dir)
   if not os.fs.access(Dir) then
      os.fs.mkdir(Dir)
      return 'Created Dir'
   end
   os.execute('cd '..Dir..' && rm -rf *')
   return 'Cleaned Dir'
end

function Rename(Guid, Name)
   local From = SCRATCH_DIR..'/'..Guid
   local To  = PACKAGE_DIR..'/'..Name
   trace(From)
   trace(To)
   os.rename(From, To)   
end

-- This routine copies the 
function mon.backupRepo()
   MkEmptyDir(SCRATCH_DIR)
   MkEmptyDir(PACKAGE_DIR)
   if os.fs.access(TAR_BALL) then
      os.remove(TAR_BALL)
   end

   local ConfigFile = iguana.workingDir()..'vcs_repo.sqlite'
   os.execute('cp '..ConfigFile..' '..SCRATCH_DIR..'/')  
   os.execute('cd '..SCRATCH_DIR..' && ../fossil open vcs_repo.sqlite')
   os.remove(SCRATCH_DIR..'/_FOSSIL_')
   mon.transform() 
   Rename('shared', 'shared')
   Rename('other', 'other')
   os.execute('cp IguanaConfiguration.xml '..PACKAGE_DIR)
   os.execute('tar -zcf '..TAR_BALL..' '..PACKAGE_DIR)
   return false
end

-- this routine takes the GUID based file structure and turns
-- directories based on channel names with postfixes describing the
-- channel type.
function mon.transform()
   local F = io.open('IguanaConfiguration.xml', 'r')
   local IC = xml.parse{data=F:read('*a')}.iguana_config.channel_config
   F:close()
   for i=1, IC:childCount("channel") do
      local C = IC:child("channel", i)
      if C.from_mapper then
         Rename(C.from_mapper.guid, C.name..'_From')
      end
      if C.message_filter and C.message_filter.translator_guid then 
         Rename(C.message_filter.translator_guid, C.name..'_Filter')
      end
      if C.to_mapper  then 
         Rename(C.to_mapper.guid, C.name..'_To')
      end
      if C.from_llp_listener and C.from_llp_listener.ack_script then
         Rename(C.from_llp_listener.ack_script, C.name.."_Ack")
      end
      if C.from_http and C.from_http.guid then
         Rename(C.from_http.guid, C.name.."_Http")
      end
   end
end

function mon.saveLastBackup()
   local Json = json.serialize{data=Config}
   local F = io.open(CONFIG_FILE, "w+")
   F:write(Json)
   F:close()
end

function mon.loadLastBackup()
   if not os.fs.access(CONFIG_FILE) then
      return
   end
   local F = io.open(CONFIG_FILE, "r")
   local File = F:read("*a")
   Config = json.parse{data=File}
end

-- Load the configuration file when the script starts up
mon.loadLastBackup()

return mon

At a high level, here is how the script works:

  1. It keeps a configuration file called ‘backup.json’ that notes when the last backup operation was run.
  2. If the vcs_repo.sqlite file’s “last modified” time has changed since the last backup, then this script constructs a tarball called ‘package.tgz’. This tarball includes ‘IguanaConfiguration.xml’ and the most recent Lua/VMD files (pulled from the Fossil repository).
  3. It updates the channel’s status tooltip and logs the backup activity.

You may also notice that this script makes use of the following:

  • cp to copy files
  • rm to clean out directories
  • tar to make a tarball called ‘package.tgz’

These could be replaced with copy, remove, and a command line zip utility in Windows. You could also install CYGNUS tools if you are working in a Windows environment.

For this solution to be useful, you’ll need to do something with the resulting tarball file. It would be quite simple to set up an Iguana instance to host a web service, then push the tarball to this service via HTTP. This Iguana instance would then unpack the archive into an enterprise source code repository. You could choose to do this slightly differently, depending on whether you want the original ‘vcs_repo.sqlite’ file or not.  You could also tweak things to retrieve the Chameleon VMD files used by classic Iguana channels (if you use that functionality).

Please note that this script translates the normal structure of the repository, which uses GUIDs for each Translator. It renames each directory based on the name of the channel the Translator script belongs to, then adds a different extension depending on the Translator type. This makes it much easier to read directory structures:

As you can see, the human-readable directory names make it much easier to navigate.

When putting this script into production, we also recommend that you add some error checking.

If you have any questions or suggestions, please let me know.

Eliot Muir,
CEO iNTERFACEWARE