Commit 603b0c8b authored by David COME's avatar David COME
Browse files

Merge remote branch 'origin/master'

parents e34ae786 258c329a
......@@ -302,7 +302,7 @@
# you can use a list like /your/mount/point/01/ /your/mount/point/02/ ...
#DiskManager MountPoints
# List of dataPools the DiskManager daemon should monitor
# List of dataPools the DiskManager daemon should monitor, garbage collect and synchronize
# you can use a list like pool1 pool2 ...
#DiskManager DataPools
......@@ -335,6 +335,13 @@
# stager catalog. The synchronization with the nameserver is not affected.
#GC DisableStagerSync no
# The period during which new files won't be considered for synchronization
# This protects in particular files being created (eg ongoing recalls) by giving
# time to the stager DB to create the associated DiskCopy. Otherwise, we would
# have a time window where the file exist on disk and can be considered by
# synchronization, while it does not exist on the stager. Thus it may be dropped
# Default is one day
#GC SyncGracePeriod 86400
## Security Configuration ######################################################
......
......@@ -906,12 +906,10 @@ INITRANS 50 PCTFREE 50 ENABLE ROW MOVEMENT;
CREATE INDEX I_DiskCopy_Castorfile ON DiskCopy (castorFile);
CREATE INDEX I_DiskCopy_FileSystem ON DiskCopy (fileSystem);
CREATE INDEX I_DiskCopy_DataPool ON DiskCopy (dataPool);
CREATE INDEX I_DiskCopy_FS_GCW ON DiskCopy (fileSystem, gcWeight);
CREATE INDEX I_DiskCopy_DP_GCW ON DiskCopy (dataPool, gcWeight);
CREATE INDEX I_DiskCopy_FS_DP_GCW ON DiskCopy (nvl(fileSystem,0)+nvl(dataPool,0), gcWeight);
-- for queries on active statuses
CREATE INDEX I_DiskCopy_Status_6 ON DiskCopy (decode(status,6,status,NULL));
CREATE INDEX I_DiskCopy_Status_7_FS ON DiskCopy (decode(status,7,status,NULL), fileSystem);
CREATE INDEX I_DiskCopy_Status_7_DP ON DiskCopy (decode(status,7,status,NULL), dataPool);
CREATE INDEX I_DiskCopy_Status_7_FS_DP ON DiskCopy (decode(status,7,status,NULL), nvl(fileSystem,0)+nvl(dataPool,0));
CREATE INDEX I_DiskCopy_Status_9 ON DiskCopy (decode(status,9,status,NULL));
-- to speed up deleteOutOfDateStageOutDCs
CREATE INDEX I_DiskCopy_Status_Open ON DiskCopy (decode(status,6,status,decode(status,5,status,decode(status,11,status,NULL))));
......
......@@ -256,12 +256,12 @@ BEGIN
-- Loop on all concerned fileSystems/DataPools in a random order.
totalCount := 0;
FOR fs IN (SELECT * FROM (SELECT FileSystem.id AS fsId, 0 AS dpId
FOR fs IN (SELECT * FROM (SELECT FileSystem.id AS fsId
FROM FileSystem, DiskServer
WHERE FileSystem.diskServer = DiskServer.id
AND DiskServer.name = diskServerName
UNION ALL
SELECT 0 AS fsId, DiskServer.dataPool AS dpId
SELECT DiskServer.dataPool AS fsId
FROM DiskServer
WHERE DiskServer.name = diskServerName)
ORDER BY dbms_random.value) LOOP
......@@ -271,7 +271,7 @@ BEGIN
SELECT totalCount + count(*), nvl(sum(DiskCopy.diskCopySize), 0)
INTO totalCount, freed
FROM DiskCopy
WHERE (fileSystem = fs.fsId OR dataPool = fs.dpId)
WHERE (fileSystem = fs.fsId OR dataPool = fs.fsId)
AND decode(status, 9, status, NULL) = 9; -- BEINGDELETED (decode used to use function-based index)
-- estimate the number of GC running the "long" query, that is the one dealing with the GCing of
......@@ -281,10 +281,10 @@ BEGIN
WHERE s.sql_id = t.sql_id AND t.sql_text LIKE '%I_DiskCopy_FS_GCW%';
-- Process diskcopies that are in an INVALID state.
UPDATE /*+ INDEX_RS_ASC(DiskCopy I_DiskCopy_Status_7_FS)) */ DiskCopy
UPDATE /*+ INDEX_RS_ASC(DiskCopy I_DiskCopy_Status_7_FS_DP)) */ DiskCopy
SET status = 9, -- BEINGDELETED
gcType = decode(gcType, NULL, dconst.GCTYPE_USER, gcType)
WHERE (fileSystem = fs.fsId OR dataPool = fs.dpId)
WHERE (nvl(fileSystem,0)+nvl(dataPool,0) = fs.fsId)
AND decode(status, 7, status, NULL) = 7 -- INVALID (decode used to use function-based index)
AND rownum <= 10000 - totalCount
RETURNING id BULK COLLECT INTO dcIds;
......@@ -315,7 +315,7 @@ BEGIN
ELSE
SELECT DiskServer.id INTO unused
FROM DiskServer
WHERE dataPool = fs.dpId
WHERE dataPool = fs.fsId
AND status IN (dconst.DISKSERVER_PRODUCTION, dconst.DISKSERVER_READONLY)
AND hwOnline = 1
AND ROWNUM < 2;
......@@ -342,15 +342,15 @@ BEGIN
SELECT decode(sign(maxFreeSpace * totalSize - free), -1, 0, maxFreeSpace * totalSize - free)
INTO toBeFreed
FROM DataPool
WHERE id = fs.dpId;
WHERE id = fs.fsId;
END IF;
-- If space is still required even after removal of INVALID files, consider
-- removing VALID files until we are below the free space watermark
IF freed < toBeFreed THEN
-- Loop on file deletions
FOR dc IN (SELECT /*+ INDEX_RS_ASC(DiskCopy I_DiskCopy_FS_GCW) */ DiskCopy.id, castorFile
FOR dc IN (SELECT /*+ INDEX_RS_ASC(DiskCopy I_DiskCopy_FS_DP_GCW) */ DiskCopy.id, castorFile
FROM DiskCopy, CastorFile
WHERE (fileSystem = fs.fsId OR dataPool = fs.dpId)
WHERE (nvl(fileSystem,0)+nvl(dataPool,0) = fs.fsId)
AND status = dconst.DISKCOPY_VALID
AND CastorFile.id = DiskCopy.castorFile
AND CastorFile.tapeStatus IN (dconst.CASTORFILE_DISKONLY, dconst.CASTORFILE_ONTAPE)
......@@ -416,12 +416,13 @@ BEGIN
AND DiskServer.name = diskServerName
UNION ALL
SELECT DiskCopy.castorFile,
DiskCopy.path AS path, DiskCopy.id,
DataPool.name || '/' || DiskCopy.path AS path, DiskCopy.id,
DiskCopy.lastAccessTime, DiskCopy.nbCopyAccesses, DiskCopy.gcWeight,
getObjStatusName('DiskCopy', 'gcType', DiskCopy.gcType) AS gcType,
getSvcClassListDP(DiskServer.dataPool) AS svcClassList
FROM DiskServer, DiskCopy
FROM DiskServer, DiskCopy, DataPool
WHERE decode(DiskCopy.status, 9, DiskCopy.status, NULL) = 9 -- BEINGDELETED
AND DiskCopy.dataPool = DataPool.id
AND DiskCopy.dataPool = DiskServer.dataPool
AND DiskServer.name = diskServerName
AND ROWNUM < 2) DC
......
......@@ -1439,7 +1439,7 @@ BEGIN
UPDATE /*+ INDEX(Subrequest PK_Subrequest_Id)*/ SubRequest
SET status = dconst.SUBREQUEST_FAILED,
errorCode = serrno.ENOSPC, -- No space left on device
errorMessage = 'File creation canceled since diskPool is full'
errorMessage = 'File creation canceled since pool is full'
WHERE id = srId;
RETURN;
END IF;
......
......@@ -24,13 +24,15 @@ cmake_minimum_required (VERSION 2.6)
################################################################################
# Rules to build and install gc
################################################################################
find_package (ceph REQUIRED)
include_directories(${RADOS_INCLUDE_DIR})
set(GCBIN_SRCS
GcDaemon.cpp
DeletionThread.cpp
SynchronizationThread.cpp)
SynchronizationThread.cpp
CephGlobals.cpp)
add_executable (gcd ${GCBIN_SRCS})
target_link_libraries (gcd castorcommon castordlf castorns castorclient)
target_link_libraries (gcd castorcommon castordlf castorns castorclient ${RADOS_LIBS})
install (TARGETS gcd DESTINATION ${CASTOR_DEST_BIN_DIR})
################################################################################
......
/******************************************************************************
* CephGlobals.cpp
*
* 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.
*
* couple of global maps needed for interfacing with ceph libraries.
* Basically a cache of connections to ceph
*
* @author Dennis Waldron
*****************************************************************************/
#include "castor/gc/CephGlobals.hpp"
#include <map>
/// global variables holding ioCtx and stripers for each ceph pool
std::map<std::string, libradosstriper::RadosStriper*> g_radosStripers;
std::map<std::string, librados::IoCtx*> g_ioCtx;
librados::IoCtx* castor::gc::getRadosIoCtx(std::string pool) {
libradosstriper::RadosStriper* striper = castor::gc::getRadosStriper(pool);
if (0 == striper) return 0;
return g_ioCtx[pool];
}
libradosstriper::RadosStriper* castor::gc::getRadosStriper(std::string pool) {
std::map<std::string, libradosstriper::RadosStriper*>::iterator it =
g_radosStripers.find(pool);
if (it == g_radosStripers.end()) {
// we need to create a new radosStriper
librados::Rados cluster;
int rc = cluster.init(0);
if (rc) return 0;
rc = cluster.conf_read_file(NULL);
if (rc) {
cluster.shutdown();
return 0;
}
cluster.conf_parse_env(NULL);
rc = cluster.connect();
if (rc) {
cluster.shutdown();
return 0;
}
g_ioCtx[pool] = new librados::IoCtx();
librados::IoCtx *ioctx = g_ioCtx[pool];
rc = cluster.ioctx_create(pool.c_str(), *ioctx);
if (rc != 0) {
g_ioCtx.erase(pool);
return 0;
}
libradosstriper::RadosStriper *newStriper = new libradosstriper::RadosStriper;
rc = libradosstriper::RadosStriper::striper_create(*ioctx, newStriper);
if (rc != 0) {
delete newStriper;
g_ioCtx.erase(pool);
return 0;
}
it = g_radosStripers.insert(std::pair<std::string, libradosstriper::RadosStriper*>
(pool, newStriper)).first;
}
return it->second;
}
/******************************************************************************
* CephGlobals.hpp
*
* 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.
*
* couple of global maps needed for interfacing with ceph libraries.
* Basically a cache of connections to ceph
*
* @author castor dev team
*****************************************************************************/
#pragma once
#include "castor/gc/CephGlobals.hpp"
#include <rados/librados.hpp>
#include <radosstriper/libradosstriper.hpp>
namespace castor {
namespace gc {
/// gets an IoCtx object for a given pool
librados::IoCtx* getRadosIoCtx(std::string pool);
/// gets a Striper object for a given pool
libradosstriper::RadosStriper* getRadosStriper(std::string pool);
}
}
......@@ -25,12 +25,14 @@
// Include files
#include "castor/gc/DeletionThread.hpp"
#include "castor/gc/CephGlobals.hpp"
#include "castor/Services.hpp"
#include "castor/Constants.hpp"
#include "castor/stager/IGCSvc.hpp"
#include "castor/stager/GCLocalFile.hpp"
#include "castor/System.hpp"
#include "getconfent.h"
#include <radosstriper/libradosstriper.hpp>
#include <vector>
#include <errno.h>
......@@ -273,13 +275,13 @@ void castor::gc::DeletionThread::run(void*) {
} // End of loop
}
//-----------------------------------------------------------------------------
// gcRemoveFilePath
//-----------------------------------------------------------------------------
void castor::gc::DeletionThread::gcRemoveFilePath
(std::string filepath, u_signed64 &filesize, u_signed64 &fileage)
{
(std::string filepath, u_signed64 &filesize, u_signed64 &fileage) {
if (!filepath.empty() and filepath[0] == '/') {
// regular file, call POSIX API
struct stat64 fileinfo;
if (::stat64(filepath.c_str(), &fileinfo) ) {
castor::exception::Exception e(errno);
......@@ -293,4 +295,32 @@ void castor::gc::DeletionThread::gcRemoveFilePath
e.getMessage() << "Failed to unlink file " << filepath;
throw e;
}
} else {
// ceph file
int slashPos = filepath.find('/');
std::string pool = filepath.substr(0,slashPos);
std::string filename = filepath.substr(slashPos+1);
libradosstriper::RadosStriper *striper = getRadosStriper(pool);
if (0 == striper) {
castor::exception::Exception e(EINVAL);
e.getMessage() << "Failed to connect to ceph while unlinking file " << filepath;
throw e;
}
time_t pmtime;
uint64_t pmsize;
int rc = striper->stat(filename, &pmsize, &pmtime);
if (rc) {
castor::exception::Exception e(-rc);
e.getMessage() << "Failed to stat ceph file " << filepath;
throw e;
}
filesize = pmsize;
fileage = time(NULL) - pmtime;
rc = striper->remove(filename);
if (rc) {
castor::exception::Exception e(-rc);
e.getMessage() << "Failed to unlink ceph file " << filepath;
throw e;
}
}
}
......@@ -140,7 +140,7 @@ castor::gc::GcDaemon::GcDaemon(std::ostream &stdOut, std::ostream &stdErr,
{ 23, "Unable to retrieve mountpoints, giving up with synchronization" },
{ 24, "Could not list filesystem directories, giving up with filesystem's synchronisation" },
{ 25, "Could not list filesystem subdirectory, ignoring it for synchronization" },
{ 27, "Deleting local file which is no longer in the nameserver" },
{ 27, "Deleting file which is no longer in the nameserver" },
{ 28, "Deletion of orphaned local file failed" },
{ 29, "Memory allocation failure" },
{ 30, "Synchronization configuration" },
......@@ -155,6 +155,10 @@ castor::gc::GcDaemon::GcDaemon(std::ostream &stdOut, std::ostream &stdErr,
{ 40, "Unexpected exception caught in synchronizeFiles" },
{ 41, "Failed to stat file" },
{ 43, "Could not get fileid from filepath, giving up for this file" },
{ 44, "Unable to get RadosStriper object. Ignoring file" },
{ 45, "Unable to retrieve IoCtx for DataPool" },
{ 46, "New synchronization grace period" },
{ 47, "Invalid GC/SyncGracePeriod option, using default" },
{ -1, "" }};
dlfInit(messages);
}
This diff is collapsed.
......@@ -75,18 +75,10 @@ namespace castor {
/**
* Read config file values
* @param chunkInterval a pointer to the chunk interval value
* @param chunkSize a pointer to the chunk size value
* @param disableStagerSync a pointer to the boolean commanding disabling
* of the synchronization with the stager
* @param firstTime whether this is a first call. used only for logging
* purposes
*/
void readConfigFile(unsigned int *chunkInterval,
unsigned int *chunkSize,
bool *disableStagerSync,
bool firstTime = false)
;
void readConfigFile(bool firstTime = false);
/**
* Parse a fileName and extract the diskCopyId
......@@ -96,8 +88,7 @@ namespace castor {
* syntax
*/
std::pair<std::string, u_signed64>
diskCopyIdFromFileName(std::string fileName)
;
diskCopyIdFromFileName(std::string fileName);
/**
* Parse a filePath and extract the fileId
......@@ -106,34 +97,80 @@ namespace castor {
* @throw exception in case the file name is not matching the expected
* syntax
*/
u_signed64 fileIdFromFilePath(std::string filePath)
;
u_signed64 fileIdFromFilePath(std::string filePath);
/**
* gets a list of files open for write in the given mountPoint
* @param mountPoint the mountPoint to be considered
* @throw exception in case of error
* synchronizes all fileSystems
*/
std::set<std::string> getFilesBeingWrittenTo(char* mountPoint)
;
void syncFileSystems();
/**
* synchronizes all dataPools
*/
void syncDataPools();
/**
* Synchronizes a given local file
* @param path the path where the file lies inside the mountPoint,
* or empty string for DataPools
* @param fileName the file name
* @param paths the differents "chunks", that is list of files, sorted by namespace
* @return whether synchronization took place
*/
bool syncLocalFile(const std::string &path,
const char* fileName,
std::map<std::string, std::map<u_signed64, std::string> > &paths);
/**
* Synchronizes a given ceph file
* @param fileName the name of the file
* @param paths the differents "chunks", that is list of files, sorted by namespace
* @return whether synchronization took place
*/
bool syncCephFile(const std::string fileName,
std::map<std::string, std::map<u_signed64, std::string> > &paths);
/**
* Synchronizes all chunks present in paths
* @param paths the differents "chunks", that is list of files, sorted by namespace
*/
void syncAllChunks(std::map<std::string, std::map<u_signed64, std::string> > &paths);
/**
* Checks whether to synchronize a chunk for the given nameserver
* and does it if needed
* @param nameServer the nameServer concerned
* @param paths the differents "chunks", that is list of files, sorted by namespace
* @param minimumNbFiles the minimumNbFiles we should have in the chunk to
* got for synchronization
* @return whether synchronization took place
*/
bool checkAndSyncChunk(const std::string &nameServer,
std::map<std::string, std::map<u_signed64, std::string> > &paths,
u_signed64 minimumNbFiles);
/**
* Synchronizes a list of files from a given filesystem with the nameserver and stager catalog
* @param nameServer the nameserver to use
* @param paths a map giving the full file name for each diskCopyId to be checked
* @param disableStagerSync whether to disable the stager synchronization
* @param mountPoint the mountPoint of the filesystem on which the files reside
*/
void synchronizeFiles(std::string nameServer,
const std::map<u_signed64, std::string> &paths,
bool disableStagerSync,
char* mountPoint)
void synchronizeFiles(const std::string &nameServer,
const std::map<u_signed64, std::string> &paths)
throw();
private:
/// The number of seconds to delay the first invocation of the run method
int m_startDelay;
/// The number of seconds to wait between two chunks' synchronization
unsigned int m_chunkInterval;
/// the size of a chunk, aka the maximum number of files to synchronize in one go
unsigned int m_chunkSize;
/// the grace period for new files. That is the period during which they are not
/// considered for synchronization
time_t m_gracePeriod;
/// Whether stager synchronization should be disabled;
bool m_disableStagerSync;
};
......
......@@ -148,8 +148,8 @@ void castor::legacymsg::VmgrProxyTcpIp::tapeMountedForRead(const std::string &vi
{
try {
castor::legacymsg::VmgrTapeMountedMsgBody msg;
msg.uid = getuid();
msg.gid = getgid();
msg.uid = geteuid();
msg.gid = getegid();
msg.jid = getpid();
msg.mode = WRITE_DISABLE;
castor::utils::copyString(msg.vid, vid.c_str());
......@@ -170,8 +170,8 @@ void castor::legacymsg::VmgrProxyTcpIp::tapeMountedForWrite(const std::string &v
{
try {
castor::legacymsg::VmgrTapeMountedMsgBody msg;
msg.uid = getuid();
msg.gid = getgid();
msg.uid = geteuid();
msg.gid = getegid();
msg.jid = getpid();
msg.mode = WRITE_ENABLE;
castor::utils::copyString(msg.vid, vid.c_str());
......@@ -337,8 +337,8 @@ void castor::legacymsg::VmgrProxyTcpIp::queryTape(const std::string &vid, legacy
castor::utils::SmartFd fd(connectToVmgr());
legacymsg::VmgrTapeInfoRqstMsgBody request;
request.uid = getuid();
request.gid = getgid();
request.uid = geteuid();
request.gid = getegid();
castor::utils::copyString(request.vid, vid.c_str());
request.side = 0; // HARDCODED side
......
......@@ -111,6 +111,8 @@ class ReporterThread(threading.Thread):
for dataPool in dataPools:
try:
totalSpace = quotas[dataPool]
if 0 == totalSpace:
raise Exception("Quota missing in datapool %s (set to 0)" % dataPool)
freeSpace = totalSpace - usedSpace[dataPool]
# fill report
reports.append((diskServerName, dataPool, maxFreeSpace, minAllowedFreeSpace,
......
......@@ -161,7 +161,7 @@ void castor::tape::tapeserver::daemon::AdminAcceptHandler::fillTapeStatDriveEntr
}
try {
entry.uid = getuid();
entry.uid = geteuid();
castor::utils::copyString(entry.dgn,
m_driveCatalogue.getDgn(unitName).c_str());
const DriveCatalogue::DriveState driveState =
......
......@@ -107,7 +107,7 @@ void castor::tape::tapeserver::daemon::DriveCatalogue::enterTpconfigLine(
entry.dgn = line.dgn;
entry.devFilename = line.devFilename;
entry.densities.push_back(line.density);
entry.state = initial2DriveState(line.initialState);
entry.state = DRIVE_STATE_DOWN;
entry.librarySlot = line.librarySlot;
entry.devType = line.devType;
m_drives[line.unitName] = entry;
......@@ -123,32 +123,6 @@ void castor::tape::tapeserver::daemon::DriveCatalogue::enterTpconfigLine(
}
}
//-----------------------------------------------------------------------------
// initial2DriveState
//-----------------------------------------------------------------------------
castor::tape::tapeserver::daemon::DriveCatalogue::DriveState
castor::tape::tapeserver::daemon::DriveCatalogue::initial2DriveState(
const utils::TpconfigLine::InitialState initialState) const {
switch(initialState) {
case utils::TpconfigLine::TPCONFIG_DRIVE_NONE:
{
castor::exception::Exception ex;
ex.getMessage() << "Failed to convert initial drive state"
": TPCONFIG_DRIVE_NONE is invalid";
throw ex;
}
case utils::TpconfigLine::TPCONFIG_DRIVE_UP: return DRIVE_STATE_UP;
case utils::TpconfigLine::TPCONFIG_DRIVE_DOWN: return DRIVE_STATE_DOWN;
default:
{
castor::exception::Exception ex;
ex.getMessage() << "Failed to convert initial drive state"
": Unknown initial drive state: initialState=" << initialState;
throw ex;
}
}
}
//-----------------------------------------------------------------------------
// checkTpconfigLine
//-----------------------------------------------------------------------------
......@@ -158,7 +132,6 @@ void castor::tape::tapeserver::daemon::DriveCatalogue::checkTpconfigLine(
checkTpconfigLineDgn(catalogueEntry.dgn, line);
checkTpconfigLineDevFilename(catalogueEntry.devFilename, line);
checkTpconfigLineDensity(catalogueEntry.densities, line);
checkTpconfigLineInitialState(catalogueEntry.state, line);
checkTpconfigLineLibrarySlot(catalogueEntry.librarySlot, line);
checkTpconfigLineDevType(catalogueEntry.devType, line);
}
......@@ -212,22 +185,6 @@ void castor::tape::tapeserver::daemon::DriveCatalogue::checkTpconfigLineDensity(
}
}
//-----------------------------------------------------------------------------
// checkTpconfigLineInitialState
//-----------------------------------------------------------------------------
void castor::tape::tapeserver::daemon::DriveCatalogue::
checkTpconfigLineInitialState(const DriveState catalogueInitialState,
const utils::TpconfigLine &line) {
if(catalogueInitialState != initial2DriveState(line.initialState)) {
castor::exception::Exception ex;
ex.getMessage() << "Invalid TPCONFIG line"
": A tape drive can only have one initial state"
": catalogueInitialState=" << catalogueInitialState <<
" lineInitialState=" << line.initialState;
throw ex;
}
}
//-----------------------------------------------------------------------------