[BACK]Return to CamCategory.c++ CVS log [TXT][DIR] Up to [Development] / failsafe / FailSafe-mgr / lib / libfscxCamCategory

File: [Development] / failsafe / FailSafe-mgr / lib / libfscxCamCategory / CamCategory.c++ (download)

Revision 1.6, Mon Aug 7 13:17:15 2000 UTC (17 years, 2 months ago) by rusty
Branch: MAIN
CVS Tags: HEAD
Changes since 1.5: +2 -1 lines

Added #include for building on SuSE.

//
// CamCategory.c++
//
//	Base class for Categories which use the cam service.
//
//
//  Copyright (c) 1998, 2000 Silicon Graphics, Inc.  All Rights Reserved.
//  
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of version 2.1 of the GNU Lesser General Public
//  License as published by the Free Software Foundation.
//  
//  This program is distributed in the hope that it would be useful, but
//  WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//  
//  Further, this software is distributed without any warranty that it is
//  free of the rightful claim of any third person regarding infringement
//  or the like.  Any license provided herein, whether implied or
//  otherwise, applies only to this software file.  Patent licenses, if
//  any, provided herein do not apply to combinations of this program
//  with other software, or any other product whatsoever.
//  
//  You should have received a copy of the GNU Lesser General Public
//  License along with this program; if not, write the Free Software
//  Foundation, Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307,
//  USA.
//  
//  Contact information: Silicon Graphics, Inc., 1600 Amphitheatre Pkwy,
//  Mountain View, CA 94043, or http://www.sgi.com/
//  
//  For further information regarding this notice, see:
//  http://oss.sgi.com/projects/GenInfo/NoticeExplan/
//

#ident "1.1"

#include <string.h>
#include <sysadm/AppContext.h>
#include <sysadm/Log.h>
#include <fsmgr/CamCategory.h>
#include <ctype.h>
#include <ci_clikeys.h>
#include <sysadm/format.h>
#include <sysadm/i18n.h>
#include <unistd.h>
#include <sys/param.h>

