def uuid(self): ids = self.get('id') if len(ids) != 1: raise GsyncdError( "volume info of %s obtained from %s: " "ambiguous uuid", self.volume, self.host) return ids[0].text
def gethostbyname(hnam): """gethostbyname wrapper""" try: return socket.gethostbyname(hnam) except socket.gaierror: ex = sys.exc_info()[1] raise GsyncdError("failed to resolve %s: %s" % (hnam, ex.strerror))
def _unlink(path): try: os.unlink(path) except (OSError, IOError): if sys.exc_info()[1].errno == ENOENT: pass else: raise GsyncdError('Unlink error: %s' % path)
def parse_url(ustr): """instantiate an url object by scheme-to-class dispatch The url classes taken into consideration are the ones in this module whose names are full-caps. """ m = UrlRX.match(ustr) if not m: ustr = desugar(ustr) m = UrlRX.match(ustr) if not m: raise GsyncdError("malformed url") sch, path = m.groups() this = sys.modules[__name__] if not hasattr(this, sch.upper()): raise GsyncdError("unknown url scheme " + sch) return getattr(this, sch.upper())(path)
def checkpt_service(self, chan, chkpt, tgt): """checkpoint service loop monitor and verify checkpoint status for @chkpt, and listen for incoming requests for whom we serve a pretty-formatted status report""" if not chkpt: # dummy loop for the case when there is no checkpt set while True: select([chan], [], []) conn, _ = chan.accept() conn.send(self.get_extra_info()) conn.close() completed = self._checkpt_param(chkpt, 'completed', xtimish=False) if completed: completed = tuple(int(x) for x in completed.split('.')) while True: s,_,_ = select([chan], [], [], (not completed) and 5 or None) # either request made and we re-check to not # give back stale data, or we still hunting for completion if self.native_xtime(tgt) and self.native_xtime(tgt) < self.volmark: # indexing has been reset since setting the checkpoint status = "is invalid" else: xtr = self.xtime('.', self.slave) if isinstance(xtr, int): raise GsyncdError("slave root directory is unaccessible (%s)", os.strerror(xtr)) ncompleted = self.xtime_geq(xtr, tgt) if completed and not ncompleted: # stale data logging.warn("completion time %s for checkpoint %s became stale" % \ (self.humantime(*completed), chkpt)) completed = None gconf.confdata.delete('checkpoint-completed') if ncompleted and not completed: # just reaching completion completed = "%.6f" % time.time() self._set_checkpt_param(chkpt, 'completed', completed, xtimish=False) completed = tuple(int(x) for x in completed.split('.')) logging.info("checkpoint %s completed" % chkpt) status = completed and \ "completed at " + self.humantime(completed[0]) or \ "not reached yet" if s: conn = None try: conn, _ = chan.accept() try: conn.send(" | checkpoint %s %s %s" % (chkpt, status,self.get_extra_info())) except: exc = sys.exc_info()[1] if (isinstance(exc, OSError) or isinstance(exc, IOError)) and \ exc.errno == EPIPE: logging.debug('checkpoint client disconnected') else: raise finally: if conn: conn.close()
def makersc(aa, check=True): if not aa: return ([], None, None) ra = [resource.parse_url(u) for u in aa] local = ra[0] remote = None if len(ra) > 1: remote = ra[1] if check and not local.can_connect_to(remote): raise GsyncdError("%s cannot work with %s" % (local.path, remote and remote.path)) return (ra, local, remote)
def rsync(self, files, *args): """invoke rsync""" if not files: raise GsyncdError("no files to sync") logging.debug("files: " + ", ".join(files)) argv = gconf.rsync_command.split() + gconf.rsync_extra.split() + [ '-aR' ] + files + list(args) po = Popen(argv, stderr=subprocess.PIPE) po.wait() po.terminate_geterr(fail_on_err=False) return po
def distribute(*resources): master, slave = resources mvol = Volinfo(master.volume, master.host) logging.debug('master bricks: ' + repr(mvol.bricks)) prelude = [] si = slave slave_host = None slave_vol = None if isinstance(slave, SSH): prelude = gconf.ssh_command.split() + [slave.remote_addr] si = slave.inner_rsc logging.debug('slave SSH gateway: ' + slave.remote_addr) if isinstance(si, FILE): sbricks = {'host': 'localhost', 'dir': si.path} suuid = uuid.uuid5(uuid.NAMESPACE_URL, slave.get_url(canonical=True)) elif isinstance(si, GLUSTER): svol = Volinfo(si.volume, slave.remote_addr.split('@')[-1]) sbricks = svol.bricks suuid = svol.uuid slave_host = slave.remote_addr.split('@')[-1] slave_vol = si.volume else: raise GsyncdError("unknown slave type " + slave.url) logging.info('slave bricks: ' + repr(sbricks)) if isinstance(si, FILE): slaves = [slave.url] else: slavenodes = set(b['host'] for b in sbricks) if isinstance(slave, SSH) and not gconf.isolated_slave: rap = SSH.parse_ssh_address(slave) slaves = [ 'ssh://' + rap['user'] + '@' + h + ':' + si.url for h in slavenodes ] else: slavevols = [h + ':' + si.volume for h in slavenodes] if isinstance(slave, SSH): slaves = [ 'ssh://' + rap.remote_addr + ':' + v for v in slavevols ] else: slaves = slavevols workerspex = [] for idx, brick in enumerate(mvol.bricks): if is_host_local(brick['host']): is_hot = mvol.is_hot(":".join([brick['host'], brick['dir']])) workerspex.append((brick['dir'], slaves[idx % len(slaves)], get_subvol_num(idx, mvol, is_hot), is_hot)) logging.info('worker specs: ' + repr(workerspex)) return workerspex, suuid, slave_vol, slave_host, master
def _unlink(path): import os from errno import ENOENT from syncdutils import GsyncdError import sys try: os.unlink(path) except (OSError, IOError): if sys.exc_info()[1].errno == ENOENT: pass else: raise GsyncdError('Unlink error: %s' % path)
def get_sys_volinfo(self): """query volume marks on fs root err out on multiple foreign masters """ fgn_vis, nat_vi = self.master.server.aggregated.foreign_volume_infos(), \ self.master.server.aggregated.native_volume_info() fgn_vi = None if fgn_vis: if len(fgn_vis) > 1: raise GsyncdError("cannot work with multiple foreign masters") fgn_vi = fgn_vis[0] return fgn_vi, nat_vi
def _volinfo_hook_relax_foreign(self): volinfo_sys = self.get_sys_volinfo() fgn_vi = volinfo_sys[self.KFGN] if fgn_vi: expiry = fgn_vi['timeout'] - int(time.time()) + 1 logging.info('foreign volume info found, waiting %d sec for expiry' % \ expiry) time.sleep(expiry) volinfo_sys = self.get_sys_volinfo() self.volinfo_state, state_change = self.volinfo_state_machine(self.volinfo_state, volinfo_sys) if self.inter_master: raise GsyncdError("cannot be intermediate master in special mode") return (volinfo_sys, state_change)
def select_vi(vi0, vi): param.index += 1 if vi and (not vi0 or vi0['uuid'] == vi['uuid']): if not vi0 and not param.relax_mismatch: param.state_change = param.index # valid new value found; for the rest, we are graceful about # uuid mismatch param.relax_mismatch = True return vi if vi0 and vi and vi0['uuid'] != vi['uuid'] and not param.relax_mismatch: # uuid mismatch for master candidate, bail out raise GsyncdError("aborting on uuid change from %s to %s" % \ (vi0['uuid'], vi['uuid'])) # fall back to old return vi0
def crawl(self): changes = [] try: self.master.server.changelog_scan() self.crawls += 1 except OSError: self.fallback_xsync() changes = self.master.server.changelog_getchanges() if changes: xtl = self.xtime(self.FLAT_DIR_HIERARCHY) if isinstance(xtl, int): raise GsyncdError('master is corrupt') logging.debug('processing changes %s' % repr(changes)) self.process(changes) self.upd_stime(xtl)
def startup(**kw): """set up logging, pidfile grabbing, daemonization""" if getattr(gconf, 'pid_file', None) and kw.get('go_daemon') != 'postconn': if not grabpidfile(): sys.stderr.write("pidfile is taken, exiting.\n") sys.exit(2) gconf.pid_file_owned = True if kw.get('go_daemon') == 'should': x, y = os.pipe() gconf.cpid = os.fork() if gconf.cpid: os.close(x) sys.exit() os.close(y) os.setsid() dn = os.open(os.devnull, os.O_RDWR) for f in (sys.stdin, sys.stdout, sys.stderr): os.dup2(dn, f.fileno()) if getattr(gconf, 'pid_file', None): if not grabpidfile(gconf.pid_file + '.tmp'): raise GsyncdError("cannot grab temporary pidfile") os.rename(gconf.pid_file + '.tmp', gconf.pid_file) # wait for parent to terminate # so we can start up with # no messing from the dirty # ol' bustard select((x,), (), ()) os.close(x) lkw = {} if gconf.log_level: lkw['level'] = gconf.log_level if kw.get('log_file'): if kw['log_file'] in ('-', '/dev/stderr'): lkw['stream'] = sys.stderr elif kw['log_file'] == '/dev/stdout': lkw['stream'] = sys.stdout else: lkw['filename'] = kw['log_file'] GLogger.setup(label=kw.get('label'), **lkw) lkw.update({'saved_label': kw.get('label')}) gconf.log_metadata = lkw gconf.log_exit = True
def rsync(self, files, *args): """invoke rsync""" if not files: raise GsyncdError("no files to sync") logging.debug("files: " + ", ".join(files)) argv = gconf.rsync_command.split() + \ ['-aR0', '--files-from=-', '--super', '--numeric-ids', '--no-implied-dirs'] + \ gconf.rsync_options.split() + (boolify(gconf.use_rsync_xattrs) and ['--xattrs'] or []) + \ ['.'] + list(args) po = Popen(argv, stdin=subprocess.PIPE, stderr=subprocess.PIPE) for f in files: po.stdin.write(f) po.stdin.write('\0') po.stdin.close() po.wait() po.terminate_geterr(fail_on_err=False) return po
def start_fd_client(self, i, o, **opts): """set up RePCe client, handshake with server It's cut out as a separate method to let subclasses hook into client startup """ self.server = RepceClient(i, o) rv = self.server.__version__() exrv = {'proto': repce.repce_version, 'object': Server.version()} da0 = (rv, exrv) da1 = ({}, {}) for i in range(2): for k, v in da0[i].iteritems(): da1[i][k] = int(v) if da1[0] != da1[1]: raise GsyncdError( "RePCe major version mismatch: local %s, remote %s" % (exrv, rv))
def __init__(self, vol, host='localhost', prelude=[]): po = Popen(prelude + ['gluster', '--xml', '--remote-host=' + host, 'volume', 'info', vol], stdout=PIPE, stderr=PIPE) vix = po.stdout.read() po.wait() po.terminate_geterr() vi = XET.fromstring(vix) if vi.find('opRet').text != '0': if prelude: via = '(via %s) ' % prelude.join(' ') else: via = ' ' raise GsyncdError('getting volume info of %s%s ' 'failed with errorcode %s' % (vol, via, vi.find('opErrno').text)) self.tree = vi self.volume = vol self.host = host
def update_to(self, dct, allow_unresolved=False): """update @dct from key/values of ours. key/values are collected from .config by filtering the regexp sections according to match, and from .section. The values are treated as templates, which are substituted from .auxdicts and (in case of regexp sections) match groups. """ if not self.peers: raise GsyncdError('no peers given, cannot select matching options') def update_from_sect(sect, mud): for k, v in self.config._sections[sect].items(): # Template expects String to be passed # if any config value is not string then it # fails with ValueError v = "{0}".format(v) if k == '__name__': continue if allow_unresolved: dct[k] = Template(v).safe_substitute(mud) else: dct[k] = Template(v).substitute(mud) for sect in self.ord_sections(): sp = self.parse_section(sect) if isinstance(sp[0], re_type) and len(sp) == len(self.peers): match = True mad = {} for i in range(len(sp)): m = sp[i].search(self.peers[i]) if not m: match = False break for j in range(len(m.groups())): mad['match%d_%d' % (i + 1, j + 1)] = m.groups()[j] if match: update_from_sect(sect, MultiDict(dct, mad, *self.auxdicts)) if self.config.has_section(self.section()): update_from_sect(self.section(), MultiDict(dct, *self.auxdicts))
def __init__(self, args, *a, **kw): """customizations for subprocess.Popen instantiation - 'close_fds' is taken to be the default - if child's stderr is chosen to be managed, register it with the error handler thread """ self.args = args if 'close_fds' not in kw: kw['close_fds'] = True self.lock = threading.Lock() try: sup(self, args, *a, **kw) except: ex = sys.exc_info()[1] if not isinstance(ex, OSError): raise raise GsyncdError("""execution of "%s" failed with %s (%s)""" % \ (args[0], errno.errorcode[ex.errno], os.strerror(ex.errno))) if kw['stderr'] == subprocess.PIPE: assert (getattr(self, 'errhandler', None)) self.errstore[self] = []
def desugar(ustr): """transform sugared url strings to standard <scheme>://<urlbody> form parsing logic enforces the constraint that sugared forms should contatin a ':' or a '/', which ensures that sugared urls do not conflict with gluster volume names. """ m = re.match('([^:]*):(.*)', ustr) if m: if not m.groups()[0]: return "gluster://localhost" + ustr elif '@' in m.groups()[0] or re.search('[:/]', m.groups()[1]): return "ssh://" + ustr else: return "gluster://" + ustr else: if ustr[0] != '/': raise GsyncdError("cannot resolve sugared url '%s'" % ustr) ap = os.path.normpath(ustr) if ap.startswith('//'): ap = ap[1:] return "file://" + ap
def crawl(self, path='.', xtr=None, done=0): """ generate a CHANGELOG file consumable by process_change """ if path == '.': self.open() self.crawls += 1 if not xtr: # get the root stime and use it for all comparisons xtr = self.xtime('.', self.slave) if isinstance(xtr, int): if xtr != ENOENT: raise GsyncdError('slave is corrupt') xtr = self.minus_infinity xtl = self.xtime(path) if isinstance(xtl, int): raise GsyncdError('master is corrupt') if xtr == xtl: if path == '.': self.close() return self.xtime_reversion_hook(path, xtl, xtr) logging.debug("entering " + path) dem = self.master.server.entries(path) pargfid = self.master.server.gfid(path) if isinstance(pargfid, int): logging.warn('skipping directory %s' % (path)) for e in dem: bname = e e = os.path.join(path, e) st = lstat(e) if isinstance(st, int): logging.warn('%s got purged in the interim..' % e) continue gfid = self.master.server.gfid(e) if isinstance(gfid, int): logging.warn('skipping entry %s..' % (e)) continue xte = self.xtime(e) if isinstance(xte, int): raise GsyncdError('master is corrupt') if not self.need_sync(e, xte, xtr): continue mo = st.st_mode if stat.S_ISDIR(mo): self.write_entry_change( "E", [gfid, 'MKDIR', escape(os.path.join(pargfid, bname))]) self.crawl(e, xtr) elif stat.S_ISLNK(mo): rl = errno_wrap(os.readlink, [en], [ENOENT]) if isinstance(rl, int): continue self.write_entry_change("E", [ gfid, 'SYMLINK', escape(os.path.join(pargfid, bname)), rl ]) else: # if a file has a hardlink, create a Changelog entry as 'LINK' so the slave # side will decide if to create the new entry, or to create link. if st.st_nlink == 1: self.write_entry_change( "E", [gfid, 'MKNOD', escape(os.path.join(pargfid, bname))]) else: self.write_entry_change( "E", [gfid, 'LINK', escape(os.path.join(pargfid, bname))]) if stat.S_ISREG(mo): self.write_entry_change("D", [gfid]) if path == '.': logging.info('processing xsync changelog %s' % self.fname()) self.close() self.process([self.fname()], done) self.upd_stime(xtl)
def service_loop(self, *args): """enter service loop - if slave given, instantiate GMaster and pass control to that instance, which implements master behavior - else do that's what's inherited """ if args: slave = args[0] if gconf.local_path: class brickserver(FILE.FILEServer): local_path = gconf.local_path aggregated = self.server @classmethod def entries(cls, path): e = super(brickserver, cls).entries(path) # on the brick don't mess with /.glusterfs if path == '.': try: e.remove('.glusterfs') except ValueError: pass return e @classmethod def lstat(cls, e): """ path based backend stat """ return super(brickserver, cls).lstat(e) @classmethod def gfid(cls, e): """ path based backend gfid fetch """ return super(brickserver, cls).gfid(e) @classmethod def linkto_check(cls, e): return super(brickserver, cls).linkto_check(e) if gconf.slave_id: # define {,set_}xtime in slave, thus preempting # the call to remote, so that it takes data from # the local brick slave.server.xtime = types.MethodType( lambda _self, path, uuid: ( brickserver.xtime(path, uuid + '.' + gconf.slave_id) ), slave.server) slave.server.stime = types.MethodType( lambda _self, path, uuid: ( brickserver.stime(path, uuid + '.' + gconf.slave_id) ), slave.server) slave.server.set_stime = types.MethodType( lambda _self, path, uuid, mark: ( brickserver.set_stime(path, uuid + '.' + gconf.slave_id, mark) ), slave.server) (g1, g2, g3) = self.gmaster_instantiate_tuple(slave) g1.master.server = brickserver g2.master.server = brickserver g3.master.server = brickserver else: (g1, g2, g3) = self.gmaster_instantiate_tuple(slave) g1.master.server.aggregated = gmaster.master.server g2.master.server.aggregated = gmaster.master.server g3.master.server.aggregated = gmaster.master.server # bad bad bad: bad way to do things like this # need to make this elegant # register the crawlers and start crawling # g1 ==> Xsync, g2 ==> config.change_detector(changelog by default) # g3 ==> changelog History (inf, ouf, ra, wa) = gconf.rpc_fd.split(',') os.close(int(ra)) os.close(int(wa)) changelog_agent = RepceClient(int(inf), int(ouf)) rv = changelog_agent.version() if int(rv) != CHANGELOG_AGENT_CLIENT_VERSION: raise GsyncdError( "RePCe major version mismatch(changelog agent): " "local %s, remote %s" % (CHANGELOG_AGENT_CLIENT_VERSION, rv)) g1.register() try: (workdir, logfile) = g2.setup_working_dir() # register with the changelog library # 9 == log level (DEBUG) # 5 == connection retries changelog_agent.register(gconf.local_path, workdir, logfile, 9, 5) g2.register(changelog_agent) g3.register(changelog_agent) except ChangelogException as e: logging.debug("Changelog register failed: %s - %s" % (e.errno, e.strerror)) # oneshot: Try to use changelog history api, if not # available switch to FS crawl # Note: if config.change_detector is xsync then # it will not use changelog history api try: g3.crawlwrap(oneshot=True) except (ChangelogException, NoPurgeTimeAvailable, PartialHistoryAvailable) as e: if isinstance(e, ChangelogException): logging.debug('Changelog history crawl failed, failback ' 'to xsync: %s - %s' % (e.errno, e.strerror)) elif isinstance(e, NoPurgeTimeAvailable): logging.debug('Using xsync crawl since no purge time ' 'available') elif isinstance(e, PartialHistoryAvailable): logging.debug('Using xsync crawl after consuming history ' 'till %s' % str(e)) g1.crawlwrap(oneshot=True) # crawl loop: Try changelog crawl, if failed # switch to FS crawl try: g2.crawlwrap() except ChangelogException as e: logging.debug('Changelog crawl failed, failback to xsync: ' '%s - %s' % (e.errno, e.strerror)) g1.crawlwrap() else: sup(self, *args)
def xtime_reversion_hook(self, path, xtl, xtr): if xtr > xtl: raise GsyncdError("timestamp corruption for " + path)
def main_i(): """internal main routine parse command line, decide what action will be taken; we can either: - query/manipulate configuration - format gsyncd urls using gsyncd's url parsing engine - start service in following modes, in given stages: - agent: startup(), ChangelogAgent() - monitor: startup(), monitor() - master: startup(), connect_remote(), connect(), service_loop() - slave: startup(), connect(), service_loop() """ rconf = {'go_daemon': 'should'} def store_abs(opt, optstr, val, parser): if val and val != '-': val = os.path.abspath(val) setattr(parser.values, opt.dest, val) def store_local(opt, optstr, val, parser): rconf[opt.dest] = val def store_local_curry(val): return lambda o, oo, vx, p: store_local(o, oo, val, p) def store_local_obj(op, dmake): return lambda o, oo, vx, p: store_local( o, oo, FreeObject(op=op, **dmake(vx)), p) op = OptionParser( usage="%prog [options...] <master> <slave>", version="%prog 0.0.1") op.add_option('--gluster-command-dir', metavar='DIR', default='') op.add_option('--gluster-log-file', metavar='LOGF', default=os.devnull, type=str, action='callback', callback=store_abs) op.add_option('--gluster-log-level', metavar='LVL') op.add_option('--gluster-params', metavar='PRMS', default='') op.add_option( '--glusterd-uuid', metavar='UUID', type=str, default='', help=SUPPRESS_HELP) op.add_option( '--gluster-cli-options', metavar='OPTS', default='--log-file=-') op.add_option('--mountbroker', metavar='LABEL') op.add_option('-p', '--pid-file', metavar='PIDF', type=str, action='callback', callback=store_abs) op.add_option('-l', '--log-file', metavar='LOGF', type=str, action='callback', callback=store_abs) op.add_option('--iprefix', metavar='LOGD', type=str, action='callback', callback=store_abs) op.add_option('--changelog-log-file', metavar='LOGF', type=str, action='callback', callback=store_abs) op.add_option('--log-file-mbr', metavar='LOGF', type=str, action='callback', callback=store_abs) op.add_option('--state-file', metavar='STATF', type=str, action='callback', callback=store_abs) op.add_option('--state-detail-file', metavar='STATF', type=str, action='callback', callback=store_abs) op.add_option('--georep-session-working-dir', metavar='STATF', type=str, action='callback', callback=store_abs) op.add_option('--ignore-deletes', default=False, action='store_true') op.add_option('--isolated-slave', default=False, action='store_true') op.add_option('--use-rsync-xattrs', default=False, action='store_true') op.add_option('--pause-on-start', default=False, action='store_true') op.add_option('-L', '--log-level', metavar='LVL') op.add_option('-r', '--remote-gsyncd', metavar='CMD', default=os.path.abspath(sys.argv[0])) op.add_option('--volume-id', metavar='UUID') op.add_option('--slave-id', metavar='ID') op.add_option('--session-owner', metavar='ID') op.add_option('--local-id', metavar='ID', help=SUPPRESS_HELP, default='') op.add_option( '--local-path', metavar='PATH', help=SUPPRESS_HELP, default='') op.add_option('-s', '--ssh-command', metavar='CMD', default='ssh') op.add_option('--ssh-command-tar', metavar='CMD', default='ssh') op.add_option('--rsync-command', metavar='CMD', default='rsync') op.add_option('--rsync-options', metavar='OPTS', default='') op.add_option('--rsync-ssh-options', metavar='OPTS', default='--compress') op.add_option('--timeout', metavar='SEC', type=int, default=120) op.add_option('--connection-timeout', metavar='SEC', type=int, default=60, help=SUPPRESS_HELP) op.add_option('--sync-jobs', metavar='N', type=int, default=3) op.add_option('--replica-failover-interval', metavar='N', type=int, default=1) op.add_option('--changelog-archive-format', metavar='N', type=str, default="%Y%m") op.add_option( '--turns', metavar='N', type=int, default=0, help=SUPPRESS_HELP) op.add_option('--allow-network', metavar='IPS', default='') op.add_option('--socketdir', metavar='DIR') op.add_option('--state-socket-unencoded', metavar='SOCKF', type=str, action='callback', callback=store_abs) op.add_option('--checkpoint', metavar='LABEL', default='') # tunables for failover/failback mechanism: # None - gsyncd behaves as normal # blind - gsyncd works with xtime pairs to identify # candidates for synchronization # wrapup - same as normal mode but does not assign # xtimes to orphaned files # see crawl() for usage of the above tunables op.add_option('--special-sync-mode', type=str, help=SUPPRESS_HELP) # changelog or xtime? (TODO: Change the default) op.add_option( '--change-detector', metavar='MODE', type=str, default='xtime') # sleep interval for change detection (xtime crawl uses a hardcoded 1 # second sleep time) op.add_option('--change-interval', metavar='SEC', type=int, default=3) # working directory for changelog based mechanism op.add_option('--working-dir', metavar='DIR', type=str, action='callback', callback=store_abs) op.add_option('--use-tarssh', default=False, action='store_true') op.add_option('-c', '--config-file', metavar='CONF', type=str, action='callback', callback=store_local) # duh. need to specify dest or value will be mapped to None :S op.add_option('--monitor', dest='monitor', action='callback', callback=store_local_curry(True)) op.add_option('--agent', dest='agent', action='callback', callback=store_local_curry(True)) op.add_option('--resource-local', dest='resource_local', type=str, action='callback', callback=store_local) op.add_option('--resource-remote', dest='resource_remote', type=str, action='callback', callback=store_local) op.add_option('--feedback-fd', dest='feedback_fd', type=int, help=SUPPRESS_HELP, action='callback', callback=store_local) op.add_option('--rpc-fd', dest='rpc_fd', type=str, help=SUPPRESS_HELP) op.add_option('--listen', dest='listen', help=SUPPRESS_HELP, action='callback', callback=store_local_curry(True)) op.add_option('-N', '--no-daemon', dest="go_daemon", action='callback', callback=store_local_curry('dont')) op.add_option('--verify', type=str, dest="verify", action='callback', callback=store_local) op.add_option('--create', type=str, dest="create", action='callback', callback=store_local) op.add_option('--delete', dest='delete', action='callback', callback=store_local_curry(True)) op.add_option('--debug', dest="go_daemon", action='callback', callback=lambda *a: (store_local_curry('dont')(*a), setattr( a[-1].values, 'log_file', '-'), setattr(a[-1].values, 'log_level', 'DEBUG'), setattr(a[-1].values, 'changelog_log_file', '-'))) op.add_option('--path', type=str, action='append') for a in ('check', 'get'): op.add_option('--config-' + a, metavar='OPT', type=str, dest='config', action='callback', callback=store_local_obj(a, lambda vx: {'opt': vx})) op.add_option('--config-get-all', dest='config', action='callback', callback=store_local_obj('get', lambda vx: {'opt': None})) for m in ('', '-rx', '-glob'): # call this code 'Pythonic' eh? # have to define a one-shot local function to be able # to inject (a value depending on the) # iteration variable into the inner lambda def conf_mod_opt_regex_variant(rx): op.add_option('--config-set' + m, metavar='OPT VAL', type=str, nargs=2, dest='config', action='callback', callback=store_local_obj('set', lambda vx: { 'opt': vx[0], 'val': vx[1], 'rx': rx})) op.add_option('--config-del' + m, metavar='OPT', type=str, dest='config', action='callback', callback=store_local_obj('del', lambda vx: { 'opt': vx, 'rx': rx})) conf_mod_opt_regex_variant(m and m[1:] or False) op.add_option('--normalize-url', dest='url_print', action='callback', callback=store_local_curry('normal')) op.add_option('--canonicalize-url', dest='url_print', action='callback', callback=store_local_curry('canon')) op.add_option('--canonicalize-escape-url', dest='url_print', action='callback', callback=store_local_curry('canon_esc')) tunables = [norm(o.get_opt_string()[2:]) for o in op.option_list if (o.callback in (store_abs, 'store_true', None) and o.get_opt_string() not in ('--version', '--help'))] remote_tunables = ['listen', 'go_daemon', 'timeout', 'session_owner', 'config_file', 'use_rsync_xattrs'] rq_remote_tunables = {'listen': True} # precedence for sources of values: 1) commandline, 2) cfg file, 3) # defaults for this to work out we need to tell apart defaults from # explicitly set options... so churn out the defaults here and call # the parser with virgin values container. defaults = op.get_default_values() opts, args = op.parse_args(values=optparse.Values()) args_orig = args[:] r = rconf.get('resource_local') if r: if len(args) == 0: args.append(None) args[0] = r r = rconf.get('resource_remote') if r: if len(args) == 0: raise GsyncdError('local resource unspecfied') elif len(args) == 1: args.append(None) args[1] = r confdata = rconf.get('config') if not (len(args) == 2 or (len(args) == 1 and rconf.get('listen')) or (len(args) <= 2 and confdata) or rconf.get('url_print')): sys.stderr.write("error: incorrect number of arguments\n\n") sys.stderr.write(op.get_usage() + "\n") sys.exit(1) verify = rconf.get('verify') if verify: logging.info(verify) logging.info("Able to spawn gsyncd.py") return restricted = os.getenv('_GSYNCD_RESTRICTED_') if restricted: allopts = {} allopts.update(opts.__dict__) allopts.update(rconf) bannedtuns = set(allopts.keys()) - set(remote_tunables) if bannedtuns: raise GsyncdError('following tunables cannot be set with ' 'restricted SSH invocaton: ' + ', '.join(bannedtuns)) for k, v in rq_remote_tunables.items(): if not k in allopts or allopts[k] != v: raise GsyncdError('tunable %s is not set to value %s required ' 'for restricted SSH invocaton' % (k, v)) confrx = getattr(confdata, 'rx', None) def makersc(aa, check=True): if not aa: return ([], None, None) ra = [resource.parse_url(u) for u in aa] local = ra[0] remote = None if len(ra) > 1: remote = ra[1] if check and not local.can_connect_to(remote): raise GsyncdError("%s cannot work with %s" % (local.path, remote and remote.path)) return (ra, local, remote) if confrx: # peers are regexen, don't try to parse them if confrx == 'glob': args = ['\A' + fnmatch.translate(a) for a in args] canon_peers = args namedict = {} else: dc = rconf.get('url_print') rscs, local, remote = makersc(args_orig, not dc) if dc: for r in rscs: print(r.get_url(**{'normal': {}, 'canon': {'canonical': True}, 'canon_esc': {'canonical': True, 'escaped': True}}[dc])) return pa = ([], [], []) urlprms = ( {}, {'canonical': True}, {'canonical': True, 'escaped': True}) for x in rscs: for i in range(len(pa)): pa[i].append(x.get_url(**urlprms[i])) _, canon_peers, canon_esc_peers = pa # creating the namedict, a dict representing various ways of referring # to / repreenting peers to be fillable in config templates mods = (lambda x: x, lambda x: x[ 0].upper() + x[1:], lambda x: 'e' + x[0].upper() + x[1:]) if remote: rmap = {local: ('local', 'master'), remote: ('remote', 'slave')} else: rmap = {local: ('local', 'slave')} namedict = {} for i in range(len(rscs)): x = rscs[i] for name in rmap[x]: for j in range(3): namedict[mods[j](name)] = pa[j][i] namedict[name + 'vol'] = x.volume if name == 'remote': namedict['remotehost'] = x.remotehost if not 'config_file' in rconf: rconf['config_file'] = os.path.join( os.path.dirname(sys.argv[0]), "conf/gsyncd_template.conf") upgrade_config_file(rconf['config_file']) gcnf = GConffile( rconf['config_file'], canon_peers, defaults.__dict__, opts.__dict__, namedict) checkpoint_change = False if confdata: opt_ok = norm(confdata.opt) in tunables + [None] if confdata.op == 'check': if opt_ok: sys.exit(0) else: sys.exit(1) elif not opt_ok: raise GsyncdError("not a valid option: " + confdata.opt) if confdata.op == 'get': gcnf.get(confdata.opt) elif confdata.op == 'set': gcnf.set(confdata.opt, confdata.val, confdata.rx) elif confdata.op == 'del': gcnf.delete(confdata.opt, confdata.rx) # when modifying checkpoint, it's important to make a log # of that, so in that case we go on to set up logging even # if its just config invocation if confdata.opt == 'checkpoint' and confdata.op in ('set', 'del') and \ not confdata.rx: checkpoint_change = True if not checkpoint_change: return gconf.__dict__.update(defaults.__dict__) gcnf.update_to(gconf.__dict__) gconf.__dict__.update(opts.__dict__) gconf.configinterface = gcnf delete = rconf.get('delete') if delete: logging.info('geo-replication delete') # Delete pid file, status file, socket file cleanup_paths = [] if getattr(gconf, 'pid_file', None): cleanup_paths.append(gconf.pid_file) if getattr(gconf, 'state_file', None): cleanup_paths.append(gconf.state_file) if getattr(gconf, 'state_detail_file', None): cleanup_paths.append(gconf.state_detail_file) if getattr(gconf, 'state_socket_unencoded', None): cleanup_paths.append(gconf.state_socket_unencoded) cleanup_paths.append(rconf['config_file'][:-11] + "*") # Cleanup changelog working dirs if getattr(gconf, 'working_dir', None): try: shutil.rmtree(gconf.working_dir) except (IOError, OSError): if sys.exc_info()[1].errno == ENOENT: pass else: raise GsyncdError( 'Error while removing working dir: %s' % gconf.working_dir) for path in cleanup_paths: # To delete temp files for f in glob.glob(path + "*"): _unlink(f) return if restricted and gconf.allow_network: ssh_conn = os.getenv('SSH_CONNECTION') if not ssh_conn: # legacy env var ssh_conn = os.getenv('SSH_CLIENT') if ssh_conn: allowed_networks = [IPNetwork(a) for a in gconf.allow_network.split(',')] client_ip = IPAddress(ssh_conn.split()[0]) allowed = False for nw in allowed_networks: if client_ip in nw: allowed = True break if not allowed: raise GsyncdError("client IP address is not allowed") ffd = rconf.get('feedback_fd') if ffd: fcntl.fcntl(ffd, fcntl.F_SETFD, fcntl.FD_CLOEXEC) # normalize loglevel lvl0 = gconf.log_level if isinstance(lvl0, str): lvl1 = lvl0.upper() lvl2 = logging.getLevelName(lvl1) # I have _never_ _ever_ seen such an utterly braindead # error condition if lvl2 == "Level " + lvl1: raise GsyncdError('cannot recognize log level "%s"' % lvl0) gconf.log_level = lvl2 if not privileged() and gconf.log_file_mbr: gconf.log_file = gconf.log_file_mbr if checkpoint_change: try: GLogger._gsyncd_loginit(log_file=gconf.log_file, label='conf') if confdata.op == 'set': logging.info('checkpoint %s set' % confdata.val) gcnf.delete('checkpoint_completed') gcnf.delete('checkpoint_target') elif confdata.op == 'del': logging.info('checkpoint info was reset') # if it is removing 'checkpoint' then we need # to remove 'checkpoint_completed' and 'checkpoint_target' too gcnf.delete('checkpoint_completed') gcnf.delete('checkpoint_target') except IOError: if sys.exc_info()[1].errno == ENOENT: # directory of log path is not present, # which happens if we get here from # a peer-multiplexed "config-set checkpoint" # (as that directory is created only on the # original node) pass else: raise return create = rconf.get('create') if create: if getattr(gconf, 'state_file', None): update_file(gconf.state_file, lambda f: f.write(create + '\n')) return go_daemon = rconf['go_daemon'] be_monitor = rconf.get('monitor') be_agent = rconf.get('agent') rscs, local, remote = makersc(args) if not be_monitor and isinstance(remote, resource.SSH) and \ go_daemon == 'should': go_daemon = 'postconn' log_file = None else: log_file = gconf.log_file if be_monitor: label = 'monitor' elif be_agent: label = 'agent' elif remote: # master label = gconf.local_path else: label = 'slave' startup(go_daemon=go_daemon, log_file=log_file, label=label) resource.Popen.init_errhandler() if be_agent: os.setsid() logging.debug('rpc_fd: %s' % repr(gconf.rpc_fd)) return agent(Changelog(), gconf.rpc_fd) if be_monitor: return monitor(*rscs) logging.info("syncing: %s" % " -> ".join(r.url for r in rscs)) if remote: go_daemon = remote.connect_remote(go_daemon=go_daemon) if go_daemon: startup(go_daemon=go_daemon, log_file=gconf.log_file) # complete remote connection in child remote.connect_remote(go_daemon='done') local.connect() if ffd: os.close(ffd) local.service_loop(*[r for r in [remote] if r])
def subcmd_delete(args): import logging import shutil import glob import sys from errno import ENOENT, ENODATA import struct from syncdutils import GsyncdError, Xattr, errno_wrap import gsyncdconfig as gconf logging.info('geo-replication delete') # remove the stime xattr from all the brick paths so that # a re-create of a session will start sync all over again stime_xattr_prefix = gconf.get('stime-xattr-prefix', None) # Delete pid file, status file, socket file cleanup_paths = [] cleanup_paths.append(gconf.get("pid-file")) # Cleanup Session dir try: shutil.rmtree(gconf.get("georep-session-working-dir")) except (IOError, OSError): if sys.exc_info()[1].errno == ENOENT: pass else: raise GsyncdError( 'Error while removing working dir: %s' % gconf.get("georep-session-working-dir")) # Cleanup changelog working dirs try: shutil.rmtree(gconf.get("working-dir")) except (IOError, OSError): if sys.exc_info()[1].errno == ENOENT: pass else: raise GsyncdError( 'Error while removing working dir: %s' % gconf.get("working-dir")) for path in cleanup_paths: # To delete temp files for f in glob.glob(path + "*"): _unlink(f) if args.reset_sync_time and stime_xattr_prefix: for p in args.paths: if p != "": # set stime to (0,0) to trigger full volume content resync # to slave on session recreation # look at master.py::Xcrawl hint: zero_zero errno_wrap(Xattr.lsetxattr, (p, stime_xattr_prefix + ".stime", struct.pack("!II", 0, 0)), [ENOENT, ENODATA]) errno_wrap(Xattr.lremovexattr, (p, stime_xattr_prefix + ".entry_stime"), [ENOENT, ENODATA]) return
def xtime_reversion_hook(self, path, xtl, xtr): if not isinstance(xtr[0], int) and \ (isinstance(xtl[0], int) or xtr[0] > xtl[0]): raise GsyncdError("timestamp corruption for " + path)
def crawl(self, path='.', xtl=None): """crawling... Standing around All the right people Crawling Tennis on Tuesday The ladder is long It is your nature You've gotta suntan Football on Sunday Society boy Recursively walk the master side tree and check if updates are needed due to xtime differences. One invocation of crawl checks children of @path and do a recursive enter only on those directory children where there is an update needed. Way of updates depend on file type: - for symlinks, sync them directy and synchronously - for regular children, register jobs for @path (cf. .add_job) to start and wait on their rsync - for directory children, register a job for @path which waits (.wait) on jobs for the given child (other kind of filesystem nodes are not considered) Those slave side children which do not exist on master are simply purged (see Server.purge). Behavior is fault tolerant, synchronization is adaptive: if some action fails, just go on relentlessly, adding a fail job (see .add_failjob) which will prevent the .sendmark on @path, so when the next crawl will arrive to @path it will not see it as up-to-date and will try to sync it again. While this semantics can be supported by funky design principles (http://c2.com/cgi/wiki?LazinessImpatienceHubris), the ultimate reason which excludes other possibilities is simply transience: we cannot assert that the file systems (master / slave) underneath do not change and actions taken upon some condition will not lose their context by the time they are performed. """ if path == '.': if self.start: self.crawls += 1 logging.debug("... crawl #%d done, took %.6f seconds" % \ (self.crawls, time.time() - self.start)) time.sleep(1) self.start = time.time() should_display_info = self.start - self.lastreport['time'] >= 60 if should_display_info: logging.info("completed %d crawls, %d turns", self.crawls - self.lastreport['crawls'], self.turns - self.lastreport['turns']) self.lastreport.update(crawls = self.crawls, turns = self.turns, time = self.start) volinfo_sys, state_change = self.volinfo_hook() if self.inter_master: self.volinfo = volinfo_sys[self.KFGN] else: self.volinfo = volinfo_sys[self.KNAT] if state_change == self.KFGN or (state_change == self.KNAT and not self.inter_master): logging.info('new master is %s', self.uuid) if self.volinfo: logging.info("%s master with volume id %s ..." % \ (self.inter_master and "intermediate" or "primary", self.uuid)) if state_change == self.KFGN: gconf.configinterface.set('volume_id', self.uuid) if self.volinfo: if self.volinfo['retval']: raise GsyncdError ("master is corrupt") self.start_checkpoint_thread() else: if should_display_info or self.crawls == 0: if self.inter_master: logging.info("waiting for being synced from %s ..." % \ self.volinfo_state[self.KFGN]['uuid']) else: logging.info("waiting for volume info ...") return logging.debug("entering " + path) if not xtl: xtl = self.xtime(path) if isinstance(xtl, int): self.add_failjob(path, 'no-local-node') return xtr = self.xtime(path, self.slave) if isinstance(xtr, int): if xtr != ENOENT: self.slave.server.purge(path) try: self.slave.server.mkdir(path) except OSError: self.add_failjob(path, 'no-remote-node') return xtr = self.minus_infinity else: self.xtime_reversion_hook(path, xtl, xtr) if xtl == xtr: if path == '.' and self.change_seen: self.turns += 1 self.change_seen = False if self.total_turns: logging.info("finished turn #%s/%s" % \ (self.turns, self.total_turns)) if self.turns == self.total_turns: logging.info("reached turn limit") self.terminate = True return if path == '.': self.change_seen = True try: dem = self.master.server.entries(path) except OSError: self.add_failjob(path, 'local-entries-fail') return random.shuffle(dem) try: des = self.slave.server.entries(path) except OSError: self.slave.server.purge(path) try: self.slave.server.mkdir(path) des = self.slave.server.entries(path) except OSError: self.add_failjob(path, 'remote-entries-fail') return dd = set(des) - set(dem) if dd: self.purge_missing(path, dd) chld = [] for e in dem: e = os.path.join(path, e) xte = self.xtime(e) if isinstance(xte, int): logging.warn("irregular xtime for %s: %s" % (e, errno.errorcode[xte])) elif self.need_sync(e, xte, xtr): chld.append((e, xte)) def indulgently(e, fnc, blame=None): if not blame: blame = path try: return fnc(e) except (IOError, OSError): ex = sys.exc_info()[1] if ex.errno == ENOENT: logging.warn("salvaged ENOENT for " + e) self.add_failjob(blame, 'by-indulgently') return False else: raise for e, xte in chld: st = indulgently(e, lambda e: os.lstat(e)) if st == False: continue mo = st.st_mode adct = {'own': (st.st_uid, st.st_gid)} if stat.S_ISLNK(mo): if indulgently(e, lambda e: self.slave.server.symlink(os.readlink(e), e)) == False: continue self.sendmark(e, xte, adct) elif stat.S_ISREG(mo): logging.debug("syncing %s ..." % e) pb = self.syncer.add(e) timeA=datetime.now() def regjob(e, xte, pb): if pb.wait(): logging.debug("synced " + e) self.sendmark_regular(e, xte) timeB=datetime.now() self.lastSyncTime=timeB-timeA self.syncTime=(self.syncTime+self.lastSyncTime.microseconds)/(10.0**6) self.filesSynced=self.filesSynced+1 return True else: logging.warn("failed to sync " + e) self.add_job(path, 'reg', regjob, e, xte, pb) elif stat.S_ISDIR(mo): adct['mode'] = mo if indulgently(e, lambda e: (self.add_job(path, 'cwait', self.wait, e, xte, adct), self.crawl(e, xte), True)[-1], blame=e) == False: continue else: # ignore fifos, sockets and special files pass if path == '.': self.wait(path, xtl)
def main_i(): """internal main routine parse command line, decide what action will be taken; we can either: - query/manipulate configuration - format gsyncd urls using gsyncd's url parsing engine - start service in following modes, in given stages: - monitor: startup(), monitor() - master: startup(), connect_remote(), connect(), service_loop() - slave: startup(), connect(), service_loop() """ rconf = {'go_daemon': 'should'} def store_abs(opt, optstr, val, parser): if val and val != '-': val = os.path.abspath(val) setattr(parser.values, opt.dest, val) def store_local(opt, optstr, val, parser): rconf[opt.dest] = val def store_local_curry(val): return lambda o, oo, vx, p: store_local(o, oo, val, p) def store_local_obj(op, dmake): return lambda o, oo, vx, p: store_local(o, oo, FreeObject(op=op, **dmake(vx)), p) op = OptionParser(usage="%prog [options...] <master> <slave>", version="%prog 0.0.1") op.add_option('--gluster-command-dir', metavar='DIR', default='') op.add_option('--gluster-log-file', metavar='LOGF', default=os.devnull, type=str, action='callback', callback=store_abs) op.add_option('--gluster-log-level', metavar='LVL') op.add_option('--gluster-params', metavar='PRMS', default='') op.add_option('--gluster-cli-options', metavar='OPTS', default='--log-file=-') op.add_option('--mountbroker', metavar='LABEL') op.add_option('-p', '--pid-file', metavar='PIDF', type=str, action='callback', callback=store_abs) op.add_option('-l', '--log-file', metavar='LOGF', type=str, action='callback', callback=store_abs) op.add_option('--state-file', metavar='STATF', type=str, action='callback', callback=store_abs) op.add_option('--ignore-deletes', default=False, action='store_true') op.add_option('-L', '--log-level', metavar='LVL') op.add_option('-r', '--remote-gsyncd', metavar='CMD', default=os.path.abspath(sys.argv[0])) op.add_option('--volume-id', metavar='UUID') op.add_option('--session-owner', metavar='ID') op.add_option('-s', '--ssh-command', metavar='CMD', default='ssh') op.add_option('--rsync-command', metavar='CMD', default='rsync') op.add_option('--timeout', metavar='SEC', type=int, default=120) op.add_option('--connection-timeout', metavar='SEC', type=int, default=60, help=SUPPRESS_HELP) op.add_option('--sync-jobs', metavar='N', type=int, default=3) op.add_option('--turns', metavar='N', type=int, default=0, help=SUPPRESS_HELP) op.add_option('--allow-network', metavar='IPS', default='') op.add_option('-c', '--config-file', metavar='CONF', type=str, action='callback', callback=store_local) # duh. need to specify dest or value will be mapped to None :S op.add_option('--monitor', dest='monitor', action='callback', callback=store_local_curry(True)) op.add_option('--feedback-fd', dest='feedback_fd', type=int, help=SUPPRESS_HELP, action='callback', callback=store_local) op.add_option('--listen', dest='listen', help=SUPPRESS_HELP, action='callback', callback=store_local_curry(True)) op.add_option('-N', '--no-daemon', dest="go_daemon", action='callback', callback=store_local_curry('dont')) op.add_option('--debug', dest="go_daemon", action='callback', callback=lambda *a: (store_local_curry('dont')(*a), setattr(a[-1].values, 'log_file', '-'), setattr(a[-1].values, 'log_level', 'DEBUG'))), for a in ('check', 'get'): op.add_option('--config-' + a, metavar='OPT', type=str, dest='config', action='callback', callback=store_local_obj(a, lambda vx: {'opt': vx})) op.add_option('--config-get-all', dest='config', action='callback', callback=store_local_obj('get', lambda vx: {'opt': None})) for m in ('', '-rx', '-glob'): # call this code 'Pythonic' eh? # have to define a one-shot local function to be able to inject (a value depending on the) # iteration variable into the inner lambda def conf_mod_opt_regex_variant(rx): op.add_option('--config-set' + m, metavar='OPT VAL', type=str, nargs=2, dest='config', action='callback', callback=store_local_obj('set', lambda vx: {'opt': vx[0], 'val': vx[1], 'rx': rx})) op.add_option('--config-del' + m, metavar='OPT', type=str, dest='config', action='callback', callback=store_local_obj('del', lambda vx: {'opt': vx, 'rx': rx})) conf_mod_opt_regex_variant(m and m[1:] or False) op.add_option('--normalize-url', dest='url_print', action='callback', callback=store_local_curry('normal')) op.add_option('--canonicalize-url', dest='url_print', action='callback', callback=store_local_curry('canon')) op.add_option('--canonicalize-escape-url', dest='url_print', action='callback', callback=store_local_curry('canon_esc')) tunables = [ norm(o.get_opt_string()[2:]) for o in op.option_list if o.callback in (store_abs, 'store_true', None) and o.get_opt_string() not in ('--version', '--help') ] remote_tunables = [ 'listen', 'go_daemon', 'timeout', 'session_owner', 'config_file' ] rq_remote_tunables = { 'listen': True } # precedence for sources of values: 1) commandline, 2) cfg file, 3) defaults # -- for this to work out we need to tell apart defaults from explicitly set # options... so churn out the defaults here and call the parser with virgin # values container. defaults = op.get_default_values() opts, args = op.parse_args(values=optparse.Values()) confdata = rconf.get('config') if not (len(args) == 2 or \ (len(args) == 1 and rconf.get('listen')) or \ (len(args) <= 2 and confdata) or \ rconf.get('url_print')): sys.stderr.write("error: incorrect number of arguments\n\n") sys.stderr.write(op.get_usage() + "\n") sys.exit(1) restricted = os.getenv('_GSYNCD_RESTRICTED_') if restricted: allopts = {} allopts.update(opts.__dict__) allopts.update(rconf) bannedtuns = set(allopts.keys()) - set(remote_tunables) if bannedtuns: raise GsyncdError('following tunables cannot be set with restricted SSH invocaton: ' + \ ', '.join(bannedtuns)) for k, v in rq_remote_tunables.items(): if not k in allopts or allopts[k] != v: raise GsyncdError('tunable %s is not set to value %s required for restricted SSH invocaton' % \ (k, v)) confrx = getattr(confdata, 'rx', None) if confrx: # peers are regexen, don't try to parse them if confrx == 'glob': args = [ '\A' + fnmatch.translate(a) for a in args ] canon_peers = args namedict = {} else: rscs = [resource.parse_url(u) for u in args] dc = rconf.get('url_print') if dc: for r in rscs: print(r.get_url(**{'normal': {}, 'canon': {'canonical': True}, 'canon_esc': {'canonical': True, 'escaped': True}}[dc])) return local = remote = None if rscs: local = rscs[0] if len(rscs) > 1: remote = rscs[1] if not local.can_connect_to(remote): raise GsyncdError("%s cannot work with %s" % (local.path, remote and remote.path)) pa = ([], [], []) urlprms = ({}, {'canonical': True}, {'canonical': True, 'escaped': True}) for x in rscs: for i in range(len(pa)): pa[i].append(x.get_url(**urlprms[i])) peers, canon_peers, canon_esc_peers = pa # creating the namedict, a dict representing various ways of referring to / repreenting # peers to be fillable in config templates mods = (lambda x: x, lambda x: x[0].upper() + x[1:], lambda x: 'e' + x[0].upper() + x[1:]) if remote: rmap = { local: ('local', 'master'), remote: ('remote', 'slave') } else: rmap = { local: ('local', 'slave') } namedict = {} for i in range(len(rscs)): x = rscs[i] for name in rmap[x]: for j in range(3): namedict[mods[j](name)] = pa[j][i] if x.scheme == 'gluster': namedict[name + 'vol'] = x.volume if not 'config_file' in rconf: rconf['config_file'] = os.path.join(os.path.dirname(sys.argv[0]), "conf/gsyncd.conf") gcnf = GConffile(rconf['config_file'], canon_peers, defaults.__dict__, opts.__dict__, namedict) if confdata: opt_ok = norm(confdata.opt) in tunables + [None] if confdata.op == 'check': if opt_ok: sys.exit(0) else: sys.exit(1) elif not opt_ok: raise GsyncdError("not a valid option: " + confdata.opt) if confdata.op == 'get': gcnf.get(confdata.opt) elif confdata.op == 'set': gcnf.set(confdata.opt, confdata.val, confdata.rx) elif confdata.op == 'del': gcnf.delete(confdata.opt, confdata.rx) return gconf.__dict__.update(defaults.__dict__) gcnf.update_to(gconf.__dict__) gconf.__dict__.update(opts.__dict__) gconf.configinterface = gcnf if restricted and gconf.allow_network: ssh_conn = os.getenv('SSH_CONNECTION') if not ssh_conn: #legacy env var ssh_conn = os.getenv('SSH_CLIENT') if ssh_conn: allowed_networks = [ IPNetwork(a) for a in gconf.allow_network.split(',') ] client_ip = IPAddress(ssh_conn.split()[0]) allowed = False for nw in allowed_networks: if client_ip in nw: allowed = True break if not allowed: raise GsyncdError("client IP address is not allowed") ffd = rconf.get('feedback_fd') if ffd: fcntl.fcntl(ffd, fcntl.F_SETFD, fcntl.FD_CLOEXEC) #normalize loglevel lvl0 = gconf.log_level if isinstance(lvl0, str): lvl1 = lvl0.upper() lvl2 = logging.getLevelName(lvl1) # I have _never_ _ever_ seen such an utterly braindead # error condition if lvl2 == "Level " + lvl1: raise GsyncdError('cannot recognize log level "%s"' % lvl0) gconf.log_level = lvl2 go_daemon = rconf['go_daemon'] be_monitor = rconf.get('monitor') if not be_monitor and isinstance(remote, resource.SSH) and \ go_daemon == 'should': go_daemon = 'postconn' log_file = None else: log_file = gconf.log_file if be_monitor: label = 'monitor' elif remote: #master label = '' else: label = 'slave' startup(go_daemon=go_daemon, log_file=log_file, label=label) if be_monitor: return monitor() logging.info("syncing: %s" % " -> ".join(peers)) resource.Popen.init_errhandler() if remote: go_daemon = remote.connect_remote(go_daemon=go_daemon) if go_daemon: startup(go_daemon=go_daemon, log_file=gconf.log_file) # complete remote connection in child remote.connect_remote(go_daemon='done') local.connect() if ffd: os.close(ffd) local.service_loop(*[r for r in [remote] if r])
def inhibit(self, *a): """inhibit a gluster filesystem Mount glusterfs over a temporary mountpoint, change into the mount, and lazy unmount the filesystem. """ mpi, mpo = os.pipe() mh = Popen.fork() if mh: os.close(mpi) fcntl.fcntl(mpo, fcntl.F_SETFD, fcntl.FD_CLOEXEC) d = None margv = self.make_mount_argv(*a) if self.mntpt: # mntpt is determined pre-mount d = self.mntpt os.write(mpo, d + '\0') po = Popen(margv, **self.mountkw) self.handle_mounter(po) po.terminate_geterr() logging.debug('auxiliary glusterfs mount in place') if not d: # mntpt is determined during mount d = self.mntpt os.write(mpo, d + '\0') os.write(mpo, 'M') t = syncdutils.Thread(target=lambda: os.chdir(d)) t.start() tlim = gconf.starttime + int(gconf.connection_timeout) while True: if not t.isAlive(): break if time.time() >= tlim: syncdutils.finalize(exval=1) time.sleep(1) os.close(mpo) _, rv = syncdutils.waitpid(mh, 0) if rv: rv = (os.WIFEXITED(rv) and os.WEXITSTATUS(rv) or 0) - \ (os.WIFSIGNALED(rv) and os.WTERMSIG(rv) or 0) logging.warn('stale mount possibly left behind on ' + d) raise GsyncdError("cleaning up temp mountpoint %s failed with status %d" % \ (d, rv)) else: rv = 0 try: os.setsid() os.close(mpo) mntdata = '' while True: c = os.read(mpi, 1) if not c: break mntdata += c if mntdata: mounted = False if mntdata[-1] == 'M': mntdata = mntdata[:-1] assert (mntdata) mounted = True assert (mntdata[-1] == '\0') mntpt = mntdata[:-1] assert (mntpt) if mounted: po = self.umount_l(mntpt) po.terminate_geterr(fail_on_err=False) if po.returncode != 0: po.errlog() rv = po.returncode self.cleanup_mntpt(mntpt) except: logging.exception('mount cleanup failure:') rv = 200 os._exit(rv) logging.debug('auxiliary glusterfs mount prepared')
def __init__(self, path, pattern): m = re.search(pattern, path) if not m: raise GsyncdError("malformed path") self.path = path return m.groups()