class Core(object): def __init__(self, configFile=None, basePath=None): self.basePath = os.path.abspath( basePath) if basePath else os.path.expanduser( os.path.join('~', '.substance')) self.enginesPath = os.path.join(self.basePath, "engines") self.boxesPath = os.path.join(self.basePath, "boxes") self.dbFile = os.path.join(self.basePath, "db.json") configFile = configFile if configFile else "substance.yml" configFile = os.path.join(self.basePath, configFile) self.config = Config(configFile) self.insecureKey = None self.insecurePubKey = None self.assumeYes = False self.initialized = False def getBasePath(self): return self.basePath def getEnginesPath(self): return self.enginesPath def getBoxesPath(self): return self.boxesPath def getDbFile(self): return self.dbFile def initialize(self): if self.initialized: return OK(None) return self.assertPaths().then(self.assertConfig).then( self.initializeDB).then(defer(self.setInitialized, b=True)) def setInitialized(self, b): self.initialized = b def assertPaths(self): return OK([self.basePath, self.enginesPath, self.boxesPath]).mapM(Shell.makeDirectory) def assertConfig(self): return self.config.loadConfigFile() \ .catchError(FileDoesNotExist, self.makeDefaultConfig) def getDefaultConfig(self): defaults = OrderedDict() defaults['assumeYes'] = False defaults['drivers'] = ['virtualbox'] defaults['tld'] = '.dev' defaults['devroot'] = os.path.join('~', 'substance') defaults['current'] = OrderedDict() defaults['engine'] = None defaults['subenv'] = None return defaults def makeDefaultConfig(self, data=None): logger.info("Generating default substance configuration in %s", self.config.getConfigFile()) defaults = self.getDefaultConfig() for kkk, vvv in defaults.iteritems(): self.config.set(kkk, vvv) self.config.set("basePath", self.basePath) return self.config.saveConfig() # -- Use def setUse(self, engine, subenvName=None): ops = [self.setCurrentEngine(engine)] if subenvName: ops.append(engine.envSwitch(subenvName)) return Try.sequence(ops) def setCurrentEngine(self, engine): current = self.config.get('current') current.update({'engine': engine.name}) self.config.set('current', current) return OK(self) def readCurrentEngineName(self): current = self.config.get('current', {}) name = current.get('engine', None) if not name: return Fail( EngineNotFoundError( "No current engine is specified. Check the 'use' command for details." )) return OK(name) def loadCurrentEngine(self, name=None): current = self.config.get('current', {}) engineName = name if not engineName: engineName = current.get('engine', None) if not engineName: return Fail( EngineNotFoundError( "No current engine is specified. Check the 'use' command for details." )) engine = self.loadEngine(engineName) \ .bind(Engine.loadConfigFile) \ .bind(Engine.loadState) if engine.isFail(): return engine engine = engine.getOK() if engine.state is not EngineStates.RUNNING: return Fail( EngineNotRunning("Engine '%s' is not running." % engine.name)) return OK(engine) # -- Runtime def setAssumeYes(self, ay): self.assumeYes = True return True def getAssumeYes(self): if self.config.get('assumeYes', False): return True elif self.assumeYes: return True return False def getDefaultBoxString(self): return DefaultEngineBox # -- Engine library management def getEngines(self): ddebug("getEngines()") dirs = [ d for d in os.listdir(self.enginesPath) if os.path.isdir(os.path.join(self.enginesPath, d)) ] return OK(dirs) def loadEngines(self, engines=[]): return OK([self.loadEngine(x) for x in engines]) def loadEngine(self, name): enginePath = os.path.join(self.enginesPath, name) if not os.path.isdir(enginePath): return Fail( EngineNotFoundError("Engine \"%s\" does not exist." % name)) else: return OK(Engine(name, enginePath=enginePath, core=self)) def createEngine(self, name, config=None, profile=None): enginePath = os.path.join(self.enginesPath, name) newEngine = Engine(name, enginePath=enginePath, core=self) return newEngine.create(config=config, profile=profile) def removeEngine(self, name): return self.loadEngine(name) \ >> Engine.remove # -- Driver handling def getDrivers(self): return self.config.get('drivers', []) def validateDriver(self, driver): if driver in self.getDrivers(): return OK(driver) return Fail(ValueError("Driver '%s' is not a valid driver.")) def getDriver(self, name): cls = {'virtualbox': VirtualBoxDriver}.get(name, 'virtualbox') driver = cls(core=self) return driver # -- Link handling def getLink(self, type="ssh"): link = Link(keyFile=self.getInsecureKeyFile(), keyFormat='RSA') return link # -- Database def getDB(self): return self.db def initializeDB(self): db = DB(self.dbFile) db = db.initialize() if db.isFail(): return db self.db = db.getOK() return OK(self.db) # -- Box handling def readBox(self, boxstring): return Box.parseBoxString(boxstring) \ .map(lambda p: Box(core=self, **p)) def pullBox(self, box): return box.fetch() def removeBox(self, box): return box.delete() def getBoxes(self): return self.getDB().getBoxRecords() \ .mapM(lambda r: OK(Box(self, r.get('name'), r.get('version'), r.get('namespace'), r.get('registry'), r.get('boxstring'), r.get('archiveSHA1')))) def getInsecureKeyFile(self): return getSupportFile('support/substance_insecure') def getInsecurePubKeyFile(self): return getSupportFile('support/substance_insecure.pub')
class VirtualBoxDriver(Driver): ''' Substance VirtualBox driver class. Interface to virtual box manager. ''' def __init__(self, core): super(self.__class__, self).__init__(core) self.config = Config( os.path.join(self.core.getBasePath(), "virtualbox.yml")) def assertSetup(self): return self.assertConfig().then(self.assertNetworking) # -- Configuration def getDefaultConfig(self): defaults = OrderedDict() defaults['network'] = "172.21.21.0/24" defaults['interface'] = None return defaults def makeDefaultConfig(self): self.logAdapter.info("Generating default virtualbox config in %s" % self.config.getConfigFile()) defaults = self.getDefaultConfig() for kkk, vvv in defaults.items(): self.config.set(kkk, vvv) return self.config.saveConfig() def assertConfig(self): return self.config.loadConfigFile() \ .catchError(FileDoesNotExist, lambda err: self.makeDefaultConfig()) # ---- Networking setup def getNetworkInterface(self): return self.config.get('interface', None) def readNetworkConfig(self): netrange = IPNetwork(self.config.get('network')) netconfig = { 'gateway': netrange[1].format(), 'netmask': netrange.netmask.format(), 'lowerIP': netrange[5].format(), 'upperIP': netrange[-5].format() } return OK(netconfig) def assertNetworking(self): interface = self.getNetworkInterface() netconfig = self.readNetworkConfig() if interface: hoif = network.readHostOnlyInterface(interface) \ .catchError(VirtualBoxObjectNotFound, lambda err: OK(None)) dhcp = network.readDHCP(interface) \ .catchError(VirtualBoxObjectNotFound, lambda err: OK(None)) return Try.sequence((netconfig, hoif, dhcp)) \ .bind(self.assertNetworkConfiguration) else: return netconfig.bind(self.provisionNetworking) def assertNetworkConfiguration(self, netparts): (netconfig, hoif, dhcp) = netparts if not hoif: return self.provisionNetworking(netconfig) elif hoif.v4ip.format() != netconfig['gateway']: self.logAdapter.warning( "VirtualBox interface \"%s\" is not properly configured. Creating a new host-only network." % hoif.name) return self.provisionNetworking(netconfig) elif dhcp is None: self.logAdapter.warning( "VirtualBox interface \"%s\" does not have DHCP enabled. Re-Establishing now." % hoif.name) return self.provisionDHCP(hoif.name, netconfig) return OK(hoif) def findInterface(self, netconfig, interfaces): matches = list( filter(lambda x: x.v4ip == netconfig['gateway'], interfaces)) return OK(None) if len(matches) <= 0 else OK(matches[0]) def provisionNetworking(self, netconfig): self.logAdapter.info( "Provisioning VirtualBox networking for substance") hoif = network.readHostOnlyInterfaces() \ .bind(defer(self.findInterface, netconfig)) if hoif.isFail(): return hoif hoif = hoif.getOK() if not hoif: ifm = network.addHostOnlyInterface() if ifm.isFail(): return ifm iface = ifm.getOK() else: iface = hoif.name return network.configureHostOnlyInterface(iface, ip=netconfig['gateway'], netmask=netconfig['netmask']) \ .then(defer(self.provisionDHCP, interface=iface, netconfig=netconfig)) \ .then(defer(self.saveInterface, iface=iface)) \ .then(defer(network.readHostOnlyInterface, name=iface)) def provisionDHCP(self, interface, netconfig): self.logAdapter.info( "Provisioning DHCP service for host only interface") network.removeDHCP(interface).catch(lambda x: OK(interface)) \ .then(defer(network.addDHCP, interface, **netconfig)) def saveInterface(self, iface): self.config.set('interface', iface) return self.config.saveConfig() # ---- Machine API def importMachine(self, name, ovfFile, engineProfile=None): return self.assertSetup() \ .then(defer(machine.inspectOVF, ovfFile)) \ .bind(defer(machine.makeImportParams, name=name, engineProfile=engineProfile)) \ .bind(defer(machine.importOVF, ovfFile=ovfFile, name=name)) def startMachine(self, uuid): ''' Start the machine by driver identifier. ''' state = machine.readMachineState(uuid) if state.isFail(): return state if state.getOK() in [machine.MachineStates.PAUSED]: return machine.resume(uuid) else: return machine.start(uuid) def readMachineWaitForNetwork(self, uuid, timeout=5000): return machine.readWaitGuestProperty(uuid, "/VirtualBox/GuestInfo/Net", timeout) def readMachineNetworkInfo(self, uuid): def format(res): info = OrderedDict() info['sshPort'] = res[0].hostPort if res[0] else None info['sshIP'] = '127.0.0.1' info['privateIP'] = res[1] info['publicIP'] = res[2] return info return Try.sequence([ network.readPortForward(uuid, name="substance-ssh"), machine.readGuestProperty(uuid, "/VirtualBox/GuestInfo/Net/0/V4/IP"), machine.readGuestProperty(uuid, "/VirtualBox/GuestInfo/Net/1/V4/IP") ]).map(format) def configureMachine(self, uuid, engine): def engineFolderToVBoxFolder(acc, x): if x.mode == 'sharedfolder': acc.append( machine.SharedFolder(name=x.name, hostPath=x.hostPath)) return acc folders = reduce(engineFolderToVBoxFolder, engine.getEngineFolders(), []) desiredPort = engine.getSSHPort() return self.assertSetup() \ .then(defer(self.resolvePortConflict, uuid=uuid, desiredPort=desiredPort)) \ .then(defer(self.configureMachineAdapters, uuid=uuid)) \ .then(defer(self.configureMachineFolders, uuid=uuid, folders=folders)) \ .then(defer(self.configureMachineProfile, uuid=uuid, engine=engine)) def configureMachineProfile(self, uuid, engine): self.logAdapter.info("Configure machine profile") profile = engine.getProfile() return machine.configureProfile(uuid, profile.cpus, profile.memory) def configureMachineFolders(self, folders, uuid): self.logAdapter.info("Configure machine shared folders") return machine.clearSharedFolders(uuid) \ .then(defer(machine.addSharedFolders, folders=folders, uuid=uuid)) def resolvePortConflict(self, uuid, desiredPort): self.logAdapter.info("Checking for port conflicts") return network.readAllPortForwards(ignoreUUIDs=[uuid]) \ .bind(defer(self.determinePort, desiredPort=desiredPort)) \ .bind(defer(self.configurePort, uuid=uuid)) def determinePort(self, usedPorts, desiredPort): basePort = 4500 self.logAdapter.debug("Base port: %s" % basePort) self.logAdapter.debug("Desired port: %s" % desiredPort) unavailable = [x.hostPort for x in usedPorts] port = desiredPort if desiredPort >= basePort else basePort while port in unavailable or port < basePort: self.logAdapter.debug("Port %s is in use." % port) port += 1 self.logAdapter.info("Determined SSH port as %s" % port) return OK(port) def configurePort(self, port, uuid): self.logAdapter.debug("Configure substance-ssh port on port %s" % port) pf = network.PortForward("substance-ssh", 1, "tcp", None, port, None, 22) return network.removePortForwards([pf], uuid) \ .catch(lambda err: OK(None)) \ .then(defer(network.addPortForwards, [pf], uuid)) def configureMachineAdapters(self, uuid): interface = self.getNetworkInterface() adapters = [ machine.AdapterSettings(machine.AdapterTypes.NAT, 'default', None, False), machine.AdapterSettings(machine.AdapterTypes.HOSTONLY, interface, None, False), machine.AdapterSettings(machine.AdapterTypes.NONE, None, None, False), machine.AdapterSettings(machine.AdapterTypes.NONE, None, None, False) ] return Try.sequence([ machine.configureAdapter(uuid, i + 1, x) for i, x in enumerate(adapters) ]) def suspendMachine(self, uuid): ''' Suspend the machine. ''' return machine.suspend(uuid) def haltMachine(self, uuid): ''' Halt the machine. ''' return machine.halt(uuid) def terminateMachine(self, uuid): ''' Terminate the machine forcefully. ''' return machine.terminate(uuid) def deleteMachine(self, uuid): ''' Delete the machine by driver identifier. ''' return machine.delete(uuid) def exportMachine(self, uuid): # XXX To be implemented pass def getMachines(self): ''' Retrieve the list of machines and their driver identifiers. ''' return machine.readMachines() # -- Parse results from Virtual Box def getMachineID(self, name): ''' Retrieve the driver specific machine ID for a machine name. ''' return machine.findMachineID(name) def exists(self, uuid): ''' Check in the driver that the specified identifier exists. ''' return machine.readMachineExists(uuid) def isRunning(self, uuid): if self.getMachineState(uuid) is EngineStates.RUNNING: return True def isStopped(self, uuid): if self.getMachineState(uuid) is not EngineStates.RUNNING: return True def isSuspended(self, uuid): if self.getMachineState(uuid) is EngineStates.SUSPENDED: return True def getMachineState(self, uuid): ''' Retrieve the Substance machine state for this driver id ''' return machine.readMachineState(uuid) \ .bind(self.machineStateToEngineState) def machineStateToEngineState(self, vboxState): ''' Resolve a vbox machine state to a substance engine state. ''' mapping = { machine.MachineStates.POWEROFF: EngineStates.STOPPED, machine.MachineStates.SAVED: EngineStates.SUSPENDED, machine.MachineStates.PAUSED: EngineStates.SUSPENDED, machine.MachineStates.ABORTED: EngineStates.STOPPED, machine.MachineStates.STUCK: EngineStates.STOPPED, machine.MachineStates.RESTORING: EngineStates.STOPPED, machine.MachineStates.SNAPSHOTTING: EngineStates.STOPPED, machine.MachineStates.SETTING_UP: EngineStates.STOPPED, machine.MachineStates.ONLINE_SNAPSHOTTING: EngineStates.STOPPED, machine.MachineStates.RESTORING_SNAPSHOT: EngineStates.STOPPED, machine.MachineStates.DELETING_SNAPSHOT: EngineStates.STOPPED, machine.MachineStates.LIVE_SNAPSHOTTING: EngineStates.RUNNING, machine.MachineStates.RUNNING: EngineStates.RUNNING, machine.MachineStates.STARTING: EngineStates.RUNNING, machine.MachineStates.STOPPING: EngineStates.RUNNING, machine.MachineStates.SAVING: EngineStates.RUNNING, machine.MachineStates.UNKNOWN: EngineStates.UNKNOWN, machine.MachineStates.INACCESSIBLE: EngineStates.INEXISTENT, machine.MachineStates.INEXISTENT: EngineStates.INEXISTENT } state = mapping.get(vboxState, EngineStates.UNKNOWN) ddebug("Machine state: %s : %s", vboxState, state) return OK(state)