diff --git a/json-schema/response.yaml b/json-schema/response.yaml index a8cc91018..db2631287 100644 --- a/json-schema/response.yaml +++ b/json-schema/response.yaml @@ -1054,6 +1054,23 @@ definitions: $ref: /definitions/uuid device_report_id: $ref: /definitions/uuid + ReportValidationResults: + type: object + additionalProperties: false + required: + - device_id + - validation_plan_id + - status + - results + properties: + device_id: + $ref: /definitions/device_id + validation_plan_id: + $ref: /definitions/uuid + status: + $ref: /definitions/validation_status + results: + $ref: /definitions/ValidationResults WorkspaceRelays: type: array uniqueItems: true diff --git a/lib/Conch/Controller/DeviceReport.pm b/lib/Conch/Controller/DeviceReport.pm index d79ac2ef0..5baa04731 100644 --- a/lib/Conch/Controller/DeviceReport.pm +++ b/lib/Conch/Controller/DeviceReport.pm @@ -32,7 +32,7 @@ Response uses the ValidationStateWithResults json schema. sub process ($c) { my $unserialized_report = $c->validate_input('DeviceReport'); if (not $unserialized_report) { - $c->log->debug('Device report input failed validation'); + $c->log->debug('Device report input did not match json schema specification'); if (not $c->db_devices->active->search({ id => $c->stash('device_id') })->exists) { $c->log->debug('Device id '.$c->stash('device_id').' does not exist; cannot store bad report'); @@ -137,6 +137,7 @@ sub process ($c) { device => $c->db_ro_devices->find($device->id), device_report => $device_report, ); + return $c->status(400, { error => 'no validations ran' }) if not $validation_state; $c->log->debug("Validations ran with result: ".$validation_state->status); # calculate the device health based on the validation results. @@ -407,6 +408,72 @@ sub get ($c) { return $c->status(200, $c->stash('device_report_rs')->single); } +=head2 validate_report + +Process a device report without writing anything to the database; otherwise behaves like +L. The described device does not have to exist. + +Response uses the ReportValidationResults json schema. + +=cut + +sub validate_report ($c) { + my $unserialized_report = $c->validate_input('DeviceReport'); + if (not $unserialized_report) { + $c->log->debug('Device report input did not match json schema specification'); + return; + } + + my $hw = $c->_get_hardware_product($unserialized_report); + return $c->status(409, { error => 'Could not locate hardware product' }) if not $hw; + return $c->status(409, { error => 'Hardware product does not contain a profile' }) + if not $hw->hardware_product_profile; + + my $validation_plan = $c->_get_validation_plan($unserialized_report); + return $c->status(500, { error => 'failed to find validation plan' }) if not $validation_plan; + $c->log->debug('Running validation plan '.$validation_plan->id.': '.$validation_plan->name.'"'); + + my ($status, @validation_results); + $c->txn_wrapper(sub ($c) { + my $device = $c->db_devices->update_or_create({ + id => $unserialized_report->{serial_number}, + system_uuid => $unserialized_report->{system_uuid}, + hardware_product_id => $hw->id, + state => $unserialized_report->{state}, + health => 'unknown', + last_seen => \'now()', + uptime_since => $unserialized_report->{uptime_since}, + hostname => $unserialized_report->{os}{hostname}, + updated => \'now()', + deactivated => undef, + }); + + # we do not call _record_device_configuration, because no validations + # should be using that information, instead choosing to respect the report data. + + ($status, @validation_results) = Conch::ValidationSystem->new( + schema => $c->ro_schema, + log => $c->log, + )->run_validation_plan( + validation_plan => $validation_plan, + device => $device, + data => $unserialized_report, + no_save_db => 1, + ); + + die 'rollback: device used for report validation should not be persisted'; + }); + + return $c->status(400, { error => 'no validations ran' }) if not @validation_results; + + $c->status(200, { + device_id => $unserialized_report->{serial_number}, + validation_plan_id => $validation_plan->id, + status => $status, + results => \@validation_results, + }); +} + =head2 _get_hardware_product Find the hardware product for the device referenced by the report. diff --git a/lib/Conch/Route/DeviceReport.pm b/lib/Conch/Route/DeviceReport.pm index ad3458d4a..ae1c87629 100644 --- a/lib/Conch/Route/DeviceReport.pm +++ b/lib/Conch/Route/DeviceReport.pm @@ -14,6 +14,7 @@ Conch::Route::DeviceReport Sets up the routes for /device_report: + POST /device_report GET /device_report/:device_report_id =cut @@ -22,6 +23,8 @@ sub routes { my $class = shift; my $device_report = shift; # secured, under /device_report + $device_report->post('/')->to('device_report#validate_report'); + # chainable action that extracts and looks up device_report_id from the path # and device_id from the device_report my $with_device_report = $device_report->under('/:device_report_id') diff --git a/t/integration/device-reports.t b/t/integration/device-reports.t new file mode 100644 index 000000000..f5e16ccbd --- /dev/null +++ b/t/integration/device-reports.t @@ -0,0 +1,53 @@ +use v5.26; +use Mojo::Base -strict, -signatures; + +use Test::More; +use Test::Warnings; +use Path::Tiny; +use Test::Deep; +use Test::Conch; + +my $t = Test::Conch->new; + +my $ro_user = $t->load_fixture('ro_user_global_workspace')->user_account; +$t->authenticate(user => $ro_user->email); + +# matches report's product_name = Joyent-G1 +$t->load_fixture('hardware_product_profile_compute'); +my $hardware_product = $t->load_fixture('hardware_product_compute'); + +# create a validation plan with all current validations in it +Conch::ValidationSystem->new(log => $t->app->log, schema => $t->app->schema)->load_validations; +my @validations = $t->app->db_validations->all; +my ($validation_plan) = $t->load_validation_plans([{ + name => 'Conch v1 Legacy Plan: Server', + description => 'Test Plan', + validations => [ map $_->module, @validations ], +}]); + +subtest 'run report without an existing device' => sub { + my $report = path('t/integration/resource/passing-device-report.json')->slurp_utf8; + $t->post_ok('/device_report', { 'Content-Type' => 'application/json' }, $report) + ->status_is(200) + ->json_schema_is('ReportValidationResults') + ->json_cmp_deeply({ + device_id => 'TEST', + validation_plan_id => $validation_plan->id, + status => any(qw(error fail pass)), # likely some validations will hate this report. + # validations each produce one or more results + results => array_each(any(map +{ + id => undef, + validation_id => $_->id, + category => $_->module->category, + component_id => ignore, + device_id => 'TEST', + hardware_product_id => $hardware_product->id, + hint => ignore, + message => ignore, + order => ignore, + status => any(qw(error fail pass)), + }, @validations)), + }); +}; + +done_testing;