# -*- test-case-name: mv3d.test.service.test_realmserver -*- # Copyright (C) 2006-2012 Mortal Coil Games # See LICENSE for details. """ Code and interface for a realm server """ import os import logging from twisted.internet import defer, reactor from twisted.internet.defer import inlineCallbacks, returnValue from twisted.spread import pb from twisted.application.service import Service from twisted.python.log import deferr from zope.interface import implements #@UnresolvedImport import mv3d from mv3d.net.client import ServiceLoc from mv3d.net.security import Securable, requirePermissions from mv3d.util.classgen import ClassGenerator from mv3d.util.modifier import Modifiable from mv3d.util.conductor import parsePermissionConfig, parseInterfaceConfig from mv3d.server.model.realm import RealmHolder from mv3d.server.service import checkServicePermissions, viewed from mv3d.server.cluster import WithPools, ViewWithPools from mv3d.server.iserver import IRealmService from mv3d.server.persist import SQLiteStore from mv3d.util.guide import ChangeNotifier, NotifierProperty class RealmError(Exception): """ Raised when there is an issue with the realm server """ class RealmServiceView(pb.Viewable, ViewWithPools): """ A service that hosts Realms """ def __init__(self, service): self.service = service def getProtocol(self): """ Return what protocol we implement """ return "pb" @inlineCallbacks @checkServicePermissions("read") def view_getRealm(self, client, realm): """ Get a realm to cache must be one that we have locally """ realm = yield self.service.getRealm(realm, local=True) if hasattr(realm, "getClassGenerator"): cgs = realm.getClassGenerator() else: cgs = ClassGenerator().setFrom(realm) yield client.readyClass(cgs) returnValue(realm) @inlineCallbacks @checkServicePermissions("read") def view_getRealmView(self, client, realm): """ Get a view of a local realm """ realm = yield self.service.getRealm(realm, local=True) view = realm.getView() if hasattr(view, "getClassGenerator"): cgs = view.getClassGenerator() else: cgs = ClassGenerator().setFrom(view) yield client.readyClass(cgs) yield client.readyClass(view.getClassDependencies()) returnValue(view) @checkServicePermissions("read") @viewed def view_getStatistics(self, client): """ Retrieves the statistics from the realm server """ @checkServicePermissions("read") @viewed def view_getConfig(self, client): """ Returns the config for this realm service. """ @checkServicePermissions("modify") @viewed def view_setStore(self, client, store): """ Sets the datastore this realm service uses """ @checkServicePermissions("modify") @viewed def view_setDirectoryServices(self, client, services): """ Sets the list of directory services """ @checkServicePermissions("modify") @viewed def view_setPublicLocation(self, client, location): """ Sets the public location of the realm service """ @checkServicePermissions("read") @viewed def view_listRealms(self, client): """ Returns a list of local realms """ @checkServicePermissions("read") @viewed def view_listAreas(self, client, realmID): """ Returns a list of local areas in a realm """ @checkServicePermissions("modify") @viewed def view_newMasterRealm(self, client, cgen, realm=None): """ Create a new realm! """ @checkServicePermissions("modify") @viewed def view_destroyRealm(self, client, rid): """ Destroy a whole realm. This will probably break stuff and/or delete way more then you intended. Use with care. """ class RealmService(Service, Securable, Modifiable, WithPools): """ This is a realm server. It stores and deals with realms. """ implements(IRealmService) classModifiers = dict( web=[ClassGenerator("mv3d.server.editor.Stats")], guide=[ClassGenerator("mv3d.server.realm.RealmConfig")] ) publicLocation = None def __init__(self): Securable.__init__(self) Modifiable.__init__(self) self.directoryservers = [] self.store = SQLiteStore() self.store.open() self.interfaces = {} self.pools = {} self.config = {} def configure(self, name, cfg): """ Configure this interface """ self.config.update(dict(cfg.items(name))) if cfg.has_option(name, "store"): self.store = SQLiteStore() fname = cfg.get(name, "store") if os.path.abspath(fname) != fname: fname = os.path.join(self.parent.dataRoot, fname) dirname = os.path.dirname(fname) if not os.path.exists(dirname): os.makedirs(dirname) self.store.open(fname) if cfg.has_option(name, "grantPermissions"): parsePermissionConfig(cfg.get(name, "grantPermissions"), self, True) if cfg.has_option(name, "denyPermissions"): parsePermissionConfig(cfg.get(name, "denyPermissions"), self, False) if cfg.has_option(name, "directoryServers"): svrs = cfg.get(name, "directoryServers").split(",") for svr in svrs: self.directoryservers.append(ServiceLoc(svr.strip())) if cfg.has_option(name, "interfaces"): ifaces = parseInterfaceConfig(cfg.get(name, "interfaces"), self) for proto, iface in ifaces.items(): self.interfaces[proto] = iface if cfg.has_option(name, "publicLocation"): self.publicLocation = ServiceLoc(cfg.get(name, "publicLocation")) @inlineCallbacks def startService(self): """ Load realms and activate """ yield self.loadPools() def updatePool(pool): # all local let the directory service know dsvc = yield self.parent.getOneService( self.directoryservers) yield dsvc.setLocation("Realms", pool.uid, pool.uid, [self.publicLocation]) #now that the pools are loaded for pool in self.pools.values(): if isinstance(pool, RealmHolder): for guard in pool.guards.values(): if not guard.local: break else: # hack-- this needs to be called once all services # have started. reactor.callLater(0.0, updatePool, pool) #@UndefinedVariable @inlineCallbacks def stopService(self): """ Shut down the service. """ yield self.onShutdown() self.store.close() def getDirectoryServers(self): """ return all of our directory servers """ return self.directoryservers def addDirectoryServer(self, svr): """ Add a directory server (s = ServiceLoc) """ self.directoryservers.append(svr) def remDirectoryServer(self, svr): """ Remove a directory server (s=ServiceLoc) if we don't have it, return 0, else 1 """ self.directoryservers.remove(svr) @inlineCallbacks def getRealm(self, realmid, local=False): """ Returns the actual realm (not the holder). Likely users of this method would be remote callers. """ if self.pools.has_key(realmid): returnValue(self.pools[realmid].realm) if local: raise KeyError("Realm %d not found locally!" % realmid) dsvr = yield self.parent.getOneService(self.directoryservers) locator = yield dsvr.getItem("Realms", realmid) realmPool = yield locator.getItem(self.parent) realm = yield realmPool.pool.callRemote("getRealm") returnValue(realm) def hasRealm(self, rid): """ Returns true if we have realm with id rid locallly """ return self.pools.has_key(rid) def remRealm(self, rid): """ Remove a realm alltogether. (currently nonfunctional) """ self.parent.log("%s removed realm %s." % (self.name, rid), logging.INFO) @inlineCallbacks def newMasterRealm(self, cgen, realm=None): """ Create a new realm that we will be the master for. """ if realm is None: realm = yield defer.maybeDeferred(cgen.construct) dsvc = yield self.parent.getOneService(self.directoryservers) realm.rid = yield dsvc.newID("Realms") realm.save(self.store) holder = RealmHolder(self.publicLocation, self.store, realm.rid) holder.save(self.store) holder.realm = realm holder.iddisp.setParentID(realm.rid) self.addPool(holder) yield dsvc.setLocation("Realms", realm.rid, holder.uid, [self.publicLocation]) yield holder.new(self) rPool = self.getPool(holder.sharedPoolID) yield rPool.addNewItem(holder.realm) holder.realm.clusterID = holder.uid holder.realm.grantPermission(u"reference", u"all") holder.realm.grantPermission(u"read", u"all") returnValue(holder.realm.rid) @inlineCallbacks def destroyRealm(self, rid): """ Destroy the realm. Pretty scary stuff. Be careful with this. """ assert self.pools.has_key(rid) dsvc = yield self.parent.getOneService(self.directoryservers) yield self.pools[rid].destroy(self.parent) yield dsvc.removeItem("Realms", rid) self.removePool(rid) @inlineCallbacks def joinRealm(self, realmID): """ Join an existing realm. """ holder = RealmHolder(self.publicLocation, self.store, realmID) holder.save(self.store) self.addPool(holder) dsvc = yield self.parent.getOneService(self.directoryservers) locator = yield dsvc.getItem("Realms", realmID) yield locator.getItem(self.parent) yield holder.join(self.parent, locator.foundPool.locations) def getStatistics(self): """ Returns a dict of stats """ stats = {} stats["Realms"] = len(self.pools) return stats def getConfig(self): """ Returns a dict with the config of this sim. """ return self.config def setStore(self, store): """ Sets the datastore this realm service uses """ if store != self.store.filename: self.store.close() self.store = SQLiteStore() self.store.open(store) def setDirectoryServices(self, services): """ Sets the list of directory services to use """ self.directoryservers = [ServiceLoc(svc) for svc in services] def setPublicLocation(self, location): """ Sets the public location of the realm service """ self.publicLocation = ServiceLoc(location) def listRealms(self): """ Returns a list of local realms TODO: make this work across multiple servers """ realms = [] for pool in self.pools.values(): if isinstance(pool, RealmHolder): realms.append((pool.uid, pool.realm.name)) return realms @inlineCallbacks def listAreas(self, realmID): """ Returns a list of areas for a given realm TODO: make this work across multiple servers """ realm = yield self.getRealm(realmID) areas = [] for areaID, area in realm.areas.items(): areas.append((areaID, area.name)) returnValue(areas) @requirePermissions("reference") def getInterface(self, _client, protocol): """ Hand out public interfaces """ return self.interfaces[protocol] def isLocal(self): """ We are local so return true """ return True class RealmConfig(ChangeNotifier): """ Guide interface for changing sim properties """ store = NotifierProperty("_store", "store/sim") directoryServices = NotifierProperty("_directoryServices}", "self/Dir") publicLocation = NotifierProperty("_publicLocation", "self/Sim") window = None def __init__(self, realm): self.realm = realm self.getConfig() self.doneDeferred = defer.Deferred() @inlineCallbacks def getConfig(self): """ Called on startup to load everything """ config = yield self.realm.getConfig() self.store = config["store"] self.directoryServices = ", ".join([str(svc) for svc in config.get( "directoryServices", [])]) self.publicLocation = config.get("publicLocation") try: from mv3d.tools.guidewx import WxParser parser = WxParser() fname = os.path.abspath(os.path.join(os.path.dirname(mv3d.__file__), "..", "templates", "guide", "realmconfig.xml")) self.window = parser.load(fname, self) except: deferr() self.window.show() def cancel(self, _obj, _property): """ Called when the user hits the cancel button """ if self.doneDeferred is not None: self.doneDeferred.callback(False) self.doneDeferred = None self.window.destroy() @inlineCallbacks def submit(self, _obj, _property): """ Called when the user hits the submit button. """ if self.doneDeferred is not None: yield self.realm.setStore(self.store) dirsvcs = self.directoryServices.split(",") yield self.realm.setDirectoryServices(dirsvcs) yield self.realm.setPublicLocation(self.publicLocation) self.doneDeferred.callback(True) self.doneDeferred = None self.window.destroy()