mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-20 05:23:44 +00:00
Unify Dashboard & Webhook events (#394)
This change unifies the previously adhoc SignalR events and Webhooks into a single event format.
This commit is contained in:
@ -5,17 +5,55 @@
|
||||
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
import json
|
||||
from onefuzztypes.enums import TaskType, ContainerType, ErrorCode
|
||||
from onefuzztypes.models import TaskConfig, TaskDetails, TaskContainers, Error, UserInfo
|
||||
from onefuzztypes.webhooks import (
|
||||
WebhookMessage,
|
||||
WebhookEventPing,
|
||||
WebhookEventTaskCreated,
|
||||
WebhookEventTaskStopped,
|
||||
WebhookEventTaskFailed,
|
||||
from onefuzztypes.primitives import Region, Container
|
||||
from onefuzztypes.enums import (
|
||||
TaskType,
|
||||
ContainerType,
|
||||
ErrorCode,
|
||||
OS,
|
||||
Architecture,
|
||||
NodeState,
|
||||
)
|
||||
from onefuzztypes.enums import WebhookEventType
|
||||
from onefuzztypes.models import (
|
||||
TaskConfig,
|
||||
TaskDetails,
|
||||
TaskContainers,
|
||||
TaskState,
|
||||
Error,
|
||||
UserInfo,
|
||||
JobConfig,
|
||||
Report,
|
||||
BlobRef,
|
||||
)
|
||||
from onefuzztypes.events import (
|
||||
Event,
|
||||
EventPing,
|
||||
EventCrashReported,
|
||||
EventFileAdded,
|
||||
EventTaskCreated,
|
||||
EventTaskStopped,
|
||||
EventTaskFailed,
|
||||
EventProxyCreated,
|
||||
EventProxyDeleted,
|
||||
EventProxyFailed,
|
||||
EventPoolCreated,
|
||||
EventPoolDeleted,
|
||||
EventScalesetCreated,
|
||||
EventScalesetFailed,
|
||||
EventScalesetDeleted,
|
||||
EventJobCreated,
|
||||
EventJobStopped,
|
||||
EventTaskStateUpdated,
|
||||
EventNodeStateUpdated,
|
||||
EventNodeCreated,
|
||||
EventNodeDeleted,
|
||||
get_event_type,
|
||||
EventType,
|
||||
)
|
||||
from onefuzztypes.webhooks import WebhookMessage
|
||||
|
||||
EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
ZERO_SHA256 = "0" * len(EMPTY_SHA256)
|
||||
|
||||
|
||||
def layer(depth: int, title: str, content: Optional[str] = None) -> None:
|
||||
@ -29,28 +67,9 @@ def typed(depth: int, title: str, content: str, data_type: str) -> None:
|
||||
|
||||
|
||||
def main():
|
||||
examples = {
|
||||
WebhookEventType.ping: WebhookEventPing(ping_id=UUID(int=0)),
|
||||
WebhookEventType.task_stopped: WebhookEventTaskStopped(
|
||||
job_id=UUID(int=0),
|
||||
task_id=UUID(int=0),
|
||||
user_info=UserInfo(
|
||||
application_id=UUID(int=0),
|
||||
object_id=UUID(int=0),
|
||||
upn="example@contoso.com",
|
||||
),
|
||||
),
|
||||
WebhookEventType.task_failed: WebhookEventTaskFailed(
|
||||
job_id=UUID(int=0),
|
||||
task_id=UUID(int=0),
|
||||
error=Error(code=ErrorCode.TASK_FAILED, errors=["example error message"]),
|
||||
user_info=UserInfo(
|
||||
application_id=UUID(int=0),
|
||||
object_id=UUID(int=0),
|
||||
upn="example@contoso.com",
|
||||
),
|
||||
),
|
||||
WebhookEventType.task_created: WebhookEventTaskCreated(
|
||||
examples = [
|
||||
EventPing(ping_id=UUID(int=0)),
|
||||
EventTaskCreated(
|
||||
job_id=UUID(int=0),
|
||||
task_id=UUID(int=0),
|
||||
config=TaskConfig(
|
||||
@ -75,13 +94,122 @@ def main():
|
||||
upn="example@contoso.com",
|
||||
),
|
||||
),
|
||||
}
|
||||
EventTaskStopped(
|
||||
job_id=UUID(int=0),
|
||||
task_id=UUID(int=0),
|
||||
user_info=UserInfo(
|
||||
application_id=UUID(int=0),
|
||||
object_id=UUID(int=0),
|
||||
upn="example@contoso.com",
|
||||
),
|
||||
),
|
||||
EventTaskFailed(
|
||||
job_id=UUID(int=0),
|
||||
task_id=UUID(int=0),
|
||||
error=Error(code=ErrorCode.TASK_FAILED, errors=["example error message"]),
|
||||
user_info=UserInfo(
|
||||
application_id=UUID(int=0),
|
||||
object_id=UUID(int=0),
|
||||
upn="example@contoso.com",
|
||||
),
|
||||
),
|
||||
EventTaskStateUpdated(
|
||||
job_id=UUID(int=0), task_id=UUID(int=0), state=TaskState.init
|
||||
),
|
||||
EventProxyCreated(region=Region("eastus")),
|
||||
EventProxyDeleted(region=Region("eastus")),
|
||||
EventProxyFailed(
|
||||
region=Region("eastus"),
|
||||
error=Error(code=ErrorCode.PROXY_FAILED, errors=["example error message"]),
|
||||
),
|
||||
EventPoolCreated(
|
||||
pool_name="example", os=OS.linux, arch=Architecture.x86_64, managed=True
|
||||
),
|
||||
EventPoolDeleted(pool_name="example"),
|
||||
EventScalesetCreated(
|
||||
scaleset_id=UUID(int=0),
|
||||
pool_name="example",
|
||||
vm_sku="Standard_D2s_v3",
|
||||
image="Canonical:UbuntuServer:18.04-LTS:latest",
|
||||
region=Region("eastus"),
|
||||
size=10,
|
||||
),
|
||||
EventScalesetFailed(
|
||||
scaleset_id=UUID(int=0),
|
||||
pool_name="example",
|
||||
error=Error(
|
||||
code=ErrorCode.UNABLE_TO_RESIZE, errors=["example error message"]
|
||||
),
|
||||
),
|
||||
EventScalesetDeleted(scaleset_id=UUID(int=0), pool_name="example"),
|
||||
EventJobCreated(
|
||||
job_id=UUID(int=0),
|
||||
config=JobConfig(
|
||||
project="example project",
|
||||
name="example name",
|
||||
build="build 1",
|
||||
duration=24,
|
||||
),
|
||||
),
|
||||
EventJobStopped(
|
||||
job_id=UUID(int=0),
|
||||
config=JobConfig(
|
||||
project="example project",
|
||||
name="example name",
|
||||
build="build 1",
|
||||
duration=24,
|
||||
),
|
||||
),
|
||||
EventNodeCreated(machine_id=UUID(int=0), pool_name="example"),
|
||||
EventNodeDeleted(machine_id=UUID(int=0), pool_name="example"),
|
||||
EventNodeStateUpdated(
|
||||
machine_id=UUID(int=0), pool_name="example", state=NodeState.setting_up
|
||||
),
|
||||
EventCrashReported(
|
||||
container=Container("container-name"),
|
||||
filename="example.json",
|
||||
report=Report(
|
||||
input_blob=BlobRef(
|
||||
account="contoso-storage-account",
|
||||
container=Container("crashes"),
|
||||
name="input.txt",
|
||||
),
|
||||
executable="fuzz.exe",
|
||||
crash_type="example crash report type",
|
||||
crash_site="example crash site",
|
||||
call_stack=["#0 line", "#1 line", "#2 line"],
|
||||
call_stack_sha256=ZERO_SHA256,
|
||||
input_sha256=EMPTY_SHA256,
|
||||
asan_log="example asan log",
|
||||
task_id=UUID(int=0),
|
||||
job_id=UUID(int=0),
|
||||
scariness_score=10,
|
||||
scariness_description="example-scariness",
|
||||
),
|
||||
),
|
||||
EventFileAdded(container=Container("container-name"), filename="example.txt"),
|
||||
]
|
||||
|
||||
for event in Event.__args__:
|
||||
seen = False
|
||||
for value in examples:
|
||||
if isinstance(value, event):
|
||||
seen = True
|
||||
break
|
||||
assert seen, "missing event type definition: %s" % event.__name__
|
||||
|
||||
event_types = [get_event_type(x) for x in examples]
|
||||
|
||||
for event_type in EventType:
|
||||
assert event_type in event_types, (
|
||||
"missing event type definition: %s" % event_type.name
|
||||
)
|
||||
|
||||
message = WebhookMessage(
|
||||
webhook_id=UUID(int=0),
|
||||
event_id=UUID(int=0),
|
||||
event_type=WebhookEventType.ping,
|
||||
event=examples[WebhookEventType.ping],
|
||||
event_type=EventType.ping,
|
||||
event=EventPing(ping_id=UUID(int=0)),
|
||||
)
|
||||
|
||||
layer(
|
||||
@ -96,11 +224,18 @@ def main():
|
||||
)
|
||||
|
||||
typed(3, "Example", message.json(indent=4, exclude_none=True), "json")
|
||||
layer(2, "Event Types (WebhookEventType)")
|
||||
layer(2, "Event Types (EventType)")
|
||||
|
||||
for webhook_type in WebhookEventType:
|
||||
example = examples[webhook_type]
|
||||
layer(3, webhook_type.name)
|
||||
event_map = {get_event_type(x).name: x for x in examples}
|
||||
|
||||
for name in sorted(event_map.keys()):
|
||||
print(f"* [{name}](#{name})")
|
||||
|
||||
print()
|
||||
|
||||
for name in sorted(event_map.keys()):
|
||||
example = event_map[name]
|
||||
layer(3, name)
|
||||
typed(4, "Example", example.json(indent=4, exclude_none=True), "json")
|
||||
typed(4, "Schema", example.schema_json(indent=4), "json")
|
||||
|
||||
|
@ -252,6 +252,7 @@ class ErrorCode(Enum):
|
||||
INVALID_NODE = 469
|
||||
NOTIFICATION_FAILURE = 470
|
||||
UNABLE_TO_UPDATE = 471
|
||||
PROXY_FAILED = 472
|
||||
|
||||
|
||||
class HeartbeatType(Enum):
|
||||
@ -368,13 +369,6 @@ class TaskDebugFlag(Enum):
|
||||
keep_node_on_completion = "keep_node_on_completion"
|
||||
|
||||
|
||||
class WebhookEventType(Enum):
|
||||
task_created = "task_created"
|
||||
task_stopped = "task_stopped"
|
||||
task_failed = "task_failed"
|
||||
ping = "ping"
|
||||
|
||||
|
||||
class WebhookMessageState(Enum):
|
||||
queued = "queued"
|
||||
retrying = "retrying"
|
||||
|
225
src/pytypes/onefuzztypes/events.py
Normal file
225
src/pytypes/onefuzztypes/events.py
Normal file
@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional, Union
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import BaseModel, Extra, Field
|
||||
|
||||
from .enums import OS, Architecture, NodeState, TaskState
|
||||
from .models import AutoScaleConfig, Error, JobConfig, Report, TaskConfig, UserInfo
|
||||
from .primitives import Container, Region
|
||||
from .responses import BaseResponse
|
||||
|
||||
|
||||
class BaseEvent(BaseModel):
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
|
||||
|
||||
class EventTaskStopped(BaseEvent):
|
||||
job_id: UUID
|
||||
task_id: UUID
|
||||
user_info: Optional[UserInfo]
|
||||
|
||||
|
||||
class EventTaskFailed(BaseEvent):
|
||||
job_id: UUID
|
||||
task_id: UUID
|
||||
error: Error
|
||||
user_info: Optional[UserInfo]
|
||||
|
||||
|
||||
class EventJobCreated(BaseEvent):
|
||||
job_id: UUID
|
||||
config: JobConfig
|
||||
user_info: Optional[UserInfo]
|
||||
|
||||
|
||||
class EventJobStopped(BaseEvent):
|
||||
job_id: UUID
|
||||
config: JobConfig
|
||||
user_info: Optional[UserInfo]
|
||||
|
||||
|
||||
class EventTaskCreated(BaseEvent):
|
||||
job_id: UUID
|
||||
task_id: UUID
|
||||
config: TaskConfig
|
||||
user_info: Optional[UserInfo]
|
||||
|
||||
|
||||
class EventTaskStateUpdated(BaseEvent):
|
||||
job_id: UUID
|
||||
task_id: UUID
|
||||
state: TaskState
|
||||
end_time: Optional[datetime]
|
||||
|
||||
|
||||
class EventPing(BaseResponse):
|
||||
ping_id: UUID
|
||||
|
||||
|
||||
class EventScalesetCreated(BaseEvent):
|
||||
scaleset_id: UUID
|
||||
pool_name: str
|
||||
vm_sku: str
|
||||
image: str
|
||||
region: Region
|
||||
size: int
|
||||
|
||||
|
||||
class EventScalesetFailed(BaseEvent):
|
||||
scaleset_id: UUID
|
||||
pool_name: str
|
||||
error: Error
|
||||
|
||||
|
||||
class EventScalesetDeleted(BaseEvent):
|
||||
scaleset_id: UUID
|
||||
pool_name: str
|
||||
|
||||
|
||||
class EventPoolDeleted(BaseEvent):
|
||||
pool_name: str
|
||||
|
||||
|
||||
class EventPoolCreated(BaseEvent):
|
||||
pool_name: str
|
||||
os: OS
|
||||
arch: Architecture
|
||||
managed: bool
|
||||
autoscale: Optional[AutoScaleConfig]
|
||||
|
||||
|
||||
class EventProxyCreated(BaseEvent):
|
||||
region: Region
|
||||
|
||||
|
||||
class EventProxyDeleted(BaseEvent):
|
||||
region: Region
|
||||
|
||||
|
||||
class EventProxyFailed(BaseEvent):
|
||||
region: Region
|
||||
error: Error
|
||||
|
||||
|
||||
class EventNodeCreated(BaseEvent):
|
||||
machine_id: UUID
|
||||
scaleset_id: Optional[UUID]
|
||||
pool_name: str
|
||||
|
||||
|
||||
class EventNodeDeleted(BaseEvent):
|
||||
machine_id: UUID
|
||||
scaleset_id: Optional[UUID]
|
||||
pool_name: str
|
||||
|
||||
|
||||
class EventNodeStateUpdated(BaseEvent):
|
||||
machine_id: UUID
|
||||
scaleset_id: Optional[UUID]
|
||||
pool_name: str
|
||||
state: NodeState
|
||||
|
||||
|
||||
class EventCrashReported(BaseEvent):
|
||||
report: Report
|
||||
container: Container
|
||||
filename: str
|
||||
|
||||
|
||||
class EventFileAdded(BaseEvent):
|
||||
container: Container
|
||||
filename: str
|
||||
|
||||
|
||||
Event = Union[
|
||||
EventJobCreated,
|
||||
EventJobStopped,
|
||||
EventNodeStateUpdated,
|
||||
EventNodeCreated,
|
||||
EventNodeDeleted,
|
||||
EventPing,
|
||||
EventPoolCreated,
|
||||
EventPoolDeleted,
|
||||
EventProxyFailed,
|
||||
EventProxyCreated,
|
||||
EventProxyDeleted,
|
||||
EventScalesetFailed,
|
||||
EventScalesetCreated,
|
||||
EventScalesetDeleted,
|
||||
EventTaskFailed,
|
||||
EventTaskStateUpdated,
|
||||
EventTaskCreated,
|
||||
EventTaskStopped,
|
||||
EventCrashReported,
|
||||
EventFileAdded,
|
||||
]
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
job_created = "job_created"
|
||||
job_stopped = "job_stopped"
|
||||
node_created = "node_created"
|
||||
node_deleted = "node_deleted"
|
||||
node_state_updated = "node_state_updated"
|
||||
ping = "ping"
|
||||
pool_created = "pool_created"
|
||||
pool_deleted = "pool_deleted"
|
||||
proxy_created = "proxy_created"
|
||||
proxy_deleted = "proxy_deleted"
|
||||
proxy_failed = "proxy_failed"
|
||||
scaleset_created = "scaleset_created"
|
||||
scaleset_deleted = "scaleset_deleted"
|
||||
scaleset_failed = "scaleset_failed"
|
||||
task_created = "task_created"
|
||||
task_failed = "task_failed"
|
||||
task_state_updated = "task_state_updated"
|
||||
task_stopped = "task_stopped"
|
||||
crash_reported = "crash_reported"
|
||||
file_added = "file_added"
|
||||
|
||||
|
||||
EventTypeMap = {
|
||||
EventType.job_created: EventJobCreated,
|
||||
EventType.job_stopped: EventJobStopped,
|
||||
EventType.node_created: EventNodeCreated,
|
||||
EventType.node_deleted: EventNodeDeleted,
|
||||
EventType.node_state_updated: EventNodeStateUpdated,
|
||||
EventType.ping: EventPing,
|
||||
EventType.pool_created: EventPoolCreated,
|
||||
EventType.pool_deleted: EventPoolDeleted,
|
||||
EventType.proxy_created: EventProxyCreated,
|
||||
EventType.proxy_deleted: EventProxyDeleted,
|
||||
EventType.proxy_failed: EventProxyFailed,
|
||||
EventType.scaleset_created: EventScalesetCreated,
|
||||
EventType.scaleset_deleted: EventScalesetDeleted,
|
||||
EventType.scaleset_failed: EventScalesetFailed,
|
||||
EventType.task_created: EventTaskCreated,
|
||||
EventType.task_failed: EventTaskFailed,
|
||||
EventType.task_state_updated: EventTaskStateUpdated,
|
||||
EventType.task_stopped: EventTaskStopped,
|
||||
EventType.crash_reported: EventCrashReported,
|
||||
EventType.file_added: EventFileAdded,
|
||||
}
|
||||
|
||||
|
||||
def get_event_type(event: Event) -> EventType:
|
||||
|
||||
for (event_type, event_class) in EventTypeMap.items():
|
||||
if isinstance(event, event_class):
|
||||
return event_type
|
||||
|
||||
raise NotImplementedError("unsupported event type: %s" % type(event))
|
||||
|
||||
|
||||
class EventMessage(BaseEvent):
|
||||
event_id: UUID = Field(default_factory=uuid4)
|
||||
event_type: EventType
|
||||
event: Event
|
@ -17,8 +17,8 @@ from .enums import (
|
||||
PoolState,
|
||||
ScalesetState,
|
||||
TaskState,
|
||||
WebhookEventType,
|
||||
)
|
||||
from .events import EventType
|
||||
from .models import AutoScaleConfig, NotificationConfig
|
||||
from .primitives import Container, PoolName, Region
|
||||
|
||||
@ -215,7 +215,7 @@ class CanScheduleRequest(BaseRequest):
|
||||
class WebhookCreate(BaseRequest):
|
||||
name: str
|
||||
url: AnyHttpUrl
|
||||
event_types: List[WebhookEventType]
|
||||
event_types: List[EventType]
|
||||
secret_token: Optional[str]
|
||||
|
||||
|
||||
@ -230,7 +230,7 @@ class WebhookGet(BaseModel):
|
||||
class WebhookUpdate(BaseModel):
|
||||
webhook_id: UUID
|
||||
name: Optional[str]
|
||||
event_types: Optional[List[WebhookEventType]]
|
||||
event_types: Optional[List[EventType]]
|
||||
url: Optional[AnyHttpUrl]
|
||||
secret_token: Optional[str]
|
||||
|
||||
|
@ -3,53 +3,17 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from typing import List, Optional, Union
|
||||
from typing import List, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import AnyHttpUrl, BaseModel, Field
|
||||
|
||||
from .enums import WebhookEventType, WebhookMessageState
|
||||
from .models import Error, TaskConfig, UserInfo
|
||||
from .responses import BaseResponse
|
||||
from .enums import WebhookMessageState
|
||||
from .events import EventMessage, EventType
|
||||
|
||||
|
||||
class WebhookEventTaskStopped(BaseModel):
|
||||
job_id: UUID
|
||||
task_id: UUID
|
||||
user_info: Optional[UserInfo]
|
||||
|
||||
|
||||
class WebhookEventTaskFailed(BaseModel):
|
||||
job_id: UUID
|
||||
task_id: UUID
|
||||
error: Error
|
||||
user_info: Optional[UserInfo]
|
||||
|
||||
|
||||
class WebhookEventTaskCreated(BaseModel):
|
||||
job_id: UUID
|
||||
task_id: UUID
|
||||
config: TaskConfig
|
||||
user_info: Optional[UserInfo]
|
||||
|
||||
|
||||
class WebhookEventPing(BaseResponse):
|
||||
ping_id: UUID = Field(default_factory=uuid4)
|
||||
|
||||
|
||||
WebhookEvent = Union[
|
||||
WebhookEventTaskCreated,
|
||||
WebhookEventTaskStopped,
|
||||
WebhookEventTaskFailed,
|
||||
WebhookEventPing,
|
||||
]
|
||||
|
||||
|
||||
class WebhookMessage(BaseModel):
|
||||
class WebhookMessage(EventMessage):
|
||||
webhook_id: UUID
|
||||
event_id: UUID = Field(default_factory=uuid4)
|
||||
event_type: WebhookEventType
|
||||
event: WebhookEvent
|
||||
|
||||
|
||||
class WebhookMessageLog(WebhookMessage):
|
||||
@ -61,5 +25,5 @@ class Webhook(BaseModel):
|
||||
webhook_id: UUID = Field(default_factory=uuid4)
|
||||
name: str
|
||||
url: Optional[AnyHttpUrl]
|
||||
event_types: List[WebhookEventType]
|
||||
event_types: List[EventType]
|
||||
secret_token: Optional[str]
|
||||
|
Reference in New Issue
Block a user