diff --git a/app/lib/google_cloud_compute/compute_attributes.rb b/app/lib/google_cloud_compute/compute_attributes.rb index bc05469..dcdfbee 100644 --- a/app/lib/google_cloud_compute/compute_attributes.rb +++ b/app/lib/google_cloud_compute/compute_attributes.rb @@ -29,12 +29,14 @@ def for_create(instance) end def for_instance(instance) + first_nic = instance.network_interfaces[0] + { name: instance.name, hostname: instance.name, creation_timestamp: instance.creation_timestamp.to_datetime, zone_name: instance.zone.split('/').last, machine_type: instance.machine_type, - network: instance.network_interfaces[0].network.split('/').last, + network: first_nic&.network&.split('/')&.last, network_interfaces: instance.network_interfaces, volumes: instance.disks, metadata: instance.metadata } diff --git a/app/models/foreman_google/google_compute.rb b/app/models/foreman_google/google_compute.rb index 35bfdef..54205a5 100644 --- a/app/models/foreman_google/google_compute.rb +++ b/app/models/foreman_google/google_compute.rb @@ -35,12 +35,12 @@ def status alias_method :state, :status def start - raise Foreman::Exception('unable to start machine that is not persisted') unless persisted? + raise Foreman::Exception, N_('unable to start machine that is not persisted') unless persisted? @client.start(@zone, identity) end def stop - raise Foreman::Exception('unable to stop machine that is not persisted') unless persisted? + raise Foreman::Exception, N_('unable to stop machine that is not persisted') unless persisted? @client.stop(@zone, identity) end diff --git a/test/controllers/foreman_google/api/v2/compute_resources_extensions_test.rb b/test/controllers/foreman_google/api/v2/compute_resources_extensions_test.rb new file mode 100644 index 0000000..c734f05 --- /dev/null +++ b/test/controllers/foreman_google/api/v2/compute_resources_extensions_test.rb @@ -0,0 +1,51 @@ +require 'test_google_helper' + +module ForemanGoogle + module Api + module V2 + class ComputeResourcesExtensionsTest < GoogleTestCase + let(:controller_class) do + Class.new do + def self.before_action(*) + end + + include ForemanGoogle::Api::V2::ComputeResourcesExtensions + + attr_accessor :params + + def compute_resource_params + params[:compute_resource] + end + end + end + + describe '#read_key' do + it 'reads key file content into password for GCE provider' do + key_content = '{"type": "service_account", "project_id": "test"}' + + Tempfile.open('gce_key') do |f| + f.write(key_content) + f.flush + + ctrl = controller_class.new + ctrl.params = { 'compute_resource' => { 'provider' => 'GCE', 'key_path' => f.path } }.with_indifferent_access + ctrl.send(:read_key) + + assert_equal key_content, ctrl.params[:compute_resource][:password] + assert_not ctrl.params[:compute_resource].key?('key_path') + end + end + + it 'skips when provider is not GCE' do + ctrl = controller_class.new + ctrl.params = { 'compute_resource' => { 'provider' => 'EC2', 'key_path' => '/nonexistent' } }.with_indifferent_access + ctrl.send(:read_key) + + assert_nil ctrl.params[:compute_resource][:password] + assert_equal '/nonexistent', ctrl.params[:compute_resource]['key_path'] + end + end + end + end + end +end diff --git a/test/models/concerns/foreman_google/host_managed_extensions_test.rb b/test/models/concerns/foreman_google/host_managed_extensions_test.rb new file mode 100644 index 0000000..3a2c922 --- /dev/null +++ b/test/models/concerns/foreman_google/host_managed_extensions_test.rb @@ -0,0 +1,46 @@ +require 'test_google_helper' + +module ForemanGoogle + class HostManagedExtensionsTest < GoogleTestCase + let(:host_class) do + Class.new do + include ForemanGoogle::HostManagedExtensions + attr_accessor :vm + end + end + + describe '#ip_addresses' do + it 'returns vm ip_addresses when vm exists' do + vm = mock('GoogleCompute') + vm.expects(:ip_addresses).returns(['1.2.3.4', '10.0.0.1']) + + host = host_class.new + host.vm = vm + assert_equal ['1.2.3.4', '10.0.0.1'], host.ip_addresses + end + + it 'returns empty array when vm is nil' do + host = host_class.new + host.vm = nil + assert_empty host.ip_addresses + end + end + + describe '#vm_ip_address' do + it 'returns vm public IP when vm exists' do + vm = mock('GoogleCompute') + vm.expects(:vm_ip_address).returns('1.2.3.4') + + host = host_class.new + host.vm = vm + assert_equal '1.2.3.4', host.vm_ip_address + end + + it 'returns nil when vm is nil' do + host = host_class.new + host.vm = nil + assert_nil host.vm_ip_address + end + end + end +end diff --git a/test/models/foreman_google/gce_test.rb b/test/models/foreman_google/gce_test.rb index 4dde8c8..c93ce5d 100644 --- a/test/models/foreman_google/gce_test.rb +++ b/test/models/foreman_google/gce_test.rb @@ -2,7 +2,7 @@ module ForemanGoogle class GCETest < GoogleTestCase - subject { ForemanGoogle::GCE.new(zone: 'zone', password: gauth_json) } + subject { ForemanGoogle::GCE.new(zone: 'us-east1-b', password: gauth_json) } let(:service) { mock('GoogleAdapter') } let(:instance_args) do @@ -18,6 +18,142 @@ class GCETest < GoogleTestCase service.stubs(:project_id).returns('project_id') end + describe '#to_label' do + it 'includes name, zone, and provider' do + subject.name = 'my-gce' + expected = 'my-gce (us-east1-b-Google)' + assert_equal expected, subject.to_label + end + end + + describe '#capabilities' do + it 'returns image and new_volume' do + assert_equal %i[image new_volume], subject.capabilities + end + end + + describe '#provided_attributes' do + it 'includes ip mapped to vm_ip_address' do + attrs = subject.provided_attributes + assert_equal :vm_ip_address, attrs[:ip] + end + end + + describe '#user_data_supported?' do + it 'returns true' do + assert subject.user_data_supported? + end + end + + describe '#zone' do + it 'reads from url attribute' do + subject.url = 'us-central1-a' + assert_equal 'us-central1-a', subject.zone + end + end + + describe '#zone=' do + it 'writes to url attribute' do + subject.zone = 'europe-west1-b' + assert_equal 'europe-west1-b', subject.url + end + end + + describe 'validations' do + it 'is valid with name, zone, and password' do + subject.name = 'test-gce' + assert subject.valid?, "Expected subject to be valid, got errors: #{subject.errors.full_messages}" + end + + it 'requires password' do + cr = ForemanGoogle::GCE.new(zone: 'us-east1-b', password: nil) + assert_not cr.valid? + assert_includes cr.errors.attribute_names, :password + end + + it 'requires zone' do + cr = ForemanGoogle::GCE.new(zone: nil, password: gauth_json) + assert_not cr.valid? + assert_includes cr.errors.attribute_names, :zone + end + + it 'requires name' do + cr = ForemanGoogle::GCE.new(name: nil, zone: 'us-east1-b', password: gauth_json) + assert_not cr.valid? + assert_includes cr.errors.attribute_names, :name + end + end + + describe '#zones' do + it 'returns zone names from client' do + zones = [OpenStruct.new(name: 'us-east1-b'), OpenStruct.new(name: 'us-west1-a')] + service.expects(:zones).returns(zones) + assert_equal %w[us-east1-b us-west1-a], subject.zones + end + + it 'is aliased as available_zones' do + zones = [OpenStruct.new(name: 'us-east1-b')] + service.expects(:zones).returns(zones) + assert_equal %w[us-east1-b], subject.available_zones + end + end + + describe '#networks' do + it 'returns network names from client' do + networks = [OpenStruct.new(name: 'default'), OpenStruct.new(name: 'custom')] + service.expects(:networks).returns(networks) + assert_equal %w[default custom], subject.networks + end + end + + describe '#available_networks' do + it 'returns full network objects from client' do + network_objects = [OpenStruct.new(name: 'default')] + service.expects(:networks).returns(network_objects) + assert_equal network_objects, subject.available_networks + end + end + + describe '#machine_types' do + it 'returns machine types for the zone from client' do + types = [OpenStruct.new(name: 'e2-micro')] + service.expects(:machine_types).with('us-east1-b').returns(types) + assert_equal types, subject.machine_types + end + + it 'is aliased as available_flavors' do + types = [OpenStruct.new(name: 'n1-standard-1')] + service.expects(:machine_types).with('us-east1-b').returns(types) + assert_equal types, subject.available_flavors + end + end + + describe '#available_images' do + it 'delegates to client.images with filter' do + images = [OpenStruct.new(name: 'centos-7')] + service.expects(:images).with(filter: nil).returns(images) + assert_equal images, subject.available_images + end + + it 'passes custom filter to client' do + images = [OpenStruct.new(name: 'centos-7')] + service.expects(:images).with(filter: 'name = "centos"').returns(images) + assert_equal images, subject.available_images(filter: 'name = "centos"') + end + end + + describe '#filter_for_images' do + it 'defaults to nil' do + assert_nil subject.filter_for_images + end + end + + describe '#google_project_id' do + it 'delegates to client' do + assert_equal 'project_id', subject.google_project_id + end + end + describe '#find_vm_by_uuid' do it 'does query gce' do instance = OpenStruct.new(**instance_args) @@ -48,11 +184,29 @@ class GCETest < GoogleTestCase end it 'iteration over the vms array' do - subject.vms.each_with_index { |instance, i| assert instance.name, instances[i].name } + subject.vms.each_with_index { |instance, i| assert_equal instances[i].name, instance.name } end it 'all method' do - subject.vms.all.each_with_index { |instance, i| assert instance.name, instances[i].name } + subject.vms.all.each_with_index { |instance, i| assert_equal instances[i].name, instance.name } + end + end + + describe '#new_vm' do + it 'assigns provided attributes' do + vm = subject.new_vm(name: 'test-vm', machine_type: 'e2-micro') + assert_equal 'test-vm', vm.name + assert_equal 'e2-micro', vm.machine_type + end + + it 'converts volumes_attributes nested hash' do + attrs = { + 'name' => 'test-vm', + 'volumes_attributes' => { '0' => { 'size_gb' => '20' } }, + } + vm = subject.new_vm(attrs) + assert_equal 1, vm.volumes.size + assert_equal 20, vm.volumes.first.size_gb end end @@ -65,5 +219,123 @@ class GCETest < GoogleTestCase value { subject.create_vm({ image_id: 0 }) }.must_raise(::Foreman::Exception) end end + + describe '#create_vm success path' do + let(:os_image) { OpenStruct.new(username: 'gce_user', name: 'centos-7') } + let(:gce_image) { OpenStruct.new(id: 42, self_link: 'image-link') } + let(:key_pair_obj) { OpenStruct.new(public: 'ssh-rsa AAAA') } + let(:vm) { mock('GoogleCompute') } + + setup do + service.stubs(:image).returns(gce_image) + service.stubs(:instance) + subject.stubs(:images).returns(stub(find_by: os_image)) + subject.stubs(:key_pair).returns(key_pair_obj) + subject.stubs(:new_vm).returns(vm) + vm.stubs(:hostname).returns('test-vm') + + vm.expects(:create_volumes) + vm.expects(:create_instance) + vm.expects(:set_disk_auto_delete) + end + + it 'creates volumes, instance, and sets auto-delete' do + result = subject.create_vm({ image_id: 42 }) + assert_kind_of ForemanGoogle::GoogleCompute, result + end + end + + describe '#create_vm rollback on error' do + let(:os_image) { OpenStruct.new(username: 'gce_user', name: 'centos-7') } + let(:gce_image) { OpenStruct.new(id: 42, self_link: 'image-link') } + let(:key_pair_obj) { OpenStruct.new(public: 'ssh-rsa AAAA') } + let(:vm) { mock('GoogleCompute') } + + setup do + subject.stubs(:images).returns(stub(find_by: os_image)) + service.stubs(:image).returns(gce_image) + subject.stubs(:key_pair).returns(key_pair_obj) + subject.stubs(:new_vm).returns(vm) + vm.stubs(:create_volumes).raises(::Google::Cloud::Error.new('quota exceeded')) + + vm.expects(:destroy_volumes) + end + + it 'destroys volumes and raises WrappedException' do + assert_raises(Foreman::WrappedException) { subject.create_vm({ image_id: 42 }) } + end + end + + describe '#destroy_vm' do + it 'calls set_disk_auto_delete and delete_instance' do + service.expects(:set_disk_auto_delete).with('us-east1-b', 'vm-uuid') + service.expects(:delete_instance).with('us-east1-b', 'vm-uuid') + subject.destroy_vm('vm-uuid') + end + + it 'returns true when VM not found' do + service.expects(:set_disk_auto_delete).raises(ActiveRecord::RecordNotFound) + assert subject.destroy_vm('missing-vm') + end + end + + describe '#new_volume' do + it 'returns an AttachedDisk with 20 GB default' do + vol = subject.new_volume + assert_kind_of Google::Cloud::Compute::V1::AttachedDisk, vol + assert_equal 20, vol.disk_size_gb + end + + it 'merges provided attributes' do + vol = subject.new_volume(device_name: 'boot-disk') + assert_equal 'boot-disk', vol.device_name + assert_equal 20, vol.disk_size_gb + end + end + + describe '#console' do + let(:vm) { mock('GoogleCompute') } + + setup do + subject.stubs(:find_vm_by_uuid).returns(vm) + end + + it 'returns serial output when VM is ready' do + vm.stubs(:ready?).returns(true) + vm.stubs(:serial_port_output).returns('boot log...') + vm.stubs(:name).returns('test-vm') + + result = subject.console('test-vm') + assert_equal 'boot log...', result['output'] + assert_equal 'log', result[:type] + assert_equal 'test-vm', result[:name] + assert result['timestamp'].is_a?(Time) + end + + it 'raises when VM is not ready' do + vm.stubs(:ready?).returns(false) + assert_raises(::Foreman::Exception) { subject.console('test-vm') } + end + end + + describe '#associated_host' do + it 'calls associate_by with ip addresses' do + vm = OpenStruct.new(public_ip_address: '1.2.3.4', private_ip_address: '10.0.0.1') + subject.expects(:associate_by).with('ip', ['1.2.3.4', '10.0.0.1']) + subject.associated_host(vm) + end + end + + describe '#vm_ready' do + it 'polls until vm is ready' do + vm = mock('GoogleCompute') + vm.stubs(:reload).returns(vm) + vm.stubs(:ready?).returns(false, true) + vm.stubs(:wait_for).yields.returns({ duration: 2 }) + + result = subject.vm_ready(vm) + assert_equal({ duration: 2 }, result) + end + end end end diff --git a/test/models/foreman_google/google_compute_test.rb b/test/models/foreman_google/google_compute_test.rb index bfcd714..6f29867 100644 --- a/test/models/foreman_google/google_compute_test.rb +++ b/test/models/foreman_google/google_compute_test.rb @@ -194,20 +194,209 @@ class GoogleComputeTest < GoogleTestCase it '@network' do cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone) - assert cr.network, 'default' + assert_equal 'default', cr.network cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, args: { network: 'my-network' }) - assert cr.network, 'my-network' + assert_equal 'my-network', cr.network end it '@zone' do cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone) - assert zone, cr.zone + assert_equal zone, cr.zone end it '@zone_name' do cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, instance: instance) - assert zone, cr.zone_name + assert_equal zone, cr.zone_name + end + + describe '#status' do + it 'returns status when persisted' do + instance.status = 'RUNNING' + client.expects(:instance).with(zone, identity).returns(instance) + assert_equal 'RUNNING', subject.status + end + + it 'returns false when not persisted' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone) + assert_not cr.status + end + end + + describe '#state' do + it 'is an alias for status' do + instance.status = 'TERMINATED' + client.expects(:instance).with(zone, identity).returns(instance) + assert_equal subject.status, subject.state + end + end + + describe '#start' do + it 'delegates to client.start when persisted' do + client.stubs(:instance).with(zone, identity).returns(instance) + client.expects(:start).with(zone, identity).returns(true) + subject.start + end + + it 'raises when not persisted' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone) + assert_raises(Foreman::Exception) { cr.start } + end + end + + describe '#stop' do + it 'delegates to client.stop when persisted' do + client.stubs(:instance).with(zone, identity).returns(instance) + client.expects(:stop).with(zone, identity).returns(true) + subject.stop + end + + it 'raises when not persisted' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone) + assert_raises(Foreman::Exception) { cr.stop } + end + end + + describe '#to_s' do + it 'returns the name' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, args: { name: 'my-vm' }) + assert_equal 'my-vm', cr.to_s + end + end + + describe '#interfaces' do + it 'returns network_interfaces' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone) + assert_equal cr.network_interfaces, cr.interfaces + end + end + + describe '#vm_description' do + it 'delegates to pretty_machine_type' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, instance: instance) + assert_equal cr.pretty_machine_type, cr.vm_description + end + end + + describe '#vm_ip_address - no network interfaces' do + it 'returns nil' do + empty_instance = OpenStruct.new( + name: 'instance', network_interfaces: [], + creation_timestamp: Time.zone.now, zone: zone, + machine_type: 'machineTypes/e2-micro' + ) + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, instance: empty_instance) + assert_nil cr.vm_ip_address + end + end + + describe '#private_ip_address - no network interfaces' do + it 'returns nil' do + empty_instance = OpenStruct.new( + name: 'instance', network_interfaces: [], + creation_timestamp: Time.zone.now, zone: zone, + machine_type: 'machineTypes/e2-micro' + ) + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, instance: empty_instance) + assert_nil cr.private_ip_address + end + end + + describe '#pretty_image_name - no disks' do + it 'returns nil' do + instance.disks = [] + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, instance: instance) + assert_nil cr.pretty_image_name + end + end + + describe '#ip_addresses' do + it 'returns public and private IP' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, instance: instance) + assert_equal ['1.2.3.4', '10.10.10.23'], cr.ip_addresses + end + end + + describe '#serial_port_output' do + it 'delegates to client' do + client.stubs(:instance).with(zone, identity).returns(instance) + client.expects(:serial_port_output).with(zone, identity).returns(OpenStruct.new(contents: 'boot log')) + assert_equal 'boot log', subject.serial_port_output + end + + it 'returns nil when client returns nil' do + client.stubs(:instance).with(zone, identity).returns(instance) + client.expects(:serial_port_output).with(zone, identity).returns(nil) + assert_nil subject.serial_port_output + end + end + + describe '#volumes_attributes=' do + it 'is a no-op' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone) + cr.volumes_attributes = { '0' => { size_gb: 10 } } + assert cr.volumes.any? + end + end + + describe '#reload - without identity' do + it 'returns nil' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone) + assert_nil cr.reload + end + end + + describe '#create_volumes' do + it 'inserts disks and waits for READY' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, args: { volumes: [{ size_gb: '20' }] }) + + client.expects(:insert_disk).with(zone, cr.volumes.first.insert_attrs) + client.expects(:disk).with(zone, cr.volumes.first.device_name).returns(OpenStruct.new(status: 'READY')) + client.expects(:wait_for).yields.returns({ duration: 1 }) + + cr.create_volumes + end + end + + describe '#destroy_volumes' do + it 'deletes each volume disk' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, args: { volumes: [{ size_gb: '20' }] }) + client.expects(:delete_disk).with(zone, cr.volumes.first.device_name) + cr.destroy_volumes + end + end + + describe '#create_instance' do + it 'calls insert_instance via ComputeAttributes' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, args: { name: 'test-vm', machine_type: 'e2-micro' }) + client.expects(:insert_instance).with(zone, has_key(:name)) + cr.create_instance + end + end + + describe '#set_disk_auto_delete' do + it 'delegates to client' do + args = { name: 'my-vm' } + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, args: args) + client.expects(:set_disk_auto_delete).with(zone, 'my-vm') + cr.set_disk_auto_delete + end + end + + describe '#wait_for' do + it 'delegates to client.wait_for' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone) + client.expects(:wait_for).yields.returns({ duration: 0 }) + result = cr.wait_for { true } + assert_equal({ duration: 0 }, result) + end + end + + describe '#public_ip_address' do + it 'is an alias for vm_ip_address' do + cr = ForemanGoogle::GoogleCompute.new(client: client, zone: zone, instance: instance) + assert_equal cr.vm_ip_address, cr.public_ip_address + end end end end diff --git a/test/unit/foreman_google/google_compute_adapter_test.rb b/test/unit/foreman_google/google_compute_adapter_test.rb index 3cf68b4..7171d83 100644 --- a/test/unit/foreman_google/google_compute_adapter_test.rb +++ b/test/unit/foreman_google/google_compute_adapter_test.rb @@ -131,7 +131,7 @@ class GoogleComputeAdapterTest < GoogleTestCase result = subject.insert_instance('us-east1-b', args) - assert 'insert', result.operation.operation_type + assert_equal 'insert', result.operation.operation_type assert_includes result.operation.target_link, 'foreman-test-google' end @@ -156,14 +156,14 @@ class GoogleComputeAdapterTest < GoogleTestCase stub_request(:post, 'https://compute.googleapis.com/compute/v1/projects/coastal-haven-123456/zones/us-east1-b/instances/instance_name/start') .to_return(status: 200, body: File.read(File.join(__dir__, '..', '..', 'fixtures', 'instance_start.json'))) result = subject.start('us-east1-b', 'instance_name') - assert 'start', result.operation.operation_type + assert_equal 'start', result.operation.operation_type end it '#stop' do stub_request(:post, 'https://compute.googleapis.com/compute/v1/projects/coastal-haven-123456/zones/us-east1-b/instances/instance_name/stop') .to_return(status: 200, body: File.read(File.join(__dir__, '..', '..', 'fixtures', 'instance_stop.json'))) result = subject.stop('us-east1-b', 'instance_name') - assert 'stop', result.operation.operation_type + assert_equal 'stop', result.operation.operation_type end it '#set_disk_auto_delete' do @@ -174,7 +174,7 @@ class GoogleComputeAdapterTest < GoogleTestCase .to_return(status: 200, body: File.read(File.join(__dir__, '..', '..', 'fixtures', 'instance_set_disk_auto_delete.json'))) result = subject.set_disk_auto_delete('us-east1-b', 'instance_name') - assert 'device-1', result[0].source + assert_equal 'instance-1', result[0].device_name assert result[0].auto_delete end end @@ -186,14 +186,14 @@ class GoogleComputeAdapterTest < GoogleTestCase result = subject.insert_disk('us-east1-b', { name: 'foreman-disk1', size_gb: 23 }) assert_includes result.operation.target_link, 'foreman-disk1' - assert 'insert', result.operation.operation_type + assert_equal 'insert', result.operation.operation_type end it '#get' do stub_request(:get, 'https://compute.googleapis.com/compute/v1/projects/coastal-haven-123456/zones/us-east1-b/disks/foreman-disk1') .to_return(status: 200, body: File.read(File.join(__dir__, '..', '..', 'fixtures', 'disks_get.json'))) result = subject.disk('us-east1-b', 'foreman-disk1') - assert 'foreman-disk1', result.name + assert_equal 'foreman-disk1', result.name end it '#delete' do @@ -202,7 +202,59 @@ class GoogleComputeAdapterTest < GoogleTestCase result = subject.delete_disk('us-east1-b', 'foreman-disk1') assert_includes result.operation.target_link, 'foreman-disk1' - assert 'delete', result.operation.operation_type + assert_equal 'delete', result.operation.operation_type + end + end + + describe '#delete_instance' do + it 'deletes an instance' do + stub_request(:delete, 'https://compute.googleapis.com/compute/v1/projects/coastal-haven-123456/zones/us-east1-b/instances/instance_name') + .to_return(status: 200, body: File.read(File.join(__dir__, '..', '..', 'fixtures', 'instance_stop.json'))) + result = subject.delete_instance('us-east1-b', 'instance_name') + assert_equal 'stop', result.operation.operation_type + end + end + + describe '#image' do + setup do + stub_request(:get, 'https://compute.googleapis.com/compute/v1/projects/coastal-haven-123456/global/images') + .to_return(body: File.read(File.join(__dir__, '..', '..', 'fixtures', 'images_coastal.json'))) + end + + it 'finds image by numeric id' do + subject.stub(:all_projects, []) do + result = subject.image(1) + assert_equal 'coastal-image', result.name + end + end + + it 'returns nil when image not found' do + subject.stub(:all_projects, []) do + assert_nil subject.image(999) + end + end + end + + describe '#serial_port_output' do + it 'returns serial port output' do + body = { contents: 'boot log output', next: 12_345, start: 0, selfLink: 'self-link' }.to_json + stub_request(:get, 'https://compute.googleapis.com/compute/v1/projects/coastal-haven-123456/zones/us-east1-b/instances/instance_name/serialPort') + .to_return(status: 200, body: body) + result = subject.serial_port_output('us-east1-b', 'instance_name') + assert_equal 'boot log output', result.contents + end + end + + describe '#project_id' do + it 'returns the project_id from auth JSON' do + assert_equal 'coastal-haven-123456', subject.project_id + end + end + + describe '#wait_for' do + it 'returns immediately when block yields true' do + result = subject.wait_for { true } + assert result[:duration] <= 1 end end end diff --git a/test/unit/google_cloud_compute/compute_attributes_test.rb b/test/unit/google_cloud_compute/compute_attributes_test.rb new file mode 100644 index 0000000..1724b11 --- /dev/null +++ b/test/unit/google_cloud_compute/compute_attributes_test.rb @@ -0,0 +1,235 @@ +require 'test_google_helper' + +module GoogleCloudCompute + class ComputeAttributesTest < GoogleTestCase + let(:client) { mock('GoogleAdapter') } + subject { GoogleCloudCompute::ComputeAttributes.new(client) } + + setup do + client.stubs(:project_id).returns('my-project') + end + + describe '#for_new' do + describe 'name handling' do + it 'generates a default name with foreman- prefix when name is nil' do + result = subject.for_new({}) + assert_match(/\Aforeman-\d+\z/, result[:name]) + assert_equal result[:name], result[:hostname] + end + + it 'parameterizes the provided name' do + result = subject.for_new(name: 'My VM Name') + assert_equal 'my-vm-name', result[:name] + assert_equal 'my-vm-name', result[:hostname] + end + end + + describe 'network handling' do + it 'defaults to default network' do + result = subject.for_new({}) + assert_equal 'default', result[:network] + end + + it 'uses custom network name' do + result = subject.for_new(network: 'custom-net') + assert_equal 'custom-net', result[:network] + end + + it 'builds full URL without external IP' do + result = subject.for_new(network: 'default') + expected_url = 'https://compute.googleapis.com/compute/v1/projects/my-project/global/networks/default' + assert_equal expected_url, result[:network_interfaces][0][:network] + end + + it 'adds NAT access config with external IP' do + result = subject.for_new(associate_external_ip: '1') + nic = result[:network_interfaces][0] + assert_equal 'global/networks/default', nic[:network] + assert_equal [{ name: 'External NAT', type: 'ONE_TO_ONE_NAT' }], nic[:access_configs] + end + + it 'uses custom network interfaces with external IP' do + result = subject.for_new( + associate_external_ip: '1', + network_interfaces: [{ network: 'global/networks/my-net' }] + ) + nic = result[:network_interfaces][0] + assert_equal 'global/networks/my-net', nic[:network] + assert_equal [{ name: 'External NAT', type: 'ONE_TO_ONE_NAT' }], nic[:access_configs] + end + end + + describe 'associate_external_ip boolean casting' do + it 'casts string 1 to true' do + result = subject.for_new(associate_external_ip: '1') + assert result[:associate_external_ip] + end + + it 'casts string 0 to false' do + result = subject.for_new(associate_external_ip: '0') + assert_not result[:associate_external_ip] + end + + it 'defaults to false when absent' do + result = subject.for_new({}) + assert_not result[:associate_external_ip] + end + end + + describe 'volume construction' do + it 'creates a default 20 GB disk when no volumes' do + result = subject.for_new({}) + assert_equal 1, result[:volumes].length + assert_equal 20, result[:volumes].first.disk_size_gb + end + + it 'creates volumes with custom size' do + result = subject.for_new(volumes: [{ size_gb: '50' }]) + assert_equal 50, result[:volumes].first.disk_size_gb + end + + it 'sets source image on first volume when image_id is provided' do + image = OpenStruct.new(id: 1, self_link: 'projects/my-project/global/images/centos-7') + client.stubs(:image).with(1).returns(image) + + result = subject.for_new( + volumes: [{ size_gb: '20' }, { size_gb: '30' }], + image_id: '1' + ) + + assert_equal 'projects/my-project/global/images/centos-7', result[:volumes][0].source + assert_empty result[:volumes][1].source + end + + it 'names disks sequentially' do + result = subject.for_new(name: 'test-vm', volumes: [{ size_gb: '20' }, { size_gb: '30' }]) + assert_equal 'test-vm-disk1', result[:volumes][0].device_name + assert_equal 'test-vm-disk2', result[:volumes][1].device_name + end + + it 'accepts disk_size_gb as an alternative key' do + result = subject.for_new(volumes: [{ disk_size_gb: '40' }]) + assert_equal 40, result[:volumes].first.disk_size_gb + end + end + + describe 'metadata construction' do + it 'includes ssh-keys' do + result = subject.for_new(username: 'gce_user', public_key: 'ssh-rsa AAAA') + ssh_item = result[:metadata][:items].find { |i| i[:key] == 'ssh-keys' } + assert_equal 'gce_user:ssh-rsa AAAA', ssh_item[:value] + end + + it 'includes user-data when present' do + result = subject.for_new(username: 'user', public_key: 'key', user_data: '#cloud-config') + ud_item = result[:metadata][:items].find { |i| i[:key] == 'user-data' } + assert_equal '#cloud-config', ud_item[:value] + end + + it 'omits user-data when blank' do + result = subject.for_new(username: 'user', public_key: 'key') + keys = result[:metadata][:items].map { |i| i[:key] } + assert_not_includes keys, 'user-data' + end + end + + it 'passes through machine_type and image_id' do + result = subject.for_new(machine_type: 'e2-micro', image_id: '42') + assert_equal 'e2-micro', result[:machine_type] + assert_equal '42', result[:image_id] + end + end + + describe '#for_create' do + let(:instance) do + vol1 = Google::Cloud::Compute::V1::AttachedDisk.new(device_name: 'vm-disk1') + vol2 = Google::Cloud::Compute::V1::AttachedDisk.new(device_name: 'vm-disk2') + OpenStruct.new( + name: 'test-vm', + zone: 'us-east1-b', + machine_type: 'e2-micro', + volumes: [vol1, vol2], + network_interfaces: [{ network: 'global/networks/default' }], + metadata: { items: [{ key: 'ssh-keys', value: 'user:key' }] } + ) + end + + it 'builds machine_type URL from zone' do + result = subject.for_create(instance) + assert_equal 'zones/us-east1-b/machineTypes/e2-micro', result[:machine_type] + end + + it 'builds disk source paths' do + result = subject.for_create(instance) + assert_equal 'zones/us-east1-b/disks/vm-disk1', result[:disks][0][:source] + assert_equal 'zones/us-east1-b/disks/vm-disk2', result[:disks][1][:source] + end + + it 'marks first disk as boot' do + result = subject.for_create(instance) + assert result[:disks][0][:boot] + assert_not result[:disks][1][:boot] + end + + it 'includes network_interfaces and metadata' do + result = subject.for_create(instance) + assert_equal instance.network_interfaces, result[:network_interfaces] + assert_equal instance.metadata, result[:metadata] + end + + it 'includes name' do + result = subject.for_create(instance) + assert_equal 'test-vm', result[:name] + end + end + + describe '#for_instance' do + let(:nics) do + [OpenStruct.new(network: 'projects/my-project/global/networks/custom', network_i_p: '10.0.0.1')] + end + let(:instance) do + OpenStruct.new( + name: 'existing-vm', + creation_timestamp: Time.zone.parse('2024-01-15 10:00:00'), + zone: 'projects/my-project/zones/us-west1-a', + machine_type: 'zones/us-west1-a/machineTypes/n1-standard-1', + network_interfaces: nics, + disks: [OpenStruct.new(device_name: 'disk1')], + metadata: { items: [] } + ) + end + + it 'extracts zone_name from full zone URL' do + result = subject.for_instance(instance) + assert_equal 'us-west1-a', result[:zone_name] + end + + it 'extracts network name from full URL' do + result = subject.for_instance(instance) + assert_equal 'custom', result[:network] + end + + it 'converts creation_timestamp to DateTime' do + result = subject.for_instance(instance) + assert_kind_of DateTime, result[:creation_timestamp] + end + + it 'passes through name and hostname' do + result = subject.for_instance(instance) + assert_equal 'existing-vm', result[:name] + assert_equal 'existing-vm', result[:hostname] + end + + it 'passes through disks as volumes and metadata' do + result = subject.for_instance(instance) + assert_equal instance.disks, result[:volumes] + assert_equal instance.metadata, result[:metadata] + end + + it 'passes through machine_type' do + result = subject.for_instance(instance) + assert_equal instance.machine_type, result[:machine_type] + end + end + end +end diff --git a/test/unit/google_cloud_compute/compute_collection_test.rb b/test/unit/google_cloud_compute/compute_collection_test.rb new file mode 100644 index 0000000..461d997 --- /dev/null +++ b/test/unit/google_cloud_compute/compute_collection_test.rb @@ -0,0 +1,78 @@ +require 'test_google_helper' + +module GoogleCloudCompute + class ComputeCollectionTest < GoogleTestCase + let(:client) { mock('GoogleAdapter') } + let(:zone) { 'us-east1-b' } + + setup do + client.stubs(:project_id).returns('project_id') + end + + describe 'with instances' do + let(:instances) do + nics = [OpenStruct.new(access_configs: [OpenStruct.new(nat_i_p: '1.2.3.4')], + network: 'projects/project_id/global/networks/default', network_i_p: '10.0.0.1')] + [ + OpenStruct.new(id: 1, name: 'vm-1', network_interfaces: nics, + creation_timestamp: Time.zone.now, zone: zone, machine_type: 'machineTypes/e2-micro'), + OpenStruct.new(id: 2, name: 'vm-2', network_interfaces: nics, + creation_timestamp: Time.zone.now, zone: zone, machine_type: 'machineTypes/e2-small'), + ] + end + + setup do + client.stubs(:instances).with(zone).returns(instances) + end + + it 'wraps instances as GoogleCompute objects' do + collection = GoogleCloudCompute::ComputeCollection.new(client, zone) + collection.each do |vm| + assert_kind_of ForemanGoogle::GoogleCompute, vm + end + end + + it 'iterates via each' do + collection = GoogleCloudCompute::ComputeCollection.new(client, zone) + names = collection.map(&:name) + assert_equal %w[vm-1 vm-2], names + end + + it 'returns all VMs via all' do + collection = GoogleCloudCompute::ComputeCollection.new(client, zone) + assert_equal 2, collection.all.length + end + + it 'is Enumerable' do + collection = GoogleCloudCompute::ComputeCollection.new(client, zone) + assert_kind_of Enumerable, collection + assert_equal 2, collection.count + end + end + + describe 'with empty instance list' do + setup do + client.stubs(:instances).with(zone).returns([]) + end + + it 'returns empty collection' do + collection = GoogleCloudCompute::ComputeCollection.new(client, zone) + assert_empty collection.all + end + + it 'iterates zero times' do + collection = GoogleCloudCompute::ComputeCollection.new(client, zone) + count = 0 + collection.each { count += 1 } + assert_equal 0, count + end + end + + describe 'with attrs' do + it 'passes attrs to client.instances' do + client.expects(:instances).with(zone, filter: 'name = "test"').returns([]) + GoogleCloudCompute::ComputeCollection.new(client, zone, filter: 'name = "test"') + end + end + end +end diff --git a/test/unit/google_extensions/attached_disk_test.rb b/test/unit/google_extensions/attached_disk_test.rb index 52a3cfa..922dce0 100644 --- a/test/unit/google_extensions/attached_disk_test.rb +++ b/test/unit/google_extensions/attached_disk_test.rb @@ -12,6 +12,41 @@ class AttachedDiskTest < GoogleTestCase disk = Google::Cloud::Compute::V1::AttachedDisk.new assert_empty disk.insert_attrs[:source_image] end + + it 'includes device_name as name' do + disk = Google::Cloud::Compute::V1::AttachedDisk.new(device_name: 'boot-disk', disk_size_gb: 30) + attrs = disk.insert_attrs + assert_equal 'boot-disk', attrs[:name] + assert_equal 30, attrs[:size_gb] + end + end + + describe '#size_gb' do + it 'returns disk_size_gb' do + disk = Google::Cloud::Compute::V1::AttachedDisk.new(disk_size_gb: 50) + assert_equal 50, disk.size_gb + end + end + + describe '#persisted?' do + it 'returns nil' do + disk = Google::Cloud::Compute::V1::AttachedDisk.new + assert_nil disk.persisted? + end + end + + describe '#id' do + it 'returns nil' do + disk = Google::Cloud::Compute::V1::AttachedDisk.new + assert_nil disk.id + end + end + + describe '#_delete' do + it 'returns nil' do + disk = Google::Cloud::Compute::V1::AttachedDisk.new + assert_nil disk._delete + end end end end