namespace fsmgr {

using namespace sysadm;

//
// These are here for populating CamCategory::_typeTable, because
// _typeTable is a DictionaryOf and it needs pointers to its
// elements.
//
const Attribute::EType CamCategory::STRING = Attribute::STRING;
const Attribute::EType CamCategory::LONG = Attribute::LONG;
const Attribute::EType CamCategory::BOOLEAN = Attribute::BOOLEAN;
const Attribute::EType CamCategory::DOUBLE = Attribute::DOUBLE;

DictionaryOf<const Attribute::EType> CamCategory::_typeTable;

//
// Table of invisible Attributes keys.  These are the keys of
// Attributes that should not go into log files, such as passwords.
//
DictionaryOf<String> CamCategory::_invisibleAttrs;

//
// CamCategory constructor.
//
CamCategory::CamCategory(const String& selector, cam_category_t category)
: Category(selector), _numCategories(0), _numInventories(0),
  _itemCache(NULL), _handle(NULL), _inputId(-1)
{
    initTypeTable();
    addCategory(category);
}

//
// CamCategory destructor.
//
CamCategory::~CamCategory()
{
    if (_inputId != -1) {
	AppContext::getAppContext().unregisterMonitor(_inputId);
    }
    if (_handle != NULL) {
	cam_close(_handle);
    }
    if (_itemCache != NULL) {
	for (int ii = 0; ii < _numCategories; ii++) {
	    RemoveAndDestroyContentsOf(&_itemCache[ii]);
	}
	delete [] _itemCache;
    }
}

#ifdef CAM_WAR
//
// This is a workaround for bugs 626229, 626513
//
void CamCategory::addItem(const Item& newItem)
{

    ConstIteratorOver<Item> iter(getItems());
    Item* item;
    while ((item = iter()) != NULL) {
	if (item->getSelector() == newItem.getSelector()) {
	    Log::debug("CamCategory", "Mapping add to change for %s",
		       (const char*)newItem.getSelector());
	    changeItem(newItem);
	    return;
	}
    }
    Category::addItem(newItem);
}
#endif

//
//  void CamCategory::addCategory(cam_category_t category)
//
//  Description:
//      Add a category to be monitored.
//
//  Parameters:
//      category  cam category to be monitored.
//
void CamCategory::addCategory(cam_category_t category)
{
    assert(_numCategories < MAX_CATEGORIES);
    // Illegal to call addCategory() after calling startMonitor().
    assert(!isMonitoring());
    _categories[_numCategories++] = category;
}

//
//  void CamCategory::startMonitor()
//
//  Description:
//      Called when it's time to start monitoring.  Set up our
//      connection with libcam.
//
void CamCategory::startMonitor()
{
    Log::trace(getSelector(), "startMonitor()");
    assert(_numCategories > 0);

    cam_error_t error;
    _handle = cam_open(NULL, &error);
    if (_handle == NULL) {
	char buf[Log::MAXLEN];
	char hostname[MAXHOSTNAMELEN];
	if (gethostname(hostname, MAXHOSTNAMELEN) != 0) {
	    strcpy(hostname, "server");
	}
	switch (error) {
	    case cam_error_no_server:
		SaStringFormat(buf, sizeof buf,
			       i18n("Unable to connect to CAM daemon on %s. "
		                    "Try running \"/etc/rc.d/init.d/fs_cluster start\" "
		                    "on %s as root."),
                               hostname,
                               hostname);
		break;
	    default:
		SaStringFormat(buf, sizeof buf,
			       i18n("Unable to open connection with CAM. "
		                    "Error: %d."),
			       error);
	}
	//  If the call to notifyError(buf) is present below, the GUI
	//  displays the error message and terminates.  If it's removed, the
	//  AppContext::exit() will cause the "connection lost - retry?"
	//  dialog to be shown.  The notifyError() was removed because cad
	//  was dying too often, and was put back as part of a fix for
	//  bug 767083.
	notifyError(buf);
	Log::fatal(getSelector(), buf);
	AppContext::getAppContext().exit(1);
	return;
    }

    _itemCache = new DictionaryOf<Item>[_numCategories];
    // Register categories in reverse order so we get inventories from
    // status categories first -- if they come at all.  This is a
    // workaround for bug 631445.
    for (int ii = _numCategories - 1; ii >= 0; ii--) {
	if (cam_register(_handle, _categories[ii]) == -1) {
	    char buf[Log::MAXLEN];
	    error = cam_error(_handle);

	    //  FilesystemCategory registers cam_category_none
	    //  intentionally & expects it to fail; see the comment in
	    //  category/filesystem/FilesystemCategory.c++.
	    if (_categories[ii] == cam_category_none) {
		continue;
	    }

	    switch (error) {
		case cam_error_license:
		    SaStringFormat(buf, sizeof buf,
		                   i18n("No CXFS license found. See the CXFS "
		                        "Administration Guide for more "
		                        "information."));
		    break;
		default:
		    SaStringFormat(buf, sizeof buf,
				   i18n("Unable to register category %d. "
				        "Error: %d."),
				   _categories[ii],
				   error);
	    }
	    //  See notifyError() comment above.
	    notifyError(buf);
	    Log::fatal(getSelector(), buf);
	    AppContext::getAppContext().exit(1);
	    return;
	}
    }
    _inputId = AppContext::getAppContext().
	registerMonitor(cam_get_select_fd(_handle),
			handleInput, this, AppContext::INPUT);
}

#ifdef CAM_WAR
//
//  bool CamCategory::checkCategory(cam_category_t category)
//
//  Description:
//      Check to see if "category" is one of the categories we're
//      monitoring.  This is to work around a bug in libcam that
//      causes us to get events for categories we did not register
//      for.
//
//  Parameters:
//      category  cam category to check.
//
//  Returns:
//	true if this is a category we're registered for, false
//	otherwise.
//
bool CamCategory::checkCategory(cam_category_t category)
{
    for (int ii = 0; ii < _numCategories; ii++) {
	if (_categories[ii] == category) {
	    return true;
	}
    }

    Log::error(getSelector(), "Got an event for foreign category %d",
	       category);
    return false;
}
#endif

//
//  void CamCategory::handleInput(void* clientData, int /*id*/, int /*fd*/)
//
//  Description:
//      Called when we receive some input from libcam's file
//      descriptor.  Get a cam_event and parse it into a Category
//      notification.
//
//  Parameters:
//      clientData  CamCategory* (this is a static method)
//
void CamCategory::handleInput(void* clientData, int /*id*/, int /*fd*/)
{
    CamCategory* self = (CamCategory*)clientData;

    Log::trace(self->getSelector(), "handleInput()");

    const cam_event_t* event = cam_get_event(self->_handle);
    if (event == NULL) {
	cam_error_t error = cam_error(self->_handle);
	if (error != cam_error_again) {
	    char buf[Log::MAXLEN];
	    SaStringFormat(buf, sizeof buf,
			   i18n("Unable to get CAM event. Error: %d."), error);
	    self->notifyError(buf);
	    Log::fatal(self->getSelector(), buf);
	    AppContext::getAppContext().exit(1);
	}
	return;
    }

    if ((event->valid & CAM_EV_ACTION) == 0) {
	cam_free_event(self->_handle, event);
	return;
    }

    switch (event->action) {
    default:
    case cam_action_none:
	break;
    case cam_action_add:
	self->handleAdded(*event);
	break;
    case cam_action_inventory:
	self->handleInventory(*event);
	break;
    case cam_action_modify:
	self->handleModify(*event);
	break;
    case cam_action_remove:
	self->handleRemove(*event);
	break;
    }
    cam_free_event(self->_handle, event);
}

//
//  void CamCategory::addOrChangeObject(const cam_object_t& obj)
//
//  Description:
//      Deal appropriately with an object that libcam told us about.
//      If we already know about the object; change it.  Otherwise,
//      add it.
//
//  Parameters:
//      obj  object to add or change.
//
void CamCategory::addOrChangeObject(const cam_object_t& obj)
{
    String selector(getSelectorFromObject(obj));
    bool alreadyExisted = _itemCache[0].lookupByKey(selector);
    Item* item(createItemFromObject(obj));
    if (item == NULL) {
	return;
    }

    modifyItemOnCamEvent(item);

    // Don't actually add the Item unless it was from category 0.  If
    // it wasn't from category 0 but we have a corresponding Item in
    // category 0, that means we've already added an object with this
    // selector and so we need to do a change instead.
    //
    // If it's from a category other than category 0 and there is no
    // corresponding Item in category 0, we will wait until we get an
    // add for this Item in category 0 before adding to our Category.
    if (alreadyExisted) {
	CamCategory::changeItem(*item);
    } else if (_itemCache[0].lookupByKey(selector)) {
	CamCategory::addItem(*item);
    }
    delete item;
}

//
//  void CamCategory::handleAdded(const cam_event_t& event)
//
//  Description:
//      Handle a libcam added action.
//
//  Parameters:
//      event  A libcam added action.
//
void CamCategory::handleAdded(const cam_event_t& event)
{
    Log::trace(getSelector(), "handleAdded()");

    if ((event.valid & CAM_EV_OBJECT) == 0) {
	return;
    }

#ifdef CAM_WAR
    if (!checkCategory(event.data.object->category)) {
	return;
    }
#endif

    addOrChangeObject(*event.data.object);
}

//
//  void CamCategory::handleInventory(const cam_event_t& event)
//
//  Description:
//      Handle a libcam inventory action.
//
//  Parameters:
//      event  A libcam inventory action.
//
void CamCategory::handleInventory(const cam_event_t& event)
{
    Log::trace(getSelector(), "handleInventory()");

#ifdef CAM_WAR
    if (!checkCategory(event.data.inventory->category)) {
	return;
    }
#endif

    if ((event.valid & CAM_EV_INVENTORY) != CAM_EV_INVENTORY
	|| (event.data.inventory->valid & CAM_IV_ALL) != CAM_IV_ALL) {
	return;
    }

    if (event.data.inventory->num_objects > 0) {
	beginBlockChanges();
	for (int ii = 0; ii < event.data.inventory->num_objects; ii++) {
	    addOrChangeObject(*event.data.inventory->objects[ii]);
	}
	endBlockChanges();
    }
#ifdef CAM_WAR
    if (event.data.inventory->num_objects == 0) {
	Log::debug("CamCategory",
		   "Received inventory of zero (0) objects on category %d",
		   event.data.inventory->category);

	beginBlockChanges();

	// find category
	for (int ii = 0; ii < _numCategories; ii++) {
	    if (_categories[ii] == event.data.inventory->category) {
		DictionaryOfIterator<Item> iter(&_itemCache[ii]);
		Item* item;

		while ((item = iter()) != NULL) {
		    String selector(item->getSelector());

		    Log::debug("CamCategory",
			       "Clearing object %s in category %d",
			       (const char*)selector,
			       event.data.inventory->category);

		    Item toChange(getSelector(), selector);

		    bool removeIt = false;
		    for (int jj = 0; jj < _numCategories; jj++) {
			if (_categories[jj] == event.data.inventory->category) {
			    delete _itemCache[jj].remove(selector);
			    if (jj == 0) {
				removeIt = true;
			    }
			} else {
			    copyAttrs(_itemCache[jj].lookupByKey(selector),
				      toChange);
			}
		    }

		    // Don't remove the Item unless this remove notification is
		    // coming from category 0.  If this notification is coming
		    // from a category other than category 0, and we have an
		    // Item in category 0, then change the Item in the
		    // Category.  This will effectively strip off the
		    // attributes corresponding to the cam object we're getting
		    // removal notification for.
		    if (removeIt) {
			removeItem(selector);
		    } else if (_itemCache[0].lookupByKey(selector) != NULL) {
			modifyItemOnCamEvent(&toChange);
			Log::debug("CamCategory",
			       "Cleared object %s",
			       (const char*)toChange.toString());

			changeItem(toChange);
		    }
		}
	    }
	}

	endBlockChanges();
    }
#endif

    if (!hasExistsEnded()
	&& event.data.inventory->category == _categories[0]) {
	endExists();
    }
}

//
//  void CamCategory::handleModify(const cam_event_t& event)
//
//  Description:
//      Handle a libcam modify action.
//
//  Parameters:
//      event  a libcam modify action.
//
void CamCategory::handleModify(const cam_event_t& event)
{
    Log::trace(getSelector(), "handleModify()");

    if ((event.valid & CAM_EV_OBJECT) == 0) {
	return;
    }

#ifdef CAM_WAR
    if (!checkCategory(event.data.object->category)) {
	return;
    }
#endif

    Item* item(createItemFromObject(*event.data.object));
    if (item == NULL) {
	return;
    }

    modifyItemOnCamEvent(item);

    // Don't change it unless we've already added it.  We won't have
    // added it unless it's in the cache for category 0.
    if (_itemCache[0].lookupByKey(item->getSelector()) != NULL) {
	changeItem(*item);
    }
    delete item;
}

//
//  void CamCategory::handleRemove(const cam_event_t& event)
//
//  Description:
//      Handle a libcam remove action.
//
//  Parameters:
//      event  a libcam remove action.
//
void CamCategory::handleRemove(const cam_event_t& event)
{
    Log::trace(getSelector(), "handleRemove()");

    if ((event.valid & CAM_EV_OBJECT) == 0
	|| (event.data.object->valid & CAM_OV_NAME) != CAM_OV_NAME) {
	return;
    }

#ifdef CAM_WAR
    if (!checkCategory(event.data.object->category)) {
	return;
    }
#endif

    String selector(getSelectorFromObject(*event.data.object));
    Item toChange(getSelector(), selector);
    bool removeIt = false;
    for (int ii = 0; ii < _numCategories; ii++) {
	if (_categories[ii] == event.data.object->category) {
	    delete _itemCache[ii].remove(selector);
	    if (ii == 0) {
		removeIt = true;
	    }
	} else {
	    copyAttrs(_itemCache[ii].lookupByKey(selector), toChange);
	}
    }

    // Don't remove the Item unless this remove notification is coming
    // from category 0.  If this notification is coming from a
    // category other than category 0, and we have an Item in category
    // 0, then change the Item in the Category.  This will effectively
    // strip off the attributes corresponding to the cam object we're
    // getting removal notification for.
    if (removeIt) {
	removeItem(selector);
    } else if (_itemCache[0].lookupByKey(selector) != NULL) {
	modifyItemOnCamEvent(&toChange);
	changeItem(toChange);
    }
}

//
//  void CamCategory::modifyItemOnCamEvent(Item* item)
//
//  Description:
//      Create any attributes that need to be derived from the original object.
//	This is called before calling Category::addItem and
//	Category::changeItem.
//
//  Parameters:
//      item	Item for which to derive any additional attributes
//
void CamCategory::modifyItemOnCamEvent(Item*)
{
    // by default there are no additional attributes
}

//
//  Item* CamCategory::createItemFromObject(const cam_object_t& obj)
//
//  Description:
//      Create an Item that corresponds to "obj".
//
//  Parameters:
//      obj  cam_object to create an Item for.
//
//  Returns:
//	An Item for "obj".
//
Item* CamCategory::createItemFromObject(const cam_object_t& obj)
{
    #define OBJECT_MASK (CAM_OV_NAME | CAM_OV_NUM_INFO | CAM_OV_INFO | \
				 CAM_OV_CATEGORY)

