]> git.sur5r.net Git - bacula/bacula/blobdiff - regress/scripts/functions
Backport from Bacula Enterprise
[bacula/bacula] / regress / scripts / functions
index 7a29bc48a1ba2a43ac2ae0ee8a7b19ecae9c412d..9a21653ee4bb9ed07047ad3b5a5b9b0fb3eaad1b 100644 (file)
@@ -2,20 +2,47 @@
 # A set of useful functions to be sourced in each test
 #
 
+
+check_encoding()
+{
+   if ${bin}/bacula-dir -d50 -t -c ${conf}/bacula-dir.conf 2>&1 | grep 'Wanted SQL_ASCII, got UTF8' >/dev/null ; then
+       echo "Found database encoding problem, please modify the database encoding (SQL_ASCII)"
+       exit 1
+   fi
+}
+
 start_test()
 {
-   # Turn off email
-   outf="tmp/sed_tmp"
-   echo "s%  mail =%# mail = %g" >${outf}
-   echo "s%  operator =%# operator =%g" >>${outf}
-   cp bin/bacula-dir.conf tmp/1
-   sed -f ${outf} tmp/1 >bin/bacula-dir.conf
+   check_encoding
+   rm -rf ${working}/@*
+   variant_name=""
+
+   # If no or dummy smtp_host, use dummy email handler
+   if [ x${SMTP_HOST} = x -o x${SMTP_HOST} = xdummy ]; then
+      cp scripts/dummy_bsmtp bin/bsmtp
+      chmod 755 bin/bsmtp
+   else
+      # Deactivate all email
+      outf="${tmp}/sed_tmp"
+      echo "s%  mail =%# mail = %g" >${outf}
+      echo "s%  operator =%# operator =%g" >>${outf}
+      cp ${conf}/bacula-dir.conf ${tmp}/1
+      sed -f ${outf} ${tmp}/1 > ${conf}/bacula-dir.conf
+   fi
+   ./test_starttime
    echo " "
    echo " "
-   echo " === Starting $TestName at `date +%R:%S` ==="
-   echo " === Starting $TestName at `date +%R:%S` ===" >>working/log
+   echo " === Starting $TestName ${variant_name} at `date +%R:%S` ==="
+   echo " === Starting $TestName ${variant_name} at `date +%R:%S` ===" >> ${working}/log
    echo " "
+   export TestName
    export zstat
+   export estat
+   estat=0
+   zstat=0
+   bstat=0
+   rstat=0
+   dstat=0
 }
 
 require_root()
@@ -55,6 +82,22 @@ if test x${AUTOCHANGER} = x/dev/null ; then
 fi
 }
 
