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
class SubenvSpec(object): def __init__(self, specPath, basePath, name=None, vars={}, lastApplied=None): self.specPath = specPath self.basePath = basePath self.envPath = None self.name = name self.envFiles = [] self.overrides = vars self.vars = OrderedDict() self.lastApplied = lastApplied self.current = False self.struct = {'files': [], 'dirs': []} def setEnvPath(self, path): self.envPath = path self.config = Config(os.path.join(self.envPath, CONFFILE)) @staticmethod def fromEnvPath(path): if not os.path.isdir(path): return Fail( InvalidOptionError("Specified path '%s' does not exist." % path)) envPath = path name = os.path.basename(envPath) envVars = Try.attempt(readDotEnv, os.path.join(envPath, ENVFILE)) if envVars.isFail(): return envVars envVars = envVars.getOK() reserved = [ 'SUBENV_NAME', 'SUBENV_LASTAPPLIED', 'SUBENV_ENVPATH', 'SUBENV_SPECPATH', 'SUBENV_BASEPATH' ] vars = envVars.copy() for k in vars.keys(): if k in reserved: del vars[k] lastApplied = None if 'SUBENV_LASTAPPLIED' in envVars: lastApplied = envVars['SUBENV_LASTAPPLIED'] env = SubenvSpec(envVars['SUBENV_SPECPATH'], envVars['SUBENV_BASEPATH'], envVars['SUBENV_NAME'], vars, lastApplied) env.setEnvPath(envPath) return env @staticmethod def fromSpecPath(path, vars={}): if not os.path.isdir(path): return Fail( InvalidEnvError("Specified path '%s' does not exist." % path)) if os.path.basename(path) == SPECDIR or not os.path.isdir( os.path.join(path, SPECDIR)): return Fail( InvalidOptionError( "Invalid path specified. Please pass a path to a folder with a %s directory." % SPECDIR)) specPath = os.path.join(path, SPECDIR) name = os.path.basename(path) return SubenvSpec(specPath, path, name, vars).scan() def getLastAppliedDateTime(self, fmt='%Y-%m-%d %H:%M:%S'): if self.lastApplied: return time.strftime(fmt, time.localtime(float(self.lastApplied))) return None def scan(self): return self.loadEnvVars(self.overrides) \ .then(self.loadEnvStruct) def applyTo(self, envPath): self.setEnvPath(envPath) return self.clearEnv() \ .then(self.applyDirs) \ .then(self.applyFiles) \ .then(self.writeEnv) \ .then(self.linkCode) \ .then(self.assertConfig) \ .then(self.applyScript) \ .then(lambda: OK(self)) def linkCode(self): return Try.sequence([ Try.attempt(makeSymlink, self.basePath, os.path.join(self.envPath, CODELINK), True), Try.attempt(makeSymlink, self.specPath, os.path.join(self.envPath, SPECDIR), True) ]) def clearEnv(self): if os.path.isfile(self.config.configFile): return Shell.rmFile(self.config.configFile) return OK(None) def writeEnv(self): dotenv = os.path.join(self.envPath, ENVFILE) logger.debug("Writing environment to: %s" % dotenv) envVars = OrderedDict(self.vars, **self.overrides) envVars.update({ 'SUBENV_NAME': self.name, 'SUBENV_LASTAPPLIED': time.time(), 'SUBENV_ENVPATH': self.envPath, 'SUBENV_SPECPATH': self.specPath, 'SUBENV_BASEPATH': self.basePath }) env = "\n".join(["%s=\"%s\"" % (k, v) for k, v in envVars.iteritems()]) return Try.attempt(writeToFile, dotenv, env) def assertConfig(self): if not os.path.isfile(self.config.configFile): return OK({}) return self.config.loadConfigFile() def applyScript(self): commands = self.config.get('script', []) return Try.sequence(map(self.applyCommand, commands)) def applyCommand(self, cmd): logger.info("Running environment command: %s" % cmd) return Shell.call(cmd, cwd=self.envPath, shell=True) def applyDirs(self): ops = [Shell.makeDirectory(self.envPath, 0750)] for dir in self.struct['dirs']: sourceDir = os.path.join(self.specPath, dir) destDir = os.path.join(self.envPath, dir) mode = os.stat(sourceDir).st_mode & 0777 logger.debug("Creating directory '%s' mode: %s" % (dir, mode)) ops.append( Shell.makeDirectory(destDir).bind(defer(Shell.chmod, mode=mode))) return Try.sequence(ops) def applyFiles(self): ops = [] for file in self.struct['files']: fname, ext = os.path.splitext(file) source = os.path.join(self.specPath, file) dest = os.path.join(self.envPath, file) if fname == ENVFILE: continue elif ext == '.jinja': logger.debug("Rendering '%s' to %s" % (file, dest)) dest = os.path.splitext(dest)[0] ops.append(self.renderFile(file, dest, self.vars)) else: logger.debug("Copying '%s' to %s" % (file, dest)) ops.append( Shell.copyFile(os.path.join(self.specPath, file), os.path.join(self.envPath, file))) return Try.sequence(ops) def getEnvVars(self): envVars = self.overrides.copy() envVars.update({ 'SUBENV_NAME': self.name, 'SUBENV_LASTAPPLIED': self.lastApplied, 'SUBENV_ENVPATH': self.envPath, 'SUBENV_SPECPATH': self.specPath, 'SUBENV_BASEPATH': self.basePath }) return envVars def renderFile(self, source, dest, vars={}): try: tplEnv = jinja2.Environment( loader=jinja2.FileSystemLoader(self.specPath)) tpl = tplEnv.get_template(source) tplVars = self.getEnvVars() tplVars.update({'subenv': self}) tplVars.update({'SUBENV_VARS': self.getEnvVars()}) with open(dest, 'wb') as fh: fh.write(tpl.render(**tplVars)) return OK(None) except Exception as err: return Fail(err) def loadEnvStruct(self): return self.scanEnvStruct().bind(self.setEnvStruct) def scanEnvStruct(self): struct = {'dirs': [], 'files': []} for root, dirs, files in os.walk(self.specPath): relPath = os.path.relpath(root, self.specPath).strip('./').strip('/') for dir in dirs: struct['dirs'].append(os.path.join(relPath, dir)) for file in files: struct['files'].append(os.path.join(relPath, file)) return OK(struct) def setEnvStruct(self, struct): self.struct = struct return OK(self) def setEnvVars(self, e={}): self.overrides = e return OK(self) def loadEnvVars(self, env={}): return self.readEnvFiles() \ .map(lambda e: dict(e, **env)) \ .bind(self.setEnvVars) def readEnvFiles(self): specEnvFile = os.path.join(self.specPath, ENVFILE) if os.path.isfile(specEnvFile): self.envFiles.append(specEnvFile) baseEnvFile = os.path.join(self.basePath, ENVFILE) if os.path.isfile(baseEnvFile): self.envFiles.append(baseEnvFile) map(lambda x: logger.info("Loading dotenv file: '%s'" % x), self.envFiles) return Try.sequence(map(Try.attemptDeferred(readDotEnv), self.envFiles)) \ .map(lambda envs: reduce(lambda acc, x: dict(acc, **x), envs, {})) def __repr__(self): return "SubEnvSpec(%(name)s) spec:%(specPath)s base:%(basePath)s envFile:%(envFiles)s vars:%(vars)s files:%(struct)s" % self.__dict__
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)
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')
def main(self): tpl = self.getArg(0) if not tpl: return self.exitError("You MUST provide a template name or git repository URL!") ref = self.getArg(1) if not ref: ref = "master" if not tpl.startswith('ssh://') and not tpl.startswith('https://') and not tpl.startswith('file://'): splt = tpl.split("/", 2) if len(splt) == 2: tpl = 'https://github.com/%s/%s/archive/%s.tar.gz' % ( splt[0], splt[1], ref) else: tpl = 'https://github.com/turbulent/template-%s/archive/%s.tar.gz' % ( tpl, ref) cwd = os.getcwd() if os.listdir(cwd): print "\n!!! Current directory is not empty! Some files may be overwritten !!!\n" print "You are about to hatch a new project in the current working directory." print " Template used: %s" % tpl print " Ref (version): %s" % ref if not tpl.endswith('.tar.gz'): print " Ref (version): %s" % ref print " Path : %s" % cwd print "" if not self.confirm("Are you SURE you wish to proceed?"): return self.exitOK("Come back when you've made up your mind!") print "Downloading template archive..." if tpl.endswith('.tar.gz'): # With tar archives, everything is usually packaged in a single directory at root of archive strip = int(self.getOption('strip')) urllib.urlretrieve(tpl, 'tpl.tar.gz') else: strip = 0 # git archive never packages in a single root directory if self.proc(['git', 'archive', '-o', 'tpl.tar.gz', '--remote=' + tpl, ref]): return self.exitError('Could not download template %s@%s!' % (tpl, ref)) print "Extracting template archive..." if self.proc(['tar', '--strip', str(strip), '-xf', 'tpl.tar.gz']): return self.exitError('Could not extract template archive!') # Acquire list of files print "Getting list of files in template..." out = subprocess.check_output(['tar', '-tf', 'tpl.tar.gz'], universal_newlines=True) tplFiles = ['/'.join(l.split('/')[strip:]) for l in out.split('\n') if l] tplFiles = [l for l in tplFiles if os.path.isfile(l)] print "Cleaning up template archive..." if self.proc(['rm', 'tpl.tar.gz']): return self.exitError('Could not unlink temporary template archive!') # At this point, we have all the files we need hatchfile = os.path.join('.substance', 'hatch.yml') if os.path.isfile(hatchfile): config = Config(hatchfile) res = config.loadConfigFile() if res.isFail(): return self.exitError("Could not open %s for reading: %s" % (hatchfile, res.getError())) # Execute pre-script if any for cmd in config.get('pre-script', []): print(cmd) subprocess.call(cmd, shell=True) vardefs = config.get('vars', {}) # Autogenerate a secret chars = string.ascii_lowercase + string.ascii_uppercase + string.digits variables = { '%hatch_secret%': ''.join(random.SystemRandom().choice(chars) for _ in range(32)) } if vardefs: print "This project has variables. You will now be prompted to enter values for each variable." for varname in vardefs: val = '' required = vardefs[varname].get('required', False) default = vardefs[varname].get('default', '') description = vardefs[varname].get('description', '') while not val: val = raw_input("%s (%s) [%s]: " % ( varname, description, default)) if default and not val: val = default if not required: break variables[varname] = val summary = "\n".join([" %s: %s" % (k, variables[k]) for k in variables.keys()]) print "Summary: " print summary if not self.confirm("OK to replace tokens?"): return self.exitOK("Operation aborted.") print "Replacing tokens in files..." sed = "; ".join(["s/%s/%s/g" % (k, variables[k].replace('/', '\\/')) for k in variables.keys()]) for tplFile in tplFiles: if self.proc(['sed', '-i.orig', sed, tplFile]): return self.exitError("Could not replace variables in files!") bakFile = tplFile + ".orig" if os.path.isfile(bakFile): if self.proc(['rm', bakFile]): logger.warn( "Could not unlink backup file %s; you may have to remove it manually.", bakFile) # Execute post-script if any for cmd in config.get('post-script', []): print(cmd) subprocess.call(cmd, shell=True) # Remove hatchfile if self.proc(['rm', hatchfile]): return self.exitError('Could not unlink %s!' % hatchfile) print "Project hatched!" return self.exitOK()