diff --git a/docs/modules/Conch::Controller::Rack.md b/docs/modules/Conch::Controller::Rack.md index a8e0c2ba6..e8dd5cae7 100644 --- a/docs/modules/Conch::Controller::Rack.md +++ b/docs/modules/Conch::Controller::Rack.md @@ -24,12 +24,17 @@ Get all racks Response uses the Racks json schema. -## layouts +## get\_layouts Gets all the layouts for the specified rack. Response uses the RackLayouts json schema. +## overwrite\_layouts + +Given the layout definitions for an entire rack, removes all existing layouts that are not in +the new definition, as well as removing any device\_location assignments in those layouts. + ## update Update an existing rack. diff --git a/docs/modules/Conch::Route::Rack.md b/docs/modules/Conch::Route::Rack.md index fb5ed223b..fdf809b5b 100644 --- a/docs/modules/Conch::Route::Rack.md +++ b/docs/modules/Conch::Route::Rack.md @@ -42,6 +42,12 @@ Unless otherwise noted, all routes require authentication. - User requires the read-only role on a workspace that contains the rack - Response: response.yaml#/RackLayouts +### `POST /rack/:rack_id/layouts` + +- User requires the read/write role on a workspace that contains the rack +- Request: request.yaml#/RackLayouts +- Response: Redirect to the rack's layouts + ### `GET /rack/:rack_id/assignment` - User requires the read-only role on a workspace that contains the rack diff --git a/json-schema/request.yaml b/json-schema/request.yaml index ade6975be..77a7a43e6 100644 --- a/json-schema/request.yaml +++ b/json-schema/request.yaml @@ -188,6 +188,20 @@ definitions: $ref: common.yaml#/definitions/uuid rack_unit_start: $ref: common.yaml#/definitions/positive_integer + RackLayouts: + type: array + uniqueItems: true + items: + type: object + additionalProperties: false + required: + - hardware_product_id + - rack_unit_start + properties: + hardware_product_id: + $ref: common.yaml#/definitions/uuid + rack_unit_start: + $ref: common.yaml#/definitions/positive_integer RackLayoutUpdate: type: object additionalProperties: false diff --git a/lib/Conch/Controller/Rack.pm b/lib/Conch/Controller/Rack.pm index d5f3c8b88..086e78a9a 100644 --- a/lib/Conch/Controller/Rack.pm +++ b/lib/Conch/Controller/Rack.pm @@ -2,7 +2,7 @@ package Conch::Controller::Rack; use Mojo::Base 'Mojolicious::Controller', -signatures; -use List::Util qw(any none first uniq); +use List::Util qw(any none first uniq max); =pod @@ -97,7 +97,7 @@ sub get_all ($c) { $c->status(200, \@racks); } -=head2 layouts +=head2 get_layouts Gets all the layouts for the specified rack. @@ -105,7 +105,7 @@ Response uses the RackLayouts json schema. =cut -sub layouts ($c) { +sub get_layouts ($c) { my @layouts = $c->stash('rack_rs') ->related_resultset('rack_layouts') ->with_rack_unit_size @@ -116,6 +116,87 @@ sub layouts ($c) { $c->status(200, \@layouts); } +=head2 overwrite_layouts + +Given the layout definitions for an entire rack, removes all existing layouts that are not in +the new definition, as well as removing any device_location assignments in those layouts. + +=cut + +sub overwrite_layouts ($c) { + my $input = $c->validate_request('RackLayouts'); + return if not $input; + + my %layout_sizes = map +($_->{id} => $_->{rack_unit_size}), + $c->db_hardware_products->active->search({ id => { -in => [ map $_->{hardware_product_id}, $input->@* ] } }) + ->columns([qw(id rack_unit_size)]) + ->hri->all; + + my %desired_slots; # map of all slots that will be occupied (slot => rack_unit_start) + foreach my $layout ($input->@*) { + my $size = $layout_sizes{$layout->{hardware_product_id}}; + return $c->status(409, { error => 'hardware_product_id '.$layout->{hardware_product_id}.' does not exist' }) if not $size; + my @slots = $layout->{rack_unit_start} .. $layout->{rack_unit_start} + $size - 1; + my @overlaps = grep defined, map $desired_slots{$_}, @slots; + return $c->status(409, { error => 'layouts starting at rack_units '.$overlaps[0].' and '.$layout->{rack_unit_start}.' overlap' }) if @overlaps; + $desired_slots{$_} = $layout->{rack_unit_start} foreach @slots; + } + + if (my $last_slot = max(keys %desired_slots)) { + return $c->status(409, { error => 'layout starting at rack_unit '.$desired_slots{$last_slot}.' will extend beyond the end of the rack' }) + if $last_slot > $c->stash('rack_rs')->related_resultset('rack_role')->get_column('rack_size')->single; + } + + my @existing_layouts = $c->stash('rack_rs') + ->related_resultset('rack_layouts') + ->columns([qw(hardware_product_id rack_unit_start)]) + ->hri->all; + + my @layouts_to_delete = grep { + my $existing_layout = $_; + none { + $existing_layout->{hardware_product_id} eq $_->{hardware_product_id} + and $existing_layout->{rack_unit_start} eq $_->{rack_unit_start} + } $input->@*; + } + @existing_layouts; + + my @layouts_to_create = grep { + my $new_layout = $_; + none { + $new_layout->{hardware_product_id} eq $_->{hardware_product_id} + and $new_layout->{rack_unit_start} eq $_->{rack_unit_start} + } @existing_layouts; + } + $input->@*; + + $c->txn_wrapper(sub ($c) { + my $layouts_rs = $c->stash('rack_rs') + ->search_related('rack_layouts', [ map +( +{ + 'rack_layouts.hardware_product_id' => $_->{hardware_product_id}, + 'rack_layouts.rack_unit_start' => $_->{rack_unit_start}, + } ), @layouts_to_delete ] ); + + my $device_locations_rs = $layouts_rs->related_resultset('device_location'); + + my $deleted_device_locations = 0+$device_locations_rs->delete; + my $deleted_layouts = 0+$layouts_rs->delete; + $c->db_rack_layouts->populate([ map +{ rack_id => $c->stash('rack_id'), $_->%*, }, @layouts_to_create ]); + + $c->log->debug( + join(', ', + ($deleted_device_locations ? ('unlocated '.$deleted_device_locations.' devices') : ()), + ($deleted_layouts ? ('deleted '.$deleted_layouts.' rack layouts') : ()), + (@layouts_to_create ? ('created '.scalar(@layouts_to_create).' rack layouts') : ()), + ).' for rack '.$c->stash('rack_id')); + }); + + # if the result code was already set, we errored and rolled back the db... + return if $c->res->code; + + $c->status(303, '/rack/'.$c->stash('rack_id').'/layouts'); +} + =head2 update Update an existing rack. diff --git a/lib/Conch/Route/Rack.pm b/lib/Conch/Route/Rack.pm index c6f7d731e..9f7229c5e 100644 --- a/lib/Conch/Route/Rack.pm +++ b/lib/Conch/Route/Rack.pm @@ -37,7 +37,9 @@ sub routes { $with_rack->require_system_admin->delete('/')->to('#delete'); # GET /rack/:rack_id/layouts - $with_rack->get('/layouts')->to('#layouts'); + $with_rack->get('/layouts')->to('#get_layouts'); + # POST /rack/:rack_id/layouts + $with_rack->post('/layouts')->to('#overwrite_layouts'); # GET /rack/:rack_id/assignment $with_rack->get('/assignment')->to('#get_assignment'); @@ -121,6 +123,18 @@ Unless otherwise noted, all routes require authentication. =back +=head3 C + +=over 4 + +=item * User requires the read/write role on a workspace that contains the rack + +=item * Request: request.yaml#/RackLayouts + +=item * Response: Redirect to the rack's layouts + +=back + =head3 C =over 4 diff --git a/t/integration/crud/rack-layouts.t b/t/integration/crud/rack-layouts.t index 1d1901342..7f3ea7478 100644 --- a/t/integration/crud/rack-layouts.t +++ b/t/integration/crud/rack-layouts.t @@ -321,5 +321,97 @@ $t->delete_ok('/layout/'.$layout_3_6->id) $t->get_ok('/layout/'.$layout_3_6->id) ->status_is(404); +# now we have these assigned slots: +# start 1, width 2 +# start 11, width 4 +# start 20, width 4 # occupied by 'my device' +# start 42, width 1 + +$t->post_ok('/rack/'.$rack_id.'/layouts', + json => [{ rack_unit_start => 1, hardware_product_id => create_uuid_str }]) + ->status_is(409) + ->json_cmp_deeply({ error => re(qr/^hardware_product_id ${\Conch::UUID::UUID_FORMAT} does not exist$/) }); + +$t->post_ok('/rack/'.$rack_id.'/layouts', + json => [{ rack_unit_start => 42, hardware_product_id => $hw_product_compute->id }]) + ->status_is(409) + ->json_is({ error => 'layout starting at rack_unit 42 will extend beyond the end of the rack' }); + +$t->post_ok('/rack/'.$rack_id.'/layouts', + json => [ + { rack_unit_start => 1, hardware_product_id => $hw_product_compute->id }, + { rack_unit_start => 2, hardware_product_id => $hw_product_compute->id }, + ]) + ->status_is(409) + ->json_is({ error => 'layouts starting at rack_units 1 and 2 overlap' }); + +$t->post_ok('/rack/'.$rack_id.'/layouts', + json => [ + # unchanged + { rack_unit_start => 1, hardware_product_id => $hw_product_compute->id }, + # unchanged, and has a located device + { rack_unit_start => 20, hardware_product_id => $hw_product_storage->id }, + # new layout + { rack_unit_start => 26, hardware_product_id => $hw_product_compute->id }, + ]) + ->status_is(303) + ->location_is('/rack/'.$rack_id.'/layouts') + ->log_debug_is('deleted 2 rack layouts, created 1 rack layouts for rack '.$rack_id); + +$t->get_ok('/rack/'.$rack_id.'/layouts') + ->status_is(200) + ->json_schema_is('RackLayouts') + ->json_cmp_deeply([ + superhashof({ rack_unit_start => 1, hardware_product_id => $hw_product_compute->id }), + superhashof({ rack_unit_start => 20, hardware_product_id => $hw_product_storage->id }), + superhashof({ rack_unit_start => 26, hardware_product_id => $hw_product_compute->id }), + ]); + +$t->get_ok('/rack/'.$rack_id.'/assignment') + ->status_is(200) + ->json_schema_is('RackAssignments') + ->json_cmp_deeply([ + { rack_unit_start => 1, rack_unit_size => 2, hardware_product_name => $hw_product_compute->name, device_id => undef, device_asset_tag => undef }, + { rack_unit_start => 20, rack_unit_size => 4, hardware_product_name => $hw_product_storage->name, device_id => $device->id, device_asset_tag => undef }, + { rack_unit_start => 26, rack_unit_size => 2, hardware_product_name => $hw_product_compute->name, device_id => undef, device_asset_tag => undef }, + ]); + +$t->post_ok('/rack/'.$rack_id.'/layouts', + json => [ + { rack_unit_start => 3, hardware_product_id => $hw_product_compute->id }, + ]) + ->status_is(303) + ->location_is('/rack/'.$rack_id.'/layouts') + ->log_debug_is('unlocated 1 devices, deleted 3 rack layouts, created 1 rack layouts for rack '.$rack_id); + +$t->get_ok('/rack/'.$rack_id.'/layouts') + ->status_is(200) + ->json_schema_is('RackLayouts') + ->json_cmp_deeply([ + superhashof({ rack_unit_start => 3, hardware_product_id => $hw_product_compute->id }), + ]); + +$t->get_ok('/rack/'.$rack_id.'/assignment') + ->status_is(200) + ->json_schema_is('RackAssignments') + ->json_cmp_deeply([ + { rack_unit_start => 3, rack_unit_size => 2, hardware_product_name => $hw_product_compute->name, device_id => undef, device_asset_tag => undef }, + ]); + +$t->post_ok('/rack/'.$rack_id.'/layouts', json => []) + ->status_is(303) + ->location_is('/rack/'.$rack_id.'/layouts') + ->log_debug_is('deleted 1 rack layouts for rack '.$rack_id); + +$t->get_ok('/rack/'.$rack_id.'/layouts') + ->status_is(200) + ->json_schema_is('RackLayouts') + ->json_is([]); + +$t->get_ok('/rack/'.$rack_id.'/assignment') + ->status_is(200) + ->json_schema_is('RackAssignments') + ->json_is([]); + done_testing; # vim: set ts=4 sts=4 sw=4 et :