Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/bosh-director/lib/bosh/director.rb
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ module Director
require 'bosh/director/jobs/helpers'
require 'bosh/director/jobs/db_job'
require 'bosh/director/jobs/orphan_disk'
require 'bosh/director/jobs/dynamic_disks/create_dynamic_disk'
require 'bosh/director/jobs/dynamic_disks/attach_dynamic_disk'
require 'bosh/director/jobs/dynamic_disks/provide_dynamic_disk'
require 'bosh/director/jobs/dynamic_disks/detach_dynamic_disk'
require 'bosh/director/jobs/dynamic_disks/delete_dynamic_disk'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,56 @@ module Api::Controllers
class DynamicDisksController < BaseController
include ValidationHelper

get '/', scope: :list_dynamic_disks do
disks = Models::DynamicDisk.all.map do |disk|
Comment thread
Copilot marked this conversation as resolved.
Outdated
{
name: disk.name,
disk_cid: disk.disk_cid,
deployment: disk.deployment&.name,
instance: disk.vm&.instance&.name,
availability_zone: disk.availability_zone,
size: disk.size,
cpi: disk.cpi,
}
end
json_encode(disks)
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.

post '/', scope: :create_dynamic_disks, consumes: :json do
request_hash = JSON.parse(request.body.read)

instance_id = safe_property(request_hash, 'instance_id', class: String, min_length: 1)
disk_name = safe_property(request_hash, 'disk_name', class: String, min_length: 1)
disk_pool_name = safe_property(request_hash, 'disk_pool_name', class: String, min_length: 1)
disk_size = safe_property(request_hash, 'disk_size', class: Integer, min: 1)
metadata = safe_property(request_hash, 'metadata', class: Hash, optional: true)

task = JobQueue.new.enqueue(
current_user,
Jobs::DynamicDisks::CreateDynamicDisk,
'create dynamic disk',
[instance_id, disk_name, disk_pool_name, disk_size, metadata],
)

redirect "/tasks/#{task.id}"
end

post '/:disk_name/attach', scope: :attach_dynamic_disks, consumes: :json do
disk_name = safe_property(params, 'disk_name', class: String, min_length: 1)

request_hash = JSON.parse(request.body.read)
instance_id = safe_property(request_hash, 'instance_id', class: String, min_length: 1)

task = JobQueue.new.enqueue(
current_user,
Jobs::DynamicDisks::AttachDynamicDisk,
'attach dynamic disk',
[disk_name, instance_id],
)

redirect "/tasks/#{task.id}"
end

post '/provide', scope: :provide_dynamic_disks, consumes: :json do
request_hash = JSON.parse(request.body.read)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Bosh::Director
module Jobs::DynamicDisks
class AttachDynamicDisk < Jobs::BaseJob
include Jobs::Helpers::DynamicDiskHelpers
include LockHelper

@queue = :dynamic_disks

def self.job_type
:attach_dynamic_disk
end

def initialize(disk_name, instance_id)
@disk_name = disk_name
@instance_id = instance_id
end

def perform
disk_model = Models::DynamicDisk.find(name: @disk_name)
raise "disk `#{@disk_name}` not found" if disk_model.nil?

instance = Models::Instance.find(uuid: @instance_id)
raise "instance `#{@instance_id}` not found" if instance.nil?

vm = instance.active_vm
raise "no active vm found for instance `#{@instance_id}`" if vm.nil?

unless disk_model.vm.nil?
return "disk `#{@disk_name}` is already attached to vm `#{disk_model.vm.cid}`" if disk_model.vm.id == vm.id

raise "disk `#{@disk_name}` is already attached to a different vm `#{disk_model.vm.cid}`"
end

cloud = Bosh::Director::CloudFactory.create.get(disk_model.cpi, vm.stemcell_api_version)
disk_hint = with_vm_lock(vm.cid, timeout: VM_LOCK_TIMEOUT) { cloud.attach_disk(vm.cid, disk_model.disk_cid) }
disk_model.update(vm_id: vm.id, availability_zone: vm.instance.availability_zone, disk_hint: disk_hint)

agent_client = AgentClient.with_agent_id(vm.agent_id, instance.name)
agent_client.add_dynamic_disk(disk_model.disk_cid, disk_model.disk_hint)
Comment on lines +56 to +57

