diff --git a/debian/castor-dbtools.install.perm b/debian/castor-dbtools.install.perm
index b5394580f5a9d5ed6dfed9ee8feaa24de625aeb8..e37c9c41e5bdfc0e15f79b6b732522b022cd49bd 100644
--- a/debian/castor-dbtools.install.perm
+++ b/debian/castor-dbtools.install.perm
@@ -8,6 +8,10 @@
 %attr(0755,root,root) usr/bin/enterdiskpool
 %attr(0755,root,root) usr/bin/deletediskpool
 %attr(0755,root,root) usr/bin/printdiskpool
+%attr(0755,root,root) usr/bin/enterrecallgroup
+%attr(0755,root,root) usr/bin/modifyrecallgroup
+%attr(0755,root,root) usr/bin/deleterecallgroup
+%attr(0755,root,root) usr/bin/printrecallgroup
 %attr(0755,root,root) usr/bin/entertapepool
 %attr(0755,root,root) usr/bin/modifytapepool
 %attr(0755,root,root) usr/bin/deletetapepool
diff --git a/debian/castor-dbtools.manpages b/debian/castor-dbtools.manpages
index 07a2ee3bdfc70f1aeae00cf5e2d4eb2e2d364c73..5d28e863e3646513dbd5244709cbd17330fb13b0 100644
--- a/debian/castor-dbtools.manpages
+++ b/debian/castor-dbtools.manpages
@@ -8,6 +8,10 @@ debian/castor/usr/share/man/man1/printsvcclass.1castor.gz
 debian/castor/usr/share/man/man1/enterdiskpool.1castor.gz
 debian/castor/usr/share/man/man1/deletediskpool.1castor.gz
 debian/castor/usr/share/man/man1/printdiskpool.1castor.gz
+debian/castor/usr/share/man/man1/enterrecallgroup.1castor.gz
+debian/castor/usr/share/man/man1/modifyrecallgroup.1castor.gz
+debian/castor/usr/share/man/man1/deleterecallgroup.1castor.gz
+debian/castor/usr/share/man/man1/printrecallgroup.1castor.gz
 debian/castor/usr/share/man/man1/entertapepool.1castor.gz
 debian/castor/usr/share/man/man1/modifytapepool.1castor.gz
 debian/castor/usr/share/man/man1/deletetapepool.1castor.gz
diff --git a/hsmtools/Imakefile b/hsmtools/Imakefile
index 28cc5ad6181a188921380677ab1f311a9336324b..3b486fc8199213fab2498b7dd9319b3ac0fde499 100644
--- a/hsmtools/Imakefile
+++ b/hsmtools/Imakefile
@@ -42,6 +42,11 @@ InstallTarget(enterdiskpool,755)
 InstallTarget(deletediskpool,755)
 InstallTarget(printdiskpool,755)
 
+InstallTarget(enterrecallgroup,755)
+InstallTarget(modifyrecallgroup,755)
+InstallTarget(deleterecallgroup,755)
+InstallTarget(printrecallgroup,755)
+
 InstallTarget(entertapepool,755)
 InstallTarget(modifytapepool,755)
 InstallTarget(deletetapepool,755)
@@ -80,6 +85,11 @@ EXEMANPAGE(enterdiskpool)
 EXEMANPAGE(deletediskpool)
 EXEMANPAGE(printdiskpool)
 
+EXEMANPAGE(enterrecallgroup)
+EXEMANPAGE(modifyrecallgroup)
+EXEMANPAGE(deleterecallgroup)
+EXEMANPAGE(printrecallgroup)
+
 EXEMANPAGE(entertapepool)
 EXEMANPAGE(modifytapepool)
 EXEMANPAGE(deletetapepool)
