diff --git a/src/ApiService/ApiService/Functions/Containers.cs b/src/ApiService/ApiService/Functions/Containers.cs index 96554c880e..5178f5be0f 100644 --- a/src/ApiService/ApiService/Functions/Containers.cs +++ b/src/ApiService/ApiService/Functions/Containers.cs @@ -16,11 +16,12 @@ public ContainersFunction(ILogger logger, IOnefuzzContext co [Function("Containers")] [Authorize(Allow.User)] - public Async.Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "GET", "POST", "DELETE")] HttpRequestData req) + public Async.Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "GET", "POST", "PATCH", "DELETE")] HttpRequestData req) => req.Method switch { "GET" => Get(req), "POST" => Post(req), "DELETE" => Delete(req), + "PATCH" => Patch(req), _ => throw new NotSupportedException(), }; @@ -108,4 +109,21 @@ private async Async.Task Post(HttpRequestData req) { SasUrl: sas, Metadata: post.Metadata)); } + + private async Async.Task Patch(HttpRequestData req) { + var request = await RequestHandling.ParseRequest(req); + if (!request.IsOk) { + return await _context.RequestHandling.NotOk(req, request.ErrorV, context: "container update"); + } + + var toUpdate = request.OkV; + _logger.LogInformation("updating {ContainerName}", toUpdate.Name); + var updated = await _context.Containers.CreateOrUpdateContainerTag(toUpdate.Name, StorageType.Corpus, toUpdate.Metadata.ToDictionary(x => x.Key, x => x.Value)); + + if (!updated.IsOk) { + return await _context.RequestHandling.NotOk(req, updated.ErrorV, "container update"); + } + + return await RequestHandling.Ok(req, new ContainerInfoBase(toUpdate.Name, toUpdate.Metadata)); + } } diff --git a/src/ApiService/ApiService/Functions/QueueFileChanges.cs b/src/ApiService/ApiService/Functions/QueueFileChanges.cs index 8ef77bd2a5..f2aa08b306 100644 --- a/src/ApiService/ApiService/Functions/QueueFileChanges.cs +++ b/src/ApiService/ApiService/Functions/QueueFileChanges.cs @@ -2,6 +2,8 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Azure.Core; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; @@ -60,7 +62,11 @@ public async Async.Task Run( try { var result = await FileAdded(storageAccount, fileChangeEvent); if (!result.IsOk) { - await RequeueMessage(msg, result.ErrorV.Code == ErrorCode.ADO_WORKITEM_PROCESSING_DISABLED ? TimeSpan.FromDays(1) : null); + if (result.ErrorV.Code == ErrorCode.ADO_WORKITEM_PROCESSING_DISABLED) { + await RequeueMessage(msg, TimeSpan.FromDays(1), incrementDequeueCount: false); + } else { + await RequeueMessage(msg); + } } } catch (Exception e) { _log.LogError(e, "File Added failed"); @@ -83,21 +89,26 @@ private async Async.Task FileAdded(ResourceIdentifier storage _log.LogInformation("file added : {Container} - {Path}", container.String, path); + var account = await _storage.GetBlobServiceClientForAccount(storageAccount); + var containerClient = account.GetBlobContainerClient(container.String); + var containerProps = await containerClient.GetPropertiesAsync(); + + if (_context.NotificationOperations.ShouldPauseNotificationsForContainer(containerProps.Value.Metadata)) { + return Error.Create(ErrorCode.ADO_WORKITEM_PROCESSING_DISABLED, $"container {container} has a metadata tag set to pause notifications processing"); + } + var (_, result) = await ( - ApplyRetentionPolicy(storageAccount, container, path), + ApplyRetentionPolicy(containerClient, containerProps, path), _notificationOperations.NewFiles(container, path)); return result; } - private async Async.Task ApplyRetentionPolicy(ResourceIdentifier storageAccount, Container container, string path) { + private async Async.Task ApplyRetentionPolicy(BlobContainerClient containerClient, BlobContainerProperties containerProps, string path) { if (await _context.FeatureManagerSnapshot.IsEnabledAsync(FeatureFlagConstants.EnableContainerRetentionPolicies)) { // default retention period can be applied to the container // if one exists, we will set the expiry date on the newly-created blob, if it doesn't already have one - var account = await _storage.GetBlobServiceClientForAccount(storageAccount); - var containerClient = account.GetBlobContainerClient(container.String); - var containerProps = await containerClient.GetPropertiesAsync(); - var retentionPeriod = RetentionPolicyUtils.GetContainerRetentionPeriodFromMetadata(containerProps.Value.Metadata); + var retentionPeriod = RetentionPolicyUtils.GetContainerRetentionPeriodFromMetadata(containerProps.Metadata); if (!retentionPeriod.IsOk) { _log.LogError("invalid retention period: {Error}", retentionPeriod.ErrorV); } else if (retentionPeriod.OkV is TimeSpan period) { @@ -116,7 +127,7 @@ private async Async.Task ApplyRetentionPolicy(ResourceIdentifier storageAc return false; } - private async Async.Task RequeueMessage(string msg, TimeSpan? visibilityTimeout = null) { + private async Async.Task RequeueMessage(string msg, TimeSpan? visibilityTimeout = null, bool incrementDequeueCount = true) { var json = JsonNode.Parse(msg); // Messages that are 'manually' requeued by us as opposed to being requeued by the azure functions runtime @@ -135,7 +146,9 @@ await _context.Queue.QueueObject( StorageType.Config) .IgnoreResult(); } else { - json!["data"]!["customDequeueCount"] = newCustomDequeueCount + 1; + if (incrementDequeueCount) { + json!["data"]!["customDequeueCount"] = newCustomDequeueCount + 1; + } await _context.Queue.QueueObject( QueueFileChangesQueueName, json, diff --git a/src/ApiService/ApiService/Functions/QueueJobResult.cs b/src/ApiService/ApiService/Functions/QueueJobResult.cs index 5e3bec0048..31b39802d6 100644 --- a/src/ApiService/ApiService/Functions/QueueJobResult.cs +++ b/src/ApiService/ApiService/Functions/QueueJobResult.cs @@ -31,7 +31,12 @@ public async Async.Task Run([QueueTrigger("job-result", Connection = "AzureWebJo var job = await _jobs.Get(task.JobId); if (job == null) { - _log.LogWarning("invalid {JobId}", task.JobId); + _log.LogWarning("invalid message {JobId}", task.JobId); + return; + } + + if (jr.CreatedAt == null) { + _log.LogWarning("invalid message, no created_at field {JobId}", task.JobId); return; } @@ -52,7 +57,7 @@ public async Async.Task Run([QueueTrigger("job-result", Connection = "AzureWebJo return; } - var jobResult = await _context.JobResultOperations.CreateOrUpdate(job.JobId, jobResultType, value); + var jobResult = await _context.JobResultOperations.CreateOrUpdate(job.JobId, jr.TaskId, jr.MachineId, jr.CreatedAt.Value, jr.Version, jobResultType, value); if (!jobResult.IsOk) { _log.LogError("failed to create or update with job result {JobId}", job.JobId); } diff --git a/src/ApiService/ApiService/OneFuzzTypes/Enums.cs b/src/ApiService/ApiService/OneFuzzTypes/Enums.cs index 5a8b22527d..c0e3c68eba 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Enums.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Enums.cs @@ -53,6 +53,9 @@ public enum ErrorCode { INVALID_RETENTION_PERIOD = 497, INVALID_CLI_VERSION = 498, TRANSIENT_NOTIFICATION_FAILURE = 499, + + FAILED_CONTAINER_PROPERTIES_ACCESS = 500, + FAILED_SAVING_CONTAINER_METADATA = 501, // NB: if you update this enum, also update enums.py } diff --git a/src/ApiService/ApiService/OneFuzzTypes/Model.cs b/src/ApiService/ApiService/OneFuzzTypes/Model.cs index ab41853a74..4dd4000283 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Model.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Model.cs @@ -34,19 +34,6 @@ public enum HeartbeatType { TaskAlive, } -[SkipRename] -public enum JobResultType { - NewCrashingInput, - NoReproCrashingInput, - NewReport, - NewUniqueReport, - NewRegressionReport, - NewCoverage, - NewCrashDump, - CoverageData, - RuntimeStats, -} - public record HeartbeatData(HeartbeatType Type); public record TaskHeartbeatEntry( @@ -55,12 +42,14 @@ public record TaskHeartbeatEntry( [property: Required] Guid MachineId, HeartbeatData[] Data); -public record JobResultData(JobResultType Type); +public record JobResultData(string Type); public record TaskJobResultEntry( Guid TaskId, Guid? JobId, Guid MachineId, + DateTime? CreatedAt, + double Version, JobResultData Data, Dictionary Value ); @@ -921,26 +910,24 @@ public record SecretAddress(Uri Url) : ISecret { public record SecretData(ISecret Secret) { } +[SkipRename] +public enum JobResultType { + CoverageData, + RuntimeStats, +} + public record JobResult( - [PartitionKey][RowKey] Guid JobId, + [PartitionKey] Guid JobId, + [RowKey] string TaskIdMachineIdMetric, + Guid TaskId, + Guid MachineId, + DateTime CreatedAt, string Project, string Name, - double NewCrashingInput = 0, - double NoReproCrashingInput = 0, - double NewReport = 0, - double NewUniqueReport = 0, - double NewRegressionReport = 0, - double NewCrashDump = 0, - double InstructionsCovered = 0, - double TotalInstructions = 0, - double CoverageRate = 0, - double IterationCount = 0 -) : EntityBase() { - public JobResult(Guid JobId, string Project, string Name) : this( - JobId: JobId, - Project: Project, - Name: Name, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) { } -} + string Type, + double Version, + Dictionary MetricValue +) : EntityBase(); public record JobConfig( string Project, diff --git a/src/ApiService/ApiService/OneFuzzTypes/Requests.cs b/src/ApiService/ApiService/OneFuzzTypes/Requests.cs index db63499d30..f3a4c32965 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Requests.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Requests.cs @@ -128,6 +128,11 @@ public record ContainerDelete( IDictionary? Metadata = null ) : BaseRequest; +public record ContainerUpdate( + [property: Required] Container Name, + [property: Required] IDictionary Metadata +) : BaseRequest; + public record NotificationCreate( [property: Required] Container Container, [property: Required] bool ReplaceExisting, diff --git a/src/ApiService/ApiService/host.json b/src/ApiService/ApiService/host.json index beb2e4020b..7b83b41ae4 100644 --- a/src/ApiService/ApiService/host.json +++ b/src/ApiService/ApiService/host.json @@ -1,5 +1,6 @@ { "version": "2.0", + "functionTimeout": "12:00:00", "logging": { "applicationInsights": { "samplingSettings": { diff --git a/src/ApiService/ApiService/onefuzzlib/Containers.cs b/src/ApiService/ApiService/onefuzzlib/Containers.cs index f7bb3086a0..e004815abc 100644 --- a/src/ApiService/ApiService/onefuzzlib/Containers.cs +++ b/src/ApiService/ApiService/onefuzzlib/Containers.cs @@ -1,5 +1,6 @@ using System.IO; using System.IO.Compression; +using System.Text.Json; using System.Threading.Tasks; using ApiService.OneFuzzLib.Orm; using Azure; @@ -41,6 +42,8 @@ public interface IContainers { public Async.Task DownloadAsZip(Container container, StorageType storageType, Stream stream, string? prefix = null); public Async.Task DeleteAllExpiredBlobs(); + + public Async.Task CreateOrUpdateContainerTag(Container container, StorageType storageType, Dictionary tags); } public class Containers : Orm, IContainers { @@ -448,4 +451,29 @@ private async Async.Task DeleteExpiredBlobsForAccount(ResourceIdentifier storage } } } + + public async Task CreateOrUpdateContainerTag(Container container, StorageType storageType, Dictionary tags) { + var client = await FindContainer(container, storageType); + if (client is null || !await client.ExistsAsync()) { + return Error.Create(ErrorCode.INVALID_CONTAINER, $"Could not find container {container} in {storageType}"); + } + + var metadataRequest = await client.GetPropertiesAsync(); + if (metadataRequest is null || metadataRequest.GetRawResponse().IsError) { + return Error.Create(ErrorCode.FAILED_CONTAINER_PROPERTIES_ACCESS, $"Could not access container properties for container: {container} in {storageType}"); + } + + var metadata = metadataRequest.Value.Metadata ?? new Dictionary(); + + foreach (var kvp in tags) { + metadata[kvp.Key] = kvp.Value; + } + + var saveMetadataRequest = await client.SetMetadataAsync(metadata); + if (saveMetadataRequest is null || saveMetadataRequest.GetRawResponse().IsError) { + return Error.Create(ErrorCode.FAILED_SAVING_CONTAINER_METADATA, $"Could not save metadata to container: {container} in {storageType}. Metadata: {JsonSerializer.Serialize(metadata)}"); + } + + return OneFuzzResultVoid.Ok; + } } diff --git a/src/ApiService/ApiService/onefuzzlib/JobResultOperations.cs b/src/ApiService/ApiService/onefuzzlib/JobResultOperations.cs index 1166cf91d4..b39c654642 100644 --- a/src/ApiService/ApiService/onefuzzlib/JobResultOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/JobResultOperations.cs @@ -2,99 +2,75 @@ using Microsoft.Extensions.Logging; using Polly; namespace Microsoft.OneFuzz.Service; +using System.Net; public interface IJobResultOperations : IOrm { - Async.Task GetJobResult(Guid jobId); - Async.Task CreateOrUpdate(Guid jobId, JobResultType resultType, Dictionary resultValue); + Async.Task GetJobResult(Guid jobId, Guid taskId, Guid machineId, string metricType); + Async.Task CreateOrUpdate(Guid jobId, Guid taskId, Guid machineId, DateTime createdAt, double version, string resultType, Dictionary resultValue); } public class JobResultOperations : Orm, IJobResultOperations { + const string COVERAGE_DATA = "CoverageData"; + const string RUNTIME_STATS = "RuntimeStats"; + public JobResultOperations(ILogger log, IOnefuzzContext context) : base(log, context) { } - public async Async.Task GetJobResult(Guid jobId) { - return await SearchByPartitionKeys(new[] { jobId.ToString() }).SingleOrDefaultAsync(); + public async Async.Task GetJobResult(Guid jobId, Guid taskId, Guid machineId, string metricType) { + var data = QueryAsync(Query.SingleEntity(jobId.ToString(), string.Concat(taskId, "-", machineId, "-", metricType))); + return await data.FirstOrDefaultAsync(); } - private JobResult UpdateResult(JobResult result, JobResultType type, Dictionary resultValue) { - - var newResult = result; - double newValue; - switch (type) { - case JobResultType.NewCrashingInput: - newValue = result.NewCrashingInput + resultValue["count"]; - newResult = result with { NewCrashingInput = newValue }; - break; - case JobResultType.NewReport: - newValue = result.NewReport + resultValue["count"]; - newResult = result with { NewReport = newValue }; - break; - case JobResultType.NewUniqueReport: - newValue = result.NewUniqueReport + resultValue["count"]; - newResult = result with { NewUniqueReport = newValue }; - break; - case JobResultType.NewRegressionReport: - newValue = result.NewRegressionReport + resultValue["count"]; - newResult = result with { NewRegressionReport = newValue }; - break; - case JobResultType.NewCrashDump: - newValue = result.NewCrashDump + resultValue["count"]; - newResult = result with { NewCrashDump = newValue }; - break; - case JobResultType.CoverageData: - double newCovered = resultValue["covered"]; - double newTotalCovered = resultValue["features"]; - double newCoverageRate = resultValue["rate"]; - newResult = result with { InstructionsCovered = newCovered, TotalInstructions = newTotalCovered, CoverageRate = newCoverageRate }; - break; - case JobResultType.RuntimeStats: - double newTotalIterations = resultValue["total_count"]; - newResult = result with { IterationCount = newTotalIterations }; - break; - default: - _logTracer.LogWarning($"Invalid Field {type}."); - break; - } - _logTracer.LogInformation($"Attempting to log new result: {newResult}"); - return newResult; - } - - private async Async.Task TryUpdate(Job job, JobResultType resultType, Dictionary resultValue) { + private async Async.Task TryUpdate(Job job, Guid taskId, Guid machineId, DateTime createdAt, double version, string resultType, Dictionary resultValue) { var jobId = job.JobId; + var taskIdMachineIdMetric = string.Concat(taskId, "-", machineId, "-", resultType); - var jobResult = await GetJobResult(jobId); - - if (jobResult == null) { - _logTracer.LogInformation("Creating new JobResult for Job {JobId}", jobId); - - var entry = new JobResult(JobId: jobId, Project: job.Config.Project, Name: job.Config.Name); + var oldEntry = await GetJobResult(jobId, taskId, machineId, resultType); - jobResult = UpdateResult(entry, resultType, resultValue); - - var r = await Insert(jobResult); - if (!r.IsOk) { - throw new InvalidOperationException($"failed to insert job result {jobResult.JobId}"); + if (oldEntry == null) { + _logTracer.LogInformation($"attempt to insert new job result {taskId} and taskId+machineId+metricType {taskIdMachineIdMetric}"); + var newEntry = new JobResult(JobId: jobId, TaskIdMachineIdMetric: taskIdMachineIdMetric, TaskId: taskId, MachineId: machineId, CreatedAt: createdAt, Project: job.Config.Project, Name: job.Config.Name, resultType, Version: version, resultValue); + var result = await Insert(newEntry); + if (!result.IsOk) { + throw new InvalidOperationException($"failed to insert job result with taskId {taskId} and taskId+machineId+metricType {taskIdMachineIdMetric}"); } - _logTracer.LogInformation("created job result {JobId}", jobResult.JobId); - } else { - _logTracer.LogInformation("Updating existing JobResult entry for Job {JobId}", jobId); - - jobResult = UpdateResult(jobResult, resultType, resultValue); + return true; + } - var r = await Update(jobResult); - if (!r.IsOk) { - throw new InvalidOperationException($"failed to insert job result {jobResult.JobId}"); - } - _logTracer.LogInformation("updated job result {JobId}", jobResult.JobId); + ResultVoid<(HttpStatusCode Status, string Reason)> r; + switch (resultType) { + case COVERAGE_DATA: + case RUNTIME_STATS: + if (oldEntry.CreatedAt < createdAt) { + oldEntry = oldEntry with { CreatedAt = createdAt, MetricValue = resultValue }; + r = await Update(oldEntry); + if (!r.IsOk) { + throw new InvalidOperationException($"failed to replace job result with taskId {taskId} and machineId+metricType {taskIdMachineIdMetric}"); + } + } else { + _logTracer.LogInformation($"received an out-of-date metric. skipping."); + } + break; + default: + _logTracer.LogInformation($"attempt to update job result {taskId} and taskId+machineId+metricType {taskIdMachineIdMetric}"); + oldEntry.MetricValue["count"]++; + oldEntry = oldEntry with { MetricValue = oldEntry.MetricValue }; + r = await Update(oldEntry); + if (!r.IsOk) { + throw new InvalidOperationException($"failed to update job result with taskId {taskId} and machineId+metricType {taskIdMachineIdMetric}"); + } + break; } + return true; + } - public async Async.Task CreateOrUpdate(Guid jobId, JobResultType resultType, Dictionary resultValue) { + public async Async.Task CreateOrUpdate(Guid jobId, Guid taskId, Guid machineId, DateTime createdAt, double version, string resultType, Dictionary resultValue) { var job = await _context.JobOperations.Get(jobId); if (job == null) { @@ -106,7 +82,7 @@ public async Async.Task CreateOrUpdate(Guid jobId, JobResultT _logTracer.LogInformation("attempt to update job result {JobId}", job.JobId); var policy = Policy.Handle().WaitAndRetryAsync(50, _ => new TimeSpan(0, 0, 5)); await policy.ExecuteAsync(async () => { - success = await TryUpdate(job, resultType, resultValue); + success = await TryUpdate(job, taskId, machineId, createdAt, version, resultType, resultValue); _logTracer.LogInformation("attempt {success}", success); }); return OneFuzzResultVoid.Ok; diff --git a/src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs b/src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs index 5744d6d2f5..67369989e1 100644 --- a/src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs @@ -11,12 +11,11 @@ public interface INotificationOperations : IOrm { Async.Task> Create(Container container, NotificationTemplate config, bool replaceExisting); Async.Task GetNotification(Guid notifificationId); - System.Threading.Tasks.Task TriggerNotification(Container container, - Notification notification, IReport? reportOrRegression); + System.Threading.Tasks.Task TriggerNotification(Container container, Notification notification, IReport? reportOrRegression); + bool ShouldPauseNotificationsForContainer(IDictionary containerMetadata); } public class NotificationOperations : Orm, INotificationOperations { - public NotificationOperations(ILogger log, IOnefuzzContext context) : base(log, context) { @@ -190,4 +189,7 @@ private async Async.Task HideSecrets(NotificationTemplate public async Async.Task GetNotification(Guid notifificationId) { return await SearchByPartitionKeys(new[] { notifificationId.ToString() }).SingleOrDefaultAsync(); } + + private const string PAUSE_NOTIFICATIONS_TAG = "pauseNotifications"; + public bool ShouldPauseNotificationsForContainer(IDictionary containerMetadata) => containerMetadata.ContainsKey(PAUSE_NOTIFICATIONS_TAG) && containerMetadata[PAUSE_NOTIFICATIONS_TAG] == "true"; } diff --git a/src/ApiService/ApiService/onefuzzlib/Reports.cs b/src/ApiService/ApiService/onefuzzlib/Reports.cs index fdda7259e9..ac2e1029b2 100644 --- a/src/ApiService/ApiService/onefuzzlib/Reports.cs +++ b/src/ApiService/ApiService/onefuzzlib/Reports.cs @@ -67,7 +67,6 @@ public Reports(ILogger log, IContainers containers) { } private static T? TryDeserialize(string content) where T : class { - try { return JsonSerializer.Deserialize(content, EntityConverter.GetJsonSerializerOptions()); } catch (JsonException) { diff --git a/src/agent/Cargo.lock b/src/agent/Cargo.lock index 5393e9b767..e4143473e3 100644 --- a/src/agent/Cargo.lock +++ b/src/agent/Cargo.lock @@ -463,6 +463,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", + "serde", "winapi 0.3.9", ] @@ -2241,6 +2242,7 @@ version = "0.2.0" dependencies = [ "anyhow", "async-trait", + "chrono", "log", "onefuzz-telemetry", "reqwest", diff --git a/src/agent/onefuzz-result/Cargo.toml b/src/agent/onefuzz-result/Cargo.toml index 7c7de6615c..7e156ac91d 100644 --- a/src/agent/onefuzz-result/Cargo.toml +++ b/src/agent/onefuzz-result/Cargo.toml @@ -9,10 +9,14 @@ license = "MIT" [dependencies] anyhow = { version = "1.0", features = ["backtrace"] } async-trait = "0.1" +chrono = { version = "0.4", default-features = false, features = [ + "clock", + "std", + "serde" +] } reqwest = "0.11" serde = "1.0" storage-queue = { path = "../storage-queue" } uuid = { version = "1.4", features = ["serde", "v4"] } onefuzz-telemetry = { path = "../onefuzz-telemetry" } log = "0.4" - diff --git a/src/agent/onefuzz-result/src/job_result.rs b/src/agent/onefuzz-result/src/job_result.rs index 08f7bbc1ee..e6b4f50377 100644 --- a/src/agent/onefuzz-result/src/job_result.rs +++ b/src/agent/onefuzz-result/src/job_result.rs @@ -3,6 +3,8 @@ use anyhow::Result; use async_trait::async_trait; +use chrono::DateTime; +pub use chrono::Utc; use onefuzz_telemetry::warn; use reqwest::Url; use serde::{self, Deserialize, Serialize}; @@ -32,6 +34,8 @@ struct JobResult { job_id: Uuid, machine_id: Uuid, machine_name: String, + created_at: DateTime, + version: f64, data: JobResultData, value: HashMap, } @@ -103,7 +107,8 @@ impl JobResultSender for TaskJobResultClient { let job_id = self.context.state.job_id; let machine_id = self.context.state.machine_id; let machine_name = self.context.state.machine_name.clone(); - + let created_at = chrono::Utc::now(); + let version = 1.0; let _ = self .context .queue_client @@ -112,6 +117,8 @@ impl JobResultSender for TaskJobResultClient { job_id, machine_id, machine_name, + created_at, + version, data, value, }) diff --git a/src/agent/onefuzz-task/src/tasks/coverage/generic.rs b/src/agent/onefuzz-task/src/tasks/coverage/generic.rs index 2ebc748010..8043deee94 100644 --- a/src/agent/onefuzz-task/src/tasks/coverage/generic.rs +++ b/src/agent/onefuzz-task/src/tasks/coverage/generic.rs @@ -146,11 +146,11 @@ impl CoverageTask { bail!("input is not specified on the command line or arguments for the target"); } - context.heartbeat.alive(); - info!("report initial coverage"); context.report_coverage_stats().await; + context.heartbeat.alive(); + for dir in &self.config.readonly_inputs { debug!("recording coverage for {}", dir.local_path.display()); @@ -174,6 +174,9 @@ impl CoverageTask { context.save_and_sync_coverage().await?; } + info!("report coverage"); + context.report_coverage_stats().await; + context.heartbeat.alive(); if let Some(queue) = &self.config.input_queue { diff --git a/src/agent/onefuzz-task/src/tasks/report/crash_report.rs b/src/agent/onefuzz-task/src/tasks/report/crash_report.rs index 9ae618ce93..407d449c0f 100644 --- a/src/agent/onefuzz-task/src/tasks/report/crash_report.rs +++ b/src/agent/onefuzz-task/src/tasks/report/crash_report.rs @@ -6,7 +6,7 @@ use onefuzz::{blob::BlobUrl, monitor::DirectoryMonitor, syncdir::SyncedDir}; use onefuzz_result::job_result::{JobResultData, JobResultSender, TaskJobResultClient}; use onefuzz_telemetry::{ Event::{ - new_report, new_unable_to_reproduce, new_unique_report, regression_report, + crash_reported, new_report, new_unable_to_reproduce, new_unique_report, regression_report, regression_unable_to_reproduce, }, EventData, @@ -166,6 +166,8 @@ impl CrashTestResult { match self { Self::CrashReport(report) => { // Use SHA-256 of call stack as dedupe key. + event!(crash_reported; EventData::Path = report.unique_blob_name()); + metric!(crash_reported; 1.0; EventData::Path = report.unique_blob_name()); if let Some(jr_client) = jr_client { let _ = jr_client .send_direct( diff --git a/src/agent/onefuzz-telemetry/src/lib.rs b/src/agent/onefuzz-telemetry/src/lib.rs index f08b722695..21e427f06f 100644 --- a/src/agent/onefuzz-telemetry/src/lib.rs +++ b/src/agent/onefuzz-telemetry/src/lib.rs @@ -82,6 +82,7 @@ pub enum Event { runtime_stats, new_report, new_unique_report, + crash_reported, new_unable_to_reproduce, regression_report, regression_unable_to_reproduce, @@ -99,6 +100,7 @@ impl Event { Self::runtime_stats => "runtime_stats", Self::new_report => "new_report", Self::new_unique_report => "new_unique_report", + Self::crash_reported => "crash_reported", Self::new_unable_to_reproduce => "new_unable_to_reproduce", Self::regression_report => "regression_report", Self::regression_unable_to_reproduce => "regression_unable_to_reproduce", diff --git a/src/cli/onefuzz/api.py b/src/cli/onefuzz/api.py index 64cad8c368..4f4e152484 100644 --- a/src/cli/onefuzz/api.py +++ b/src/cli/onefuzz/api.py @@ -486,6 +486,17 @@ def delete(self, name: str) -> responses.BoolResult: "DELETE", responses.BoolResult, data=requests.ContainerDelete(name=name) ) + def update( + self, name: str, metadata: Dict[str, str] + ) -> responses.ContainerInfoBase: + """Update a container's metadata""" + self.logger.debug("update container: %s", name) + return self._req_model( + "PATCH", + responses.ContainerInfoBase, + data=requests.ContainerUpdate(name=name, metadata=metadata), + ) + def list(self) -> List[responses.ContainerInfoBase]: """Get a list of containers""" self.logger.debug("list containers") diff --git a/src/deployment/azuredeploy.bicep b/src/deployment/azuredeploy.bicep index 99c175c6e7..ac735af952 100644 --- a/src/deployment/azuredeploy.bicep +++ b/src/deployment/azuredeploy.bicep @@ -156,7 +156,6 @@ module storage 'bicep-templates/storageAccounts.bicep' = { params: { location: location owner: owner - signedExpiry: signedExpiry } } @@ -172,6 +171,7 @@ module autoscaleSettings 'bicep-templates/autoscale-settings.bicep' = { } } + module eventGrid 'bicep-templates/event-grid.bicep' = { name: 'event-grid' params: { @@ -227,8 +227,8 @@ module function 'bicep-templates/function.bicep' = { params: { name: name linux_fx_version: 'DOTNET-ISOLATED|7.0' - - app_logs_sas_url: storage.outputs.FuncSasUrlBlobAppLogs + signedExpiry: signedExpiry + logs_storage: storage.outputs.FuncName app_func_audiences: app_func_audiences app_func_issuer: app_func_issuer client_id: clientId @@ -241,6 +241,9 @@ module function 'bicep-templates/function.bicep' = { use_windows: true enable_remote_debugging: enable_remote_debugging } + dependsOn:[ + storage + ] } module functionSettings 'bicep-templates/function-settings.bicep' = { @@ -254,8 +257,9 @@ module functionSettings 'bicep-templates/function-settings.bicep' = { app_insights_app_id: operationalInsights.outputs.appInsightsAppId app_insights_key: operationalInsights.outputs.appInsightsInstrumentationKey client_secret: clientSecret - signal_r_connection_string: signalR.outputs.connectionString - func_sas_url: storage.outputs.FuncSasUrl + + signalRName: signalR.outputs.signalRName + funcStorageName: storage.outputs.FuncName func_storage_resource_id: storage.outputs.FuncId fuzz_storage_resource_id: storage.outputs.FuzzId keyvault_name: keyVaultName @@ -269,16 +273,18 @@ module functionSettings 'bicep-templates/function-settings.bicep' = { } dependsOn: [ function + storage + signalR ] } output fuzz_storage string = storage.outputs.FuzzId output fuzz_name string = storage.outputs.FuzzName -output fuzz_key string = storage.outputs.FuzzKey + output func_storage string = storage.outputs.FuncId output func_name string = storage.outputs.FuncName -output func_key string = storage.outputs.FuncKey + output scaleset_identity string = scaleset_identity output tenant_id string = tenantId diff --git a/src/deployment/bicep-templates/function-settings.bicep b/src/deployment/bicep-templates/function-settings.bicep index 742f4f39d5..8235dcbf7a 100644 --- a/src/deployment/bicep-templates/function-settings.bicep +++ b/src/deployment/bicep-templates/function-settings.bicep @@ -5,17 +5,11 @@ param app_insights_app_id string @secure() param app_insights_key string -@secure() -param func_sas_url string - param cli_app_id string param authority string param tenant_domain string param multi_tenant_domain string -@secure() -param signal_r_connection_string string - param app_config_endpoint string param func_storage_resource_id string @@ -33,8 +27,21 @@ param functions_extension_version string param enable_profiler bool +param signalRName string +param funcStorageName string + var telemetry = 'd7a73cf4-5a1a-4030-85e1-e5b25867e45a' + +resource signal_r 'Microsoft.SignalRService/signalR@2021-10-01' existing = { + name: signalRName +} + + +resource funcStorage 'Microsoft.Storage/storageAccounts@2021-08-01' existing = { + name: funcStorageName +} + resource function 'Microsoft.Web/sites@2021-02-01' existing = { name: name } @@ -44,6 +51,7 @@ var enable_profilers = enable_profiler ? { DiagnosticServices_EXTENSION_VERSION: '~3' } : {} +var func_key = funcStorage.listKeys().keys[0].value resource functionSettings 'Microsoft.Web/sites/config@2021-03-01' = { parent: function name: 'appsettings' @@ -54,13 +62,13 @@ resource functionSettings 'Microsoft.Web/sites/config@2021-03-01' = { APPINSIGHTS_INSTRUMENTATIONKEY: app_insights_key APPINSIGHTS_APPID: app_insights_app_id ONEFUZZ_TELEMETRY: telemetry - AzureWebJobsStorage: func_sas_url + AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${funcStorage.name};AccountKey=${func_key};EndpointSuffix=core.windows.net' CLI_APP_ID: cli_app_id AUTHORITY: authority TENANT_DOMAIN: tenant_domain MULTI_TENANT_DOMAIN: multi_tenant_domain AzureWebJobsDisableHomepage: 'true' - AzureSignalRConnectionString: signal_r_connection_string + AzureSignalRConnectionString: signal_r.listKeys().primaryConnectionString AzureSignalRServiceTransportType: 'Transient' APPCONFIGURATION_ENDPOINT: app_config_endpoint ONEFUZZ_INSTANCE_NAME: instance_name diff --git a/src/deployment/bicep-templates/function.bicep b/src/deployment/bicep-templates/function.bicep index a6e695ffbf..bff457f2cd 100644 --- a/src/deployment/bicep-templates/function.bicep +++ b/src/deployment/bicep-templates/function.bicep @@ -9,8 +9,9 @@ param app_func_audiences array param use_windows bool param enable_remote_debugging bool -@secure() -param app_logs_sas_url string +param logs_storage string +param signedExpiry string + @description('The degree of severity for diagnostics logs.') @allowed([ @@ -28,6 +29,14 @@ var siteconfig = (use_windows) ? { linuxFxVersion: linux_fx_version } +var storage_account_sas = { + signedExpiry: signedExpiry + signedPermission: 'rwdlacup' + signedResourceTypes: 'sco' + signedServices: 'bfqt' +} + + var commonSiteConfig = { alwaysOn: true defaultDocuments: [] @@ -45,6 +54,11 @@ var extraProperties = (use_windows && enable_remote_debugging) ? { remoteDebuggingVersion: 'VS2022' } : {} +resource funcStorage 'Microsoft.Storage/storageAccounts@2021-08-01' existing = { + name: logs_storage +} + + resource function 'Microsoft.Web/sites@2021-03-01' = { name: name location: location @@ -97,6 +111,7 @@ resource funcAuthSettings 'Microsoft.Web/sites/config@2021-03-01' = { parent: function } +var sas = funcStorage.listAccountSas('2021-08-01', storage_account_sas) resource funcLogs 'Microsoft.Web/sites/config@2021-03-01' = { name: 'logs' properties: { @@ -104,7 +119,7 @@ resource funcLogs 'Microsoft.Web/sites/config@2021-03-01' = { azureBlobStorage: { level: diagnostics_log_level retentionInDays: log_retention - sasUrl: app_logs_sas_url + sasUrl: '${funcStorage.properties.primaryEndpoints.blob}app-logs?${sas.accountSasToken}' } } } diff --git a/src/deployment/bicep-templates/signalR.bicep b/src/deployment/bicep-templates/signalR.bicep index b054f7ed12..ef393e4427 100644 --- a/src/deployment/bicep-templates/signalR.bicep +++ b/src/deployment/bicep-templates/signalR.bicep @@ -30,5 +30,4 @@ resource signalR 'Microsoft.SignalRService/signalR@2021-10-01' = { } } -var connectionString = signalR.listKeys().primaryConnectionString -output connectionString string = connectionString +output signalRName string = signalr_name diff --git a/src/deployment/bicep-templates/storageAccounts.bicep b/src/deployment/bicep-templates/storageAccounts.bicep index 27f2da21d8..530fcc436c 100644 --- a/src/deployment/bicep-templates/storageAccounts.bicep +++ b/src/deployment/bicep-templates/storageAccounts.bicep @@ -1,18 +1,10 @@ param owner string param location string -param signedExpiry string var suffix = uniqueString(resourceGroup().id) var storageAccountNameFuzz = 'fuzz${suffix}' var storageAccountNameFunc = 'func${suffix}' -var storage_account_sas = { - signedExpiry: signedExpiry - signedPermission: 'rwdlacup' - signedResourceTypes: 'sco' - signedServices: 'bfqt' -} - var storageAccountFuzzContainersParams = [ 'events' ] @@ -119,14 +111,3 @@ output FuzzId string = storageAccountFuzz.id output FuncId string = storageAccountFunc.id output FileChangesQueueName string = storageAccountFuncQueuesParams[fileChangesQueueIndex] - -var sas = storageAccountFunc.listAccountSas('2021-08-01', storage_account_sas) -output FuncSasUrlBlobAppLogs string = '${storageAccountFunc.properties.primaryEndpoints.blob}app-logs?${sas.accountSasToken}' - -var fuzz_key = storageAccountFuzz.listKeys().keys[0].value -output FuzzKey string = fuzz_key - -var func_key = storageAccountFunc.listKeys().keys[0].value -output FuncKey string = func_key - -output FuncSasUrl string = 'DefaultEndpointsProtocol=https;AccountName=${storageAccountFunc.name};AccountKey=${func_key};EndpointSuffix=core.windows.net' diff --git a/src/proxy-manager/Cargo.lock b/src/proxy-manager/Cargo.lock index 98e66a2468..e48d028374 100644 --- a/src/proxy-manager/Cargo.lock +++ b/src/proxy-manager/Cargo.lock @@ -1203,9 +1203,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.18" +version = "0.38.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" dependencies = [ "bitflags 2.4.0", "errno", diff --git a/src/pytypes/onefuzztypes/enums.py b/src/pytypes/onefuzztypes/enums.py index 446193f1d2..14315ddb5d 100644 --- a/src/pytypes/onefuzztypes/enums.py +++ b/src/pytypes/onefuzztypes/enums.py @@ -307,6 +307,8 @@ class ErrorCode(Enum): INVALID_RETENTION_PERIOD = 497 INVALID_CLI_VERSION = 498 TRANSIENT_NOTIFICATION_FAILURE = 499 + FAILED_CONTAINER_PROPERTIES_ACCESS = 500 + FAILED_SAVING_CONTAINER_METADATA = 501 # NB: if you update this enum, also update Enums.cs diff --git a/src/pytypes/onefuzztypes/requests.py b/src/pytypes/onefuzztypes/requests.py index d284fb416d..df9fb3e1f3 100644 --- a/src/pytypes/onefuzztypes/requests.py +++ b/src/pytypes/onefuzztypes/requests.py @@ -220,6 +220,11 @@ class ContainerDelete(BaseRequest): metadata: Optional[Dict[str, str]] +class ContainerUpdate(BaseRequest): + name: Container + metadata: Dict[str, str] + + class ReproGet(BaseRequest): vm_id: Optional[UUID]