+require_vtape() 
+{
+if test x${USE_VTAPE} = x ; then
+   echo "$TestName test needs the vtape driver."
+   exit 0
+fi
+}
+
+require_linux()
+{
+os=`uname`
+if [ $os != 'Linux' ]; then
+   echo "This test $TestName runs only on Linux"
+   exit 0
+fi
+}
 
 skip_if_no_autochanger()
 {
@@ -76,50 +119,99 @@ set_debug()
 
 print_debug()
 {
+   echo $* | grep ERROR > /dev/null
+   if test $? -eq 0; then
+     echo $* >> $tmp/err.log
+   fi
    if test "$debug" -eq 1 ; then
      echo $*
    fi
 }
 
+check_files_written()
+{
+    LOG=$1
+    NB=$2
+    FILES=`awk '/FD Files Written:/ { last=$4 } END { print last }' $LOG`
+
+    if [ "$NB" != "$FILES" ]; then
+        print_debug "ERROR: Expect $NB files, get $FILES"
+        bstat=2
+    fi
+}
+
+################################################################
+# Get information from logs
+get_mig_info()
+{
+    # Prev Backup JobId
+    JOBID=$1
+    LOG=$2
+    RET=`awk -F: "BEGIN { jobid=$JOBID } "'/Prev Backup JobId/ { cjid=$2 } /New Backup JobId/  { if (cjid == jobid) { print $2 } }' $LOG`
+}
+
+get_duration()
+{
+   LOG=$1
+   RET=`awk 'BEGIN {t["secs"]=1;t["sec"]=1;t["min"]=60;t["mins"]=60}; /Elapsed time:/ { last=$3*t[$4] } END { print last }' $LOG`
+}
+
+check_duration()
+{
+   LOG=$1
+   TIME=$2
+   OP=${3:-gt}
+
+   get_duration $LOG
+   if [ "$RET" -$OP "$TIME" ]; then
+       print_debug "Error, got $RET $OP $TIME sec on $LOG"
+       bstat=2
+   fi
+}
+
 run_bacula()
 {
    debug_wait
    zstat=0
+   estat=0
    if test "$debug" -eq 1 ; then
-     bin/bacula-ctl-sd start
-     bin/bacula-ctl-fd start
-     bin/bacula-ctl-dir start
-     cat tmp/bconcmds | bin/bconsole -c bin/bconsole.conf
+     ${scripts}/bacula-ctl-sd start -m
+     ${scripts}/bacula-ctl-fd start -m $1
+     ${scripts}/bacula-ctl-dir start -m
+     cat ${tmp}/bconcmds | ${bin}/bconsole -c ${conf}/bconsole.conf
+     return $?
    else
-     bin/bacula start 2>&1 >/dev/null
-     cat tmp/bconcmds | bin/bconsole -c bin/bconsole.conf  2>&1 >/dev/null
+     ${scripts}/bacula start >/dev/null 2>&1
+     cat ${tmp}/bconcmds | ${bin}/bconsole -c ${conf}/bconsole.conf >/dev/null 2>&1
+     return $?
    fi
 }
 
 run_bconsole()
 {
+   bconsole_file=${1:-${tmp}/bconcmds}
    if test "$debug" -eq 1 ; then
-     cat tmp/bconcmds | bin/bconsole -c bin/bconsole.conf
+     cat $bconsole_file | ${bin}/bconsole -c ${conf}/bconsole.conf
    else
-     cat tmp/bconcmds | bin/bconsole -c bin/bconsole.conf  2>&1 >/dev/null
+     cat $bconsole_file | ${bin}/bconsole -c ${conf}/bconsole.conf  2>&1 >/dev/null
    fi
 }
 
 run_btape()
 {
    if test "$debug" -eq 1 ; then
-     cat tmp/bconcmds | bin/btape -c bin/bacula-sd.conf DDS-4 | tee tmp/log1.out
+     cat ${tmp}/bconcmds | ${bin}/btape -c ${conf}/bacula-sd.conf tape | tee ${tmp}/log1.out
    else
-     cat tmp/bconcmds | bin/btape -c bin/bacula-sd.conf DDS-4 2>&1 >tmp/log1.out
+     cat ${tmp}/bconcmds | ${bin}/btape -c ${conf}/bacula-sd.conf tape >${tmp}/log1.out 2>&1
    fi
 }
 
 run_bscan()
 {
    if test "$debug" -eq 1 ; then
-      bin/bscan $* | tee tmp/log.out
+      ${bin}/bscan $* | tee ${tmp}/log.out
    else
-      bin/bscan $* 2>&1 >/dev/null
+      ${bin}/bscan $* 2>&1 >/dev/null
    fi
 }
 
@@ -131,8 +223,8 @@ bscan_libdbi()
    B_p=`echo $B | awk '{print $9}'`
 
    BSCANLIBDBI="${LIBDBI:+1}"
-               
-   if test "$BSCANLIBDBI" -eq "1" ; then
+                
+   if test "$BSCANLIBDBI" = "1" ; then
       BSCANLIBDBI="-D $B_D -h $B_t -t $B_p"
    else
       BSCANLIBDBI=" "
@@ -141,12 +233,16 @@ bscan_libdbi()
 
 stop_bacula()
 {
-   bin/bacula stop 2>&1 >/dev/null
+   if test "$debug" -eq 1 ; then
+      ${scripts}/bacula stop
+   else
+      ${scripts}/bacula stop 2>&1 >/dev/null
+   fi
 }
 
 check_for_zombie_jobs()
 {
-   scripts/check_for_zombie_jobs $*
+   ${rscripts}/check_for_zombie_jobs $*
 }
 
 change_jobname()
@@ -158,90 +254,239 @@ change_jobname()
       oldname=$1
       newname=$2
    fi
-   rm -f bin/1
-   mv bin/bacula-dir.conf bin/1
-   echo "s%${oldname}%${newname}%g" >tmp/1
-   sed -f tmp/1 bin/1 >bin/bacula-dir.conf
+   rm -f $tmp/1 $tmp/2
+   mv ${conf}/bacula-dir.conf $tmp/1
+   echo "s%${oldname}%${newname}%g" >$tmp/2
+   sed -f $tmp/2 $tmp/1 >$conf/bacula-dir.conf
 #  echo "Job ${oldname} changed to ${newname}"
 }
 
 check_two_logs()
 {
-   grep "^  Termination: *Backup OK" tmp/log1.out 2>&1 >/dev/null
-   bstat=$?
-   grep "^  Termination: .*Backup Error" tmp/log1.out 2>&1 >/dev/null
+   grep "^  Termination: *Backup OK" ${tmp}/log1.out 2>&1 >/dev/null
+   bstat=${bstat:-$?}
+   grep "^  Termination: .*Backup Error" ${tmp}/log1.out 2>&1 >/dev/null
    if test $? -eq 0; then
       bstat=2
    fi
-   grep "^  Termination: *Restore OK" tmp/log2.out 2>&1 >/dev/null
-   rstat=$?
-   grep "^  Termination: *Restore OK -- warning file count mismatch" tmp/log2.out 2>&1 >/dev/null
+   # Do not check for restart jobs in restart tests
+   echo "$TestName" | grep restart.*-test > /dev/null
+   if [ $? -ne 0 ]; then
+      grep "^  Termination: .*Backup failed" ${tmp}/log1.out 2>&1 >/dev/null
+      if test $? -eq 0; then
+         bstat=3
+      fi
+   fi
+   grep "^  Termination: *Restore OK" ${tmp}/log2.out 2>&1 >/dev/null
+   rstat=${rstat:-$?}
+   grep "^  Termination: .*Restore Error" ${tmp}/log2.out 2>&1 >/dev/null
    if test $? -eq 0; then
       rstat=2
    fi
-   grep "^  Termination: .*Verify Differences" tmp/log2.out 2>&1 >/dev/null
+   grep "^  Termination: *Restore OK -- warning file count mismatch" ${tmp}/log2.out 2>&1 >/dev/null
    if test $? -eq 0; then
       rstat=3
    fi
+   grep "^  Termination: .*Verify Differences" ${tmp}/log2.out 2>&1 >/dev/null
+   if test $? -eq 0; then
+      rstat=4
+   fi
+   grep "Encoding error for database" ${tmp}/log1.out > /dev/null
+   if test $? -eq 0; then
+      print_debug "Found database encoding error"
+      bstat=2
+   fi
+   grep "Orphaned buffer" ${tmp}/log1.out ${tmp}/log2.out
+   if test $? -eq 0; then
+      print_debug "Found orphaned buffers"
+      estat=1
+   fi
+}
+
+dtitle()
+{
+   if test "$debug" -eq 1 ; then
+      echo "============================================================"
+      echo $*
+      echo "============================================================"
+   fi
+}
+
+dmsg()
+{
+   test "$debug" -eq 1 && echo $*
+}
+
+check_size_interval()
+{
+   # if mi == -1 then then ignore mi (idem for ma)
+   mi=$1        
+   value=$2
+   ma=$3
+   
+   test \( -1 -eq $mi -o $mi -le $value \) -a \( -1 -eq $ma -o $value -le $ma \) 
 }
 
 check_restore_diff()
 {
-   diff -r build tmp/bacula-restores${cwd}/build 2>&1 >/dev/null
-   dstat=$?
+   if test "$debug" -eq 1 ; then
+      $rscripts/diff.pl -notop -s ${src} -d ${tmp}/bacula-restores${src} 2>&1 >/tmp/d$$
+      if test $? -ne 0; then
+         dstat=1
+         cat /tmp/d$$
+         ls -Rl ${src}
+         ls -Rl ${tmp}/bacula-restores${src}
+      fi
+      rm -f /tmp/d$$
+      diff -ur ${src} ${tmp}/bacula-restores${src}
+   else 
+      $rscripts/diff.pl -notop -s ${src} -d ${tmp}/bacula-restores${src} 2>&1 >/dev/null
+      if test $? -ne 0; then
+         dstat=1
+      fi
+      diff -ur ${src} ${tmp}/bacula-restores${src} 2>&1 >/dev/null
+   fi
+   if test $? -ne 0; then
+     dstat=1
+   fi
+}
+
+check_restore_bin_diff()
+{
+   if test "$debug" -eq 1 ; then
+      $rscripts/diff.pl -notop -s ${bin} -d ${tmp}/bacula-restores${bin} 2>&1 >/tmp/d$$
+      if test $? -ne 0; then
+         dstat=1
+         cat /tmp/d$$
+         ls -Rl ${src}
+         ls -Rl ${tmp}/bacula-restores${src}
+      fi
+      rm -f /tmp/d$$
+      diff -ur ${bin} ${tmp}/bacula-restores${bin}
+   else 
+      $rscripts/diff.pl -notop -s ${bin} -d ${tmp}/bacula-restores${bin} 2>&1 >/dev/null
+      if test $? -ne 0; then
+         dstat=1
+      fi
+      diff -ur ${bin} ${tmp}/bacula-restores${bin} 2>&1 >/dev/null
+   fi
+   if test $? -ne 0; then
+      dstat=1
+   fi
 }
 