    if ((obj.valid & OBJECT_MASK) != OBJECT_MASK) {
	return NULL;
    }

    String selector(getSelectorFromObject(obj));
    Item* cacheItem = new Item(getSelector(), selector);

    for (int i = 0; i < obj.num_info; i++) {
	if ((obj.info[i]->valid & CAM_OIV_ALL) == CAM_OIV_ALL) {
	    const char* key = obj.info[i]->key;
	    const Attribute::EType* type = _typeTable.lookupByKey(String(key));
	    if (type == NULL) {
                // Maybe this is a key like _KEY_1, in which case we
                // remove the digits at the end and try again.
                if (isdigit(key[strlen(key)-1])) {
                    char * keycopy = strdup(key);
                    int pos = strlen(keycopy) -1;
                    while (isdigit(keycopy[pos])) {
                        keycopy[pos--] = '\0';
                    }
                    type = _typeTable.lookupByKey(String(keycopy));
                    free(keycopy);
                    if (type == NULL) {
                        type = &STRING;
                    }
                } else {
                    type = &STRING;
                }

	    }
	    cacheItem->setAttr(Attribute(key, *type, obj.info[i]->value));
	    if (_invisibleAttrs.lookupByKey(String(key)) != NULL) {
		cacheItem->setAttrVisible(key, false);
	    }
	}
    }