diff --git a/hsmtools/deleterecallgroup b/hsmtools/deleterecallgroup
new file mode 100755
index 0000000000000000000000000000000000000000..3035c3a5ce9cc7866b4f39643c35d4517c65489d
--- /dev/null
+++ b/hsmtools/deleterecallgroup
@@ -0,0 +1,129 @@
+#!/usr/bin/python
+#/******************************************************************************
+# *                      deleterecallgroup
+# *
+# * This file is part of the Castor project.
+# * See http://castor.web.cern.ch/castor
+# *
+# * Copyright (C) 2003  CERN
+# * This program is free software; you can redistribute it and/or
+# * modify it under the terms of the GNU General Public License
+# * as published by the Free Software Foundation; either version 2
+# * of the License, or (at your option) any later version.
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# *
+# * @author Castor Dev team, castor-dev@cern.ch
+# *****************************************************************************/
+
+'''command line deleting the recall group(s)'''
+
+import sys
+import getopt
+import castor_tools
+
+# usage function
+def usage(exitCode):
+    '''prints usage'''
+    print 'Usage : ' + sys.argv[0] + ' [-h|--help] <recallGroupName>[:...]'
+    sys.exit(exitCode)
+
+# first parse the options
+try:
+    options, args = getopt.getopt(sys.argv[1:], 'hv', ['help', 'verbose'])
+except Exception, e:
+    print e
+    usage(1)
+verbose = False
+for f, v in options:
+    if f == '-h' or f == '--help':
+        usage(0)
+    elif f == '-v' or f == '--verbose':
+        verbose = True
+    else:
+        print "unknown option : " + f
+        usage(1)
+
+# Deal with arguments
+recallGroups = set([])
+if len(args) != 0:
+    recallGroups = set(args[0].split(':'))
+else:
+    print "Missing arguments"
+    usage(1)
+
+try:
+    # connect to stager
+    stconn = castor_tools.connectToStager()
+    stcur = stconn.cursor()
+    # check tape pool existence
+    stcur.execute("SELECT name FROM RecallGroup WHERE name IN ('" + "', '".join(recallGroups) + "')")
+    existingRecallGroups = set([row[0] for row in stcur.fetchall()])
+    nonexitingRecallGroups = recallGroups - existingRecallGroups
+    # forbid deletion of the default group
+    if 'default' in existingRecallGroups:
+        print 'Recall group default should be deleted. Giving up'
+        sys.exit(1)
+    # check that the recall groups are not containing any user
+    stcur.execute('''SELECT RecallGroup.name, count(*)
+                       FROM RecallUser, RecallGroup
+                      WHERE RecallUser.recallGroup = RecallGroup.id
+                        AND RecallGroup.name IN (\'''' + "', '".join(existingRecallGroups) + "') " + '''
+                        GROUP BY RecallGroup.name''')
+    rows = stcur.fetchall()
+    if len(rows) > 0:
+        print 'RecallGroups cannot be dropped when they still contain users :'
+        for rg, nb in rows:
+            print '  - recall group %s contains %d user(s)' % (rg, nb)
+        print 'You may want to use deleterecalluser or modifyrecalluser to correct this'
+        sys.exit(1)
+    # check that the tape pools are not used by any ongoing RecallJob
+    stcur.execute('''SELECT RecallGroup.name, count(*)
+                       FROM RecallJob, RecallGroup
+                      WHERE RecallJob.recallGroup = RecallGroup.id
+                        AND RecallGroup.name IN (\'''' + "', '".join(existingRecallGroups) + '''\')
+                      GROUP BY RecallGroup.name''')
+    rows = stcur.fetchall()
+    if len(rows) > 0:
+        print 'RecallGroups cannot be dropped when used by some recall jobs :'
+        for tp, nb in rows:
+            print '  - recall group %s is in use by %d recall jobs' % (tp, nb)
+        print 'You probably have to wait that the recall of these files is over'
+        sys.exit(1)
+    # check that the tape pools are not used by any ongoing RecallMount
+    stcur.execute('''SELECT RecallGroup.name, count(*)
+                       FROM RecallMount, RecallGroup
+                      WHERE RecallMount.recallGroup = RecallGroup.id
+                        AND RecallGroup.name IN (\'''' + "', '".join(existingRecallGroups) + '''\')
+                      GROUP BY RecallGroup.name''')
+    rows = stcur.fetchall()
+    if len(rows) > 0:
+        print 'RecallGroups cannot be dropped when used by some recall mount :'
+        for tp, nb in rows:
+            print '  - recall group %s is in use by %d recall mounts' % (tp, nb)
+        print 'You probably have to wait that the mount is over. Note that there was no RecallJob left so it should be a matter of seconds'
+        sys.exit(1)
+    # drop recall group
+    if existingRecallGroups:
+        stcur.execute("DELETE FROM RecallGroup WHERE name IN ('" + "', '".join(existingRecallGroups) + "')")
+        stconn.commit()
+        print 'successfully dropped the following recall groups : ' + ', '.join(existingRecallGroups)
+    # warn for non existing tape pools
+    if nonexitingRecallGroups:
+        print 'WARNING : some recall group(s) did not exist : ' + ', '.join(nonexitingRecallGroups)
+    # close DB connections
+    try:
+        castor_tools.disconnectDB(stconn)
+    except Exception:
+        pass
+except Exception, e:
+    print e
+    if verbose:
+        import traceback
+        traceback.print_exc()
+    sys.exit(-1)
diff --git a/hsmtools/deleterecallgroup.man b/hsmtools/deleterecallgroup.man
new file mode 100644
index 0000000000000000000000000000000000000000..6c30f05c9e77e2793c2fc313db9fcc937502a972
--- /dev/null
+++ b/hsmtools/deleterecallgroup.man
@@ -0,0 +1,77 @@
+.TH DELETERECALLGROUP 1 "2011" CASTOR "stager catalogue administrative commands"
+.SH NAME
+deleterecallgroup \- deletes existing recall groups from the stager catalogue
+
+.SH SYNOPSIS
+.B deleterecallgroup
+[
+.BI -h
+]
+.BI <recallGroupName>
+[:...]
+
+.SH DESCRIPTION
+.B deleterecallgroup
+deletes existing recall groups from the CASTOR stager catalog
+.LP
+.BI \-h,\ \-\-help
+Get usage information
+.TP
+.BI <recallGroupName>
+name of recall groups to delete. Several names can be given, colon separated
+
+.SH EXAMPLES
+.nf
+.ft CW
+# printrecallgroup
+           NAME NBDRIVES MINAMOUNTDATA MINNBFILES MAXFILEAGE VDQMPRIORITY     ID            LASTEDITOR          LASTEDITION
+-----------------------------------------------------------------------------------------------------------------------
+ newrecallgroup        0        100GiB       1000        12h            0 263333                  root 30-May-2012 17:29:12
+        default       20        100GiB       1000        12h            0 133068 2.1.13 upgrade script 03-May-2012 16:44:50
+testrecallgroup        2         23MiB       1000    12h23mn          456 263419                  root 30-May-2012 17:31:11
+
+# deleterecallgroup newrecallgroup
+successfully dropped the following recall groups : newrecallgroup
+
+# printrecallgroup
+           NAME NBDRIVES MINAMOUNTDATA MINNBFILES MAXFILEAGE VDQMPRIORITY     ID            LASTEDITOR          LASTEDITION
+---------------------------------------------------------------------------------------------------------------------------
+        default       20        100GiB       1000        12h            0 133068 2.1.13 upgrade script 03-May-2012 16:44:50
+testrecallgroup        2         23MiB       1000    12h23mn          456 263419                  root 30-May-2012 17:31:11
+
+# deleterecallgroup newrecallgroup:testrecallgroup
+successfully dropped the following recall groups : testrecallgroup
+WARNING : some recall group(s) did not exist : newrecallgroup
+
+# printrecallgroup
+   NAME NBDRIVES MINAMOUNTDATA MINNBFILES MAXFILEAGE VDQMPRIORITY     ID            LASTEDITOR          LASTEDITION
+-------------------------------------------------------------------------------------------------------------------
+default       20        100GiB       1000        12h            0 133068 2.1.13 upgrade script 03-May-2012 16:44:50
+
+# deleterecallgroup default
+Recall group default should be deleted. Giving up
+
+In case a user still belongs to a recall group, you may get this :
+
+# deleterecallgroup testrecallgroup
+RecallGroups cannot be dropped when they still contain users :
+  - recall group testrecallgroup contains 1 users
+You may want to use deleterecalluser or modifyrecalluser to correct this
+
+.SH NOTES
+This command requires database client access to the stager catalogue.
+Configuration for the database access is taken from castor.conf.
+
+.SH SEE ALSO
+.BR enterrecallgroup,
+.BR modifyrecallgroup,
+.BR printrecallgroup,
+.BR deletediskpool,
+.BR deletesvcclass,
+.BR deletetapepool,
+.BR deletefileclass,
+.BR deletemigrationroute,
+.BR adminMultiInstance
+
+.SH AUTHOR
+\fBCASTOR\fP Team <castor.support@cern.ch>
diff --git a/hsmtools/enterrecallgroup b/hsmtools/enterrecallgroup
new file mode 100755
index 0000000000000000000000000000000000000000..647aaed16aee06a60f758685b095c0e5b088b9e2
--- /dev/null
+++ b/hsmtools/enterrecallgroup
@@ -0,0 +1,161 @@
+#!/usr/bin/python
+#/******************************************************************************
+# *                      enterrecallgroup
+# *
+# * This file is part of the Castor project.
+# * See http://castor.web.cern.ch/castor
+# *
+# * Copyright (C) 2003  CERN
+# * This program is free software; you can redistribute it and/or
+# * modify it under the terms of the GNU General Public License
+# * as published by the Free Software Foundation; either version 2
+# * of the License, or (at your option) any later version.
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# *
+# * @author castor dev team
+# *****************************************************************************/
+
+'''allows to enter a new recall group into the castor stager'''
+
+import sys, re, os, pwd
+import getopt
+import castor_tools
+
+# usage function
+def usage(exitCode):
+    '''prints usage'''
+    print 'Usage : ' + sys.argv[0] + ' [-h|--help] [--nbdrives <nbDrives>] ' + \
+          '[--minamountdata <minAmountDataForAMount>] [--minnbfiles <minNbFilesForAMount>] ' + \
+          '[--maxfileage <maxFileAgeBeforeForcedMount>] [--priority <VDQMPriority>] <recallGroupName>'
+    sys.exit(exitCode)
+
+def parsePositiveInt(name, svalue):
+    '''parses a positive int value and exits with proper error message in case the value does not fit'''
+    try:
+        value = int(svalue)
+        if value < 0:
+            raise ValueError
+        return value
+    except ValueError:
+        print 'Invalid %s %s' % (name, svalue)
+        usage(1)
+
+def parseDataAmount(name, svalue):
+    '''parses a value describing an amount of data. Exits with proper error message in case the value does not fit'''
+    try:
+        exts = {'K' : 1000, 'M' : 1000*1000, 'G' : 1000*1000*1000, 'T' : 1000*1000*1000*1000,
+                'Ki' : 1024, 'Mi' : 1024*1024, 'Gi' : 1024*1024*1024, 'Ti' : 1024*1024*1024*1024}
+        regexp = re.compile('^(?P<nb>\d+)(?P<ext>(?:[KMGT]i?)?)B?$')
+        m = regexp.match(svalue)
+        if not m:
+            raise ValueError
+        value = int(m.group('nb'))
+        if m.group('ext'):
+            value *= exts[m.group('ext')]
+        return value
+    except ValueError:
+        print 'Invalid %s %s' % (name, svalue)
+        usage(1)
+
+def parseTimeDuration(name, svalue):
+    '''parses a value describing a time duration. Exits with proper error message in case the value does not fit'''
+    try:
+        # check validity
+        if not re.compile('^(\d+(s|mn?|h|d)?)+$').match(svalue):
+            raise ValueError
+        # parse
+        exts = {'s' : 1, 'm' : 60, 'mn' : 60, 'h' : 3600, 'd' : 86400}
+        regexp = re.compile('(\d+)(s|mn?|h|d)?')
+        value = 0
+        for nb, ext in regexp.findall(svalue):
+            partvalue = int(nb)
+            if ext:
+                partvalue *= exts[ext]
+            value += partvalue
+        return value
+    except ValueError:
+        print 'Invalid %s %s' % (name, svalue)
+        usage(1)
+
+# first parse the options
+try:
+    options, args = getopt.getopt(sys.argv[1:], 'hv', ['help', 'verbose', 'nbdrives=', 'minamountdata=',
+                                                       'minnbfiles=', 'maxfileage=', 'priority='])
+except Exception, e:
+    print e
+    usage(1)
+verbose = False
+nbdrives = 0
+minamountdata = 100*1024*1024*1024  # 100GiB
+minnbfiles = 1000
+maxfileage = 43200 # 12h
+priority = 0
+for f, v in options:
+    if f == '-h' or f == '--help':
+        usage(0)
+    elif f == '-v' or f == '--verbose':
+        verbose = True
+    elif f == '--nbdrives':
+        nbdrives = parsePositiveInt('nbdrives', v)
+    elif f == '--minamountdata':
+        minamountdata = parseDataAmount('minamountdata', v)
+    elif f == '--minnbfiles':
+        minnbfiles = parsePositiveInt('minnbfiles', v)
+    elif f == '--maxfileage':
+        maxfileage = parseTimeDuration('maxfileage', v)
+    elif f == '--priority':
+        priority = parsePositiveInt('priority', v)
+    else:
+        print "unknown option : " + f
+        usage(1)
+
+# Deal with arguments
+if len(args) == 0:
+    print "Missing arguments"
+    usage(1)
+elif len(args) > 1:
+    print "Too many arguments"
+    usage(1)
+recallGroupName = args[0]
+
+try:
+    # connect to stager
+    stconn = castor_tools.connectToStager()
+    stcur = stconn.cursor()
+    # check whether the recall group already exists
+    stcur.execute('SELECT id FROM RecallGroup WHERE name=:name', name=recallGroupName)
+    rows = stcur.fetchall()
+    if len(rows) != 0:
+        print 'RecallGroup %s already exists in the stager DB' % recallGroupName
+        print 'You may want to use modifyrecallgroup'
+        sys.exit(1)
+    # get info on user running this command
+    lasteditor = pwd.getpwuid(os.getuid())[0]
+    # insert new recallGroup
+    stcur.execute('''INSERT INTO RecallGroup
+                     (name, nbDrives, minAmountDataForMount, minNbFilesForMount, maxFileAgeBeforeMount,
+                      VDQMPriority, lastEditor, lastEditionTime, id)
+                     VALUES
+                     (:name, :nbdrives, :minamountdata, :minnbfiles, :maxfileage,
+                      :priority, :lasteditor, gettime(), ids_seq.nextval)''',
+                  name=recallGroupName, nbdrives=nbdrives, minamountdata=minamountdata,
+                  minnbfiles=minnbfiles, maxfileage=maxfileage, priority=priority, lasteditor=lasteditor)
+    stconn.commit()
+    print 'inserted recall group %s successfully' % recallGroupName
+    # close DB connection
+    try:
+        castor_tools.disconnectDB(stconn)
+    except Exception:
+        pass
+except Exception, e:
+    print e
+    if verbose:
+        import traceback
+        traceback.print_exc()
+    sys.exit(-1)
diff --git a/hsmtools/enterrecallgroup.man b/hsmtools/enterrecallgroup.man
new file mode 100644
index 0000000000000000000000000000000000000000..5e0c7ac30a138ed6f529229a609af7a6065292b5
--- /dev/null
+++ b/hsmtools/enterrecallgroup.man
@@ -0,0 +1,108 @@
+.TH ENTERRECALLGROUP 1 "2011" CASTOR "stager catalog administrative commands"
+.SH NAME
+enterrecallgroup \- enter a new tape pool in the stager catalog
+.SH SYNOPSIS
+.B enterrecallgroup
+[
+.BI -h
+]
+[
+.BI --nbdrives
+<nbDrives>
+]
+[
+.BI --minamountdata
+.B <minAmountDataForAMount>
+]
+[
+.BI --minnbfiles
+.B <minNbFilesForAMount>
+]
+[
+.BI --maxfileage
+.B <maxFileAgeBeforeForcedMount>
+]
+[
+.BI --priority
+.B <VDQMPriority>
+]
+.BI <recallGroupName>
+.SH DESCRIPTION
+.B enterrecallgroup
+enters a new recall group in the CASTOR stager catalog.
+
+The parameters of the recall group define the policy used to trigger tape mounts in case users of this group want to recall some files.
+The behavior is the following :
+  - never more than <nbDrives> drives can be used concurrently
+  - a new tape will be mounted whenever we have either more than <minAmountDataForAMount> of data or <minNbFilesForAMount> files waiting for recall. In practice, if some migrations are already running, a new one is trigerred if one of these 2 numbers will still be exceeded per mount, even with the new mount.
+  - a tape mount will be forced if nothing is mounted and one file becomes older than maxFileAgeBeforeForcedMount
+  - if a tape mount is triggered, VDQM will be called to get a drive with the given priority
+.TP
+.BI \-h,\ \-\-help
+Get usage information
+.TP
+.BI \-\-nbdrives <nbDrives>
+The maximum number of tape drives this tape pool is allowed to use concurrently
+for migration. Default is 0 is not provided
+.TP
+.BI \-\-minamountdata <minAmountDataForAMount>
+The minimum amount of data needed to trigger a new mount in bytes. Default is 100GiB if not provided.
+The value can be given using the standard K/M/G/T extensions, with or without B (i.e. both KB and K are accepted).
+These have the ISO meaning of powers of 10. Ki/Mi/Gi/Ti[B] extensions are also accepted and deal with powers of 2.
+.TP
+.BI \-\-minnbfiles <minNbFilesForAMount>
+The minimum number of files needed to trigger a new mount. Default is 1000 if not provided.
+.TP
+.BI \-\-maxfileage <maxFileAgeBeforeForcedMount>
+The maximum age of a file before a new mount is triggered. Default is 12h if not provided.
+The value can be given using the extensions s/m/mn/h/d for seconds, minutes(both m and mn), hours and days.
+.TP
+.BI \-\-priority <VDQMpriority>
+The priority to be used when accessing VDQM to get a drive. Default is 0 if not provided.
+.TP
+.BI <recallGroupName>
+name of a tape pool to create.
+
+.SH EXAMPLES
+.nf
+.ft CW
+# enterrecallgroup newrecallgroup
+inserted recall group newrecallgroup successfully
+
+# printrecallgroup
+          NAME NBDRIVES MINAMOUNTDATA MINNBFILES MAXFILEAGE VDQMPRIORITY     ID            LASTEDITOR          LASTEDITION
+----------------------------------------------------------------------------------------------------------------------
+newrecallgroup        0        100GiB       1000        12h            0 263333                  root 30-May-2012 17:29:12
+       default       20        100GiB       1000        12h            0 133068 2.1.13 upgrade script 03-May-2012 16:44:50
+
+# enterrecallgroup --nbdrives 2 newrecallgroup
+Recallgroup newrecallgroup already exists in the stager DB
+You may want to use modifyrecallgroup
+
+# enterrecallgroup --nbdrives 2 --minamountdata 23MiB --maxfileage 12h23mn --priority 456 testrecallgroup
+inserted recall group testrecallgroup successfully
+
+# printrecallgroup
+           NAME NBDRIVES MINAMOUNTDATA MINNBFILES MAXFILEAGE VDQMPRIORITY     ID            LASTEDITOR          LASTEDITION
+-----------------------------------------------------------------------------------------------------------------------
+ newrecallgroup        0        100GiB       1000        12h            0 263333                  root 30-May-2012 17:29:12
+        default       20        100GiB       1000        12h            0 133068 2.1.13 upgrade script 03-May-2012 16:44:50
+testrecallgroup        2         23MiB       1000    12h23mn          456 263419                  root 30-May-2012 17:31:11
+
+.SH NOTES
+This command requires database client access to the stager catalog and nameserver DBs.
+Configuration for the database accesses is taken from castor.conf.
+
+.SH SEE ALSO
+.BR deleterecallgroup,
+.BR modifyrecallgroup,
+.BR printrecallgroup,
+.BR enterdiskpool,
+.BR entersvcclass,
+.BR enterfileclass,
+.BR entertapepool,
+.BR entermigrationroute,
+.BR adminMultiInstance
+
+.SH AUTHOR
+\fBCASTOR\fP Team <castor.support@cern.ch>
diff --git a/hsmtools/modifyrecallgroup b/hsmtools/modifyrecallgroup
new file mode 100755
index 0000000000000000000000000000000000000000..861d22c07c70a4249532efbb2ced93988c099ff3
--- /dev/null
+++ b/hsmtools/modifyrecallgroup
@@ -0,0 +1,172 @@
+#!/usr/bin/python
+#/******************************************************************************
+# *                      modifyrecallgroup
+# *
+# * This file is part of the Castor project.
+# * See http://castor.web.cern.ch/castor
+# *
+# * Copyright (C) 2003  CERN
+# * This program is free software; you can redistribute it and/or
+# * modify it under the terms of the GNU General Public License
+# * as published by the Free Software Foundation; either version 2
+# * of the License, or (at your option) any later version.
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# *
+# * @author castor dev team
+# *****************************************************************************/
+
+'''allows to modify an existing recall group in the castor stager'''
+
+import sys, re, os, pwd
+import getopt
+import castor_tools
+
+# usage function
+def usage(exitCode):
+    '''prints usage'''
+    print 'Usage : ' + sys.argv[0] + ' [-h|--help] [--nbdrives <nbDrives>] ' + \
+          '[--minamountdata <minAmountDataForAMount>] [--minnbfiles <minNbFilesForAMount>] ' + \
+          '[--maxfileage <maxFileAgeBeforeForcedMount>] [--priority <VDQMPriority>] <recallGroupName>'
+    sys.exit(exitCode)
+
+def parsePositiveInt(name, svalue):
+    '''parses a positive int value and exits with proper error message in case the value does not fit'''
+    try:
+        value = int(svalue)
+        if value < 0:
+            raise ValueError
+        return value
+    except ValueError:
+        print 'Invalid %s %s' % (name, svalue)
+        usage(1)
+
+def parseDataAmount(name, svalue):
+    '''parses a value describing an amount of data. Exits with proper error message in case the value does not fit'''
+    try:
+        exts = {'K' : 1000, 'M' : 1000*1000, 'G' : 1000*1000*1000, 'T' : 1000*1000*1000*1000,
+                'Ki' : 1024, 'Mi' : 1024*1024, 'Gi' : 1024*1024*1024, 'Ti' : 1024*1024*1024*1024}
+        regexp = re.compile('^(?P<nb>\d+)(?P<ext>(?:[KMGT]i?)?)B?$')
+        m = regexp.match(svalue)
+        if not m:
+            raise ValueError
+        value = int(m.group('nb'))
+        if m.group('ext'):
+            value *= exts[m.group('ext')]
+        return value
+    except ValueError:
+        print 'Invalid %s %s' % (name, svalue)
+        usage(1)
+
+def parseTimeDuration(name, svalue):
+    '''parses a value describing a time duration. Exits with proper error message in case the value does not fit'''
+    try:
+        # check validity
+        if not re.compile('^(\d+(s|mn?|h|d)?)+$').match(svalue):
+            raise ValueError
+        # parse
+        exts = {'s' : 1, 'm' : 60, 'mn' : 60, 'h' : 3600, 'd' : 86400}
+        regexp = re.compile('(\d+)(s|mn?|h|d)?')
+        value = 0
+        for nb, ext in regexp.findall(svalue):
+            partvalue = int(nb)
+            if ext:
+                partvalue *= exts[ext]
+            value += partvalue
+        return value
+    except ValueError:
+        print 'Invalid %s %s' % (name, svalue)
+        usage(1)
+
+# first parse the options
+try:
+    options, args = getopt.getopt(sys.argv[1:], 'hvm:', ['help', 'verbose', 'nbdrives=', 'minamountdata=',
+                                                       'minnbfiles=', 'maxfileage=', 'priority='])
+except Exception, e:
+    print e
+    usage(1)
+verbose = False
+nbdrives = None
+minamountdata = None
+minnbfiles = None
+maxfileage = None
+priority = None
+for f, v in options:
+    if f == '-h' or f == '--help':
+        usage(0)
+    elif f == '-v' or f == '--verbose':
+        verbose = True
+    elif f == '--nbdrives':
+        nbdrives = parsePositiveInt('nbdrives', v)
+    elif f == '--minamountdata':
+        minamountdata = parseDataAmount('minamountdata', v)
+    elif f == '--minnbfiles':
+        minnbfiles = parsePositiveInt('minnbfiles', v)
+    elif f == '--maxfileage':
+        maxfileage = parseTimeDuration('maxfileage', v)
+    elif f == '--priority':
+        priority = parsePositiveInt('priority', v)
+    else:
+        print "unknown option : " + f
+        usage(1)
+
+# Deal with arguments
+if len(args) == 0:
+    print "Missing arguments"
+    usage(1)
+elif len(args) > 1:
+    print "Too many arguments"
+    usage(1)
+else:
+    recallgroupname = args[0]
+
+# check we have something to do
+if nbdrives == None and minamountdata == None and minnbfiles == None and maxfileage == None:
+    print 'Nothing to modify. Did you forget some argument ?'
+    usage(1)
+
+try:
+    # connect to stager
+    stconn = castor_tools.connectToStager()
+    stcur = stconn.cursor()
+    # check that the recall group exists
+    stcur.execute('SELECT id FROM RecallGroup WHERE name=:name', name=recallgroupname)
+    rows = stcur.fetchall()
+    if len(rows) == 0:
+        print 'RecallGroup %s does not exist in the stager DB' % recallgroupname
+        print 'Please use enterrecallgroup if you would like to create it'
+        sys.exit(1)
+    recallGroupId = rows[0][0]
+    # deal with the update
+    if nbdrives != None:
+        stcur.execute('UPDATE RecallGroup SET nbDrives=:value WHERE id = :recallGroupId', value=nbdrives, recallGroupId=recallGroupId)
+    if minamountdata != None:
+        stcur.execute('UPDATE RecallGroup SET minAmountdataForMount=:value WHERE id = :recallGroupId', value=minamountdata, recallGroupId=recallGroupId)
+    if minnbfiles != None:
+        stcur.execute('UPDATE RecallGroup SET minNbFilesForMount=:value WHERE id = :recallGroupId', value=minnbfiles, recallGroupId=recallGroupId)
+    if maxfileage != None:
+        stcur.execute('UPDATE RecallGroup SET maxFileAgeBeforeMount=:value WHERE id = :recallGroupId', value=maxfileage, recallGroupId=recallGroupId)
+    if priority != None:
+        stcur.execute('UPDATE RecallGroup SET VDQMPriority=:value WHERE id = :recallGroupId', value=priority, recallGroupId=recallGroupId)
+    # in all cases, update the lastEditor and lastEditionTime
+    lasteditor = pwd.getpwuid(os.getuid())[0]
+    stcur.execute('UPDATE RecallGroup SET lastEditor=:lasteditor, lastEditionTime=gettime() WHERE id = :recallGroupId', lasteditor=lasteditor, recallGroupId=recallGroupId)
+    # commit insertion and tell user
+    stconn.commit()
+    print 'modified recall group %s successfully' % recallgroupname
+    # close DB connections
+    try:
+        castor_tools.disconnectDB(stconn)
+    except Exception:
+        pass
+except Exception, e:
+    print e
+    if verbose:
+        import traceback
+        traceback.print_exc()
+    sys.exit(-1)
diff --git a/hsmtools/modifyrecallgroup.man b/hsmtools/modifyrecallgroup.man
new file mode 100644
index 0000000000000000000000000000000000000000..4f50b1c045eee97fa5ebab5269d2481206b87554
--- /dev/null
+++ b/hsmtools/modifyrecallgroup.man
@@ -0,0 +1,99 @@
+.TH MODIFYRECALLGROUP 1 "2011" CASTOR "stager catalogue administrative commands"
+.SH NAME
+modifyrecallgroup \- modifies an existing recall group in the stager catalog
+.SH SYNOPSIS
+.B modifyrecallgroup
+[
+.BI -h
+]
+[
+.BI --nbdrives
+<nbDrives>
+]
+[
+.BI --minamountdata
+.B <minAmountDataForAMount>
+]
+[
+.BI --minnbfiles
+.B <minNbFilesForAMount>
+]
+[
+.BI --maxfileage
+.B <maxFileAgeBeforeForcedMount>
+]
+[
+.BI --priority
+.B <VDQMpriority>
+]
+<recallGroupName>
+
+.SH DESCRIPTION
+.B modifyrecallgroup
+modifies an existing recall group in the CASTOR stager catalog
+
+The parameters of the recallgroup define the policy used to trigger tape mounts for recalls.
+The behavior is the following :
+  - never more than <nbDrives> drives can be used concurrently
+  - a new tape will be mounted whenever we have either more than <minAmountDataForAMount> of data or <minNbFilesForAMount> files waiting for recall. In practice, if some migrations are already running, a new one is trigerred if one of these 2 numbers will still be exceeded per mount, even with the new mount.
+  - a tape mount will be forced if nothing is mounted and one file becomes older than maxFileAgeBeforeForcedMount
+  - if a tape mount is triggered, VDQM will be called to get a drive with the given priority
+.TP
+.BI \-h,\ \-\-help
+Get usage information
+.TP
+.BI \-\-nbdrives <nbDrives>
+The maximum number of tape drives this recall group is allowed to use concurrently
+for migration.
+.TP
+.BI \-\-minamountdata <minAmountDataForAMount>
+The minimum amount of data needed to trigger a new mount in bytes.
+The value can be given using the standard K/M/G/T extensions, with or without B (i.e. both KB and K are accepted).
+These have the ISO meaning of powers of 10. Ki/Mi/Gi/Ti[B] extensions are also accepted and deal with powers of 2.
+.TP
+.BI \-\-minnbfiles <minNbFilesForAMount>
+The minimum number of files needed to trigger a new mount.
+.TP
+.BI \-\-maxfileage <maxFileAgeBeforeForcedMount>
+The maximum age of a file before a new mount is triggered.
+The value can be given using the extensions s/m/mn/h/d for seconds, minutes(both m and mn), hours and days.
+.TP
+.BI \-\-priority <VDQMpriority>
+The priority to be used when accessing VDQM to get a drive.
+.TP
+.BI <tapePoolName>
+name of the recall group to modify.
+
+.SH EXAMPLES
+.nf
+.ft CW
+# printrecallgroup
+          NAME NBDRIVES MINAMOUNTDATA MINNBFILES MAXFILEAGE VDQMPRIORITY     ID            LASTEDITOR          LASTEDITION
+----------------------------------------------------------------------------------------------------------------------
+newrecallgroup        0        100GiB       1000        12h            0 263333                  root 30-May-2012 17:29:12
+       default       20        100GiB       1000        12h            0 133068 2.1.13 upgrade script 03-May-2012 16:44:50
+
+# modifyrecallgroup --nbdrives 1 --minamountdata 57MiB --minnbfiles 234 --maxfileage 1d5h --priority 2 newrecallgroup
+modified recall group newrecallgroup successfully
+
+# printrecallgroup
+          NAME NBDRIVES MINAMOUNTDATA MINNBFILES MAXFILEAGE VDQMPRIORITY     ID            LASTEDITOR          LASTEDITION
+----------------------------------------------------------------------------------------------------------------------
+newrecallgroup        1         57MiB        234       1d5h            2 263333                  root 06-Jun-2012 09:35:11
+       default       20        100GiB       1000        12h            0 133068 2.1.13 upgrade script 03-May-2012 16:44:50
+
+.SH NOTES
+This command requires database client access to the stager catalog and nameserver DBs.
+Configuration for the database accesses is taken from castor.conf.
+
+.SH SEE ALSO
+.BR deleterecallgroup,
+.BR enterrecallgroup,
+.BR printrecallgroup,
+.BR modifytapepool
+.BR modifysvcclass,
+.BR modifymigrationroute,
+.BR adminMultiInstance
+
+.SH AUTHOR
+\fBCASTOR\fP Team <castor.support@cern.ch>
diff --git a/hsmtools/printrecallgroup b/hsmtools/printrecallgroup
new file mode 100755
index 0000000000000000000000000000000000000000..3a6c35d027646d10c6162f3942caf7fe69da2d38
--- /dev/null
+++ b/hsmtools/printrecallgroup
@@ -0,0 +1,137 @@
+#!/usr/bin/python
+#/******************************************************************************
+# *                      printrecallgroup
+# *
+# * This file is part of the Castor project.
+# * See http://castor.web.cern.ch/castor
+# *
+# * Copyright (C) 2003  CERN
+# * This program is free software; you can redistribute it and/or
+# * modify it under the terms of the GNU General Public License
+# * as published by the Free Software Foundation; either version 2
+# * of the License, or (at your option) any later version.
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# *
+# * @author Castor Dev team, castor-dev@cern.ch
+# *****************************************************************************/
+
+'''command line printing the given recall group(s)'''
+
+import sys
+import getopt, time
+import castor_tools
+
+# usage function
+def usage(exitCode):
+    '''prints usage'''
+    print 'Usage : ' + sys.argv[0] + ' [-h|--help] [<recallGroupName> [...]]'
+    sys.exit(exitCode)
+
+def secsToDate(s):
+    '''converts number of seconds since the epoch into readable date'''
+    return time.strftime('%d-%b-%Y %H:%M:%S', time.localtime(s))
+
+def nbToDataAmount(n):
+    '''converts a number into a readable amount of data'''
+    ext = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
+    magn = 0
+    while n / 1024 > 5:
+        magn += 1
+        n = n / 1024
+    return str(n) + ext[magn]
+
+def nbToAge(n):
+    '''converts a number of seconds into a readable age'''
+    s = ''
+    if n >= 86400:
+        s = s + str(n/86400) + 'd'
+        n = n % 86400
+    if n >= 3600:
+        s = s + str(n/3600) + 'h'
+        n = n % 3600
+    if n >= 60:
+        s = s + str(n/60) + 'mn'
+        n = n % 60
+    if n > 0:
+        s = s + str(n) + 's'
+    if len(s) == 0:
+        s = '0s'
+    return s
+
+# first parse the options
+try:
+    options, args = getopt.getopt(sys.argv[1:], 'hv', ['help', 'verbose'])
+except Exception, e:
+    print e
+    usage(1)
+verbose = False
+for f, v in options:
+    if f == '-h' or f == '--help':
+        usage(0)
+    elif f == '-v' or f == '--verbose':
+        verbose = True
+    else:
+        print "unknown option : " + f
+        usage(1)
+
+# Deal with arguments
+recallGroups = None
+if len(args) != 0:
+    recallGroups = set(args)
+
+try:
+    # connect to stager and prepare statements
+    stconn = castor_tools.connectToStager()
+    stcur = stconn.cursor()
+    sqlStatement = '''
+    SELECT name, nbDrives, minAmountDataForMount, minNbFilesForMount, maxFileAgeBeforeMount, VDQMPriority,
+           lastEditor, lastEditionTime, id
+      FROM RecallGroup'''
+    if recallGroups:
+        sqlStatement = sqlStatement + " WHERE name IN ('" + "', '".join(recallGroups) + "')"
+    stcur.execute(sqlStatement)
+    # get results
+    rows = stcur.fetchall()
+    existingRecallGroups = set([row[0] for row in rows])
+    if recallGroups:
+        unknownRecallGroups = recallGroups - existingRecallGroups
+    # loop over tape pools and print them
+    if existingRecallGroups:
+        maxNameLen = max([4] + [len(row[0]) for row in rows])
+        maxMinAmountDataLen = max([13] + [len(nbToDataAmount(row[2])) for row in rows])
+        maxMinNbFilesLen = max([10] + [len(str(row[3])) for row in rows])
+        maxMaxFileAgeLen = max([10] + [len(nbToAge(row[4])) for row in rows])
+        maxLastEditorLen = max([10] + [len(row[6]) for row in rows if row[6]])
+        maxLastEditionTimeLen = max([11] + [len(secsToDate(row[7])) for row in rows])
+        maxIdLen = max([2] + [len(str(row[8])) for row in rows])
+        print '%*s NBDRIVES %*s %*s %*s VDQMPRIORITY %*s %*s %*s' % \
+              (maxNameLen, 'NAME', maxMinAmountDataLen, 'MINAMOUNTDATA', maxMinNbFilesLen, \
+               'MINNBFILES', maxMaxFileAgeLen, 'MAXFILEAGE', maxIdLen, 'ID', \
+               maxLastEditorLen, 'LASTEDITOR', maxLastEditionTimeLen, 'LASTEDITION')
+        print '-' * (28 + maxNameLen + maxMinAmountDataLen + maxMinNbFilesLen + maxMaxFileAgeLen + \
+                     maxLastEditorLen + maxLastEditionTimeLen + maxIdLen)
+        for name, nbdrives, minamountdata, minnbfiles, maxfileage, priority, lastEditor, lastEdition, tpid in rows:
+            print '%*s %8d %*s %*d %*s %12d %*d %*s %*s' % \
+                  (maxNameLen, name, nbdrives, maxMinAmountDataLen, nbToDataAmount(minamountdata), \
+                   maxMinNbFilesLen, minnbfiles, maxMaxFileAgeLen, nbToAge(maxfileage), priority, maxIdLen, tpid, \
+                   maxLastEditorLen, lastEditor, maxLastEditionTimeLen, secsToDate(lastEdition))
+    # check unknown tape pools
+    if recallGroups and unknownRecallGroups:
+        print 'WARNING : the following recall groups do not exist : ' + ', '.join(unknownRecallGroups)
+    # close DB connections
+    try:
+        castor_tools.disconnectDB(stconn)
+    except Exception:
+        pass
+except Exception, e:
+    print e
+    if verbose:
+        import traceback
+        traceback.print_exc()
+    sys.exit(-1)
diff --git a/hsmtools/printrecallgroup.man b/hsmtools/printrecallgroup.man
new file mode 100644
index 0000000000000000000000000000000000000000..60fe713d14082be19e5b9451de5b797a0aed03eb
--- /dev/null
+++ b/hsmtools/printrecallgroup.man
@@ -0,0 +1,57 @@
+.TH PRINTRECALLGROUP 1 "2011" CASTOR "Prints out the given recall group(s)"
+.SH NAME
+printrecallgroup
+.SH SYNOPSIS
+.B printrecallgroup
+[
+.BI -h
+]
+[
+<recallGroupName>
+[...]
+]
+
+.SH DESCRIPTION
+.B printrecallgroup
+prints out definitions of the given tape pools
+.LP
+.BI \-h,\ \-\-help
+Get usage information
+.TP
+.BI <RecallGroupName>
+when recall group names are given, only the definitions of the listed recall groups are printed.
+If no recall group name is given, all recall group definitions are printed.
+
+.SH EXAMPLES
+.nf
+.ft CW
+# printrecallgroup
+           NAME NBDRIVES MINAMOUNTDATA MINNBFILES MAXFILEAGE VDQMPRIORITY     ID            LASTEDITOR          LASTEDITION
+---------------------------------------------------------------------------------------------------------------------------
+ newrecallgroup        0        100GiB       1000        12h            0 263333                  root 30-May-2012 17:29:12
+        default       20        100GiB       1000        12h            0 133068 2.1.13 upgrade script 03-May-2012 16:44:50
+testrecallgroup        2         23MiB       1000    12h23mn          456 263419                  root 30-May-2012 17:31:11
+
+# printrecallgroup testrecallgroup nonexisting
+           NAME NBDRIVES MINAMOUNTDATA MINNBFILES MAXFILEAGE VDQMPRIORITY     ID            LASTEDITOR          LASTEDITION
+---------------------------------------------------------------------------------------------------------------------------
+testrecallgroup        2         23MiB       1000    12h23mn          456 263419                  root 30-May-2012 17:31:11
+WARNING : the following recall groups do not exist : nonexisting
+
+.SH NOTES
+This command requires database client access to the stager catalogue.
+Configuration for the database access is taken from castor.conf.
+
+.SH SEE ALSO
+.BR enterrecallgroup,
+.BR modifyrecallgroup,
+.BR deleterecallgroup,
+.BR printdiskpool,
+.BR printsvcclass,
+.BR printfileclass,
+.BR printtapepool,
+.BR printmigrationroute,
+.BR adminMultiInstance
+
+.SH AUTHOR
+\fBCASTOR\fP Team <castor.support@cern.ch>