"attached disk `#{@disk_name}` to vm `#{vm.cid}` in deployment `#{instance.deployment.name}`"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module Bosh::Director
module Jobs::DynamicDisks
class CreateDynamicDisk < Jobs::BaseJob
include Jobs::Helpers::DynamicDiskHelpers

@queue = :dynamic_disks

def self.job_type
:create_dynamic_disk
end

def initialize(instance_id, disk_name, disk_pool_name, disk_size, metadata)
@instance_id = instance_id
@disk_name = disk_name
@disk_pool_name = disk_pool_name
@disk_size = disk_size
@metadata = metadata
end

def perform
instance = Models::Instance.find(uuid: @instance_id)
raise "instance `#{@instance_id}` not found" if instance.nil?

vm = instance.active_vm
raise "no active vm found for instance `#{@instance_id}`" if vm.nil?

cloud_properties = find_disk_cloud_properties(instance, @disk_pool_name)
cloud = Bosh::Director::CloudFactory.create.get(vm.cpi)

disk_model = Models::DynamicDisk.find(name: @disk_name)
if disk_model.nil?
cloud_properties['name'] = @disk_name
disk_cid = cloud.create_disk(@disk_size, cloud_properties, vm.cid)
Comment thread
aramprice marked this conversation as resolved.
Outdated
disk_model = Models::DynamicDisk.create(
name: @disk_name,
disk_cid: disk_cid,
deployment_id: instance.deployment.id,
size: @disk_size,
disk_pool_name: @disk_pool_name,
cpi: vm.cpi,
)
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

if !@metadata.nil? && disk_model.metadata != @metadata
MetadataUpdater.build.update_dynamic_disk_metadata(cloud, disk_model, @metadata)
disk_model.update(metadata: @metadata)
end

disk_info = { disk_cid: disk_model.disk_cid }

task_result.write(JSON.generate(disk_info))
task_result.write("\n")

"created disk `#{@disk_name}` in deployment `#{instance.deployment.name}`"
end
end
end
end
1 change: 1 addition & 0 deletions src/bosh-director/spec/support/test_identity_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def user_scopes
'dynamic-disks-attacher' => ['bosh.dynamic-disks.attach'],
'dynamic-disks-detacher' => ['bosh.dynamic-disks.detach'],
'dynamic-disks-deleter' => ['bosh.dynamic-disks.delete'],
'dynamic-disks-lister' => ['bosh.dynamic-disks.list'],
'outsider' => ['uaa.admin'],
}
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,213 @@ module Api
end
end

describe 'GET', '/' do
context 'when user is reader' do
before { basic_authorize('reader', 'reader') }

it 'forbids access' do
expect(get('/').status).to eq(401)
end
end

context 'when user has bosh.dynamic-disks.list scope' do
before { basic_authorize('dynamic-disks-lister', 'dynamic-disks-lister') }

context 'when there are no dynamic disks' do
it 'returns an empty list' do
get '/'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq([])
end
end

context 'when dynamic disks exist' do
let!(:deployment) { FactoryBot.create(:models_deployment, name: 'my-deployment') }
let!(:disk1) do
FactoryBot.create(:models_dynamic_disk,
name: 'disk-a',
disk_cid: 'cid-a',
deployment: deployment,
size: 1024,
)
end
let!(:disk2) do
FactoryBot.create(:models_dynamic_disk,
name: 'disk-b',
disk_cid: 'cid-b',
deployment: deployment,
size: 2048,
)
end

it 'returns all dynamic disks' do
get '/'
expect(last_response.status).to eq(200)
body = JSON.parse(last_response.body)
expect(body.size).to eq(2)
expect(body).to include(hash_including('name' => 'disk-a', 'disk_cid' => 'cid-a', 'deployment' => 'my-deployment', 'size' => 1024))
expect(body).to include(hash_including('name' => 'disk-b', 'disk_cid' => 'cid-b', 'deployment' => 'my-deployment', 'size' => 2048))
end
end
end

context 'when user has admin permissions' do
before { authorize 'admin', 'admin' }

it 'returns a list of dynamic disks' do
get '/'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to eq([])
end
end
end

describe 'POST', '/' do
let(:content) do
JSON.generate({
'instance_id' => instance_id,
'disk_pool_name' => disk_pool_name,
'disk_name' => disk_name,
'disk_size' => disk_size,
'metadata' => metadata,
})
end

context 'when user is reader' do
before { basic_authorize('reader', 'reader') }

it 'forbids access' do
expect(post('/', content, { 'CONTENT_TYPE' => 'application/json' }).status).to eq(401)
end
end