    Item* item = new Item(*cacheItem);
    for (int ii = 0; ii < _numCategories; ii++) {
	if (_categories[ii] == obj.category) {
	    delete _itemCache[ii].remove(selector);
	    // Note that at this point _itemCache owns cacheItem, so
	    // that we should not delete it here.
	    _itemCache[ii].add(cacheItem, new String(selector));
	} else {
	    copyAttrs(_itemCache[ii].lookupByKey(selector), *item);
	}
    }

    return item;
}

//
//  String CamCategory::getSelectorFromObject(const cam_object_t& obj)
//
//  Description:
//      Called to get the selector from an object.
//
//  Parameters:
//      obj  Object to get selector from.
//
//  Returns:
//	Selector for obj.
//
String CamCategory::getSelectorFromObject(const cam_object_t& obj)
{
    return obj.name;
}

//
//  void CamCategory::copyAttrs(const Item* source, Item& dest)
//
//  Description:
//      Copy Attributes from one item to another.
//
//  Parameters:
//      source  Source of Attributes to copy.
//      dest    Destination of Attributes to copy.
//
void CamCategory::copyAttrs(const Item* source, Item& dest)
{
    if (source == NULL) {
	return;
    }
    CollectionOf<Attribute> attrs(source->copyAttrList());
    IteratorOver<Attribute> iter(&attrs);
    Attribute* attr;
    while ((attr = iter()) != NULL) {
	dest.setAttr(*attr);
    }
}

