From bc5f0007512fe07ed2b09f9ff3427a7366126f8c Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 11 Oct 2012 13:01:21 -0400 Subject: wrote FAM docs --- doc/development/fam.txt | 72 +++++++++ src/lib/Bcfg2/Server/FileMonitor/Fam.py | 32 +++- src/lib/Bcfg2/Server/FileMonitor/Gamin.py | 49 ++++-- src/lib/Bcfg2/Server/FileMonitor/Inotify.py | 66 +++++++- src/lib/Bcfg2/Server/FileMonitor/Pseudo.py | 13 +- src/lib/Bcfg2/Server/FileMonitor/__init__.py | 232 ++++++++++++++++++++++++--- 6 files changed, 412 insertions(+), 52 deletions(-) create mode 100644 doc/development/fam.txt diff --git a/doc/development/fam.txt b/doc/development/fam.txt new file mode 100644 index 000000000..c2c3b14f5 --- /dev/null +++ b/doc/development/fam.txt @@ -0,0 +1,72 @@ +.. -*- mode: rst -*- + +.. _development-fam: + +========================== + File Monitor Development +========================== + +Bcfg2 depends heavily on file activity monitoring (FAM) to reload data +from disk when it changes. A number of FAM backends are supported +(documented thoroughly below), but you may wish to develop additional +backends. For instance, the current best FAM backend on Linux is +INotify, but if you are running a non-Linux system that lacks INotify +support you may wish to write a backend for your OS (e.g., a kqueue +backend for BSD-based Bcfg2 servers). This page documents the FAM API +and the existing FAM backends. + +.. _development-fam-event-codes: + +Event Codes +=========== + +Five event codes are generally understood: + ++----------+-----------------------------------------------------------+ +| Event | Description | ++==========+===========================================================+ +| exists | Produced when a monitor is added to a file or directory | +| | that exists, and produced for all files or directories | +| | inside a directory that is monitored (non-recursively). | ++----------+-----------------------------------------------------------+ +| endExist | Produced immediately after ``exists``. No plugins should | +| | process this event meaningfully, so FAM backends do not | +| | need to produce it. | ++----------+-----------------------------------------------------------+ +| created | Produced when a file is created inside a monitored | +| | directory. | ++----------+-----------------------------------------------------------+ +| changed | Produced when a monitored file, or a file inside a | +| | monitored directory, is changed. | ++----------+-----------------------------------------------------------+ +| deleted | Produced when a monitored file, or a file inside a | +| | monitored directory, is deleted. | ++----------+-----------------------------------------------------------+ + +Basics +====== + +.. automodule:: Bcfg2.Server.FileMonitor + +Existing FAM Backends +===================== + +Pseudo +------ + +.. automodule:: Bcfg2.Server.FileMonitor.Pseudo + +Fam +--- + +.. automodule:: Bcfg2.Server.FileMonitor.Fam + +Gamin +----- + +.. automodule:: Bcfg2.Server.FileMonitor.Gamin + +Inotify +------- + +.. automodule:: Bcfg2.Server.FileMonitor.Inotify diff --git a/src/lib/Bcfg2/Server/FileMonitor/Fam.py b/src/lib/Bcfg2/Server/FileMonitor/Fam.py index d1420c105..a392af185 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Fam.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Fam.py @@ -1,7 +1,8 @@ -""" Fam provides FAM support for file alteration events """ +""" File monitor backend with support for the `File Alteration Monitor +`_.""" import os -import _fam +#import _fam import stat import logging from time import time @@ -11,30 +12,37 @@ LOGGER = logging.getLogger(__name__) class Fam(FileMonitor): - """ file monitor with support for FAM """ + """ File monitor backend with support for the `File Alteration + Monitor `_ (also abbreviated + "FAM").""" - __priority__ = 90 + #: FAM is the worst actual monitor backend, so give it a low + #: priority. + __priority__ = 10 def __init__(self, ignore=None, debug=False): FileMonitor.__init__(self, ignore=ignore, debug=debug) self.filemonitor = _fam.open() self.users = {} + __init__.__doc__ = FileMonitor.__init__.__doc__ def fileno(self): - """Return fam file handle number.""" return self.filemonitor.fileno() + fileno.__doc__ = FileMonitor.fileno.__doc__ def handle_event_set(self, _=None): self.Service() + handle_event_set.__doc__ = FileMonitor.handle_event_set.__doc__ def handle_events_in_interval(self, interval): now = time() while (time() - now) < interval: if self.Service(): now = time() + handle_events_in_interval.__doc__ = \ + FileMonitor.handle_events_in_interval.__doc__ def AddMonitor(self, path, obj, _=None): - """Add a monitor to path, installing a callback to obj.HandleEvent.""" mode = os.stat(path)[stat.ST_MODE] if stat.S_ISDIR(mode): handle = self.filemonitor.monitorDirectory(path, None) @@ -44,9 +52,19 @@ class Fam(FileMonitor): if obj != None: self.users[handle.requestID()] = obj return handle.requestID() + AddMonitor.__doc__ = FileMonitor.AddMonitor.__doc__ def Service(self, interval=0.50): - """Handle all fam work.""" + """ Handle events for the specified period of time (in + seconds). This call will block for ``interval`` seconds. + + :param interval: The interval, in seconds, during which events + should be handled. Any events that are + already pending when :func:`Service` is + called will also be handled. + :type interval: int + :returns: None + """ count = 0 collapsed = 0 rawevents = [] diff --git a/src/lib/Bcfg2/Server/FileMonitor/Gamin.py b/src/lib/Bcfg2/Server/FileMonitor/Gamin.py index 23f5424d0..9134758b8 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Gamin.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Gamin.py @@ -1,4 +1,5 @@ -""" Gamin driver for file alteration events """ +""" File monitor backend with `Gamin +`_ support. """ import os import stat @@ -7,12 +8,12 @@ from gamin import WatchMonitor, GAMCreated, GAMExists, GAMEndExist, \ from Bcfg2.Server.FileMonitor import Event, FileMonitor - class GaminEvent(Event): - """ - This class provides an event analogous to - python-fam events based on gamin sources. - """ + """ This class maps Gamin event constants to FAM :ref:`event codes + `. """ + + #: The map of gamin event constants (which mirror FAM event names + #: closely) to :ref:`event codes ` action_map = {GAMCreated: 'created', GAMExists: 'exists', GAMChanged: 'changed', GAMDeleted: 'deleted', GAMEndExist: 'endExist'} @@ -21,19 +22,38 @@ class GaminEvent(Event): Event.__init__(self, request_id, filename, code) if code in self.action_map: self.action = self.action_map[code] + __init__.__doc__ = Event.__init__.__doc__ class Gamin(FileMonitor): - """ file monitor with gamin support """ - __priority__ = 10 + """ File monitor backend with `Gamin + `_ support. """ + + #: The Gamin backend is fairly decent, particularly newer + #: releases, so it has a fairly high priority. + __priority__ = 90 def __init__(self, ignore=None, debug=False): FileMonitor.__init__(self, ignore=ignore, debug=debug) + + #: The :class:`Gamin.WatchMonitor` object for this monitor. self.mon = None + + #: The counter used to produce monotonically increasing + #: monitor handle IDs self.counter = 0 + + #: The queue used to record monitors that are added before + #: :func:`start` has been called and :attr:`mon` is created. self.add_q = [] + __init__.__doc__ = FileMonitor.__init__.__doc__ def start(self): + """ The Gamin watch monitor in :attr:`mon` must be created by + the daemonized process, so is created in ``start()``. Before + the :class:`Gamin.WatchMonitor` object is created, monitors + are added to :attr:`add_q`, and are created once the watch + monitor is created.""" FileMonitor.start(self) self.mon = WatchMonitor() for monitor in self.add_q: @@ -41,14 +61,18 @@ class Gamin(FileMonitor): self.add_q = [] def fileno(self): - return self.mon.get_fd() + if self.started: + return self.mon.get_fd() + else: + return None + fileno.__doc__ = FileMonitor.fileno.__doc__ def queue(self, path, action, request_id): - """queue up the event for later handling""" + """ Create a new :class:`GaminEvent` and add it to the + :attr:`events` queue for later handling. """ self.events.append(GaminEvent(request_id, path, action)) def AddMonitor(self, path, obj, handle=None): - """Add a monitor to path, installing a callback to obj.""" if handle is None: handle = self.counter self.counter += 1 @@ -69,11 +93,14 @@ class Gamin(FileMonitor): self.mon.watch_file(path, self.queue, handle) self.handles[handle] = obj return handle + AddMonitor.__doc__ = FileMonitor.AddMonitor.__doc__ def pending(self): return FileMonitor.pending(self) or self.mon.event_pending() + pending.__doc__ = FileMonitor.pending.__doc__ def get_event(self): if self.mon.event_pending(): self.mon.handle_one_event() return FileMonitor.get_event(self) + get_event.__doc__ = FileMonitor.get_event.__doc__ diff --git a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py index 175df65c8..d5aa8e4ad 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py @@ -1,4 +1,5 @@ -""" Inotify driver for file alteration events """ +"""File monitor backend with `inotify `_ +support. """ import os import logging @@ -11,29 +12,75 @@ LOGGER = logging.getLogger(__name__) class Inotify(Pseudo, pyinotify.ProcessEvent): - """ file monitor with inotify support """ + """ File monitor backend with `inotify + `_ support. """ + + #: Inotify is the best FAM backend, so it gets a very high + #: priority + __priority__ = 99 - __priority__ = 1 # pylint: disable=E1101 + #: Map pyinotify event constants to FAM :ref:`event codes + #: `. The mapping is not + #: terrifically exact. action_map = {pyinotify.IN_CREATE: 'created', pyinotify.IN_DELETE: 'deleted', pyinotify.IN_MODIFY: 'changed', pyinotify.IN_MOVED_FROM: 'deleted', pyinotify.IN_MOVED_TO: 'created'} # pylint: enable=E1101 + + #: The pyinotify event mask. We only ask for events that are + #: listed in :attr:`action_map` mask = reduce(lambda x, y: x | y, action_map.keys()) def __init__(self, ignore=None, debug=False): Pseudo.__init__(self, ignore=ignore, debug=debug) pyinotify.ProcessEvent.__init__(self) + + #: inotify can't set useful monitors directly on files, only + #: on directories, so when a monitor is added on a file we add + #: its parent directory to ``event_filter`` and then only + #: produce events on a file in that directory if the file is + #: listed in ``event_filter``. Keys are directories -- the + #: parent directories of individual files that are monitored + #: -- and values are lists of full paths to files in each + #: directory that events *should* be produced for. An event + #: on a file whose parent directory is in ``event_filter`` but + #: which is not itself listed will be silently suppressed. self.event_filter = dict() + + #: inotify doesn't like monitoring a path twice, so we keep a + #: dict of :class:`pyinotify.Watch` objects, keyed by monitor + #: path, to avoid trying to create duplicate monitors. + #: (Duplicates can happen if an object accidentally requests + #: duplicate monitors, or if two files in a single directory + #: are both individually monitored, since inotify can't set + #: monitors on the files but only on the parent directories.) self.watches_by_path = dict() - # these are created in start() after the server is done forking + + #: The :class:`pyinotify.ThreadedNotifier` object. This is + #: created in :func:`start` after the server is done + #: daemonizing. self.notifier = None + + #: The :class:`pyinotify.WatchManager` object. This is created + #: in :func:`start` after the server is done daemonizing. self.watchmgr = None + + #: The queue used to record monitors that are added before + #: :func:`start` has been called and :attr:`notifier` and + #: :attr:`watchmgr` are created. self.add_q = [] def start(self): + """ The inotify notifier and manager objects in + :attr:`notifier` and :attr:`watchmgr` must be created by the + daemonized process, so they are created in ``start()``. Before + those objects are created, monitors are added to + :attr:`add_q`, and are created once the + :class:`pyinotify.ThreadedNotifier` and + :class:`pyinotify.WatchManager` objects are created.""" Pseudo.start(self) self.watchmgr = pyinotify.WatchManager() self.notifier = pyinotify.ThreadedNotifier(self.watchmgr, self) @@ -47,8 +94,17 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): return self.watchmgr.get_fd() else: return None + fileno.__doc__ = Pseudo.fileno.__doc__ def process_default(self, ievent): + """ Process all inotify events received. This process a + :class:`pyinotify._Event` object, creates a + :class:`Bcfg2.Server.FileMonitor.Event` object from it, and + adds that event to :attr:`events`. + + :param ievent: Event to be processed + :type ievent: pyinotify._Event + """ action = ievent.maskname for amask, aname in self.action_map.items(): if ievent.mask & amask: @@ -142,7 +198,9 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): else: self.handles[path] = obj return path + AddMonitor.__doc__ = Pseudo.AddMonitor.__doc__ def shutdown(self): if self.notifier: self.notifier.stop() + shutdown.__doc__ = Pseudo.shutdown.__doc__ diff --git a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py index 9062cbfd8..24cd099d0 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py @@ -1,17 +1,20 @@ -""" Pseudo provides static monitor support for file alteration events """ +""" Pseudo provides static monitor support for file alteration events. +That is, it only produces "exists" and "endExist" events and does not +monitor for ongoing changes. """ import os from Bcfg2.Server.FileMonitor import FileMonitor, Event class Pseudo(FileMonitor): - """ file monitor that only produces events on server startup and - doesn't actually monitor at all """ + """ File monitor that only produces events on server startup and + doesn't actually monitor for ongoing changes at all. """ - __priority__ = 99 + #: The ``Pseudo`` monitor should only be used if no other FAM + #: backends are available. + __priority__ = 1 def AddMonitor(self, path, obj, handleID=None): - """add a monitor to path, installing a callback to obj.HandleEvent""" if handleID is None: handleID = len(list(self.handles.keys())) self.events.append(Event(handleID, path, 'exists')) diff --git a/src/lib/Bcfg2/Server/FileMonitor/__init__.py b/src/lib/Bcfg2/Server/FileMonitor/__init__.py index 1b12ab703..ec21a744c 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/__init__.py +++ b/src/lib/Bcfg2/Server/FileMonitor/__init__.py @@ -1,4 +1,49 @@ -"""Bcfg2.Server.FileMonitor provides the support for monitoring files.""" +""" Bcfg2.Server.FileMonitor provides the support for monitoring +files. The FAM acts as a dispatcher for events: An event is detected +on a file (e.g., the file content is changed), and then that event is +dispatched to the ``HandleEvent`` method of an object that knows how +to handle the event. Consequently, +:func:`Bcfg2.Server.FileMonitor.FileMonitor.AddMonitor` takes two +arguments: the path to monitor, and the object that handles events +detected on that event. + +``HandleEvent`` is called with a single argument, the +:class:`Bcfg2.Server.FileMonitor.Event` object to be handled. + +Assumptions +----------- + +The FAM API Bcfg2 uses is based on the API of SGI's `File Alteration +Monitor `_ (also called "FAM"). +Consequently, a few assumptions apply: + +* When a file or directory is monitored for changes, we call that a + "monitor"; other backends my use the term "watch," but for + consistency we will use "monitor." +* Monitors can be set on files or directories. +* A monitor set on a directory monitors all files within that + directory, non-recursively. If the object that requested the + monitor wishes to monitor recursively, it must implement that + itself. +* Setting a monitor immediately produces "exists" and "endExist" + events for the monitored file or directory and all files or + directories contained within it (non-recursively). +* An event on a file or directory that is monitored directly yields + the full path to the file or directory. +* An event on a file or directory that is *only* contained within a + monitored directory yields the relative path to the file or + directory within the monitored parent. It is the responsibility of + the handler to reconstruct full paths as necessary. +* Each monitor that is set must have a unique ID that identifies it, + in order to make it possible to reconstruct full paths as + necessary. This ID will be stored in + :attr:`Bcfg2.Server.FileMonitor.FileMonitor.handles`. It may be any + hashable value; some FAM backends use monotonically increasing + integers, while others use the path to the monitor. + +Base Classes +------------ +""" import os import sys @@ -10,14 +55,45 @@ LOGGER = logging.getLogger(__name__) class Event(object): - """ Base class for all FAM events """ + """ Base class for all FAM events. """ + def __init__(self, request_id, filename, code): + """ + :param request_id: The handler ID of the monitor that produced + this event + :type request_id: Varies + :param filename: The file or directory on which the event was + detected. An event on a file or directory + that is monitored directly yields the full + path to the file or directory; an event on a + file or directory that is *only* contained + within a monitored directory yields the + relative path to the file or directory within + the monitored parent. + :type filename: string + :param code: The :ref:`event code + ` produced. I.e., + the type of event. + :type code: string + """ + #: The handler ID of the monitor that produced this event self.requestID = request_id + + #: The file or directory on which the event was detected. An + #: event on a file or directory that is monitored directly + #: yields the full path to the file or directory; an event on + #: a file or directory that is *only* contained within a + #: monitored directory yields the relative path to the file or + #: directory within the monitored parent. self.filename = filename + + #: The :ref:`event code ` + #: produced. I.e., the type of event. self.action = code def code2str(self): - """return static code for event""" + """ Return the :ref:`event code ` + for this event. This is just an alias for :attr:`action`. """ return self.action def __str__(self): @@ -29,15 +105,48 @@ class Event(object): class FileMonitor(object): - """File Monitor baseclass.""" + """ The base class that all FAM implementions must inherit. + + The simplest instance of a FileMonitor subclass needs only to add + monitor objects to :attr:`handles` and received events to + :attr:`events`; the basic interface will handle the rest. """ + + #: The relative priority of this FAM backend. Better backends + #: should have higher priorities. + __priority__ = -1 + def __init__(self, ignore=None, debug=False): - object.__init__(self) + """ + :param ignore: A list of filename globs describing events that + should be ignored (i.e., not processed by any + object) + :type ignore: list of strings (filename globs) + :param debug: Produce debugging information about the events + received and handled. + :type debug: bool + + .. ----- + .. autoattribute:: __priority__ + """ + #: Whether or not to produce debug logging self.debug = debug + + #: A dict that records which objects handle which events. + #: Keys are monitor handle IDs and values are objects whose + #: ``HandleEvent`` method will be called to handle an event self.handles = dict() + + #: Queue of events to handle self.events = [] + if ignore is None: ignore = [] + #: List of filename globs to ignore events for. For events + #: that include the full path, both the full path and the bare + #: filename will be checked against ``ignore``. self.ignore = ignore + + #: Whether or not the FAM has been started. See :func:`start`. self.started = False def __str__(self): @@ -49,17 +158,34 @@ class FileMonitor(object): self.fileno()) def start(self): - """ start threads or anything else that needs to be done after - the server forks and daemonizes """ + """ Start threads or anything else that needs to be done after + the server forks and daemonizes. Note that monitors may (and + almost certainly will) be added before ``start()`` is called, + so if a backend depends on being started to add monitors, + those requests will need to be enqueued and added after + ``start()``. See + :class:`Bcfg2.Server.FileMonitor.Inotify.Inotify` for an + example of this. """ self.started = True def debug_log(self, msg): - """ log a debug message """ + """ Log a debug message. + + :param msg: The message to log iff :attr:`debug` is set.""" if self.debug: LOGGER.info(msg) def should_ignore(self, event): - """ returns true if an event should be ignored """ + """ Returns True if an event should be ignored, False + otherwise. For events that include the full path, both the + full path and the bare filename will be checked against + :attr:`ignore`. If the event is ignored, a debug message will + be logged with :func:`debug_log`. + + :param event: Check if this event matches :attr:`ignore` + :type event: Bcfg2.Server.FileMonitor.Event + :returns: bool - Whether not to ignore the event + """ for pattern in self.ignore: if (fnmatch.fnmatch(event.filename, pattern) or fnmatch.fnmatch(os.path.split(event.filename)[-1], pattern)): @@ -68,20 +194,36 @@ class FileMonitor(object): return False def pending(self): - """ returns True if there are pending events """ + """ Returns True if there are pending events (i.e., events in + :attr:`events` that have not been processed), False + otherwise. """ return bool(self.events) def get_event(self): - """ get the oldest pending event """ + """ Get the oldest pending event in :attr:`events`. + + :returns: :class:`Bcfg2.Server.FileMonitor.Event` + """ return self.events.pop(0) def fileno(self): - """ get the file descriptor of the file monitor thread """ + """ Get the file descriptor of the file monitor thread. + + :returns: int - The FD number + """ return 0 def handle_one_event(self, event): - """ handle the given event by dispatching it to the object - that handles events for the path """ + """ Handle the given event by dispatching it to the object + that handles it. This is only called by + :func:`handle_event_set`, so if a backend overrides that + method it does not necessarily need to implement this + function. + + :param event: The event to handle. + :type event: Bcfg2.Server.FileMonitor.Event + :returns: None + """ if not self.started: self.start() if self.should_ignore(event): @@ -101,15 +243,22 @@ class FileMonitor(object): (event.code2str(), event.filename, err)) def handle_event_set(self, lock=None): - """ Handle all pending events """ + """ Handle all pending events. + + :param lock: A thread lock to use while handling events. If + None, then no thread locking will be performed. + This can possibly lead to race conditions in + event handling, although it's unlikely to cause + any real problems. + :type lock: threading.Lock + :returns: None + """ if not self.started: self.start() - count = 1 - event = self.get_event() + count = 0 start = time() if lock: lock.acquire() - self.handle_one_event(event) while self.pending(): self.handle_one_event(self.get_event()) count += 1 @@ -119,8 +268,19 @@ class FileMonitor(object): LOGGER.info("Handled %d events in %.03fs" % (count, (end - start))) def handle_events_in_interval(self, interval): - """ handle events for the specified period of time (in - seconds) """ + """ Handle events for the specified period of time (in + seconds). This call will block for ``interval`` seconds and + handle all events received during that period by calling + :func:`handle_event_set`. + + :param interval: The interval, in seconds, during which events + should be handled. Any events that are + already pending when + :func:`handle_events_in_interval` is called + will also be handled. + :type interval: int + :returns: None + """ if not self.started: self.start() end = time() + interval @@ -132,17 +292,38 @@ class FileMonitor(object): sleep(0.5) def shutdown(self): - """ shutdown the monitor """ + """ Handle any tasks required to shut down the monitor. """ self.started = False def AddMonitor(self, path, obj, handleID=None): - """ watch the specified path, alerting obj to events """ + """ Monitor the specified path, alerting obj to events. This + method must be overridden by a subclass of + :class:`Bcfg2.Server.FileMonitor.FileMonitor`. + + :param path: The path to monitor + :type path: string + :param obj: The object whose ``HandleEvent`` method will be + called when an event is produced. + :type obj: Varies + :param handleID: The handle ID to use for the monitor. This + is useful when requests to add a monitor must + be enqueued and the actual monitors added + after :func:`start` is called. + :type handleID: Varies + :returns: Varies - The handler ID for the newly created + monitor + """ raise NotImplementedError +#: A dict of all available FAM backends. Keys are the human-readable +#: names of the backends, which are used in bcfg2.conf to select a +#: backend; values are the backend classes. In addition, the +#: ``default`` key will be set to the best FAM backend as determined +#: by :attr:`Bcfg2.Server.FileMonitor.FileMonitor.__priority__` available = dict() # pylint: disable=C0103 -# todo: loading the monitor drivers should be automatic +# TODO: loading the monitor drivers should be automatic from Bcfg2.Server.FileMonitor.Pseudo import Pseudo available['pseudo'] = Pseudo @@ -162,9 +343,10 @@ try: from Bcfg2.Server.FileMonitor.Inotify import Inotify available['inotify'] = Inotify except ImportError: - pass + pass -for fdrv in sorted(available.keys(), key=lambda k: available[k].__priority__): +for fdrv in reversed(sorted(available.keys(), + key=lambda k: available[k].__priority__)): if fdrv in available: available['default'] = available[fdrv] break -- cgit v1.2.3-1-g7c22