context 'when user has bosh.dynamic-disks.create scope' do
before { basic_authorize('dynamic-disks-creator', 'dynamic-disks-creator') }

it 'enqueues a CreateDynamicDisk task' do
expect_any_instance_of(Bosh::Director::JobQueue).to receive(:enqueue).with(
'dynamic-disks-creator',
Jobs::DynamicDisks::CreateDynamicDisk,
'create dynamic disk',
[instance_id, disk_name, disk_pool_name, disk_size, metadata],
).and_call_original

post '/', content, { 'CONTENT_TYPE' => 'application/json' }

expect_redirect_to_queued_task(last_response)
end
end

context 'when user has only bosh.dynamic-disks.attach scope (not create)' do
before { basic_authorize('dynamic-disks-attacher', 'dynamic-disks-attacher') }

it 'forbids access' do
expect(post('/', content, { 'CONTENT_TYPE' => 'application/json' }).status).to eq(401)
end
end

context 'when user has admin permissions' do
before { authorize 'admin', 'admin' }

it 'enqueues a CreateDynamicDisk task' do
expect_any_instance_of(Bosh::Director::JobQueue).to receive(:enqueue).with(
'admin',
Jobs::DynamicDisks::CreateDynamicDisk,
'create dynamic disk',
[instance_id, disk_name, disk_pool_name, disk_size, metadata],
).and_call_original

post '/', content, { 'CONTENT_TYPE' => 'application/json' }

expect_redirect_to_queued_task(last_response)
end

context 'when disk_name is nil' do
let(:disk_name) { nil }

it 'raises an error' do
post '/', content, { 'CONTENT_TYPE' => 'application/json' }
expect(last_response.status).to eq(400)
expect(JSON.parse(last_response.body)['description']).to include("disk_name")
end
end

context 'when disk_size is 0' do
let(:disk_size) { 0 }

it 'raises an error' do
post '/', content, { 'CONTENT_TYPE' => 'application/json' }
expect(last_response.status).to eq(400)
expect(JSON.parse(last_response.body)['description']).to include("disk_size")
end
end
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.
end

describe 'POST', '/:disk_name/attach' do
let(:content) { JSON.generate({ 'instance_id' => instance_id }) }

context 'when user is reader' do
before { basic_authorize('reader', 'reader') }

it 'forbids access' do
expect(post('/disk_name/attach', content, { 'CONTENT_TYPE' => 'application/json' }).status).to eq(401)
end
end

context 'when user has bosh.dynamic-disks.attach scope' do
before { basic_authorize('dynamic-disks-attacher', 'dynamic-disks-attacher') }

it 'enqueues an AttachDynamicDisk task' do
expect_any_instance_of(Bosh::Director::JobQueue).to receive(:enqueue).with(
'dynamic-disks-attacher',
Jobs::DynamicDisks::AttachDynamicDisk,
'attach dynamic disk',
['disk_name', instance_id],
).and_call_original

post '/disk_name/attach', content, { 'CONTENT_TYPE' => 'application/json' }

expect_redirect_to_queued_task(last_response)
end
end

context 'when user has only bosh.dynamic-disks.create scope (not attach)' do
before { basic_authorize('dynamic-disks-creator', 'dynamic-disks-creator') }

it 'forbids access' do
expect(post('/disk_name/attach', content, { 'CONTENT_TYPE' => 'application/json' }).status).to eq(401)
end
end

context 'when user has admin permissions' do
before { authorize 'admin', 'admin' }

it 'enqueues an AttachDynamicDisk task' do
expect_any_instance_of(Bosh::Director::JobQueue).to receive(:enqueue).with(
'admin',
Jobs::DynamicDisks::AttachDynamicDisk,
'attach dynamic disk',
['disk_name', instance_id],
).and_call_original

post '/disk_name/attach', content, { 'CONTENT_TYPE' => 'application/json' }

expect_redirect_to_queued_task(last_response)
end

context 'when instance_id is missing from the body' do
let(:content) { JSON.generate({}) }

it 'raises an error' do
post '/disk_name/attach', content, { 'CONTENT_TYPE' => 'application/json' }
expect(last_response.status).to eq(400)
expect(JSON.parse(last_response.body)['description']).to include("instance_id")
end
end
end
end

describe 'DELETE', '/:disk_name' do
context 'when user is reader' do
before { basic_authorize('reader', 'reader') }
Expand Down
Loading
Loading