Filter events on event connection
Currently if two triggers of the same connection type need to trigger on different events it's not possible to do so since the events are never filtered on which connection they came from. For example with the following setup where gerrit-org-1 only wants to trigger on changes to 'master' and gerrit-org-2 only wants to trigger on changes to 'develop' they will instead both trigger on 'master' and 'develop'since the events are never filtered on which connection they came from. - pipeline: name: check trigger: gerrit-org-1: - event: patchset-created branch: 'master' gerrit-org-2: - event: patchset-created branch: 'develop' Change-Id: Ia0476d71dee59c8b80db7630ac7a524bce87e6f9
This commit is contained in:
parent
3ca33f0686
commit
c81c2c6eec
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
fixes:
|
||||
- |
|
||||
Fixed a bug where multiple connections of the same type would not filter
|
||||
trigger events coming from the wrong connection.
|
|
@ -30,8 +30,10 @@
|
|||
trigger:
|
||||
another_gerrit:
|
||||
- event: patchset-created
|
||||
branch: 'master'
|
||||
review_gerrit:
|
||||
- event: patchset-created
|
||||
branch: 'develop'
|
||||
success:
|
||||
review_gerrit:
|
||||
Verified: 1
|
||||
|
|
|
@ -493,6 +493,16 @@ class TestMultipleGerrits(ZuulTestCase):
|
|||
def test_multiple_project_separate_gerrits_common_pipeline(self):
|
||||
self.executor_server.hold_jobs_in_build = True
|
||||
|
||||
self.create_branch('org/project2', 'develop')
|
||||
self.fake_another_gerrit.addEvent(
|
||||
self.fake_another_gerrit.getFakeBranchCreatedEvent(
|
||||
'org/project2', 'develop'))
|
||||
|
||||
self.fake_another_gerrit.addEvent(
|
||||
self.fake_review_gerrit.getFakeBranchCreatedEvent(
|
||||
'org/project2', 'develop'))
|
||||
self.waitUntilSettled()
|
||||
|
||||
A = self.fake_another_gerrit.addFakeChange(
|
||||
'org/project2', 'master', 'A')
|
||||
self.fake_another_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
|
@ -512,7 +522,7 @@ class TestMultipleGerrits(ZuulTestCase):
|
|||
self.fake_review_gerrit.change_number = 50
|
||||
|
||||
B = self.fake_review_gerrit.addFakeChange(
|
||||
'org/project2', 'master', 'B')
|
||||
'org/project2', 'develop', 'B')
|
||||
self.fake_review_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
|
||||
|
||||
self.waitUntilSettled()
|
||||
|
@ -528,6 +538,25 @@ class TestMultipleGerrits(ZuulTestCase):
|
|||
pipeline='common_check'),
|
||||
])
|
||||
|
||||
# NOTE(avass): This last change should not trigger any pipelines since
|
||||
# common_check is configured to only run on master for another_gerrit
|
||||
C = self.fake_another_gerrit.addFakeChange(
|
||||
'org/project2', 'develop', 'C')
|
||||
self.fake_another_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
|
||||
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertBuilds([
|
||||
dict(name='project-test2',
|
||||
changes='1,1',
|
||||
project='org/project2',
|
||||
pipeline='common_check'),
|
||||
dict(name='project-test1',
|
||||
changes='51,1',
|
||||
project='org/project2',
|
||||
pipeline='common_check'),
|
||||
])
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
|
|
@ -1366,15 +1366,16 @@ class PipelineParser(object):
|
|||
manager.ref_filters.extend(
|
||||
source.getRejectFilters(reject_config))
|
||||
|
||||
for trigger_name, trigger_config in conf.get('trigger').items():
|
||||
for connection_name, trigger_config in conf.get('trigger').items():
|
||||
if self.pcontext.tenant.allowed_triggers is not None and \
|
||||
trigger_name not in self.pcontext.tenant.allowed_triggers:
|
||||
raise UnknownConnection(trigger_name)
|
||||
connection_name not in self.pcontext.tenant.allowed_triggers:
|
||||
raise UnknownConnection(connection_name)
|
||||
trigger = self.pcontext.connections.getTrigger(
|
||||
trigger_name, trigger_config)
|
||||
connection_name, trigger_config)
|
||||
pipeline.triggers.append(trigger)
|
||||
manager.event_filters.extend(
|
||||
trigger.getEventFilters(conf['trigger'][trigger_name]))
|
||||
trigger.getEventFilters(connection_name,
|
||||
conf['trigger'][connection_name]))
|
||||
|
||||
# Pipelines don't get frozen
|
||||
return pipeline
|
||||
|
|
|
@ -179,6 +179,7 @@ class GerritEventConnector(threading.Thread):
|
|||
time.sleep(max((timestamp + self.delay) - now, 0.0))
|
||||
event = GerritTriggerEvent()
|
||||
event.timestamp = timestamp
|
||||
event.connection_name = self.connection.connection_name
|
||||
|
||||
# Gerrit events don't have an event id that could be used to globally
|
||||
# identify this event in the system so we have to generate one.
|
||||
|
|
|
@ -295,12 +295,12 @@ class GerritApprovalFilter(object):
|
|||
|
||||
|
||||
class GerritEventFilter(EventFilter, GerritApprovalFilter):
|
||||
def __init__(self, trigger, types=[], branches=[], refs=[],
|
||||
event_approvals={}, comments=[], emails=[], usernames=[],
|
||||
required_approvals=[], reject_approvals=[], uuid=None,
|
||||
scheme=None, ignore_deletes=True):
|
||||
def __init__(self, connection_name, trigger, types=[], branches=[],
|
||||
refs=[], event_approvals={}, comments=[], emails=[],
|
||||
usernames=[], required_approvals=[], reject_approvals=[],
|
||||
uuid=None, scheme=None, ignore_deletes=True):
|
||||
|
||||
EventFilter.__init__(self, trigger)
|
||||
EventFilter.__init__(self, connection_name, trigger)
|
||||
|
||||
GerritApprovalFilter.__init__(self,
|
||||
required_approvals=required_approvals,
|
||||
|
@ -325,6 +325,7 @@ class GerritEventFilter(EventFilter, GerritApprovalFilter):
|
|||
|
||||
def __repr__(self):
|
||||
ret = '<GerritEventFilter'
|
||||
ret += ' connection: %s' % self.connection_name
|
||||
|
||||
if self._types:
|
||||
ret += ' types: %s' % ', '.join(self._types)
|
||||
|
@ -358,6 +359,9 @@ class GerritEventFilter(EventFilter, GerritApprovalFilter):
|
|||
return ret
|
||||
|
||||
def matches(self, event, change):
|
||||
if not super().matches(event, change):
|
||||
return False
|
||||
|
||||
# event types are ORed
|
||||
matches_type = False
|
||||
for etype in self.types:
|
||||
|
|
|
@ -23,7 +23,7 @@ class GerritTrigger(BaseTrigger):
|
|||
name = 'gerrit'
|
||||
log = logging.getLogger("zuul.GerritTrigger")
|
||||
|
||||
def getEventFilters(self, trigger_conf):
|
||||
def getEventFilters(self, connection_name, trigger_conf):
|
||||
efilters = []
|
||||
for trigger in to_list(trigger_conf):
|
||||
approvals = {}
|
||||
|
@ -42,6 +42,7 @@ class GerritTrigger(BaseTrigger):
|
|||
usernames = to_list(trigger.get('username_filter'))
|
||||
ignore_deletes = trigger.get('ignore-deletes', True)
|
||||
f = GerritEventFilter(
|
||||
connection_name=connection_name,
|
||||
trigger=self,
|
||||
types=to_list(trigger['event']),
|
||||
branches=to_list(trigger.get('branch')),
|
||||
|
|
|
@ -137,6 +137,7 @@ class GitConnection(BaseConnection):
|
|||
|
||||
def watcherCallback(self, data):
|
||||
event = GitTriggerEvent()
|
||||
event.connection_name = self.connection_name
|
||||
event.type = 'ref-updated'
|
||||
event.timestamp = time.time()
|
||||
event.project_hostname = self.canonical_hostname
|
||||
|
|
|
@ -40,10 +40,10 @@ class GitTriggerEvent(TriggerEvent):
|
|||
|
||||
|
||||
class GitEventFilter(EventFilter):
|
||||
def __init__(self, trigger, types=None, refs=None,
|
||||
def __init__(self, connection_name, trigger, types=None, refs=None,
|
||||
ignore_deletes=True):
|
||||
|
||||
super().__init__(trigger)
|
||||
super().__init__(connection_name, trigger)
|
||||
|
||||
self._refs = refs
|
||||
self.types = types if types is not None else []
|
||||
|
@ -53,6 +53,7 @@ class GitEventFilter(EventFilter):
|
|||
|
||||
def __repr__(self):
|
||||
ret = '<GitEventFilter'
|
||||
ret += ' connection: %s' % self.connection_name
|
||||
|
||||
if self.types:
|
||||
ret += ' types: %s' % ', '.join(self.types)
|
||||
|
@ -65,6 +66,9 @@ class GitEventFilter(EventFilter):
|
|||
return ret
|
||||
|
||||
def matches(self, event, change):
|
||||
if not super().matches(event, change):
|
||||
return False
|
||||
|
||||
# event types are ORed
|
||||
matches_type = False
|
||||
for etype in self.types:
|
||||
|
|
|
@ -23,10 +23,11 @@ class GitTrigger(BaseTrigger):
|
|||
name = 'git'
|
||||
log = logging.getLogger("zuul.GitTrigger")
|
||||
|
||||
def getEventFilters(self, trigger_conf):
|
||||
def getEventFilters(self, connection_name, trigger_conf):
|
||||
efilters = []
|
||||
for trigger in to_list(trigger_conf):
|
||||
f = GitEventFilter(
|
||||
connection_name=connection_name,
|
||||
trigger=self,
|
||||
types=to_list(trigger['event']),
|
||||
refs=to_list(trigger.get('ref')),
|
||||
|
|
|
@ -406,6 +406,7 @@ class GithubEventProcessor(object):
|
|||
base_repo = self.body.get('repository')
|
||||
|
||||
event = GithubTriggerEvent()
|
||||
event.connection_name = self.connection.connection_name
|
||||
event.trigger_name = 'github'
|
||||
event.project_name = base_repo.get('full_name')
|
||||
event.type = 'push'
|
||||
|
@ -615,6 +616,7 @@ class GithubEventProcessor(object):
|
|||
|
||||
def _pull_request_to_event(self, pr_body):
|
||||
event = GithubTriggerEvent()
|
||||
event.connection_name = self.connection.connection_name
|
||||
event.trigger_name = 'github'
|
||||
|
||||
base = pr_body.get('base')
|
||||
|
|
|
@ -260,12 +260,12 @@ class GithubCommonFilter(object):
|
|||
|
||||
|
||||
class GithubEventFilter(EventFilter, GithubCommonFilter):
|
||||
def __init__(self, trigger, types=[], branches=[], refs=[],
|
||||
comments=[], actions=[], labels=[], unlabels=[],
|
||||
def __init__(self, connection_name, trigger, types=[], branches=[],
|
||||
refs=[], comments=[], actions=[], labels=[], unlabels=[],
|
||||
states=[], statuses=[], required_statuses=[],
|
||||
check_runs=[], ignore_deletes=True):
|
||||
|
||||
EventFilter.__init__(self, trigger)
|
||||
EventFilter.__init__(self, connection_name, trigger)
|
||||
|
||||
GithubCommonFilter.__init__(self, required_statuses=required_statuses)
|
||||
|
||||
|
@ -288,7 +288,7 @@ class GithubEventFilter(EventFilter, GithubCommonFilter):
|
|||
|
||||
def __repr__(self):
|
||||
ret = '<GithubEventFilter'
|
||||
|
||||
ret += ' connection: %s' % self.connection_name
|
||||
if self._types:
|
||||
ret += ' types: %s' % ', '.join(self._types)
|
||||
if self._branches:
|
||||
|
@ -318,6 +318,9 @@ class GithubEventFilter(EventFilter, GithubCommonFilter):
|
|||
return ret
|
||||
|
||||
def matches(self, event, change):
|
||||
if not super().matches(event, change):
|
||||
return False
|
||||
|
||||
# event types are ORed
|
||||
matches_type = False
|
||||
for etype in self.types:
|
||||
|
|
|
@ -23,10 +23,11 @@ class GithubTrigger(BaseTrigger):
|
|||
name = 'github'
|
||||
log = logging.getLogger("zuul.trigger.GithubTrigger")
|
||||
|
||||
def getEventFilters(self, trigger_config):
|
||||
def getEventFilters(self, connection_name, trigger_config):
|
||||
efilters = []
|
||||
for trigger in to_list(trigger_config):
|
||||
f = GithubEventFilter(
|
||||
connection_name=connection_name,
|
||||
trigger=self,
|
||||
types=to_list(trigger['event']),
|
||||
actions=to_list(trigger.get('action')),
|
||||
|
|
|
@ -86,6 +86,7 @@ class GitlabEventConnector(threading.Thread):
|
|||
|
||||
def _event_base(self, body):
|
||||
event = GitlabTriggerEvent()
|
||||
event.connection_name = self.connection.connection_name
|
||||
attrs = body.get('object_attributes')
|
||||
if attrs:
|
||||
event.updated_at = int(dateutil.parser.parse(
|
||||
|
|
|
@ -103,9 +103,9 @@ class GitlabTriggerEvent(TriggerEvent):
|
|||
|
||||
class GitlabEventFilter(EventFilter):
|
||||
def __init__(
|
||||
self, trigger, types=None, actions=None,
|
||||
self, connection_name, trigger, types=None, actions=None,
|
||||
comments=None, refs=None, labels=None, ignore_deletes=True):
|
||||
super(GitlabEventFilter, self).__init__(self)
|
||||
super().__init__(connection_name, trigger)
|
||||
self._types = types or []
|
||||
self.types = [re.compile(x) for x in self._types]
|
||||
self.actions = actions or []
|
||||
|
@ -118,6 +118,7 @@ class GitlabEventFilter(EventFilter):
|
|||
|
||||
def __repr__(self):
|
||||
ret = '<GitlabEventFilter'
|
||||
ret += ' connection: %s' % self.connection_name
|
||||
|
||||
if self._types:
|
||||
ret += ' types: %s' % ', '.join(self._types)
|
||||
|
@ -136,6 +137,9 @@ class GitlabEventFilter(EventFilter):
|
|||
return ret
|
||||
|
||||
def matches(self, event, change):
|
||||
if not super().matches(event, change):
|
||||
return False
|
||||
|
||||
matches_type = False
|
||||
for etype in self.types:
|
||||
if etype.match(event.type):
|
||||
|
|
|
@ -23,10 +23,11 @@ class GitlabTrigger(BaseTrigger):
|
|||
name = 'gitlab'
|
||||
log = logging.getLogger("zuul.trigger.GitlabTrigger")
|
||||
|
||||
def getEventFilters(self, trigger_config):
|
||||
def getEventFilters(self, connection_name, trigger_config):
|
||||
efilters = []
|
||||
for trigger in to_list(trigger_config):
|
||||
f = GitlabEventFilter(
|
||||
connection_name=connection_name,
|
||||
trigger=self,
|
||||
types=to_list(trigger['event']),
|
||||
actions=to_list(trigger.get('action')),
|
||||
|
|
|
@ -203,6 +203,7 @@ class PagureEventConnector(threading.Thread):
|
|||
|
||||
def _event_base(self, body, pull_data_field='pullrequest'):
|
||||
event = PagureTriggerEvent()
|
||||
event.connection_name = self.connection.connection_name
|
||||
|
||||
if pull_data_field in body['msg']:
|
||||
data = body['msg'][pull_data_field]
|
||||
|
|
|
@ -111,10 +111,11 @@ class PagureTriggerEvent(TriggerEvent):
|
|||
|
||||
|
||||
class PagureEventFilter(EventFilter):
|
||||
def __init__(self, trigger, types=[], refs=[], statuses=[],
|
||||
comments=[], actions=[], tags=[], ignore_deletes=True):
|
||||
def __init__(self, connection_name, trigger, types=[], refs=[],
|
||||
statuses=[], comments=[], actions=[], tags=[],
|
||||
ignore_deletes=True):
|
||||
|
||||
EventFilter.__init__(self, trigger)
|
||||
EventFilter.__init__(self, connection_name, trigger)
|
||||
|
||||
self._types = types
|
||||
self._refs = refs
|
||||
|
@ -129,6 +130,7 @@ class PagureEventFilter(EventFilter):
|
|||
|
||||
def __repr__(self):
|
||||
ret = '<PagureEventFilter'
|
||||
ret += ' connection: %s' % self.connection_name
|
||||
|
||||
if self._types:
|
||||
ret += ' types: %s' % ', '.join(self._types)
|
||||
|
@ -149,6 +151,9 @@ class PagureEventFilter(EventFilter):
|
|||
return ret
|
||||
|
||||
def matches(self, event, change):
|
||||
if not super().matches(event, change):
|
||||
return False
|
||||
|
||||
matches_type = False
|
||||
for etype in self.types:
|
||||
if etype.match(event.type):
|
||||
|
|
|
@ -23,10 +23,11 @@ class PagureTrigger(BaseTrigger):
|
|||
name = 'pagure'
|
||||
log = logging.getLogger("zuul.trigger.PagureTrigger")
|
||||
|
||||
def getEventFilters(self, trigger_config):
|
||||
def getEventFilters(self, connection_name, trigger_config):
|
||||
efilters = []
|
||||
for trigger in to_list(trigger_config):
|
||||
f = PagureEventFilter(
|
||||
connection_name=connection_name,
|
||||
trigger=self,
|
||||
types=to_list(trigger['event']),
|
||||
actions=to_list(trigger.get('action')),
|
||||
|
|
|
@ -18,8 +18,8 @@ from zuul.model import EventFilter, TriggerEvent
|
|||
|
||||
|
||||
class TimerEventFilter(EventFilter):
|
||||
def __init__(self, trigger, types=[], timespecs=[]):
|
||||
EventFilter.__init__(self, trigger)
|
||||
def __init__(self, connection_name, trigger, types=[], timespecs=[]):
|
||||
EventFilter.__init__(self, connection_name, trigger)
|
||||
|
||||
self._types = types
|
||||
self.types = [re.compile(x) for x in types]
|
||||
|
@ -27,6 +27,7 @@ class TimerEventFilter(EventFilter):
|
|||
|
||||
def __repr__(self):
|
||||
ret = '<TimerEventFilter'
|
||||
ret += ' connection: %s' % self.connection_name
|
||||
|
||||
if self._types:
|
||||
ret += ' types: %s' % ', '.join(self._types)
|
||||
|
|
|
@ -23,10 +23,11 @@ from zuul.driver.util import to_list
|
|||
class TimerTrigger(BaseTrigger):
|
||||
name = 'timer'
|
||||
|
||||
def getEventFilters(self, trigger_conf):
|
||||
def getEventFilters(self, connection_name, trigger_conf):
|
||||
efilters = []
|
||||
for trigger in to_list(trigger_conf):
|
||||
f = TimerEventFilter(trigger=self,
|
||||
f = TimerEventFilter(connection_name=connection_name,
|
||||
trigger=self,
|
||||
types=['timer'],
|
||||
timespecs=to_list(trigger['time']))
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ class ZuulDriver(Driver, TriggerInterface):
|
|||
event = ZuulTriggerEvent()
|
||||
event.type = PROJECT_CHANGE_MERGED
|
||||
event.trigger_name = self.name
|
||||
event.connection_name = "zuul"
|
||||
event.project_hostname = change.project.canonical_hostname
|
||||
event.project_name = change.project.name
|
||||
event.change_number = change.number
|
||||
|
@ -123,6 +124,7 @@ class ZuulDriver(Driver, TriggerInterface):
|
|||
def _createParentChangeEnqueuedEvent(self, change, pipeline):
|
||||
event = ZuulTriggerEvent()
|
||||
event.type = PARENT_CHANGE_ENQUEUED
|
||||
event.connection_name = "zuul"
|
||||
event.trigger_name = self.name
|
||||
event.pipeline_name = pipeline.name
|
||||
event.project_hostname = change.project.canonical_hostname
|
||||
|
|
|
@ -18,8 +18,8 @@ from zuul.model import EventFilter, TriggerEvent
|
|||
|
||||
|
||||
class ZuulEventFilter(EventFilter):
|
||||
def __init__(self, trigger, types=[], pipelines=[]):
|
||||
EventFilter.__init__(self, trigger)
|
||||
def __init__(self, connection_name, trigger, types=[], pipelines=[]):
|
||||
EventFilter.__init__(self, connection_name, trigger)
|
||||
|
||||
self._types = types
|
||||
self._pipelines = pipelines
|
||||
|
@ -28,6 +28,7 @@ class ZuulEventFilter(EventFilter):
|
|||
|
||||
def __repr__(self):
|
||||
ret = '<ZuulEventFilter'
|
||||
ret += ' connection: %s' % self.connection_name
|
||||
|
||||
if self._types:
|
||||
ret += ' types: %s' % ', '.join(self._types)
|
||||
|
@ -38,6 +39,9 @@ class ZuulEventFilter(EventFilter):
|
|||
return ret
|
||||
|
||||
def matches(self, event, change):
|
||||
if not super().matches(event, change):
|
||||
return False
|
||||
|
||||
# event types are ORed
|
||||
matches_type = False
|
||||
for etype in self.types:
|
||||
|
|
|
@ -29,10 +29,11 @@ class ZuulTrigger(BaseTrigger):
|
|||
self._handle_parent_change_enqueued_events = False
|
||||
self._handle_project_change_merged_events = False
|
||||
|
||||
def getEventFilters(self, trigger_conf):
|
||||
def getEventFilters(self, connection_name, trigger_conf):
|
||||
efilters = []
|
||||
for trigger in to_list(trigger_conf):
|
||||
f = ZuulEventFilter(
|
||||
connection_name=connection_name,
|
||||
trigger=self,
|
||||
types=to_list(trigger['event']),
|
||||
pipelines=to_list(trigger.get('pipeline')),
|
||||
|
|
|
@ -3799,6 +3799,7 @@ class TriggerEvent(AbstractEvent):
|
|||
self.project_hostname = None
|
||||
self.project_name = None
|
||||
self.trigger_name = None
|
||||
self.connection_name = None
|
||||
# Representation of the user account that performed the event.
|
||||
self.account = None
|
||||
# patchset-created, comment-added, etc.
|
||||
|
@ -3833,6 +3834,7 @@ class TriggerEvent(AbstractEvent):
|
|||
"project_hostname": self.project_hostname,
|
||||
"project_name": self.project_name,
|
||||
"trigger_name": self.trigger_name,
|
||||
"connection_name": self.connection_name,
|
||||
"account": self.account,
|
||||
"change_number": self.change_number,
|
||||
"change_url": self.change_url,
|
||||
|
@ -3862,6 +3864,7 @@ class TriggerEvent(AbstractEvent):
|
|||
self.project_hostname = d["project_hostname"]
|
||||
self.project_name = d["project_name"]
|
||||
self.trigger_name = d["trigger_name"]
|
||||
self.connection_name = d["connection_name"]
|
||||
self.account = d["account"]
|
||||
self.change_number = d["change_number"]
|
||||
self.change_url = d["change_url"]
|
||||
|
@ -3926,12 +3929,18 @@ class BaseFilter(ConfigObject):
|
|||
|
||||
class EventFilter(BaseFilter):
|
||||
"""Allows a Pipeline to only respond to certain events."""
|
||||
def __init__(self, trigger):
|
||||
def __init__(self, connection_name, trigger):
|
||||
super(EventFilter, self).__init__()
|
||||
self.connection_name = connection_name
|
||||
self.trigger = trigger
|
||||
|
||||
def matches(self, event, ref):
|
||||
# TODO(jeblair): consider removing ref argument
|
||||
|
||||
# Event came from wrong connection
|
||||
if self.connection_name != event.connection_name:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ class BaseTrigger(object, metaclass=abc.ABCMeta):
|
|||
self.config = config or {}
|
||||
|
||||
@abc.abstractmethod
|
||||
def getEventFilters(self, trigger_conf):
|
||||
def getEventFilters(self, connection_name, trigger_conf):
|
||||
"""Return a list of EventFilter's for the scheduler to match against.
|
||||
"""
|
||||
|
||||
|
|
Loading…
Reference in New Issue