+
 check_restore_tmp_build_diff()
 {
-   diff -r tmp/build tmp/bacula-restores${cwd}/tmp/build 2>&1 >/dev/null
-   dstat=$?
+   if test "$debug" -eq 1 ; then
+      $rscripts/diff.pl -notop -s ${tmpsrc} -d ${tmp}/bacula-restores${tmpsrc} 2>&1 >/tmp/d$$
+      if test $? -ne 0; then
+         dstat=1
+         cat /tmp/d$$
+         ls -Rl ${src}
+         ls -Rl ${tmp}/bacula-restores${src}
+      fi
+      rm -f /tmp/d$$
+      diff -ur ${tmpsrc} ${tmp}/bacula-restores${tmpsrc}
+   else
+      $rscripts/diff.pl -notop -s ${tmpsrc} -d ${tmp}/bacula-restores${tmpsrc} 2>&1 >/dev/null
+      if test $? -ne 0; then
+         dstat=1
+      fi
+      diff -ur ${tmpsrc} ${tmp}/bacula-restores${tmpsrc} 2>&1 >/dev/null
+   fi
+   if test $? -ne 0; then
+      dstat=1
+   fi
 }
 
+# bstat is backup error
+# dstat is diff difference
+# estat is special error status (shown by print_debug message)
+# rstat is restore status
+# zstat is zombie job(s)
+#
 end_test()
 {
+   if [ x$notracedump != xyes ]; then 
+      cat ${working}/bacula.*.traceback 2>/dev/null
+      cp -f  ${working}/bacula.*.traceback ${dumps} 2>/dev/null
+      cat ${working}/*.lockdump 2>/dev/null
+      cp -f ${working}/*.lockdump ${dumps} 2>/dev/null
+   fi
+   if [ -f $tmp/err.log ]; then
+      cat $tmp/err.log
+   fi
+   d=`./test_duration`
+   t=`date +%R:%S`
+   if [ $estat != 0 ] ; then
+      echo " "
+      echo "  !!!!! $TestName failed!!! $t $d !!!!! "
+      echo "     Status: estat=$estat zombie=$zstat backup=$bstat restore=$rstat diff=$dstat"
+      echo " " >>test.out
+      echo "  !!!!! $TestName failed!!! $t $d !!!!! " >>test.out
+      echo "     Status: estat=$estat zombie=$zstat backup=$bstat restore=$rstat diff=$dstat" >>test.out
+      echo " "
+      exit 1
+   fi
    if [ $zstat != 0 ] ; then
       echo " "
-      echo "  !!!!! $TestName failed!!! `date +%R:%S` !!!!! "
-      echo "  !!!!! $TestName failed!!! `date +%R:%S` !!!!! " >>test.out
-      echo "   Status: zombie=$zstat backup=$bstat restore=$rstat diff=$dstat" >>test.out
+      echo "  !!!!! $TestName failed!!! $t $d !!!!! "
+      echo "     Status: zombie=$zstat backup=$bstat restore=$rstat diff=$dstat"
+      echo " " >>test.out
+      echo "  !!!!! $TestName failed!!! $t $d !!!!! " >>test.out
+      echo "     Status: zombie=$zstat backup=$bstat restore=$rstat diff=$dstat" >>test.out
       echo " "
       exit 1
    fi
    if [ $dstat != 0 -o $bstat != 0 -o $rstat != 0 ] ; then
       echo " "
-      echo " "
-      echo "  !!!!! $TestName failed!!! `date +%R:%S` !!!!! "
-      echo "  !!!!! $TestName failed!!! `date +%R:%S` !!!!! " >>test.out
+      echo "  !!!!! $TestName failed!!! $t $d !!!!! "
+      echo "     Status: zombie=$zstat backup=$bstat restore=$rstat diff=$dstat"
+      echo " " >>test.out
+      echo "  !!!!! $TestName failed!!! $t $d !!!!! " >>test.out
+      echo "     Status: zombie=$zstat backup=$bstat restore=$rstat diff=$dstat" >>test.out
       if [ $bstat != 0 -o $rstat != 0 ] ; then
-         echo "  !!!!! Bad termination status       !!!!! "
-         echo "  !!!!! Bad termination status       !!!!! " >>test.out
+         echo "     !!! Bad termination status       !!! "
+         echo "     !!! Bad termination status       !!! " >>test.out
       else
-         echo "  !!!!! Restored files differ          !!!!! "
-         echo "  !!!!! Restored files differ          !!!!! " >>test.out
+         echo "     !!! Restored files differ        !!! "
+         echo "     !!! Restored files differ        !!! " >>test.out
       fi
-      echo "   Status: backup=$bstat restore=$rstat diff=$dstat"
-      echo "   Status: backup=$bstat restore=$rstat diff=$dstat" >>test.out
+      echo "     Status: backup=$bstat restore=$rstat diff=$dstat"
+      echo "     Status: backup=$bstat restore=$rstat diff=$dstat" >>test.out
+      echo "     Test owner of $SITE_NAME is $EMAIL"
+      echo "     Test owner of $SITE_NAME is $EMAIL" >>test.out
+      echo " " >>test.out
       echo " "
       exit 1
    else
-      echo "  ===== $TestName OK `date +%R:%S` ===== "
-      echo "  ===== $TestName OK `date +%R:%S` ===== " >>test.out
+      echo "  ===== $TestName OK $t $d ===== "
+      echo "  ===== $TestName OK $t $d ===== " >>test.out
       if test "$debug" -eq 0 ; then
-         scripts/cleanup
+         ${rscripts}/cleanup
       fi
    fi
 }
 
 copy_tape_confs()
 {
-   scripts/copy-tape-confs
-   scripts/cleanup-tape
+   ${rscripts}/copy-tape-confs
+   ${rscripts}/cleanup-tape
 }
 
 copy_test_confs()
 {
-   scripts/copy-test-confs
-   scripts/cleanup
+   ${rscripts}/copy-test-confs
+   ${rscripts}/cleanup
+}
+
+disable_plugins()
+{
+   for i in ${conf}/bacula-fd.conf; do
+      sed 's/Plugin/#Plugin/' $i > $tmp/1
+      cp -f $tmp/1 $i
+   done
 }
 
 debug_wait()
@@ -252,6 +497,89 @@ debug_wait()
   fi
 }
 
+init_slot()
+{
+   DRIVE=$1
+   SLOT=$2
+   if test -n "$DRIVE" -a -n "$SLOT"; then
+      if test ! -c $DRIVE -a ! -b $DRIVE -a x$USE_VTAPE != x ; then
+         dir=`dirname $DRIVE`
+         if [ ! -d "$dir" ]; then
+            mkdir -p "$dir"
+         fi
+         touch $dir/slot$SLOT
+      fi
+   fi
+}
+
+init_drive()
+{
+  if test a$USE_VTAPE = a; then
+    mt -f $1 rewind
+    mt -f $1 weof
+  else
+    cp /dev/null $1
+  fi
+}
+
+rewind_drive()
+{
+  if test a$USE_VTAPE = a; then
+    mt -f $1 rewind
+  fi
+}
+
+load_slot1() 
+{
+# Get a tape from slot1
+slot=`${scripts}/$MTX ${AUTOCHANGER} loaded 0 ${TAPE_DRIVE} $DRIVE1`
+case $slot in
+ 0)
+    ${scripts}/$MTX ${AUTOCHANGER} load $SLOT1 ${TAPE_DRIVE} $DRIVE1
+    slot=$SLOT1
+    ;;
+ $SLOT1)
+    slot=$SLOT1
+    ;;
+ *)
+    rewind_drive ${TAPE_DRIVE}
+    ${scripts}/$MTX ${AUTOCHANGER} unload $slot  ${TAPE_DRIVE} $DRIVE1
+    ${scripts}/$MTX ${AUTOCHANGER} load   $SLOT1 ${TAPE_DRIVE} $DRIVE1
+    slot=$SLOT1
+    ;;
+esac
+}
+
+#
+# $1 has currently loaded slot, load the other one i.e. if 1, load 2;
+#    if 2, load 1; if 0 load 1
+#
+load_other_slot()
+{
+rewind_drive ${TAPE_DRIVE}
+case $1 in
+ 0)
+    ${scripts}/${AUTOCHANGER_SCRIPT} ${AUTOCHANGER} load $SLOT1 ${TAPE_DRIVE} $DRIVE1
+    slot=1
+    ;;
+ $SLOT1)
+    ${scripts}/${AUTOCHANGER_SCRIPT} ${AUTOCHANGER} unload $1 ${TAPE_DRIVE} $DRIVE1
+    ${scripts}/${AUTOCHANGER_SCRIPT} ${AUTOCHANGER} load $SLOT2 ${TAPE_DRIVE} $DRIVE1
+    slot=2
+    ;;
+ $SLOT2)
+    ${scripts}/${AUTOCHANGER_SCRIPT} ${AUTOCHANGER} unload $1 ${TAPE_DRIVE} $DRIVE1
+    ${scripts}/${AUTOCHANGER_SCRIPT} ${AUTOCHANGER} load $SLOT1 ${TAPE_DRIVE} $DRIVE1
+    slot=1
+    ;;
+ *)
+    echo "Something went wrong. Expected $SLOT1 or $SLOT2, got $1"
+    exit 1
+    ;;
+esac
+}
+
+
 # Save current directory
 cwd=`pwd`
 if test "x${REGRESS_DEBUG}" = "x1"; then
@@ -262,3 +590,70 @@ fi
 
 # Source the configuration variables
 . ${cwd}/config
+
+db_name=${db_name:-"regress"}
+db_user=${db_user:-"regress"}
+db_password=${db_password:-""}
+working=${working:-"$cwd/working"}
+dumps=${dumps:-"$cwd/dumps"}
+bin=${bin:-"$cwd/bin"}
+
+# Bacula scripts
+scripts=${scripts:-"$cwd/bin"}
+
+# Bacula conf files
+conf=${conf:-"$cwd/bin"}
+
+# Regress scripts
+rscripts=${rscripts:-"$cwd/scripts"}
+
+tmp=${tmp:-"$cwd/tmp"}
+
+# Bacula source directory when copied here
+#  also build directory
+src=${src:-"$cwd/build"}
+
+# Temp source directory so we don't mess up $src
+tmpsrc=${tmpsrc:-"$cwd/tmp/build"}
+
+export bin
+export conf
+export working
+export dumps
+export scripts
+export rscripts
+export tmp
+export src
+export tmpsrc
+
+bperl="perl -Mscripts::functions"
+export bperl
+
+mkdir -p ${tmp}
+touch ${tmp}/dir.out ${tmp}/fd.out ${tmp}/sd.out
+
+CLIENT=${HOST}-fd
+
+if [ x$USE_VTAPE = xyes ]; then
+   mkdir -p $working/ach
+   SLOT1=1
+   SLOT2=2
+   TAPE_DRIVE=$working/ach/drive0
+   TAPE_DRIVE1=$working/ach/drive1
+   AUTOCHANGER=$working/ach/config
+   AUTOCHANGER_SCRIPT=disk-changer
+   DRIVE1=0
+   DRIVE2=1
+   cp /dev/null $working/ach/slot$SLOT1
+   cp /dev/null $working/ach/slot$SLOT2
+   cat > $AUTOCHANGER <<EOF
+maxdrive=8
+maxslot=80
+EOF
+fi
+
+AUTOCHANGER_SCRIPT=${AUTOCHANGER_SCRIPT:-mtx-changer}
+LD_LIBRARY_PATH=$bin:$LD_LIBRARY_PATH
+export LD_LIBRARY_PATH
+
+trap "{ estat=999; end_test; }" TERM