Example #1
0
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')
Example #2
0
class Engine(object):
    def __init__(self, name, enginePath, core):
        self.name = name
        self.enginePath = enginePath
        self.core = core
        self.link = None
        configFile = os.path.join(self.enginePath, "engine.yml")
        self.config = Config(configFile)
        self.logAdapter = EngineLogAdapter(logger, self.__dict__)
        self.currentEnv = None
        self.subenvConfig = None

    def getDefaults(self):
        defaults = OrderedDict()
        defaults['name'] = 'default'
        defaults['driver'] = 'virtualbox'
        defaults['id'] = None
        defaults['box'] = DefaultEngineBox
        defaults['profile'] = EngineProfile().__dict__
        defaults['docker'] = OrderedDict()
        defaults['docker']['port'] = 2375
        defaults['docker']['tls'] = False
        defaults['docker']['certificateFile'] = None
        defaults['network'] = OrderedDict()
        defaults['network']['privateIP'] = None
        defaults['network']['publicIP'] = None
        defaults['network']['sshIP'] = None
        defaults['network']['sshPort'] = 4500
        defaults['devroot'] = OrderedDict()
        defaults['devroot']['path'] = getHomeDirectory('devroot')
        defaults['devroot']['mode'] = 'unison'
        defaults['devroot']['syncArgs'] = [
            '-ignore', 'Path */var', '-ignore', 'Path */data'
        ]
        defaults['devroot']['excludes'] = [
            '{.*,*}.sw[pon]', '.bash*', '.composer', '.git', '.idea', '.npm',
            '.ssh', '.viminfo'
        ]
        aliases = ['make', 'composer', 'npm', 'heap', 'watch']
        defaults['aliases'] = {
            alias: {
                'container': 'web',
                'user': '******',
                'cwd': '/vol/website',
                'args': [alias]
            }
            for alias in aliases
        }
        defaults['aliases']['watch']['container'] = 'devs'
        defaults['aliases']['heap']['args'][0] = 'vendor/bin/heap'

        return defaults

    def validateConfig(self, config):
        fields = [
            'name', 'driver', 'profile', 'docker', 'network', 'devroot', 'box'
        ]

        ops = []
        ops.append(
            self.config.validateFieldsPresent(config.getConfig(), fields))
        ops.append(self.confValidateName(config.get('name', None)))
        ops.append(self.core.validateDriver(config.get('driver', None)))
        ops.append(self.confValidateDevroot(config.get('devroot', {})))

        def dd(err):
            print(("%s" % err))
            return ConfigValidationError(err.message)

        return Try.sequence(ops) \
            .catch(dd) \
            .then(lambda: OK(config.getConfig()))

    def confValidateName(self, name):
        if name != self.name:
            return Fail(
                ConfigValidationError(
                    "Invalid name property in configuration (got %s expected %s)"
                    % (config.get('name'), self.name)))
        return OK(name)

    def confValidateDevroot(self, devroot):
        path = devroot.get('path', '')
        path = os.path.expanduser(path)
        if not path:
            return Fail(
                ConfigValidationError(
                    "devroot path configuration is missing."))
        elif not os.path.isdir(path):
            self.logAdapter.info(
                "WARNING: devroot '%s' does not exist locally." % path)

        mode = devroot.get('mode', None)
        if not mode:
            return Fail(ConfigValidationError("devroot mode is not set."))
        elif mode not in ['sharedfolder', 'rsync', 'unison']:
            # XXX Fix hardcoded values.
            return Fail(
                ConfigValidationError("devroot mode '%s' is not valid." %
                                      mode))

        return OK(devroot)

    def getName(self):
        return self.name

    def getID(self):
        return self.config.id

    def getEnginePath(self):
        return self.enginePath

    def getEngineFilePath(self, fileName):
        return os.path.join(self.enginePath, fileName)

    def getDockerURL(self):
        if self.getDriverID():
            return "tcp://%s:%s" % (self.getPublicIP(), self.getDockerPort())

    def getDockerPort(self):
        return self.config.get('docker', {}).get('port', 2375)

    def getDockerEnv(self):
        env = OrderedDict()
        # env['DOCKER_API_VERSION'] = '1.19'
        env['DOCKER_HOST'] = self.getDockerURL()
        env['DOCKER_TLS_VERIFY'] = ''
        return env

    def getPublicIP(self):
        return self.config.get('network').get('publicIP', None)

    def getPrivateIP(self):
        return self.config.get('network').get('privateIP', None)

    def getDriver(self):
        return self.core.getDriver(self.config.get('driver'))

    def getEngineProfile(self):
        profile = self.config.get('profile', {})
        return EngineProfile(**profile)

    def getDriverID(self):
        return self.config.get('id', None)

    def setDriverID(self, driverID):
        self.config.set('id', driverID)
        return OK(self)

    def getSSHPort(self):
        return self.config.getBlockKey('network', 'sshPort', 22)

    def getSSHIP(self):
        return self.config.getBlockKey('network', 'sshIP', 'localhost')

    def getConnectInfo(self):
        info = {'hostname': self.getSSHIP(), 'port': self.getSSHPort()}
        return info

    def getDNSName(self):
        tld = self.core.config.get('tld')
        return self.name + tld

    def getSyncher(self):
        syncMode = self.config.get('devroot').get('mode')
        keyfile = self.core.getInsecureKeyFile()
        return UnisonSyncher(engine=self, keyfile=keyfile)

    def clearDriverID(self):
        self.config.set('id', None)
        return OK(self)

    def loadConfigFile(self):
        return self.config.loadConfigFile().bind(self.validateConfig).map(
            self.chainSelf)

    def loadSubenvConfigFile(self):
        basePath = os.path.expanduser(self.config.get('devroot').get('path'))
        self.subenvConfig = Config(
            os.path.join(basePath,
                         '%s/.substance/subenv.yml' % self.currentEnv))
        return self.subenvConfig.loadConfigFile().map(self.chainSelf)

    def loadState(self):
        ddebug("Engine load state %s" % self.name)
        return self.fetchState().bind(self.setState).map(self.chainSelf)

    def setState(self, state):
        self.state = state
        return OK(None)

    def create(self, config=None, profile=None):
        if os.path.isdir(self.enginePath):
            return Fail(
                EngineExistsError("Engine \"%s\" already exists." % self.name))

        return Shell.makeDirectory(self.enginePath) \
            .then(defer(self.makeDefaultConfig, config=config, profile=profile)) \
            .bind(self.__ensureDevroot) \
            .bind(self.validateConfig) \
            .then(self.config.saveConfig) \
            .bind(dinfo("Generated default engine configuration in %s", self.config.configFile)) \
            .map(self.chainSelf)

    def __ensureDevroot(self, config):
        devroot = os.path.expanduser(config.get('devroot', {}).get('path'))
        if not os.path.isdir(devroot):
            self.logAdapter.info("Creating devroot at %s" % devroot)
            return Shell.makeDirectory(devroot).then(lambda: OK(config))
        return OK(config)

    def remove(self):
        if not os.path.isdir(self.enginePath):
            return Fail(
                FileSystemError("Engine \"%s\" does not exist." % self.name))
        if self.isProvisioned():
            return Fail(
                EngineProvisioned("Engine \"%s\" is provisioned." % self.name))
        return Shell.nukeDirectory(self.enginePath)

    def makeDefaultConfig(self, config=None, profile=None):
        default = self.getDefaults()
        default["name"] = self.name
        default["devroot"]["path"] = os.path.join(
            self.core.config.get('devroot'), self.name)

        cf = {}
        mergeDict(cf, default)
        mergeDict(cf, config)

        if profile:
            cf['profile']['cpus'] = profile.cpus
            cf['profile']['memory'] = profile.memory

        for ck, cv in cf.items():
            self.config.set(ck, cv)

        return OK(self.config)

    def getProfile(self):
        prof = self.config.get('profile', {})
        return EngineProfile(prof.get('cpus'), prof.get('memory'))

    def isProvisioned(self):
        '''
        Check that this engine has an attached Virtual Machine.
        '''
        return True if self.getDriverID() else False

    def validateProvision(self):
        '''
        Check that the provision attached to this engine is valid in the driver.
        '''
        if not self.isProvisioned():
            return OK(False)

        machID = self.getDriverID()
        return self.getDriver().exists(machID)

    def isRunning(self):
        return self.fetchState().map(lambda state:
                                     (state is EngineStates.RUNNING))

    def isSuspended(self):
        return self.fetchState().map(lambda state:
                                     (state is EngineStates.SUSPENDED))

    def fetchState(self):
        if not self.isProvisioned():
            return OK(EngineStates.INEXISTENT)
        return self.getDriver().getMachineState(self.getDriverID())

    def launch(self):
        '''
        # 1. Check that we know about a provisioned machined for this engine. Provision if not.
        # 2. Validate that the machine we know about is still provisioned. Provision if not.
        # 3. Check the machine state, boot accordingly
        # 4. Setup guest networking
        # 4. Fetch the guest IP from the machine and store it in the machine state.
        '''
        return self.provision() \
            .then(self.start) \
            .then(self.updateNetworkInfo) \
            .then(self.addToHostsFile) \
            .then(self.envLoadCurrent) \
            .catchError(EnvNotDefinedError, lambda e: OK(None)) \
            .then(dinfo("Engine \"%s\" has been launched.", self.name)) \
            .then(self.envStart if self.currentEnv else lambda: OK(None))

    def addToHostsFile(self):
        self.logAdapter.info("Registering engine as '%s' in local hosts file" %
                             self.getDNSName())
        return SubHosts.register(self.getDNSName(), self.getPublicIP())

    def removeFromHostsFile(self):
        self.logAdapter.info("Removing engine from local hosts file")
        return SubHosts.unregister(self.getDNSName())

    def updateNetworkInfo(self):
        self.logAdapter.info("Updating network info from driver")
        return self.__readDriverNetworkInfo().bind(self.saveDriverNetworkInfo)

    def postLaunch(self):
        return self.setHostname() \
            .then(self.mountFolders)

    def saveDriverNetworkInfo(self, info):
        self.logAdapter.debug("Network information for machine: %s" % info)
        net = self.config.get('network', OrderedDict())
        net.update(info)
        self.config.set('network', net)
        return self.config.saveConfig()

    def start(self):

        state = self.fetchState()
        if state.isFail():
            return state

        if state.getOK() is EngineStates.RUNNING:
            return EngineAlreadyRunning("Engine \"%s\" is already running" %
                                        self.name)

        if state.getOK() is EngineStates.SUSPENDED:
            return self.__start().then(self.__waitForReady)
        else:
            return self.__configure() \
                .then(self.updateNetworkInfo) \
                .then(self.__start) \
                .then(self.__waitForReady) \
                .then(self.__waitForNetwork) \
                .then(self.postLaunch)

    def readBox(self):
        return self.core.readBox(self.config.get('box'))

    def provision(self):

        logger.info("Provisioning engine \"%s\" with driver \"%s\"", self.name,
                    self.config.get('driver'))

        prov = self.validateProvision()
        if prov.isFail():
            return prov
        elif prov.getOK():
            return OK(self)

        box = self.readBox().bind(Box.fetch)
        if box.isFail():
            return box
        box = box.getOK()

        return self.getDriver().importMachine(self.name, box.getOVFFile(), self.getEngineProfile()) \
            .bind(self.setDriverID) \
            .then(self.config.saveConfig) \
            .map(self.chainSelf)
        # .then(self.__configure) \
        # .then(self.updateNetworkInfo) \

    def deprovision(self):

        if not self.isProvisioned():
            return Fail(
                EngineNotProvisioned(
                    "Engine \"%s\" is not provisioned." % self.name, self))

        self.__cacheLink(None)

        return self.validateProvision() \
            .thenIfTrue(self.isRunning) \
            .thenIfTrue(self.__terminate) \
            .then(self.__delete) \
            .then(self.clearDriverID)  \
            .then(self.config.saveConfig)  \
            .then(self.removeFromHostsFile) \
            .then(dinfo("Engine \"%s\" has been deprovisioned.", self.name)) \
            .map(self.chainSelf)

    def suspend(self):
        if not self.isProvisioned():
            return Fail(
                EngineNotProvisioned("Engine \"%s\" is not provisioned." %
                                     self.name))
        self.__cacheLink(None)

        return self.validateProvision() \
            .thenIfTrue(self.isRunning) \
            .thenIfFalse(failWith(EngineNotRunning("Engine \"%s\" is not running." % self.name))) \
            .then(self.__suspend) \
            .then(dinfo("Engine \"%s\" has been suspended.", self.name)) \
            .map(chainSelf)

        # XXX Insert wait for suspension

    def stop(self, force=False):

        self.__cacheLink(None)

        operation = self.validateProvision() \
            .thenIfTrue(self.isRunning) \
            .thenIfFalse(failWith(EngineNotRunning("Engine \"%s\" is not running." % self.name)))

        if (force):
            operation >> self.__terminate >> dinfo(
                "Engine \"%s\" has been terminated.", self.name)
        else:
            operation >> self.__halt >> dinfo("Engine \"%s\" has been halted.",
                                              self.name)

        # XXX insert wait for stopped state
        return operation.map(self.chainSelf)

    def readLink(self):
        if self.link is not None:
            return OK(self.link)
        link = self.core.getLink()
        return link.connectEngine(self).map(self.__cacheLink)

    def __cacheCurrentEnv(self, lr):
        env = lr.stdout.strip()
        self.logAdapter.debug("Current environment: '%s'" % (env))
        self.currentEnv = env
        return lr

    def __cacheLink(self, link):
        self.link = link
        return link

    def __start(self, *args):
        return self.getDriver().startMachine(self.getDriverID()).map(
            self.chainSelf)

    def __suspend(self, *args):
        return self.getDriver().suspendMachine(self.getDriverID()).map(
            self.chainSelf)

    def __terminate(self, *args):
        return self.getDriver().terminateMachine(self.getDriverID()).map(
            self.chainSelf)

    def __halt(self, *args):
        return self.getDriver().haltMachine(self.getDriverID()).map(
            self.chainSelf)

    def __delete(self, *args):
        return self.getDriver().deleteMachine(self.getDriverID()).map(
            self.chainSelf)

    def __readDriverNetworkInfo(self):
        return self.getDriver().readMachineNetworkInfo(self.getDriverID())

    def __configure(self):
        return self.getDriver().configureMachine(self.getDriverID(), self)

    def shell(self):
        return self.readLink() >> Link.interactive

    def __waitForNetwork(self):
        self.logAdapter.info("Waiting for machine network...")
        return self.getDriver().readMachineWaitForNetwork(self.getDriverID())

    def __waitForReady(self):
        self.logAdapter.info("Waiting for machine to boot...")
        return self.readLink()

    def setHostname(self):
        self.logAdapter.info("Configuring machine hostname")
        dns = self.getDNSName()
        echoCmd = "echo %s > /etc/hostname" % dns
        hostCmd = "hostname %s" % dns
        hostsCmd = "sed -i \"s/127.0.1.1.*/127.0.1.1\t%s/g\" /etc/hosts" % self.name
        # hostsCmd = "hostnamectl set-hostname %s" % dns
        serviceCmd = "service hostname restart"
        cmd = "sudo -- /bin/sh -c '%s && %s && %s && %s'" % (
            echoCmd, hostsCmd, hostCmd, serviceCmd)
        cmds = list(
            map(defer(self.link.runCommand, stream=True, sudo=False), [cmd]))
        return Try.sequence(cmds)

    def exposePort(self, local_port, public_port, scheme):
        keyfile = self.core.getInsecureKeyFile()
        keyfile = inner(keyfile)
        forward_descr = "0.0.0.0:%s:127.0.0.1:%s" % (public_port, local_port)
        engineIP = self.getSSHIP()
        enginePort = str(self.getSSHPort())
        cmdPath = "ssh"
        cmdArgs = [
            "ssh", "-N", "-L", forward_descr, engineIP, "-l", "substance",
            "-p", enginePort, "-o", "StrictHostKeyChecking=no", "-o",
            "UserKnownHostsFile=/dev/null", "-i", keyfile
        ]
        sudo = False
        if public_port < 1024 and not isWithinWindowsSubsystem():
            sudo = True
        self.logAdapter.info(
            "Exposing port %s as %s; kill the process (CTRL-C) to un-expose.",
            local_port, public_port)
        with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s:
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
            if scheme:
                self.logAdapter.info("%s://%s:%s is now available.", scheme,
                                     ip, public_port)
            else:
                self.logAdapter.info(
                    "Others can now connect to port %s via IP %s.",
                    public_port, ip)
        Shell.execvp(cmdPath, cmdArgs, {}, sudo=sudo)

    def getOrchestrator(self):
        if self.subenvConfig:
            return self._getOrchestrator()

        return self.envLoadCurrent() \
            .then(self.loadSubenvConfigFile) \
            .catch(lambda err: OK(None)) \
            .then(self._getOrchestrator)

    def _getOrchestrator(self):
        orchestrator = self.subenvConfig.get(
            'orchestrator') if self.subenvConfig else None
        logger.debug("Orchestrator: %s" % orchestrator)
        if orchestrator and orchestrator == Orchestrators.COMPOSE:
            return OK(Compose(self))
        elif orchestrator and orchestrator != Orchestrators.DOCKWRKR:
            return Fail(
                InvalidOrchestrator('%s is not a known orchestrator' %
                                    orchestrator))
        else:
            return OK(Dockwrkr(self))

    def getOrchestratorCommand(self, command, *args):
        orch = self.getOrchestrator()
        if orch.isFail():
            raise orch.value
        else:
            orch = orch.getOK()

        if orch:
            return getattr(orch, command)(*args)
        else:
            return ''

    def orchestrate(self, command, *args):
        commands = self.getOrchestratorCommand(command, *args)
        if len(commands) > 0:
            return list(map(lambda x: "(subenv run %s)" % x, commands))

    def envSwitch(self, subenvName, restart=False):
        self.logAdapter.info("Switch engine '%s' to subenv '%s'" %
                             (self.name, subenvName))

        self.currentEnv = subenvName
        self.loadSubenvConfigFile()

        cmds = CommandList([
            "subenv init '/substance/devroot/%s'" % (subenvName),
            "subenv use '%s'" % (subenvName),
        ])

        if restart:
            cmds.append(self.orchestrate('resetAll'))
            cmds.append(self.orchestrate('login'))
            cmds.append(self.orchestrate('startAll'))

        return self.readLink() \
            .bind(Link.runCommand, cmds.logicAnd(), stream=True, sudo=False) \
            .then(self.envRegister)

    def envRegister(self):
        return self.readLink() \
            .bind(Link.runCommand, 'subenv vars', stream=False, capture=True) \
            .bind(self.__envRegister)

    def __envRegister(self, lr):
        vars = lr.stdout.split("\n")
        env = Try.attempt(parseDotEnv, vars)
        if env.isFail():
            return env
        env = env.getOK()
        if 'SUBENV_FQDN' in env:
            return SubHosts.register(env['SUBENV_FQDN'], self.getPublicIP())
        elif 'SUBENV_NAME' in env:
            return SubHosts.register(
                env['SUBENV_NAME'] + self.core.config.get('tld'),
                self.getPublicIP())

    def envLoadCurrent(self):
        cmds = CommandList(["subenv current"])
        return self.readLink() \
            .bind(Link.runCommand, cmds.logicAnd(), stream=False, sudo=False, capture=True) \
            .map(self.__cacheCurrentEnv) \
            .catch(lambda err: Fail(EnvNotDefinedError("No current subenv is set. Check 'switch' for detais."))) \
            .then(lambda: self)

    def envStart(self, reset=False, containers=[]):
        cmds = CommandList()

        if reset:
            cmds.append(self.getOrchestratorCommand('resetAll'))

        if len(containers) > 0:
            self.logAdapter.info("Starting %s container(s)" %
                                 (' '.join(containers)))
            cmds.append(self.orchestrate('start', containers))
        else:
            self.logAdapter.info("Starting all containers...")
            cmds.append(self.orchestrate('startAll'))

        return self.readLink() \
            .bind(Link.runCommand, cmds.logicAnd(), stream=True, sudo=False)

    def envRestart(self, time=10, containers=[]):
        cmds = CommandList()

        if len(containers) > 0:
            self.logAdapter.info("Restarting %s containers" %
                                 (' '.join(containers)))
            cmds.append(self.orchestrate('restart', containers, time))
        else:
            self.logAdapter.info("Restarting all containers...")
            cmds.append(self.orchestrate('restartAll', time))

        return self.readLink() \
            .bind(Link.runCommand, cmds.logicAnd(), stream=True, sudo=False)

    def envRecreate(self, time=10, containers=[]):
        cmds = CommandList()

        if len(containers) > 0:
            self.logAdapter.info("Recreating %s containers" %
                                 (' '.join(containers)))
            cmds.append(self.orchestrate('recreate', containers, time))
        else:
            self.logAdapter.info("Recreating all containers...")
            cmds.append(self.orchestrate('recreateAll', time))

        return self.readLink() \
            .bind(Link.runCommand, cmds.logicAnd(), stream=True, sudo=False)

    def envShell(self, container=None, user=None, cwd=None):
        if container:
            return self.envEnter(container, user, cwd)
        else:
            return self.readLink().bind(Link.interactive)

    def envEnter(self, container, user=None, cwd=None):
        self.logAdapter.info("Entering %s container..." % (container))
        return self.envExec(container, ["exec /bin/bash"], cwd, user)

    def envExec(self, container, cmd, cwd=None, user=None):
        cmd = CommandList(self.orchestrate('exec', container, cmd, cwd, user))
        return self.readLink().bind(Link.runCommand,
                                    cmd=cmd.logicAnd(),
                                    interactive=True,
                                    stream=True,
                                    shell=False,
                                    capture=False)

    def envRun(self, task, args):
        cmd = CommandList(self.orchestrate('run', task, args))
        return self.readLink().bind(Link.runCommand,
                                    cmd=cmd.logicAnd(),
                                    interactive=True,
                                    stream=True,
                                    shell=False,
                                    capture=False)

    def envExecAlias(self, alias, args):
        aliases = self.config.get('aliases') if self.config.get(
            'aliases') else {}
        subenvAliases = self.subenvConfig.get(
            'aliases') if self.subenvConfig.get('aliases') else {}
        if subenvAliases:
            aliases = mergeDictOverwrite(aliases, subenvAliases)

        if aliases and alias in aliases:
            cmd = aliases[alias]
            args = cmd['args'] + args
            return self.envExec(container=cmd['container'],
                                cmd=args,
                                cwd=cmd['cwd'],
                                user=cmd['user'])
        return Fail(
            InvalidCommandError(
                "Invalid command '%s' specified for '%s'.\n\nUse 'substance help' for available commands."
                % (alias, 'substance')))

    def envStop(self, time=10, containers=[]):
        cmds = CommandList()

        if len(containers) > 0:
            self.logAdapter.info("Stopping %s container(s)" %
                                 (' '.join(containers)))

            cmds.append(self.orchestrate('stop', containers, time))
        else:
            self.logAdapter.info("Stopping containers...")
            cmds.append(self.orchestrate('stopAll', time))

        return self.readLink() \
            .bind(Link.runCommand, cmds.logicAnd(), stream=True, sudo=False)

    def envStatus(self, full=False):
        if full:
            cmds = CommandList([self.orchestrate('status')])
            return self.readLink() \
                .bind(Link.runCommand, cmds.logicAnd(), stream=False, sudo=False, capture=True) \
                .map(self.__envStatus)
        else:
            return OK(self.__envStatus())

    def envCleanup(self):
        cmds = CommandList()
        cmds.append('docker system prune -a')
        return self.readLink() \
            .bind(Link.runCommand, cmds.logicAnd(), interactive=True, stream=True, shell=False, capture=False)

    def envLogs(self, parts=[], pattern=None, follow=True, lines=None):

        if not pattern:
            pattern = "%s*.log" % ('-'.join(parts))

        cmds = CommandList()
        cmd = "tail"
        if follow:
            cmd += " -f"
        if lines:
            cmd += " -n %s" % int(lines)

        cmd += " \"logs/%s\"" % pattern

        cmds.append('subenv run %s' % cmd)
        return self.readLink() \
            .bind(Link.runCommand, cmds.logicAnd(), stream=True, capture=False, sudo=False)

    def envListLogs(self, parts=[], pattern=None):

        if not pattern:
            pattern = "%s*.log" % ('-'.join(parts))

        cmds = CommandList()
        cmd = "ls -1 \"logs/%s\" | xargs -n1 basename" % pattern

        cmds.append('subenv run %s' % cmd)
        return self.readLink() \
            .bind(Link.runCommand, cmds.logicAnd(), stream=True, capture=False, sudo=False)

    def __envStatus(self, containers=None):
        return {
            'engine':
            self,
            'containers':
            containers.stdout + containers.stderr if containers else None
        }

    def envDocker(self, command):
        cmd = "docker %s" % command
        logger.debug("DOCKER: %s" % cmd)
        return self.readLink().bind(Link.runCommand,
                                    cmd,
                                    stream=True,
                                    interactive=True,
                                    shell=False)

    def getEngineFolders(self):
        # XXX Dynamic mounts / remove hardcoded values.
        devroot = self.config.get('devroot')
        pfolder = EngineFolder(name='devroot',
                               mode=devroot.get('mode'),
                               hostPath=os.path.expanduser(
                                   devroot.get('path')),
                               guestPath='/substance/devroot',
                               uid=1000,
                               gid=1000,
                               umask="0022",
                               excludes=devroot.get('excludes', []),
                               syncArgs=devroot.get('syncArgs', []))
        return [pfolder]

    def mountFolders(self):
        self.logAdapter.info("Mounting engine folders")
        folders = self.getEngineFolders()
        return Try.of(list(map(self.mountFolder, folders)))

    def mountFolder(self, folder):
        mountCmd = "mount -t vboxsf -o umask=%(umask)s,gid=%(gid)s,uid=%(uid)s %(name)s %(guestPath)s" % folder.__dict__
        mkdirCmd = "mkdir -p %(guestPath)s && chown -R %(uid)s:%(gid)s %(guestPath)s" % folder.__dict__
        # XXX Make this non VBOX specific
        chain = self.link.runCommand(mkdirCmd, sudo=True)
        if folder.mode is 'sharedfolder':
            chain = chain.then(defer(self.link.runCommand, mountCmd,
                                     sudo=True))
        return chain

    chainSelf = chainSelf

    def __repr__(self):
        return "Engine(%s)" % self.name
Example #3
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)