//
//  void CamCategory::initTypeTable()
//
//  Description:
//	Populate our lookup table that maps attribute names to types.
//	By default, Attributes are Strings.  The table below maps
//	attribute keys (defined in ci_clikeys.h) to other types.  This
//	table is used in createItemFromObject().
//
void CamCategory::initTypeTable()
{
    if (_typeTable.getSize() == 0) {
	_typeTable.add(&LONG, new String(CICLI_NODEID));
	_typeTable.add(&LONG, new String(CICLI_NUM_ACTIONS));
	_typeTable.add(&LONG, new String(CICLI_NUM_CLUSTERS));
	_typeTable.add(&LONG, new String(CICLI_NUM_CTRLNETS));
	_typeTable.add(&LONG, new String(CICLI_NUM_DEPENDENCIES));
	_typeTable.add(&LONG, new String(CICLI_NUM_FAILOVER_ATTRS));
	_typeTable.add(&LONG, new String(CICLI_NUM_FAILOVER_SCRIPTS));
	_typeTable.add(&LONG, new String(CICLI_NUM_IN_FAILOVER_AFD));
	_typeTable.add(&LONG, new String(CICLI_NUM_LOG_FILES));
	_typeTable.add(&LONG, new String(CICLI_NUM_LOG_GROUPS));
	_typeTable.add(&LONG, new String(CICLI_NUM_LOG_SUBSYSTEMS));
	_typeTable.add(&LONG, new String(CICLI_NUM_MACHINES));
	_typeTable.add(&LONG, new String(CICLI_NUM_RESOURCE_KEYS));
	_typeTable.add(&LONG, new String(CICLI_NUM_RESOURCE_TYPES));
        _typeTable.add(&LONG, new String(CICLI_PRIORITY_));
        _typeTable.add(&BOOLEAN, new String(CICLI_HB_));
        _typeTable.add(&BOOLEAN, new String(CICLI_CTRL_));
        _typeTable.add(&LONG, new String(CICLI_ACT_MONINTERVAL));
        _typeTable.add(&LONG, new String(CICLI_ACT_STARTMONTIME));
        _typeTable.add(&LONG, new String(CICLI_ACT_MAXEXECTIME));
        _typeTable.add(&LONG, new String(CICLI_RT_ORDER));
        _typeTable.add(&LONG, new String(CICLI_RT_RESTART_COUNT));
        _typeTable.add(&LONG, new String(CICLI_RT_RESTART_MODE));
	_typeTable.add(&LONG, new String(CICLI_CLUSTER_STATUS));
	_typeTable.add(&BOOLEAN, new String(CICLI_CLUSTER_NODE_INERROR));
	_typeTable.add(&BOOLEAN, new String(CICLI_CLUSTER_RG_INERROR));
	_typeTable.add(&LONG, new String(CICLI_MACHINE_STATUS));
	_typeTable.add(&LONG, new String(CICLI_RESOURCE_STATE));
	_typeTable.add(&LONG, new String(CICLI_RESOURCE_ERROR));
	_typeTable.add(&LONG, new String(CICLI_RG_STATUS_STATE));
	_typeTable.add(&LONG, new String(CICLI_RG_ERROR));
	_typeTable.add(&LONG, new String(CICLI_NODE_TIMEOUT));
	_typeTable.add(&LONG, new String(CICLI_HEARTBEAT_PERIOD));
	_typeTable.add(&BOOLEAN, new String(CICLI_CRS_RUN_PWRFAIL));
	_typeTable.add(&BOOLEAN, new String(CICLI_LOCALHOST));
	_typeTable.add(&LONG, new String(CICLI_CMS_WAIT_FOR_ALL_TIMEOUT));

	// CXFS types
	//
	_typeTable.add(&BOOLEAN, new String(CICLI_CELL_IS_CELLULAR));
	_typeTable.add(&BOOLEAN, new String(CICLI_CELL_IS_FAILSAFE));
	_typeTable.add(&LONG, new String(CICLI_CELL_NUM_INTERCONNECTS));
	_typeTable.add(&LONG, new String(CICLI_CELL_NUM_TARGETS_));
	_typeTable.add(&LONG, new String(CICLI_CELL_WEIGHT));
	_typeTable.add(&BOOLEAN, new String(CICLI_CLUSTER_IS_CELLULAR));
	_typeTable.add(&BOOLEAN, new String(CICLI_CLUSTER_IS_FAILSAFE));
	_typeTable.add(&BOOLEAN, new String(CICLI_CLUSTER_FS_FORCE_));
	_typeTable.add(&LONG, new String(CICLI_CLUSTER_ID));
	_typeTable.add(&LONG, new String(CICLI_CLUSTER_NUM_FILESYSTEMS));
	_typeTable.add(&LONG, new String(CICLI_CLUSTER_FS_NUM_SERVERS_));
    }

    if (_invisibleAttrs.getSize() == 0) {
	String* invisibleAttr = new String(CICLI_SYSCTRL_PASSWD);
	_invisibleAttrs.add(invisibleAttr, invisibleAttr);
    }
}

} // namespace fsmgr