From 8c2b3f14c3de7604876696a8fc6149773f086a94 Mon Sep 17 00:00:00 2001 From: Tim Wade Date: Wed, 1 Feb 2017 07:02:01 -0800 Subject: [PATCH] Add snapshotting for instances in the API Addresses https://bugzilla.redhat.com/show_bug.cgi?id=1399526 --- app/controllers/api/instances_controller.rb | 1 + config/api.yml | 20 ++ spec/requests/api/snapshots_spec.rb | 244 ++++++++++++++++++++ 3 files changed, 265 insertions(+) diff --git a/app/controllers/api/instances_controller.rb b/app/controllers/api/instances_controller.rb index a3601858c5d..b48b981c3f9 100644 --- a/app/controllers/api/instances_controller.rb +++ b/app/controllers/api/instances_controller.rb @@ -1,6 +1,7 @@ module Api class InstancesController < BaseController include Subcollections::LoadBalancers + include Subcollections::Snapshots def terminate_resource(type, id = nil, _data = nil) raise BadRequestError, "Must specify an id for terminating a #{type} resource" unless id diff --git a/config/api.yml b/config/api.yml index f520cc1ec4d..00ae31ced5c 100644 --- a/config/api.yml +++ b/config/api.yml @@ -609,6 +609,7 @@ - :collection :subcollections: - :load_balancers + - :snapshots :collection_actions: :get: - :name: read @@ -657,6 +658,25 @@ :get: - :name: show :identifier: load_balancer_show + :snapshots_subcollection_actions: + :get: + - :name: read + :identifier: cloud_volume_snapshot_view + :post: + - :name: create + :identifier: cloud_volume_snapshot_create + - :name: delete + :identifier: cloud_volume_snapshot_delete + :snapshots_subresource_actions: + :get: + - :name: read + :identifier: cloud_volume_snapshot_view + :post: + - :name: delete + :identifier: cloud_volume_snapshot_delete + :delete: + - :name: delete + :identifier: cloud_volume_snapshot_delete :load_balancers: :description: Load Balancers :options: diff --git a/spec/requests/api/snapshots_spec.rb b/spec/requests/api/snapshots_spec.rb index e788c94cea0..d3d44b2bda5 100644 --- a/spec/requests/api/snapshots_spec.rb +++ b/spec/requests/api/snapshots_spec.rb @@ -240,4 +240,248 @@ end end end + + describe "as a subcollection of instances" do + describe "GET /api/instances/:c_id/snapshots" do + it "can list the snapshots of an Instance" do + api_basic_authorize(subcollection_action_identifier(:instances, :snapshots, :read, :get)) + instance = FactoryGirl.create(:vm_openstack) + snapshot = FactoryGirl.create(:snapshot, :vm_or_template => instance) + _other_snapshot = FactoryGirl.create(:snapshot) + + run_get("#{instances_url(instance.id)}/snapshots") + + expected = { + "count" => 2, + "name" => "snapshots", + "subcount" => 1, + "resources" => [ + {"href" => a_string_matching("#{instances_url(instance.id)}/snapshots/#{snapshot.id}")} + ] + } + expect(response.parsed_body).to include(expected) + expect(response).to have_http_status(:ok) + end + + it "will not list snapshots unless authorized" do + api_basic_authorize + instance = FactoryGirl.create(:vm_openstack) + _snapshot = FactoryGirl.create(:snapshot, :vm_or_template => instance) + + run_get("#{instances_url(instance.id)}/snapshots") + + expect(response).to have_http_status(:forbidden) + end + end + + describe "GET /api/instances/:c_id/snapshots/:s_id" do + it "can show an Instance's snapshot" do + api_basic_authorize(subcollection_action_identifier(:instances, :snapshots, :read, :get)) + instance = FactoryGirl.create(:vm_openstack) + create_time = Time.zone.parse("2017-01-11T00:00:00Z") + snapshot = FactoryGirl.create(:snapshot, :vm_or_template => instance, :create_time => create_time) + + run_get("#{instances_url(instance.id)}/snapshots/#{snapshot.id}") + + expected = { + "create_time" => create_time.iso8601, + "href" => a_string_matching("#{instances_url(instance.id)}/snapshots/#{snapshot.id}"), + "id" => snapshot.id, + "vm_or_template_id" => instance.id + } + expect(response.parsed_body).to include(expected) + expect(response).to have_http_status(:ok) + end + + it "will not show a snapshot unless authorized" do + api_basic_authorize + instance = FactoryGirl.create(:vm_openstack) + snapshot = FactoryGirl.create(:snapshot, :vm_or_template => instance) + + run_get("#{instances_url(instance.id)}/snapshots/#{snapshot.id}") + + expect(response).to have_http_status(:forbidden) + end + + describe "POST /api/instances/:c_id/snapshots" do + it "can queue the creation of a snapshot" do + api_basic_authorize(subcollection_action_identifier(:instances, :snapshots, :create)) + ems = FactoryGirl.create(:ems_openstack_infra) + host = FactoryGirl.create(:host_openstack_infra, :ext_management_system => ems) + instance = FactoryGirl.create(:vm_openstack, :name => "Alice's Instance", :ext_management_system => ems, :host => host) + + run_post("#{instances_url(instance.id)}/snapshots", :name => "Alice's snapshot") + + expected = { + "results" => [ + a_hash_including( + "success" => true, + "message" => "Creating snapshot Alice's snapshot for Vm id:#{instance.id} name:'Alice's Instance'", + "task_id" => anything, + "task_href" => a_string_matching(tasks_url) + ) + ] + } + expect(response.parsed_body).to include(expected) + expect(response).to have_http_status(:ok) + end + + it "renders a failed action response if snapshotting is not supported" do + api_basic_authorize(subcollection_action_identifier(:instances, :snapshots, :create)) + instance = FactoryGirl.create(:vm_openstack) + + run_post("#{instances_url(instance.id)}/snapshots", :name => "Alice's snapsnot") + + expected = { + "results" => [ + a_hash_including( + "success" => false, + "message" => "The VM is not connected to an active Provider" + ) + ] + } + expect(response.parsed_body).to include(expected) + expect(response).to have_http_status(:ok) + end + + it "renders a failed action response if a name is not provided" do + api_basic_authorize(subcollection_action_identifier(:instances, :snapshots, :create)) + ems = FactoryGirl.create(:ems_openstack_infra) + host = FactoryGirl.create(:host_openstack_infra, :ext_management_system => ems) + instance = FactoryGirl.create(:vm_openstack, :name => "Alice's Instance", :ext_management_system => ems, :host => host) + + run_post("#{instances_url(instance.id)}/snapshots", :description => "Alice's snapshot") + + expected = { + "results" => [ + a_hash_including( + "success" => false, + "message" => "Must specify a name for the snapshot" + ) + ] + } + expect(response.parsed_body).to include(expected) + expect(response).to have_http_status(:ok) + end + + it "will not create a snapshot unless authorized" do + api_basic_authorize + instance = FactoryGirl.create(:vm_openstack) + + run_post("#{instances_url(instance.id)}/snapshots", :description => "Alice's snapshot") + + expect(response).to have_http_status(:forbidden) + end + end + + describe "POST /api/instances/:c_id/snapshots/:s_id with delete action" do + it "can queue a snapshot for deletion" do + api_basic_authorize(action_identifier(:instances, :delete, :snapshots_subresource_actions, :delete)) + + ems = FactoryGirl.create(:ems_openstack_infra) + host = FactoryGirl.create(:host_openstack_infra, :ext_management_system => ems) + instance = FactoryGirl.create(:vm_openstack, :name => "Alice's Instance", :ext_management_system => ems, :host => host) + snapshot = FactoryGirl.create(:snapshot, :name => "Alice's snapshot", :vm_or_template => instance) + + run_post("#{instances_url(instance.id)}/snapshots/#{snapshot.id}", :action => "delete") + + expected = { + "message" => "Deleting snapshot Alice's snapshot for Vm id:#{instance.id} name:'Alice's Instance'", + "success" => true, + "task_href" => a_string_matching(tasks_url), + "task_id" => anything + } + expect(response.parsed_body).to include(expected) + expect(response).to have_http_status(:ok) + end + + it "renders a failed action response if deleting is not supported" do + api_basic_authorize(action_identifier(:instances, :delete, :snapshots_subresource_actions, :post)) + instance = FactoryGirl.create(:vm_openstack) + snapshot = FactoryGirl.create(:snapshot, :vm_or_template => instance) + + run_post("#{instances_url(instance.id)}/snapshots/#{snapshot.id}", :action => "delete") + + expected = { + "success" => false, + "message" => "The VM is not connected to an active Provider" + } + expect(response.parsed_body).to include(expected) + expect(response).to have_http_status(:ok) + end + + it "will not delete a snapshot unless authorized" do + api_basic_authorize + instance = FactoryGirl.create(:vm_openstack) + snapshot = FactoryGirl.create(:snapshot, :vm_or_template => instance) + + run_post("#{instances_url(instance.id)}/snapshots/#{snapshot.id}", :action => "delete") + + expect(response).to have_http_status(:forbidden) + end + end + + describe "POST /api/instances/:c_id/snapshots with delete action" do + it "can queue multiple snapshots for deletion" do + api_basic_authorize(action_identifier(:instances, :delete, :snapshots_subresource_actions, :delete)) + + ems = FactoryGirl.create(:ems_openstack_infra) + host = FactoryGirl.create(:host_openstack_infra, :ext_management_system => ems) + instance = FactoryGirl.create(:vm_openstack, :name => "Alice and Bob's Instance", :ext_management_system => ems, :host => host) + snapshot1 = FactoryGirl.create(:snapshot, :name => "Alice's snapshot", :vm_or_template => instance) + snapshot2 = FactoryGirl.create(:snapshot, :name => "Bob's snapshot", :vm_or_template => instance) + + run_post( + "#{instances_url(instance.id)}/snapshots", + :action => "delete", + :resources => [ + {:href => "#{instances_url(instance.id)}/snapshots/#{snapshot1.id}"}, + {:href => "#{instances_url(instance.id)}/snapshots/#{snapshot2.id}"} + ] + ) + + expected = { + "results" => a_collection_containing_exactly( + a_hash_including( + "message" => "Deleting snapshot Alice's snapshot for Vm id:#{instance.id} name:'Alice and Bob's Instance'", + "success" => true, + "task_href" => a_string_matching(tasks_url), + "task_id" => anything + ), + a_hash_including( + "message" => "Deleting snapshot Bob's snapshot for Vm id:#{instance.id} name:'Alice and Bob's Instance'", + "success" => true, + "task_href" => a_string_matching(tasks_url), + "task_id" => anything + ) + ) + } + expect(response.parsed_body).to include(expected) + expect(response).to have_http_status(:ok) + end + end + + describe "DELETE /api/instances/:c_id/snapshots/:s_id" do + it "can delete a snapshot" do + api_basic_authorize(action_identifier(:instances, :delete, :snapshots_subresource_actions, :delete)) + instance = FactoryGirl.create(:vm_openstack) + snapshot = FactoryGirl.create(:snapshot, :vm_or_template => instance) + + run_delete("#{instances_url(instance.id)}/snapshots/#{snapshot.id}") + + expect(response).to have_http_status(:no_content) + end + + it "will not delete a snapshot unless authorized" do + api_basic_authorize + instance = FactoryGirl.create(:vm_openstack) + snapshot = FactoryGirl.create(:snapshot, :vm_or_template => instance) + + run_delete("#{instances_url(instance.id)}/snapshots/#{snapshot.id}") + + expect(response).to have_http_status(:forbidden) + end + end + end + end end