示例#1
0
文件: core.py 项目: ashouc/substance
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')
示例#2
0
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)