Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ def initialize(config)
options['fix'] = true if params['fix'] == 'true'
options['dry_run'] = true if params['dry_run'] == 'true'

if params['recreate_vm_created_before']
begin
Time.rfc3339(params['recreate_vm_created_before'])
options['recreate_vm_created_before'] = params['recreate_vm_created_before']
rescue ArgumentError
raise ValidationInvalidValue, "Invalid RFC 3339 timestamp for recreate_vm_created_before: #{params['recreate_vm_created_before']}"
end
end

if (request.content_length.nil? || request.content_length.to_i == 0) && params['state']
manifest = deployment.manifest
latest_cloud_configs = deployment.cloud_configs
Expand Down Expand Up @@ -418,6 +427,15 @@ def initialize(config)
options['scopes'] = token_scopes
options['force_latest_variables'] = true if params['force_latest_variables'] == 'true'

if params['recreate_vm_created_before']
begin
Time.rfc3339(params['recreate_vm_created_before'])
options['recreate_vm_created_before'] = params['recreate_vm_created_before']
rescue ArgumentError
raise ValidationInvalidValue, "Invalid RFC 3339 timestamp for recreate_vm_created_before: #{params['recreate_vm_created_before']}"
end
end

# since authorizer does not look at manifest payload for deployment name
@deployment = Models::Deployment[name: deployment_name]
if @deployment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def bind_models(options = {})
@variables_interpolator,
@deployment_plan.link_provider_intents,
'recreate' => @deployment_plan.recreate,
'recreate_vm_created_before' => @deployment_plan.recreate_vm_created_before,
'use_dns_addresses' => @deployment_plan.use_dns_addresses?,
'use_short_dns_addresses' => @deployment_plan.use_short_dns_addresses?,
'use_link_dns_addresses' => @deployment_plan.use_link_dns_names?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def initialize(existing_instance:,
skip_drain: false,
recreate_deployment: false,
recreate_persistent_disks: false,
recreate_vm_created_before: nil,
use_dns_addresses: false,
use_short_dns_addresses: false,
use_link_dns_addresses: false,
Expand All @@ -27,6 +28,7 @@ def initialize(existing_instance:,
@skip_drain = skip_drain
@recreate_deployment = recreate_deployment
@recreate_persistent_disks = recreate_persistent_disks
@recreate_vm_created_before = recreate_vm_created_before
@use_dns_addresses = use_dns_addresses
@use_short_dns_addresses = use_short_dns_addresses
@use_link_dns_addresses = use_link_dns_addresses
Expand Down Expand Up @@ -124,13 +126,22 @@ def restart_requested?
def recreation_requested?
if @recreate_deployment
@logger.debug("#{__method__} job deployment is configured with \"recreate\" state")
true
elsif unresponsive_agent?
return should_recreate_based_on_vm_age?
end

if unresponsive_agent?
@logger.debug("#{__method__} instance should be recreated because of unresponsive agent")
true
return true
else
@instance.virtual_state.recreate?
return @instance.virtual_state.recreate?
end

if @instance.virtual_state == 'recreate'
@logger.debug("#{__method__} instance virtual_state is \"recreate\"")
return should_recreate_based_on_vm_age?
end

false
end

def recreate_persistent_disks_requested?
Expand Down Expand Up @@ -433,6 +444,38 @@ def stemcell_model_for_cpi(instance)

private

def should_recreate_based_on_vm_age?
# If no filter specified, always recreate
unless @recreate_vm_created_before
@logger.debug("#{__method__} no recreate_vm_created_before filter, will recreate")
return true
end

# If instance is dirty (previous update failed), always recreate
# This handles the retry scenario where a recreated VM failed to start
if @instance.dirty?
@logger.debug("#{__method__} instance is dirty (update_completed=false), will recreate despite age filter")
return true
end

# If no existing VM or no created_at, treat as should recreate
vm_created_at = @existing_instance&.active_vm&.created_at
unless vm_created_at
@logger.debug("#{__method__} no existing VM or created_at, will recreate")
return true
end

# Compare VM age against threshold
threshold_time = Time.rfc3339(@recreate_vm_created_before)
if vm_created_at < threshold_time
@logger.debug("#{__method__} VM created at #{vm_created_at} is older than threshold #{threshold_time}, will recreate")
true
else
@logger.debug("#{__method__} VM created at #{vm_created_at} is newer than threshold #{threshold_time}, skipping recreation")
false
end
end

def remove_dns_record_name_from_network_settings(network_settings)
return network_settings if network_settings.nil?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def initialize(
@instance_repo = instance_repo
@recreate_deployment = options.fetch('recreate', false)
@recreate_persistent_disks = options.fetch('recreate_persistent_disks', false)
@recreate_vm_created_before = options.fetch('recreate_vm_created_before', nil)
@states_by_existing_instance = states_by_existing_instance
@index_assigner = index_assigner
@link_provider_intents = link_provider_intents
Expand All @@ -38,6 +39,7 @@ def obsolete_instance_plan(existing_instance_model)
instance: instance,
skip_drain: @deployment_plan.skip_drain.for_job(existing_instance_model.job),
recreate_deployment: @recreate_deployment,
recreate_vm_created_before: @recreate_vm_created_before,
use_dns_addresses: @use_dns_addresses,
use_short_dns_addresses: @use_short_dns_addresses,
use_link_dns_addresses: @use_link_dns_addresses,
Expand All @@ -59,6 +61,7 @@ def desired_existing_instance_plan(existing_instance_model, desired_instance)
skip_drain: @deployment_plan.skip_drain.for_job(desired_instance.instance_group.name),
recreate_deployment: @recreate_deployment,
recreate_persistent_disks: @recreate_persistent_disks,
recreate_vm_created_before: @recreate_vm_created_before,
use_dns_addresses: @use_dns_addresses,
use_short_dns_addresses: @use_short_dns_addresses,
use_link_dns_addresses: @use_link_dns_addresses,
Expand All @@ -78,6 +81,7 @@ def desired_new_instance_plan(desired_instance)
instance: instance,
skip_drain: @deployment_plan.skip_drain.for_job(desired_instance.instance_group.name),
recreate_deployment: @recreate_deployment,
recreate_vm_created_before: @recreate_vm_created_before,
use_dns_addresses: @use_dns_addresses,
use_short_dns_addresses: @use_short_dns_addresses,
use_link_dns_addresses: @use_link_dns_addresses,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class Planner
# @return [Boolean] Indicates whether persistent disks should be recreated
attr_reader :recreate_persistent_disks

# @return [String, nil] ISO 8601 timestamp - only recreate VMs created before this time
attr_reader :recreate_vm_created_before

attr_writer :cloud_planner

# @return [DeploymentPlan::SkipDrain] Indicates whether VMs should be drained
Expand Down Expand Up @@ -102,6 +105,7 @@ def initialize(
@is_deploy_action = !!options['is_deploy_action']
@recreate = !!options['recreate']
@recreate_persistent_disks = options['recreate_persistent_disks'] == true
@recreate_vm_created_before = options['recreate_vm_created_before']
@fix = !!options['fix']

@skip_drain = SkipDrain.new(options['skip_drain'])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def parse_from_manifest(manifest, cloud_config_consolidator, runtime_configs, op
'is_deploy_action' => !!options['deploy'],
'recreate' => !!options['recreate'],
'recreate_persistent_disks' => options['recreate_persistent_disks'] == true,
'recreate_vm_created_before' => options['recreate_vm_created_before'],
'fix' => !!options['fix'],
'skip_drain' => options['skip_drain'],
'job_states' => options['job_states'] || {},
Expand Down
14 changes: 14 additions & 0 deletions src/bosh-director/lib/bosh/director/jobs/update_deployment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def perform
prepare_deployment

warn_if_any_ignored_instances
warn_if_recreate_vm_created_before_is_future

next_releases, next_stemcells = get_stemcells_and_releases

Expand Down Expand Up @@ -464,6 +465,19 @@ def warn_if_any_ignored_instances
@event_log.warn('You have ignored instances. They will not be changed.')
end

def warn_if_recreate_vm_created_before_is_future
timestamp = deployment_plan.recreate_vm_created_before
return unless timestamp

threshold_time = Time.rfc3339(timestamp)
return unless threshold_time > Time.now

@event_log.warn(
"The recreate_vm_created_before timestamp '#{timestamp}' is in the future. " \
'No VMs will be filtered by age - all VMs marked for recreation will be recreated.',
)
end

def handle_toplevel_exception(e, context)
@notifier.send_error_event e unless dry_run?
rescue Exception => e2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,30 @@ def manifest_with_errand(deployment_name='errand')
end
end

context 'with the "recreate_vm_created_before" param' do
it 'passes a valid RFC 3339 timestamp' do
expect_any_instance_of(DeploymentManager)
.to receive(:create_deployment)
.with(
anything,
anything,
anything,
anything,
anything,
hash_including('recreate_vm_created_before' => '2026-01-01T00:00:00Z'),
anything,
).and_return(OpenStruct.new(id: 1))
post '/?recreate_vm_created_before=2026-01-01T00:00:00Z', asset_content('test_conf.yaml'), 'CONTENT_TYPE' => 'text/yaml'
expect(last_response).to be_redirect
end

it 'returns 400 for invalid RFC 3339 timestamp' do
post '/?recreate_vm_created_before=invalid-timestamp', asset_content('test_conf.yaml'), 'CONTENT_TYPE' => 'text/yaml'
expect(last_response.status).to eq(400)
expect(last_response.body).to include('Invalid RFC 3339 timestamp')
end
end

context 'updates using a manifest with deployment name' do
it 'calls create deployment with deployment name' do
expect_any_instance_of(DeploymentManager)
Expand Down Expand Up @@ -835,6 +859,51 @@ def manifest_with_errand(deployment_name='errand')
end
it_behaves_like 'recreates with configs'
end

context 'with recreate_vm_created_before parameter' do
it 'passes a valid RFC 3339 timestamp' do
deployment = Models::Deployment.create(name: 'foo', manifest: YAML.dump({ 'foo' => 'bar' }))
Models::Instance.create(
deployment: deployment,
job: 'dea',
index: '0',
uuid: '0B949287-CDED-4761-9002-FC4035E11B21',
state: 'started',
variable_set: Models::VariableSet.create(deployment: deployment),
)

expect_any_instance_of(DeploymentManager)
.to receive(:create_deployment)
.with(
anything,
anything,
anything,
anything,
anything,
hash_including('recreate_vm_created_before' => '2026-01-01T00:00:00Z'),
anything,
).and_return(OpenStruct.new(id: 1))

put '/foo/jobs/dea?state=recreate&recreate_vm_created_before=2026-01-01T00:00:00Z', '', 'CONTENT_TYPE' => 'text/yaml'
expect(last_response).to be_redirect
end

it 'returns 400 for invalid RFC 3339 timestamp' do
deployment = Models::Deployment.create(name: 'foo', manifest: YAML.dump({ 'foo' => 'bar' }))
Models::Instance.create(
deployment: deployment,
job: 'dea',
index: '0',
uuid: '0B949287-CDED-4761-9002-FC4035E11B21',
state: 'started',
variable_set: Models::VariableSet.create(deployment: deployment),
)

put '/foo/jobs/dea?state=recreate&recreate_vm_created_before=invalid-timestamp', '', 'CONTENT_TYPE' => 'text/yaml'
expect(last_response.status).to eq(400)
expect(last_response.body).to include('Invalid RFC 3339 timestamp')
end
end
end

describe 'draining' do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ module Bosh::Director
allow(deployment_plan).to receive(:use_link_dns_names?).and_return(false)
allow(deployment_plan).to receive(:randomize_az_placement?).and_return(false)
allow(deployment_plan).to receive(:recreate_persistent_disks?).and_return(false)
allow(deployment_plan).to receive(:recreate_vm_created_before).and_return(nil)
allow(deployment_plan).to receive(:link_provider_intents).and_return([])
end

Expand Down Expand Up @@ -104,6 +105,7 @@ module Bosh::Director
it 'passes tags to instance plan factory' do
expected_options = {
'recreate' => false,
'recreate_vm_created_before' => nil,
'recreate_persistent_disks' => false,
'tags' => { 'key1' => 'value1' },
'use_dns_addresses' => false,
Expand Down Expand Up @@ -134,6 +136,7 @@ module Bosh::Director
it 'passes use_dns_addresses, use_short_dns_addresses and randomize_az_placement feature flags to instance plan factory' do
expected_options = {
'recreate' => false,
'recreate_vm_created_before' => nil,
'recreate_persistent_disks' => false,
'tags' => {},
'use_dns_addresses' => true,
Expand Down Expand Up @@ -161,6 +164,7 @@ module Bosh::Director
it 'passes use_short_dns_addresses to instance plan factory' do
expected_options = {
'recreate' => false,
'recreate_vm_created_before' => nil,
'recreate_persistent_disks' => false,
'tags' => {},
'use_dns_addresses' => true,
Expand Down Expand Up @@ -191,6 +195,7 @@ module Bosh::Director
it 'passes use_dns_addresses, use_short_dns_addresses and randomize_az_placement to instance plan factory' do
expected_options = {
'recreate' => false,
'recreate_vm_created_before' => nil,
'recreate_persistent_disks' => false,
'tags' => {},
'use_dns_addresses' => false,
Expand Down
Loading
Loading