mirror of
https://github.com/wanderleihuttel/vchanger.git
synced 2025-04-19 08:55:14 +00:00
994 lines
32 KiB
C++
994 lines
32 KiB
C++
|
/* diskchanger.cpp
|
||
|
*
|
||
|
* This file is part of vchanger by Josh Fisher.
|
||
|
*
|
||
|
* vchanger copyright (C) 2008-2015 Josh Fisher
|
||
|
*
|
||
|
* vchanger is free software.
|
||
|
* You may redistribute it and/or modify it under the terms of the
|
||
|
* GNU General Public License version 2, as published by the Free
|
||
|
* Software Foundation.
|
||
|
*
|
||
|
* vchanger 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 vchanger. See the file "COPYING". If not,
|
||
|
* write to: The Free Software Foundation, Inc.,
|
||
|
* 59 Temple Place - Suite 330,
|
||
|
* Boston, MA 02111-1307, USA.
|
||
|
*
|
||
|
* The bulk of the work gets done by the DiskChanger class, which does
|
||
|
* the actual "loading" and "unloading" of volumes.
|
||
|
*/
|
||
|
|
||
|
#include "config.h"
|
||
|
#include "compat_defs.h"
|
||
|
#ifdef HAVE_STDIO_H
|
||
|
#include <stdio.h>
|
||
|
#endif
|
||
|
#ifdef HAVE_STDLIB_H
|
||
|
#include <stdlib.h>
|
||
|
#endif
|
||
|
#ifdef HAVE_STRING_H
|
||
|
#include <string.h>
|
||
|
#endif
|
||
|
#ifdef HAVE_SYS_TYPES_H
|
||
|
#include <sys/types.h>
|
||
|
#endif
|
||
|
#ifdef HAVE_DIRENT_H
|
||
|
#include <dirent.h>
|
||
|
#endif
|
||
|
#ifdef HAVE_SYS_STAT_H
|
||
|
#include <sys/stat.h>
|
||
|
#endif
|
||
|
#ifdef HAVE_ERRNO_H
|
||
|
#include <errno.h>
|
||
|
#endif
|
||
|
#ifdef HAVE_TIME_H
|
||
|
#include <time.h>
|
||
|
#endif
|
||
|
#ifdef HAVE_SYS_TIME_H
|
||
|
#include <sys/time.h>
|
||
|
#endif
|
||
|
#ifdef HAVE_UNISTD_H
|
||
|
#include <unistd.h>
|
||
|
#endif
|
||
|
|
||
|
#include "compat/gettimeofday.h"
|
||
|
#include "compat/readlink.h"
|
||
|
#include "compat/sleep.h"
|
||
|
#include "compat/symlink.h"
|
||
|
#include "util.h"
|
||
|
#include "loghandler.h"
|
||
|
#include "bconsole.h"
|
||
|
#include "diskchanger.h"
|
||
|
|
||
|
|
||
|
/*=================================================
|
||
|
* Class DiskChanger
|
||
|
*=================================================*/
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* destructor
|
||
|
*-------------------------------------------------*/
|
||
|
DiskChanger::~DiskChanger()
|
||
|
{
|
||
|
Unlock();
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Protected method to read previous state of magazine bays.
|
||
|
* Returns zero on success, else negative and sets lasterr
|
||
|
*-------------------------------------------------*/
|
||
|
void DiskChanger::InitializeMagazines()
|
||
|
{
|
||
|
int n;
|
||
|
MagazineState m;
|
||
|
|
||
|
magazine.clear();
|
||
|
for (n = 0; (size_t)n < conf.magazine.size(); n++) {
|
||
|
m.SetBay(n, conf.magazine[n].c_str());
|
||
|
m.prev_num_slots = 0;
|
||
|
m.prev_start_slot = 0;
|
||
|
magazine.push_back(m);
|
||
|
/* Restore previous slot count and starting virtual slot */
|
||
|
magazine[n].restore();
|
||
|
/* Get mountpoint and build magazine slot array */
|
||
|
magazine[n].Mount();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Protected method to find the start of an empty range of
|
||
|
* 'count' virtual slots, adding slots if needed.
|
||
|
* Returns the starting slot number of the range found.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::FindEmptySlotRange(int count)
|
||
|
{
|
||
|
VirtualSlot vs;
|
||
|
int start = 0, n = 1, found = 0;
|
||
|
/* Find next empty slot */
|
||
|
while (n < (int)vslot.size() && vslot[n].mag_bay >= 0) n++;
|
||
|
start = n;
|
||
|
while (n < (int)vslot.size() && found < count) {
|
||
|
if (vslot[n].mag_bay < 0) {
|
||
|
++found;
|
||
|
++n;
|
||
|
} else {
|
||
|
found = 0;
|
||
|
while (n < (int)vslot.size() && vslot[n].mag_bay >= 0) n++;
|
||
|
start = n;
|
||
|
}
|
||
|
}
|
||
|
if (found >= count) return start;
|
||
|
while (found < count) {
|
||
|
vs.vs = n++;
|
||
|
vslot.push_back(vs);
|
||
|
++found;
|
||
|
}
|
||
|
return start;
|
||
|
}
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Protected method to initialize array of virtual slot and
|
||
|
* assign magazine volumes to virtual slots. When possible,
|
||
|
* volumes are assigned to the same slot they were in
|
||
|
* previously.
|
||
|
*------------------------------------------------*/
|
||
|
void DiskChanger::InitializeVirtSlots()
|
||
|
{
|
||
|
int s, m, v, last;
|
||
|
VirtualSlot vs;
|
||
|
bool found;
|
||
|
|
||
|
/* Create all known slots as initially empty */
|
||
|
vslot.clear();
|
||
|
for (s = 0; s <= dconf.max_slot; s++) {
|
||
|
vs.vs = s;
|
||
|
vslot.push_back(vs);
|
||
|
}
|
||
|
/* Re-create virtual slots that existed previously if possible */
|
||
|
for (m = 0; m < (int)magazine.size(); m++) {
|
||
|
/* Create slots if needed to match max slot used by previous magazines */
|
||
|
last = magazine[m].prev_start_slot + magazine[m].prev_num_slots;
|
||
|
if (last >= (int)vslot.size()) {
|
||
|
vs.clear();
|
||
|
while ((int)vslot.size() <= last) {
|
||
|
vs.vs = (int)vslot.size();
|
||
|
vslot.push_back(vs);
|
||
|
}
|
||
|
}
|
||
|
/* Check this magazine's slots */
|
||
|
if (magazine[m].empty()) {
|
||
|
log.Info("magazine %d is not mounted", m);
|
||
|
/* magazine is not currently mounted, so will have no slots assigned */
|
||
|
if (magazine[m].prev_start_slot) {
|
||
|
/* Since it was previously mounted, an 'update slots' is needed */
|
||
|
log.Warning("update slots needed. magazine %d no longer mounted; previous: %d volumes in slots %d-%d", m,
|
||
|
magazine[m].prev_num_slots, magazine[m].prev_start_slot,
|
||
|
magazine[m].prev_start_slot + magazine[m].prev_num_slots - 1);
|
||
|
needs_update = true;
|
||
|
}
|
||
|
continue;
|
||
|
}
|
||
|
/* Magazine is currently mounted, so check for change in slot assignment */
|
||
|
log.Info("magazine %d has %d volumes on %s", m, magazine[m].num_slots,
|
||
|
magazine[m].mountpoint.c_str());
|
||
|
if (magazine[m].num_slots != magazine[m].prev_num_slots) {
|
||
|
/* Number of volumes has changed or magazine was not previously mounted, so
|
||
|
* needs new slot assignment and also 'update slots' will be needed */
|
||
|
log.Warning("update slots needed. magazine %d has %d volumes, previously had %d", m,
|
||
|
magazine[m].num_slots, magazine[m].prev_num_slots);
|
||
|
needs_update = true;
|
||
|
continue;
|
||
|
}
|
||
|
if (magazine[m].num_slots == 0) {
|
||
|
/* Magazine has no volumes so needs no slot assignment */
|
||
|
continue;
|
||
|
}
|
||
|
/* Magazine is mounted, was previously mounted, and has the same volume count,
|
||
|
* so attempt to assign to the same slots previously assigned */
|
||
|
found = false;
|
||
|
for (v = magazine[m].prev_start_slot; v < (int)vslot.size(); v++) {
|
||
|
if (!vslot[v].empty()) {
|
||
|
found = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (found) {
|
||
|
/* Slot used previously has already been assigned to another magazine.
|
||
|
* Magazine will need to be assigned a new slot range, so an
|
||
|
* 'update slots' will also be needed. */
|
||
|
log.Warning("update slots needed. magazine %d previous slots %d-%d are not available", m,
|
||
|
magazine[m].prev_start_slot, magazine[m].prev_start_slot + magazine[m].prev_num_slots - 1);
|
||
|
needs_update = true;
|
||
|
continue;
|
||
|
}
|
||
|
/* Assign this magazine's volumes to the same slots as previously assigned */
|
||
|
magazine[m].start_slot = magazine[m].prev_start_slot;
|
||
|
for (s = 0; s < magazine[m].num_slots; s++) {
|
||
|
v = magazine[m].start_slot + s;
|
||
|
vslot[v].mag_bay = m;
|
||
|
vslot[v].mag_slot = s;
|
||
|
}
|
||
|
log.Notice("%d volumes on magazine %d assigned slots %d-%d", magazine[m].num_slots, m,
|
||
|
magazine[m].start_slot, magazine[m].start_slot + magazine[m].num_slots - 1);
|
||
|
}
|
||
|
|
||
|
/* Assign slots to mounted magazines that have not already been assigned. */
|
||
|
for (m = 0; m < (int)magazine.size(); m++) {
|
||
|
if (magazine[m].empty() || magazine[m].start_slot > 0) continue;
|
||
|
if (magazine[m].num_slots == 0) continue;
|
||
|
magazine[m].start_slot = FindEmptySlotRange(magazine[m].num_slots);
|
||
|
for (s = 0; s < magazine[m].num_slots; s++) {
|
||
|
v = magazine[m].start_slot + s;
|
||
|
vslot[v].mag_bay = m;
|
||
|
vslot[v].mag_slot = s;
|
||
|
}
|
||
|
log.Notice("%d volumes on magazine %d assigned slots %d-%d", magazine[m].num_slots, m,
|
||
|
magazine[m].start_slot, magazine[m].start_slot + magazine[m].num_slots - 1);
|
||
|
}
|
||
|
|
||
|
/* Save updated state of magazines */
|
||
|
for (m = 0; m < (int)magazine.size(); m++) {
|
||
|
magazine[m].save();
|
||
|
}
|
||
|
/* Update dynamic configuration info */
|
||
|
if ((int)vslot.size() >= dconf.max_slot) {
|
||
|
dconf.max_slot = (int)vslot.size() - 1;
|
||
|
dconf.save();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Protected method to initialize state of virtual drives.
|
||
|
* On success, returns zero. On error, returns non-zero.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::InitializeDrives()
|
||
|
{
|
||
|
int n, rc, max_drive = -1;
|
||
|
DIR *d;
|
||
|
struct dirent *de;
|
||
|
DriveState ds;
|
||
|
tString tmp;
|
||
|
|
||
|
/* For each drive for which a state file exists. try to restore its state */
|
||
|
d = opendir(conf.work_dir.c_str());
|
||
|
if (!d) {
|
||
|
rc = errno;
|
||
|
verr.SetErrorWithErrno(rc, "error %d accessing work directory", rc);
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return rc;
|
||
|
}
|
||
|
de = readdir(d);
|
||
|
while (de) {
|
||
|
/* Find next drive state file */
|
||
|
tmp = de->d_name;
|
||
|
if (tmp.find("drive_state-") == 0) {
|
||
|
tmp.erase(0, 12);
|
||
|
if (tmp.find_first_of("0123456789") == tString::npos) {
|
||
|
de = readdir(d);
|
||
|
continue;
|
||
|
}
|
||
|
if (tmp.find_first_not_of("0123456789") != tString::npos) {
|
||
|
de = readdir(d);
|
||
|
continue;
|
||
|
}
|
||
|
n = (int)strtol(tmp.c_str(), NULL, 10);
|
||
|
if (n > max_drive) max_drive = n;
|
||
|
}
|
||
|
de = readdir(d);
|
||
|
}
|
||
|
closedir(d);
|
||
|
if (max_drive < 0) {
|
||
|
/* No drive state files exist, so create at least one drive */
|
||
|
max_drive = 0;
|
||
|
}
|
||
|
|
||
|
/* Restore last known state of virtual drives where possible. */
|
||
|
for (n = 0; n <= max_drive; n++) {
|
||
|
ds.drv = n;
|
||
|
drive.push_back(ds);
|
||
|
/* Attempt to restore drive's last state */
|
||
|
if (RestoreDriveState(n)) {
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Protected method to ensure that there are at least n drives
|
||
|
*------------------------------------------------*/
|
||
|
void DiskChanger::SetMaxDrive(int need)
|
||
|
{
|
||
|
int n = (int)drive.size();
|
||
|
DriveState ds;
|
||
|
while (n <= need) {
|
||
|
ds.drv = n++;
|
||
|
drive.push_back(ds);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Method to create symlink for drive pointing to currently loaded volume file
|
||
|
*/
|
||
|
int DiskChanger::CreateDriveSymlink(int drv)
|
||
|
{
|
||
|
int mag, mslot, rc;
|
||
|
tString sname, fname;
|
||
|
char lname[4096];
|
||
|
|
||
|
if (drv < 0 || drv >= (int)drive.size()) {
|
||
|
verr.SetError(EINVAL, "cannot create symlink for invalid drive %d", drv);
|
||
|
return EINVAL;
|
||
|
}
|
||
|
if (drive[drv].vs <= 0) {
|
||
|
verr.SetError(ENOENT, "cannot create symlink for unloaded drive %d", drv);
|
||
|
return ENOENT;
|
||
|
}
|
||
|
mag = vslot[drive[drv].vs].mag_bay;
|
||
|
mslot = vslot[drive[drv].vs].mag_slot;
|
||
|
fname = magazine[mag].GetVolumePath(mslot);
|
||
|
if (fname.empty()) {
|
||
|
verr.SetError(ENOENT, "cannot create symlink for unloaded drive %d", drv);
|
||
|
return ENOENT;
|
||
|
}
|
||
|
tFormat(sname, "%s%s%d", conf.work_dir.c_str(), DIR_DELIM, drv);
|
||
|
rc = readlink(sname.c_str(), lname, sizeof(lname));
|
||
|
if (rc > 0) {
|
||
|
if (rc >= (int)sizeof(lname)) {
|
||
|
verr.SetError(ENAMETOOLONG, "symlink target too long on readlink for drive %d", drv);
|
||
|
return ENAMETOOLONG;
|
||
|
}
|
||
|
lname[rc] = 0;
|
||
|
if (fname == lname) {
|
||
|
/* symlink already exists */
|
||
|
log.Info("found symlink for drive %d -> %s", drv, fname.c_str());
|
||
|
return 0;
|
||
|
}
|
||
|
/* Symlink points to wrong mountpoint, so delete and re-create */
|
||
|
if (RemoveDriveSymlink(drv)) return EEXIST;
|
||
|
}
|
||
|
if (symlink(fname.c_str(), sname.c_str())) {
|
||
|
rc = errno;
|
||
|
verr.SetErrorWithErrno(rc, "error %d creating symlink for drive %d", rc, drv);
|
||
|
return rc;
|
||
|
}
|
||
|
log.Notice("created symlink for drive %d -> %s", drv, fname.c_str());
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to delete this drive's symlink.
|
||
|
* On success returns zero, else on error sets lasterr and
|
||
|
* returns errno.
|
||
|
*-------------------------------------------------*/
|
||
|
int DiskChanger::RemoveDriveSymlink(int drv)
|
||
|
{
|
||
|
int rc;
|
||
|
tString sname;
|
||
|
|
||
|
if (drv < 0 || drv >= (int)drive.size()) {
|
||
|
verr.SetError(EINVAL, "cannot delete symlink for invalid drive %d", drv);
|
||
|
return EINVAL;
|
||
|
}
|
||
|
/* Remove symlink pointing to loaded volume file */
|
||
|
tFormat(sname, "%s%s%d", conf.work_dir.c_str(), DIR_DELIM, drv);
|
||
|
if (unlink(sname.c_str())) {
|
||
|
if (errno == ENOENT) return 0; /* Ignore if not found */
|
||
|
/* System error preventing deletion of symlink */
|
||
|
rc = errno;
|
||
|
verr.SetErrorWithErrno(errno, "error %d deleting symlink for drive %d: ", rc, drv);
|
||
|
return rc;
|
||
|
}
|
||
|
log.Notice("deleted symlink for drive %d", drv);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to save current drive state, device string and
|
||
|
* volume label (filename), to a file in the work
|
||
|
* directory named "drive_state-N", where N is the drive number.
|
||
|
* On success returns zero, else on error sets lasterr and
|
||
|
* returns errno.
|
||
|
*-------------------------------------------------*/
|
||
|
int DiskChanger::SaveDriveState(int drv)
|
||
|
{
|
||
|
mode_t old_mask;
|
||
|
FILE *FS;
|
||
|
int rc, mag, mslot;
|
||
|
tString sname;
|
||
|
|
||
|
if (drv < 0 || drv >= (int)drive.size()) {
|
||
|
verr.SetError(EINVAL, "cannot save state of invalid drive %d", drv);
|
||
|
return EINVAL;
|
||
|
}
|
||
|
/* Delete old state file */
|
||
|
tFormat(sname, "%s%sdrive_state-%d", conf.work_dir.c_str(), DIR_DELIM, drv);
|
||
|
if (drive[drv].empty()) {
|
||
|
if (access(sname.c_str(), F_OK) == 0) {
|
||
|
log.Notice("deleted state file for drive %d", drv);
|
||
|
}
|
||
|
unlink(sname.c_str());
|
||
|
return 0;
|
||
|
}
|
||
|
old_mask = umask(027);
|
||
|
FS = fopen(sname.c_str(), "w");
|
||
|
if (!FS) {
|
||
|
/* Unable to open state file */
|
||
|
rc = errno;
|
||
|
umask(old_mask);
|
||
|
verr.SetErrorWithErrno(rc, "failed opening state file for drive %d", drv);
|
||
|
return rc;
|
||
|
}
|
||
|
mag = vslot[drive[drv].vs].mag_bay;
|
||
|
mslot = vslot[drive[drv].vs].mag_slot;
|
||
|
if (fprintf(FS, "%s,%s\n", magazine[mag].mag_dev.c_str(),
|
||
|
magazine[mag].GetVolumeLabel(mslot)) < 0) {
|
||
|
/* I/O error writing state file */
|
||
|
rc = errno;
|
||
|
fclose(FS);
|
||
|
umask(old_mask);
|
||
|
verr.SetErrorWithErrno(rc, "error %d writing state file for drive %d", rc, drv);
|
||
|
return rc;
|
||
|
}
|
||
|
fclose(FS);
|
||
|
umask(old_mask);
|
||
|
log.Notice("wrote state file for drive %d", drv);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to restore drive state from a file in the work directory
|
||
|
* named "drive_state-N", where N is drive_number. If the volume
|
||
|
* previously loaded is available, then restore drive to the loaded
|
||
|
* state, otherwise set drive unloaded and remove the symlink and
|
||
|
* state file for this drive.
|
||
|
* On success returns zero, else on error sets lasterr and
|
||
|
* returns errno.
|
||
|
*-------------------------------------------------*/
|
||
|
int DiskChanger::RestoreDriveState(int drv)
|
||
|
{
|
||
|
int rc, v, m, ms;
|
||
|
tString line, dev, labl;
|
||
|
size_t p;
|
||
|
struct stat st;
|
||
|
FILE *FS;
|
||
|
tString sname;
|
||
|
|
||
|
if (drv < 0 || drv >= (int)drive.size()) {
|
||
|
verr.SetError(EINVAL, "cannot restore state of invalid drive %d", drv);
|
||
|
return EINVAL;
|
||
|
}
|
||
|
drive[drv].clear();
|
||
|
|
||
|
/* Check for existing state file */
|
||
|
tFormat(sname, "%s%sdrive_state-%d", conf.work_dir.c_str(), DIR_DELIM, drv);
|
||
|
if (stat(sname.c_str(), &st)) {
|
||
|
/* drive state file not found, so drive is not loaded */
|
||
|
RemoveDriveSymlink(drv);
|
||
|
log.Info("drive %d previously unloaded", drv);
|
||
|
return 0;
|
||
|
}
|
||
|
/* Read loaded volume info from state file */
|
||
|
FS = fopen(sname.c_str(), "r");
|
||
|
if (!FS) {
|
||
|
/* Error opening state file, so leave drive unloaded */
|
||
|
verr.SetError(EACCES, "drive %d state file is not readable", drv);
|
||
|
return EACCES;
|
||
|
}
|
||
|
if (tGetLine(line, FS) == NULL) {
|
||
|
if (!feof(FS)) {
|
||
|
/* i/o error reading line from state file. Change state to unloaded */
|
||
|
rc = ferror(FS);
|
||
|
fclose(FS);
|
||
|
unlink(sname.c_str());
|
||
|
RemoveDriveSymlink(drv);
|
||
|
verr.SetErrorWithErrno(rc, "error %d reading state file for drive %d", rc, drv);
|
||
|
return rc;
|
||
|
}
|
||
|
}
|
||
|
fclose(FS);
|
||
|
tStrip(tRemoveEOL(line));
|
||
|
/* Extract the device drive was last loaded from */
|
||
|
p = 0;
|
||
|
rc = tParseCSV(dev, line, p);
|
||
|
if (rc != 1 || dev.empty()) {
|
||
|
/* Device string not found. Change state to unloaded. */
|
||
|
verr.SetError(EINVAL, "deleting corrupt state file for drive %d", drv);
|
||
|
unlink(sname.c_str());
|
||
|
RemoveDriveSymlink(drv);
|
||
|
return EINVAL;
|
||
|
}
|
||
|
/* Extract label of volume drive was last loaded from */
|
||
|
rc = tParseCSV(labl, line, p);
|
||
|
if (rc != 1 || labl.empty()) {
|
||
|
/* Label string not found. Change state to unloaded. */
|
||
|
verr.SetError(EINVAL, "deleting corrupt state file for drive %d", drv);
|
||
|
unlink(sname.c_str());
|
||
|
RemoveDriveSymlink(drv);
|
||
|
return EINVAL;
|
||
|
}
|
||
|
|
||
|
/* Find virtual slot assigned the volume file last loaded in drive */
|
||
|
for (v = 1; v < (int)vslot.size(); v++) {
|
||
|
if (labl == GetVolumeLabel(v)) break;
|
||
|
}
|
||
|
if (v >= (int)vslot.size()) {
|
||
|
/* Volume last loaded is no longer available. Change state to unloaded. */
|
||
|
log.Notice("volume %s no longer available, unloading drive %d",
|
||
|
labl.c_str(), drv);
|
||
|
unlink(sname.c_str());
|
||
|
RemoveDriveSymlink(drv);
|
||
|
return 0;
|
||
|
}
|
||
|
drive[drv].vs = v;
|
||
|
|
||
|
/* Ensure symlink exists or create it */
|
||
|
if ((rc = CreateDriveSymlink(drv)) != 0) {
|
||
|
/* Unable to create symlink */
|
||
|
drive[drv].vs = -1;
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return rc;
|
||
|
}
|
||
|
|
||
|
/* Assign drive to virtual slot */
|
||
|
vslot[v].drv = drv;
|
||
|
m = vslot[v].mag_bay;
|
||
|
ms = vslot[v].mag_slot;
|
||
|
log.Notice("drive %d previously loaded from slot %d (%s)", drv, v, magazine[m].GetVolumeLabel(ms));
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to initialize changer parameters and state of magazines,
|
||
|
* virtual slots, and virtual drives.
|
||
|
* On success, returns zero. On error, returns negative.
|
||
|
* In either case, obtains a lock on the changer unless the lock operation
|
||
|
* itself fails. The lock will be released when the DiskChanger object
|
||
|
* is destroyed.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::Initialize()
|
||
|
{
|
||
|
/* Make sure we have a lock on this changer */
|
||
|
if (Lock()) return verr.GetError();
|
||
|
magazine.clear();
|
||
|
vslot.clear();
|
||
|
drive.clear();
|
||
|
dconf.restore();
|
||
|
needs_update = false;
|
||
|
|
||
|
/* Initialize array of mounted magazines */
|
||
|
InitializeMagazines();
|
||
|
|
||
|
/* Initialize array of virtual slots */
|
||
|
InitializeVirtSlots();
|
||
|
|
||
|
/* Initialize array of virtual drives */
|
||
|
if (InitializeDrives()) return verr.GetError();
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to load virtual drive 'drv' from virtual slot 'slot'.
|
||
|
* Returns zero on success, else sets lasterr and
|
||
|
* returns negative.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::LoadDrive(int drv, int slot)
|
||
|
{
|
||
|
int rc, m, ms;
|
||
|
|
||
|
if (!changer_lock) {
|
||
|
verr.SetError(EINVAL, "changer not initialized");
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return EINVAL;
|
||
|
}
|
||
|
if (drv < 0) {
|
||
|
verr.SetError(EINVAL, "invalid drive number %d", drv);
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return EINVAL;
|
||
|
}
|
||
|
SetMaxDrive(drv);
|
||
|
if (slot < 1 || slot >= (int)vslot.size()) {
|
||
|
verr.SetError(EINVAL, "cannot load drive %d from invalid slot %d", drv, slot);
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return EINVAL;
|
||
|
}
|
||
|
if (!drive[drv].empty()) {
|
||
|
if (drive[drv].vs == slot) return 0; /* already loaded from this slot */
|
||
|
verr.SetError(EBUSY, "drive %d already loaded from slot %d", drv, slot);
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return EBUSY;
|
||
|
}
|
||
|
if (vslot[slot].empty()) {
|
||
|
verr.SetError(EINVAL, "cannot load drive %d from empty slot %d", drv, slot);
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return ENOENT;
|
||
|
}
|
||
|
/* Create symlink for drive pointing to volume file */
|
||
|
drive[drv].vs = slot;
|
||
|
if ((rc = CreateDriveSymlink(drv))) {
|
||
|
drive[drv].vs = -1;
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return rc;
|
||
|
}
|
||
|
/* Save state of newly loaded drive */
|
||
|
if ((rc = SaveDriveState(drv)) != 0) {
|
||
|
/* Error writing drive state file */
|
||
|
RemoveDriveSymlink(drv);
|
||
|
drive[drv].vs = -1;
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return rc;
|
||
|
}
|
||
|
/* Assign virtual slot to drive */
|
||
|
vslot[slot].drv = drv;
|
||
|
m = vslot[slot].mag_bay;
|
||
|
ms = vslot[slot].mag_slot;
|
||
|
log.Notice("loaded drive %d from slot %d (%s)", drv, slot, magazine[m].GetVolumeLabel(ms));
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to unload volume in virtual drive 'drv'. Deletes symlink
|
||
|
* and state file for the drive.
|
||
|
* On success, returns zero. Otherwise sets lasterr and returns
|
||
|
* errno.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::UnloadDrive(int drv)
|
||
|
{
|
||
|
int rc;
|
||
|
|
||
|
if (!changer_lock) {
|
||
|
verr.SetError(EINVAL, "changer not initialized");
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return EINVAL;
|
||
|
}
|
||
|
if (drv < 0) {
|
||
|
verr.SetError(EINVAL, "invalid drive number %d", drv);
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return EINVAL;
|
||
|
}
|
||
|
SetMaxDrive(drv);
|
||
|
if (drive[drv].empty()) {
|
||
|
/* Drive is already empty so assume successful */
|
||
|
return 0;
|
||
|
}
|
||
|
/* Remove drive's symlink */
|
||
|
if ((rc = RemoveDriveSymlink(drv)) != 0) {
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return rc;
|
||
|
}
|
||
|
/* Remove virtual slot assignment */
|
||
|
vslot[drive[drv].vs].drv = -1;
|
||
|
drive[drv].vs = -1;
|
||
|
/* Update drive state file (will delete state file due to negative slot number) */
|
||
|
if ((rc = SaveDriveState(drv)) != 0) {
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return rc;
|
||
|
}
|
||
|
log.Notice("unloaded drive %d", drv);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to create new volume files in virtual slots 'slot1' through 'slot2'.
|
||
|
* Use volume labels (barcodes) of the form prefix + '_' + mag_slot_number, where
|
||
|
* mag_slot_number is the magazine relative slot number of the magazine slot that
|
||
|
* the virtual slot maps to. If 'label_prefix' is blank, then use the magazine name
|
||
|
* of the magazine the virtual slot is mapped onto as the prefix.
|
||
|
* Returns zero on success, else returns negative and sets lasterr.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::CreateVolumes(int bay, int count, int start, const char *label_prefix_in)
|
||
|
{
|
||
|
MagazineSlot vol;
|
||
|
tString label, label_prefix(label_prefix_in);
|
||
|
int i;
|
||
|
|
||
|
if (!changer_lock) {
|
||
|
verr.SetError(EINVAL, "changer not initialized");
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return -1;
|
||
|
}
|
||
|
if (bay < 0 || bay >= (int)magazine.size()) {
|
||
|
verr.SetError(EINVAL, "invalid magazine");
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return -1;
|
||
|
}
|
||
|
if (count < 1) count = 1;
|
||
|
tStrip(tRemoveEOL(label_prefix));
|
||
|
if (label_prefix.empty()) {
|
||
|
/* Default prefix is storage-name_magazine-number */
|
||
|
tFormat(label_prefix, "%s_%d", conf.storage_name.c_str(), bay);
|
||
|
}
|
||
|
if (start < 0) {
|
||
|
/* Find highest uniqueness number for this filename prefix */
|
||
|
for (i = magazine[bay].num_slots * 5; i > 0; i--) {
|
||
|
tFormat(label, "%s_%d", label_prefix.c_str(), i);
|
||
|
if (magazine[bay].GetVolumeSlot(label) >= 0) break;
|
||
|
}
|
||
|
start = i;
|
||
|
}
|
||
|
for (i = 0; i < count; i++) {
|
||
|
tFormat(label, "%s_%d", label_prefix.c_str(), start);
|
||
|
if (!magazine[bay].empty()) {
|
||
|
while (magazine[bay].GetVolumeSlot(label) >= 0) {
|
||
|
++start;
|
||
|
tFormat(label, "%s_%d", label_prefix.c_str(), start);
|
||
|
}
|
||
|
}
|
||
|
fprintf(stdout, "creating label '%s'\n", label.c_str());
|
||
|
if (magazine[bay].CreateVolume(label)) {
|
||
|
if (i) magazine[bay].save();
|
||
|
return -1;
|
||
|
}
|
||
|
++start;
|
||
|
}
|
||
|
/* Update magazine state */
|
||
|
magazine[bay].save();
|
||
|
/* New mag state will require 'update slots' and 'label barcodes' in Bacula */
|
||
|
needs_update = true;
|
||
|
needs_label = true;
|
||
|
log.Notice("update slots needed. %d volumes added to magazine %d",count , bay);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to cause Bacula to update its catalog to reflect
|
||
|
* changes in the available volumes
|
||
|
*-------------------------------------------------*/
|
||
|
int DiskChanger::UpdateBacula()
|
||
|
{
|
||
|
int rc;
|
||
|
FILE *update_lock;
|
||
|
tString cmd;
|
||
|
char lockfile[4096];
|
||
|
|
||
|
/* Check if update needed */
|
||
|
if (!needs_update && !needs_label) return 0; /* Nothing to do */
|
||
|
/* Create update lock lockfile */
|
||
|
snprintf(lockfile, sizeof(lockfile), "%s%s%s.updatelock", conf.work_dir.c_str(), DIR_DELIM,
|
||
|
conf.storage_name.c_str());
|
||
|
rc = exclusive_fopen(lockfile, &update_lock);
|
||
|
if (rc == EEXIST) {
|
||
|
/* Update already in progress in another process, so skip */
|
||
|
return 0;
|
||
|
}
|
||
|
if (rc) {
|
||
|
/* error creating lockfile, so skip */
|
||
|
log.Error("bconsole: errno=%d creating update lockfile", rc);
|
||
|
if (needs_update)
|
||
|
log.Error("WARNING! 'update slots' needed in bconsole");
|
||
|
if (needs_label)
|
||
|
log.Error("WARNING! 'label barcodes' needed in bconsole");
|
||
|
return 0;
|
||
|
}
|
||
|
log.Debug("created update lockfile for pid %d", getpid());
|
||
|
/* Perform update slots command in bconsole */
|
||
|
if (needs_update) {
|
||
|
/* Issue update slots command in bconsole */
|
||
|
tFormat(cmd, "update slots storage=\"%s\"", conf.storage_name.c_str());
|
||
|
if(issue_bconsole_command(cmd.c_str())) {
|
||
|
log.Error("WARNING! 'update slots' needed in bconsole");
|
||
|
}
|
||
|
}
|
||
|
/* Perform label barcodes command in bconsole */
|
||
|
if (needs_label) {
|
||
|
tFormat(cmd, "label storage=\"%s\" pool=\"%s\" barcodes\nyes\nyes\n", conf.storage_name.c_str(),
|
||
|
conf.def_pool.c_str());
|
||
|
if (issue_bconsole_command(cmd.c_str())) {
|
||
|
log.Error("WARNING! 'label barcodes' needed in bconsole");
|
||
|
}
|
||
|
}
|
||
|
/* Obtain changer lock before removing update lock */
|
||
|
fclose(update_lock);
|
||
|
unlink(lockfile);
|
||
|
log.Debug("removed update lockfile for pid %d", getpid());
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to get label of volume in this slot
|
||
|
*-------------------------------------------------*/
|
||
|
const char* DiskChanger::GetVolumeLabel(int slot)
|
||
|
{
|
||
|
if (slot <= 0 || slot >= (int)vslot.size()) {
|
||
|
verr.SetError(-1, "volume label request from invalid slot %d", slot);
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return NULL;
|
||
|
}
|
||
|
if (vslot[slot].empty()) return "";
|
||
|
return magazine[vslot[slot].mag_bay].GetVolumeLabel(vslot[slot].mag_slot);
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to get filename path of volume in this slot
|
||
|
*-------------------------------------------------*/
|
||
|
const char* DiskChanger::GetVolumePath(tString &path, int slot)
|
||
|
{
|
||
|
path.clear();
|
||
|
if (slot <= 0 || slot >= (int)vslot.size()) {
|
||
|
verr.SetError(-1, "volume path request from invalid slot %d", slot);
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return NULL;
|
||
|
}
|
||
|
if (vslot[slot].empty()) return path.c_str();
|
||
|
return magazine[vslot[slot].mag_bay].GetVolumePath(path, vslot[slot].mag_slot);
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Protected method to lock changer device using a lock file such that
|
||
|
* only one process at a time may execute changer commands on the
|
||
|
* same autochanger. If another process has the lock, then this process
|
||
|
* will sleep 1 second before trying again. This try/wait loop will continue
|
||
|
* until the lock is obtained or 'timeout' seconds have expired. If
|
||
|
* timeout = 0 then only tries to obtain lock once. If timeout < 0
|
||
|
* then doesn't return until the lock is obtained.
|
||
|
* On success, returns true. Otherwise on error or timeout, sets
|
||
|
* lasterr negative and returns false.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::Lock(long timeout_seconds)
|
||
|
{
|
||
|
int rc;
|
||
|
time_t timeout = 0;
|
||
|
char lockfile[4096];
|
||
|
|
||
|
if (changer_lock) return 0;
|
||
|
|
||
|
if (timeout_seconds < 0) {
|
||
|
timeout_seconds = 3600 * 24 * 365;
|
||
|
}
|
||
|
if (timeout_seconds > 0) {
|
||
|
timeout = time(NULL) + timeout_seconds;
|
||
|
}
|
||
|
snprintf(lockfile, sizeof(lockfile), "%s%s%s.lock", conf.work_dir.c_str(), DIR_DELIM,
|
||
|
conf.storage_name.c_str());
|
||
|
rc = exclusive_fopen(lockfile, &changer_lock);
|
||
|
if (rc == EEXIST && timeout == 0) {
|
||
|
/* timeout=0 means do not wait */
|
||
|
verr.SetErrorWithErrno(rc, "cannot open lockfile");
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return -1;
|
||
|
}
|
||
|
while (rc == EEXIST) {
|
||
|
/* sleep before trying again */
|
||
|
sleep(1);
|
||
|
if (time(NULL) > timeout) {
|
||
|
verr.SetError(EBUSY, "timeout waiting for lockfile");
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return EACCES;
|
||
|
}
|
||
|
rc = exclusive_fopen(lockfile, &changer_lock);
|
||
|
}
|
||
|
if (rc) {
|
||
|
verr.SetErrorWithErrno(rc, "cannot open lockfile");
|
||
|
log.Error("ERROR! %s", verr.GetErrorMsg());
|
||
|
return -1;
|
||
|
}
|
||
|
/* Write PID to lockfile and leave open for exclusive R/W */
|
||
|
fprintf(changer_lock, "%d", getpid());
|
||
|
fflush(changer_lock);
|
||
|
log.Debug("created lockfile for pid %d", getpid());
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Protected method to unlock changer device
|
||
|
*------------------------------------------------*/
|
||
|
void DiskChanger::Unlock()
|
||
|
{
|
||
|
char lockfile[4096];
|
||
|
if (!changer_lock) return;
|
||
|
fclose(changer_lock);
|
||
|
changer_lock = NULL;
|
||
|
snprintf(lockfile, sizeof(lockfile), "%s%s%s.lock", conf.work_dir.c_str(), DIR_DELIM,
|
||
|
conf.storage_name.c_str());
|
||
|
log.Debug("removing lockfile for pid %d", getpid());
|
||
|
unlink(lockfile);
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method returns true if magazine is not mounted,
|
||
|
* else returns false.
|
||
|
*------------------------------------------------*/
|
||
|
bool DiskChanger::MagazineEmpty(int mag) const
|
||
|
{
|
||
|
if (mag < 0 || mag >= (int)magazine.size()) return true;
|
||
|
return magazine[mag].empty();
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method returns true if no magazine volume is assigned
|
||
|
* to virtual slot, else returns false.
|
||
|
*------------------------------------------------*/
|
||
|
bool DiskChanger::SlotEmpty(int slot) const
|
||
|
{
|
||
|
if (slot <= 0 || slot >= (int)vslot.size()) return true;
|
||
|
return vslot[slot].empty();
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method returns true if no magazine volume is loaded
|
||
|
* into drive drv, else returns false.
|
||
|
*------------------------------------------------*/
|
||
|
bool DiskChanger::DriveEmpty(int drv) const
|
||
|
{
|
||
|
if (drv < 0 || drv >= (int)drive.size()) return true;
|
||
|
return drive[drv].empty();
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to get virtual slot currently loaded in virtual drive 'drv'.
|
||
|
* If drive is loaded then returns the virtual slot number of the
|
||
|
* loaded volume. If unloaded, returns zero.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::GetDriveSlot(int drv) const
|
||
|
{
|
||
|
if (drv < 0 || drv >= (int)drive.size()) return 0;
|
||
|
return drive[drv].vs;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to get drive a virtual slot is currently loaded in.
|
||
|
* Returns the drive number a slot's volume is loaded in.
|
||
|
* If unloaded, returns negative.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::GetSlotDrive(int slot) const
|
||
|
{
|
||
|
if (slot <= 0 || slot >= (int)vslot.size()) return -1;
|
||
|
return vslot[slot].drv;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to return number of volumes on magazine 'mag'.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::GetMagazineSlots(int mag) const
|
||
|
{
|
||
|
if (mag < 0 || mag >= (int)magazine.size()) return 0;
|
||
|
return magazine[mag].num_slots;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to return the start of the virtual slot range
|
||
|
* that is assigned to magazine 'mag' volumes.
|
||
|
*------------------------------------------------*/
|
||
|
int DiskChanger::GetMagazineStartSlot(int mag) const
|
||
|
{
|
||
|
if (mag < 0 || mag >= (int)magazine.size()) return 0;
|
||
|
return magazine[mag].start_slot;
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------
|
||
|
* Method to return the mountpoint of magazine 'mag'.
|
||
|
*------------------------------------------------*/
|
||
|
const char* DiskChanger::GetMagazineMountpoint(int mag) const
|
||
|
{
|
||
|
if (mag < 0 || mag >= (int)magazine.size()) return "";
|
||
|
return magazine[mag].mountpoint.c_str();
|
||
|
}
|