def fork(self,migrate=False): debug("fork args",self.args) if self.volume.wip(): error("local changes not checked in") return if len(self.args) < 1: error("missing new branch name") return newbranch = self.args[0] debug("newbranch",newbranch) if migrate and newbranch == self.volname: error("%s is already on %s branch" % ( os.environ['HOSTNAME'], newbranch) ) return newvolume = ISFS.Volume( newbranch,logname=self.logname,outpin=self.outpin, histfile=self.histfile) yield kernel.wait(self.volume.pull(bg=True)) yield kernel.wait(newvolume.pull(bg=True)) if not migrate: if not newvolume.journal.copy(self.volume.journal): error("%s already exists -- did you mean 'migrate'?" % newbranch) return info("branch %s created" % (newbranch)) else: if not self.volume.journal.migrate(newvolume.journal): error("migration of this host to %s failed: conflicting changes: %s is not a superset of %s" % (newbranch,newbranch,self.volname)) return branch(newbranch) self.volname = newbranch self.volume = newvolume info("%s migrated to %s" % (os.environ['HOSTNAME'], newbranch))
def pull(self, bg=False): files = (self.mkrelative(self.p.journal), self.mkrelative(self.p.lock)) yield kernel.wait(self.pullfiles(files)) files = self.pendingfiles() if bg: kernel.spawn(self.pullfiles(files)) else: yield kernel.wait(self.pullfiles(files))
def pull(self,bg=False): files = ( self.mkrelative(self.p.journal), self.mkrelative(self.p.lock) ) yield kernel.wait(self.pullfiles(files)) files = self.pendingfiles() if bg: kernel.spawn(self.pullfiles(files)) else: yield kernel.wait(self.pullfiles(files))
def ci(self): wipdata = self.wip() if not wipdata: if not self.cklock(): return info("no outstanding updates") self.unlock() return if not self.cklock(): return jtime = self.journal.mtime() yield kernel.wait(self.pull()) if not self.cklock(): return # if jtime and jtime != self.journal.mtime(): if jtime != self.journal.mtime(): error( "someone else checked in conflicting changes -- repair wip and retry" ) return if self.journal.mtime() > getmtime_int(self.p.wip): error("journal is newer than wip -- repair and retry") return self.journal.addraw(wipdata) # XXX announce all new block files here (rather than in addwip) os.unlink(self.p.wip) self.announce(self.p.journal) info("changes checked in") self.unlock()
def ihaveRx(self,msg,ip): yield None scheme = msg['scheme'] port = msg['port'] path = msg['file'] mtime = msg.head.mtime # XXX is python's pseudo-random good enough here? # # probably, but for other cases, use 'gpg --gen-random 2 16' # to generate 128 bits of random data from entropy # challenge = str(random.random()) url = "%s://%s:%s/%s?challenge=%s" % (scheme,ip,port,path,challenge) path = path.lstrip('/') # simple check to ignore foreign domains # XXX probably want to make this a list of domains domain = os.environ['IS_DOMAIN'] if not path.startswith(domain + '/'): debug("foreign domain, ignoring: %s" % path) return fullpath = os.path.join(self.p.cache,path) mymtime = 0 debug("checking",url) if os.path.exists(fullpath): mymtime = getmtime_int(fullpath) if mtime > mymtime: debug("remote is newer:",url) if self.req.has_key(path): self.req[path]['state'] = SENDME yield kernel.wait(self.wget(path,url,challenge)) elif mtime < mymtime: debug("remote is older:",url) self.ihaveTx(path) else: debug("remote and local times are the same:",path,mtime,mymtime)
def Exec(self): yield None cwd = self.opt['cwd'] if not len(self.data): error(iserrno.EINVAL,"missing exec command") return yield kernel.wait(self.volume.Exec(self.data,cwd))
def Exec(self, argdata, cwd): # XXX what about when cwd != volroot? if not self.cklock(): return msg = FBP.mkmsg('exec', argdata + "\n", cwd=cwd) yield kernel.wait(self.addwip(msg)) argv = argdata.split("\n") info("exec done:", str(argv))
def Exec(self,argdata,cwd): # XXX what about when cwd != volroot? if not self.cklock(): return msg = FBP.mkmsg('exec', argdata + "\n", cwd=cwd) yield kernel.wait(self.addwip(msg)) argv = argdata.split("\n") info("exec done:", str(argv))
def close(self): # XXX this whole File class is a kludge, and really needs to # be gotten rid of and the useful bits moved into ISconf as part # of the refactoring to turn ISFS into ISDM # # build journal transaction message os.close(self.tmp) fbp = fbp822() parent = os.path.dirname(self.path) pathmodes = [] while True: pstat = os.stat(parent) mug = "%d:%d:%d" % (pstat.st_mode, pstat.st_uid, pstat.st_gid) pathmodes.insert(0, mug) if len(parent) == 1: assert parent == '/' break parent = os.path.dirname(parent) assert len(parent) >= 1 # XXX pathname needs to be relative to volroot msg = fbp.mkmsg('snap', '', pathname=self.path, st_mode=self.st.st_mode, st_uid=self.st.st_uid, st_gid=self.st.st_gid, st_atime=self.st.st_atime, st_mtime=self.st.st_mtime, pathmodes=','.join(pathmodes)) debug("calling addwip") yield kernel.wait(self.volume.addwip(msg=msg, tmpfn=self.tmpfn)) self.volume.closefile(self) info("snapshot done:", self.path)
def close(self): # XXX this whole File class is a kludge, and really needs to # be gotten rid of and the useful bits moved into ISconf as part # of the refactoring to turn ISFS into ISDM # # build journal transaction message os.close(self.tmp) fbp = fbp822() parent = os.path.dirname(self.path) pathmodes = [] while True: pstat = os.stat(parent) mug = "%d:%d:%d" % (pstat.st_mode,pstat.st_uid,pstat.st_gid) pathmodes.insert(0,mug) if len(parent) == 1: assert parent == '/' break parent = os.path.dirname(parent) assert len(parent) >= 1 # XXX pathname needs to be relative to volroot msg = fbp.mkmsg('snap','', pathname=self.path, st_mode = self.st.st_mode, st_uid = self.st.st_uid, st_gid = self.st.st_gid, st_atime = self.st.st_atime, st_mtime = self.st.st_mtime, pathmodes = ','.join(pathmodes) ) debug("calling addwip") yield kernel.wait(self.volume.addwip(msg=msg,tmpfn=self.tmpfn)) self.volume.closefile(self) info("snapshot done:", self.path)
def ci(self): wipdata = self.wip() if not wipdata: if not self.cklock(): return info("no outstanding updates") self.unlock() return if not self.cklock(): return jtime = self.journal.mtime() yield kernel.wait(self.pull()) if not self.cklock(): return # if jtime and jtime != self.journal.mtime(): if jtime != self.journal.mtime(): error("someone else checked in conflicting changes -- repair wip and retry") return if self.journal.mtime() > getmtime_int(self.p.wip): error("journal is newer than wip -- repair and retry") return self.journal.addraw(wipdata) # XXX announce all new block files here (rather than in addwip) os.unlink(self.p.wip) self.announce(self.p.journal) info("changes checked in") self.unlock()
def snap(self): debug("starting snap") yield None if not len(self.args): error(iserrno.EINVAL,"missing snapshot pathname") return if len(self.args) > 1: error(iserrno.EINVAL, "can only snapshot one file at a time (for now)") return path = self.args[0] cwd = self.opt['cwd'] path = os.path.join(cwd,path) if not os.path.exists(path): error(iserrno.ENOENT,path) return if not os.path.isfile(path): error(iserrno.EINVAL,"%s is not a file" % path) return st = os.stat(path) src = open(path,'r') debug("calling open") dst = self.volume.open(path,'w') if not dst: return dst.setstat(st) while True: yield kernel.sigbusy data = src.read(1024 * 1024 * 1) if not len(data): break dst.write(data) src.close() debug("calling close") yield kernel.wait(dst.close())
def lock(self): message = '' if self.opt['message']: message = self.opt['message'] if self.args: message = "%s %s" % (message,' '.join(self.args)) message = message.strip() if not len(message): error(iserrno.NEEDMSG,'did not lock %s' % self.volname) return yield kernel.wait(self.volume.lock(message))
def process(self,transport,outpin): while True: yield None # read messages from client mlist = FBP.fromFile(stream=transport) for msg in mlist: if msg in (kernel.eagain,None): continue if outpin.state == 'down': return if msg is kernel.eof: # outpin.close() return debug("from client:", str(msg)) rectype = msg.type() if rectype != 'cmd': error(iserrno.EINVAL, "first message must be cmd, got %s" % rectype) return self.verbose = msg.head.verbose self.debug = msg.head.debug data = msg.payload() opt = dict(msg.items()) if opt['message'] == 'None': opt['message'] = None # XXX why don't we just pass the whole message to Ops()? opt['reboot_ok'] = msg.head.reboot_ok debug(opt) verb = msg['verb'] debug("verb in process",verb) if verb != 'lock' and opt['message'] != None: error(iserrno.EINVAL, "-m is only valid on lock") if verb == 'exec': verb = 'Exec' # sigh if verb == 'shutdown': kernel.shutdown() return args=[] if len(data): data = data.strip() args = data.split('\n') ops = Ops(opt=opt,args=args,data=data,outpin=outpin) try: func = getattr(ops,verb) except AttributeError: error(outpin,iserrno.EINVAL,verb) return # start command processor, wait for it to finish debug("process calling",verb) yield kernel.wait(func()) return # XXX can't handle stdin yet
def update(self, reboot_ok=False): fbp = fbp822() if self.wip(): error("local changes not checked in") return info("checking for updates") yield kernel.wait(self.pull()) pending = self.pending() if not len(pending): info("no new updates") return for msg in pending: debug(msg['pathname'], time.time()) # XXX XXX XXX OUCH!!! using 'wait' here keeps us from # XXX XXX XXX checking return codes; execution of journal # XXX XXX XXX will always continue on error if msg.type() == 'snap': yield kernel.wait(self.updateSnap(msg)) if msg.type() == 'exec': yield kernel.wait(self.updateExec(msg)) if msg.type() == 'reboot': yield kernel.wait(self.updateReboot(msg, reboot_ok)) info("update done")
def update(self,reboot_ok=False): fbp = fbp822() if self.wip(): error("local changes not checked in") return info("checking for updates") yield kernel.wait(self.pull()) pending = self.pending() if not len(pending): info("no new updates") return for msg in pending: debug(msg['pathname'],time.time()) # XXX XXX XXX OUCH!!! using 'wait' here keeps us from # XXX XXX XXX checking return codes; execution of journal # XXX XXX XXX will always continue on error if msg.type() == 'snap': yield kernel.wait(self.updateSnap(msg)) if msg.type() == 'exec': yield kernel.wait(self.updateExec(msg)) if msg.type() == 'reboot': yield kernel.wait(self.updateReboot(msg,reboot_ok)) info("update done")
def lock(self,message): logname = self.logname message = "%s@%s: %s" % (logname,os.environ['HOSTNAME'],str(message)) yield kernel.wait(self.pull()) if self.locked() and not self.cklock(): return pending = self.pending() if len(pending): error("local node is out of date -- try 'isconf up' first") return open(self.p.lock,'w').write(message) self.announce(self.p.lock) info("%s locked" % self.volname) if not self.locked(): error(iserrno.NOTLOCKED,'attempt to lock %s failed' % self.volname)
def lock(self, message): logname = self.logname message = "%s@%s: %s" % (logname, os.environ['HOSTNAME'], str(message)) yield kernel.wait(self.pull()) if self.locked() and not self.cklock(): return pending = self.pending() if len(pending): error("local node is out of date -- try 'isconf up' first") return open(self.p.lock, 'w').write(message) self.announce(self.p.lock) info("%s locked" % self.volname) if not self.locked(): error(iserrno.NOTLOCKED, 'attempt to lock %s failed' % self.volname)
def ihaveRx(self, msg, ip): yield None scheme = msg['scheme'] port = msg['port'] path = msg['file'] mtime = msg.head.mtime # XXX is python's pseudo-random good enough here? # # probably, but for other cases, use 'gpg --gen-random 2 16' # to generate 128 bits of random data from entropy # challenge = str(random.random()) url = "%s://%s:%s/%s?challenge=%s" % (scheme, ip, port, path, challenge) path = path.lstrip('/') # simple check to ignore foreign domains # XXX probably want to make this a list of domains domain = os.environ['IS_DOMAIN'] if not path.startswith(domain + '/'): debug("foreign domain, ignoring: %s" % path) return fullpath = os.path.join(self.p.cache, path) mymtime = 0 debug("checking", url) if os.path.exists(fullpath): mymtime = getmtime_int(fullpath) if mtime > mymtime: debug("remote is newer:", url) if self.req.has_key(path): self.req[path]['state'] = SENDME yield kernel.wait(self.wget(path, url, challenge)) elif mtime < mymtime: debug("remote is older:", url) self.ihaveTx(path) else: debug("remote and local times are the same:", path, mtime, mymtime)
def updateSnap(self,msg): blk = msg['blk'] src = self.blk2path(blk) relsrc = self.mkrelative(src) match = re.match("(\w+)-(\w+)-(\d+)",blk) if not match: error("unable to parse block id", blk) yield False return sha1sum = match.group(1) md5sum = match.group(2) for retry in range(3): while not os.path.exists(src): info("pull", relsrc) yield kernel.wait(self.pullfiles([relsrc])) sumtask = kernel.spawn(self.sums(src),itermode=True) sums = None for sums in sumtask: yield kernel.sigbusy if not sums: error("unable to checksum",tmpfn) yield False return if sums['sha'] == sha1sum and sums['md5'] == md5sum: break debug("re-fetching corrupt block:", src) os.unlink(src) if not os.path.exists(src): error("missing block:", src) yield False return # XXX volroot dst = msg['pathname'] dstdir = os.path.dirname(dst) dstpath = dstdir.split('/') pathmodes = msg['pathmodes'].split(',') debug("dstpath %s" % repr(dstpath)) debug("pathmodes %s" % repr(pathmodes)) assert len(dstpath) == len(pathmodes) # create any missing parent dirs for i in range(len(pathmodes)): pathmode = pathmodes[i] (st_mode,st_uid,st_gid) = pathmode.split(':') curpath = '/'.join(dstpath[:i+1]) + '/' assert curpath[0] == '/' debug("checking %s" % curpath) if not os.path.isdir(curpath): debug('creating path %s as %s' % (curpath,pathmode)) os.mkdir(curpath,int(st_mode)) os.chown(curpath,int(st_uid),int(st_gid)) tmpdst = dst + ".IS.snap.tmp~" # security: create and setstat first open(tmpdst,'w') class St: pass st = St() for attr in "st_mode st_uid st_gid st_atime st_mtime".split(): setattr(st,attr,getattr(msg.head,attr)) self.setstat(tmpdst,st) # integrity: copy to tmpdst first, then rename shutil.copyfile(src,tmpdst) os.rename(tmpdst,dst) # update history self.history.add(msg) info("updated", dst) yield True debug("updateSnap done")
def migrate(self): yield None debug("migrate calling fork") yield kernel.wait(self.fork(migrate=True))
def reboot(self): if not self.cklock(): return msg = FBP.mkmsg('reboot') yield kernel.wait(self.addwip(msg))
def reboot(self): yield None debug("calling reboot") yield kernel.wait(self.volume.reboot())
def up(self): reboot_ok = bool(self.opt.get('reboot_ok',False)) if reboot_ok: debug("reboot_ok", repr(self.opt['reboot_ok'])) info("may reboot...") yield kernel.wait(self.volume.update(reboot_ok=reboot_ok))
def updateSnap(self, msg): blk = msg['blk'] src = self.blk2path(blk) relsrc = self.mkrelative(src) match = re.match("(\w+)-(\w+)-(\d+)", blk) if not match: error("unable to parse block id", blk) yield False return sha1sum = match.group(1) md5sum = match.group(2) for retry in range(3): while not os.path.exists(src): info("pull", relsrc) yield kernel.wait(self.pullfiles([relsrc])) sumtask = kernel.spawn(self.sums(src), itermode=True) sums = None for sums in sumtask: yield kernel.sigbusy if not sums: error("unable to checksum", tmpfn) yield False return if sums['sha'] == sha1sum and sums['md5'] == md5sum: break debug("re-fetching corrupt block:", src) os.unlink(src) if not os.path.exists(src): error("missing block:", src) yield False return # XXX volroot dst = msg['pathname'] dstdir = os.path.dirname(dst) dstpath = dstdir.split('/') pathmodes = msg['pathmodes'].split(',') debug("dstpath %s" % repr(dstpath)) debug("pathmodes %s" % repr(pathmodes)) assert len(dstpath) == len(pathmodes) # create any missing parent dirs for i in range(len(pathmodes)): pathmode = pathmodes[i] (st_mode, st_uid, st_gid) = pathmode.split(':') curpath = '/'.join(dstpath[:i + 1]) + '/' assert curpath[0] == '/' debug("checking %s" % curpath) if not os.path.isdir(curpath): debug('creating path %s as %s' % (curpath, pathmode)) os.mkdir(curpath, int(st_mode)) os.chown(curpath, int(st_uid), int(st_gid)) tmpdst = dst + ".IS.snap.tmp~" # security: create and setstat first open(tmpdst, 'w') class St: pass st = St() for attr in "st_mode st_uid st_gid st_atime st_mtime".split(): setattr(st, attr, getattr(msg.head, attr)) self.setstat(tmpdst, st) # integrity: copy to tmpdst first, then rename shutil.copyfile(src, tmpdst) os.rename(tmpdst, dst) # update history self.history.add(msg) info("updated", dst) yield True debug("updateSnap done")
def run(self): from SocketServer import UDPServer from isconf.fbp822 import fbp822, Error822 kernel.spawn(self.puller()) kernel.spawn(self.sender()) # XXX most of the following should be broken out into a receiver() task dir = self.p.cache udpport = self.udpport debug("UDP server serving %s on port %d" % (dir, udpport)) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock = sock sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True) sock.setblocking(0) sock.bind(('', udpport)) # laddr = sock.getsockname() # localip = os.environ['HOSTNAME'] while True: yield None self.flush() yield None try: data, addr = sock.recvfrom(8192) # XXX check against addrs or nets debug("from %s: %s" % (addr, data)) factory = fbp822() msg = factory.parse(data) type = msg.type().strip() if msg.head.tuid == self.tuid: # debug("one of ours -- ignore",str(msg)) continue if not HMAC.msgck(msg): debug("HMAC failed, dropping: %s" % msg) continue if type == 'whohas': path = msg['file'] path = path.lstrip('/') fullpath = os.path.join(dir, path) fullpath = os.path.normpath(fullpath) newer = int(msg.get('newer', None)) # security checks bad = 0 if fullpath != os.path.normpath(fullpath): bad += 1 if dir != os.path.commonprefix( (dir, os.path.abspath(fullpath))): print dir, os.path.commonprefix( (dir, os.path.abspath(fullpath))) bad += 2 if bad: warn("unsafe request %d from %s: %s" % (bad, addr, fullpath)) continue if not os.path.isfile(fullpath): debug("ignoring whohas from %s: not found: %s" % (addr, fullpath)) continue if newer is not None and newer >= getmtime_int(fullpath): debug("ignoring whohas from %s: not newer: %s" % (addr, fullpath)) continue # url = "http://%s:%d/%s" % (localip,httpport,path) self.ihaveTx(path) continue if type == 'ihave': debug("gotihave:", str(msg)) ip = addr[0] yield kernel.wait(self.ihaveRx(msg, ip)) continue warn("unsupported message type from %s: %s" % (addr, type)) except socket.error: yield kernel.sigsleep, 1 continue except Exception, e: warn("%s from %s: %s" % (e, addr, str(msg))) continue
def run(self): from SocketServer import UDPServer from isconf.fbp822 import fbp822, Error822 kernel.spawn(self.puller()) kernel.spawn(self.sender()) # XXX most of the following should be broken out into a receiver() task dir = self.p.cache udpport = self.udpport debug("UDP server serving %s on port %d" % (dir,udpport)) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock = sock sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True) sock.setblocking(0) sock.bind(('',udpport)) # laddr = sock.getsockname() # localip = os.environ['HOSTNAME'] while True: yield None self.flush() yield None try: data,addr = sock.recvfrom(8192) # XXX check against addrs or nets debug("from %s: %s" % (addr,data)) factory = fbp822() msg = factory.parse(data) type = msg.type().strip() if msg.head.tuid == self.tuid: # debug("one of ours -- ignore",str(msg)) continue if not HMAC.msgck(msg): debug("HMAC failed, dropping: %s" % msg) continue if type == 'whohas': path = msg['file'] path = path.lstrip('/') fullpath = os.path.join(dir,path) fullpath = os.path.normpath(fullpath) newer = int(msg.get('newer',None)) # security checks bad=0 if fullpath != os.path.normpath(fullpath): bad += 1 if dir != os.path.commonprefix( (dir,os.path.abspath(fullpath))): print dir,os.path.commonprefix( (dir,os.path.abspath(fullpath))) bad += 2 if bad: warn("unsafe request %d from %s: %s" % ( bad,addr,fullpath)) continue if not os.path.isfile(fullpath): debug("ignoring whohas from %s: not found: %s" % (addr,fullpath)) continue if newer is not None and newer >= getmtime_int( fullpath): debug("ignoring whohas from %s: not newer: %s" % (addr,fullpath)) continue # url = "http://%s:%d/%s" % (localip,httpport,path) self.ihaveTx(path) continue if type == 'ihave': debug("gotihave:",str(msg)) ip = addr[0] yield kernel.wait(self.ihaveRx(msg,ip)) continue warn("unsupported message type from %s: %s" % (addr,type)) except socket.error: yield kernel.sigsleep, 1 continue except Exception, e: warn("%s from %s: %s" % (e,addr,str(msg))) continue
def ci(self): yield kernel.wait(self.volume.ci())