Adding the cloud DNS API library and related methods

we setup DNS when a VM is created and tear it down when a VM is deleted
the DNS zone should exist already and is referenced by a provider setting
the dns zone is also set in order to use it for vm_ready? instead of the global
domain
instances have a label that identifies which project they belong to, so
it can be used for FW rules
This commit is contained in:
Samuel Beaulieu 2021-12-29 08:13:32 -06:00
parent f6ec318b2d
commit daa55fe5b8
No known key found for this signature in database
GPG key ID: 12030F74136D0F34
9 changed files with 167 additions and 12 deletions

1
.gitignore vendored
View file

@ -8,3 +8,4 @@ Gemfile.local
results.xml results.xml
/vmpooler.yaml /vmpooler.yaml
.idea .idea
*.json

View file

@ -14,6 +14,10 @@ GCE authorization is handled via a service account (or personal account) private
1. GOOGLE_APPLICATION_CREDENTIALS environment variable eg GOOGLE_APPLICATION_CREDENTIALS=/my/home/directory/my_account_key.json 1. GOOGLE_APPLICATION_CREDENTIALS environment variable eg GOOGLE_APPLICATION_CREDENTIALS=/my/home/directory/my_account_key.json
### DNS
DNS is integrated via Google's CloudDNS service. To enable a CloudDNS zone name must be provided in the config (see the example yaml file dns_zone_resource_name)
An A record is then created in that zone upon instance creation with the VM's internal IP, and deleted when the instance is destroyed.
### Labels ### Labels
This provider adds labels to all resources that are managed This provider adds labels to all resources that are managed
@ -27,6 +31,13 @@ This provider adds labels to all resources that are managed
Also see the usage of vmpooler's optional purge_unconfigured_resources, which is used to delete any resource found that Also see the usage of vmpooler's optional purge_unconfigured_resources, which is used to delete any resource found that
do not have the pool label, and can be configured to allow a specific list of unconfigured pool names. do not have the pool label, and can be configured to allow a specific list of unconfigured pool names.
### Pre-requisite
- A service account needs to be created and a private json key generated (see usage section)
- The service account needs given permissions to the project (broad permissions would be compute v1 admin and dns admin). A yaml file is provided that lists the least-privilege permissions needed
- if using DNS, a DNS zone needs to be created
## License ## License
vmpooler-provider-gce is distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). See the [LICENSE](LICENSE) file for more details. vmpooler-provider-gce is distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). See the [LICENSE](LICENSE) file for more details.

View file

@ -2,6 +2,7 @@
require 'googleauth' require 'googleauth'
require 'google/apis/compute_v1' require 'google/apis/compute_v1'
require "google/cloud/dns"
require 'bigdecimal' require 'bigdecimal'
require 'bigdecimal/util' require 'bigdecimal/util'
require 'vmpooler/providers/base' require 'vmpooler/providers/base'
@ -44,6 +45,7 @@ module Vmpooler
{ connection: new_conn } { connection: new_conn }
end end
@redis = redis_connection_pool @redis = redis_connection_pool
@dns = Google::Cloud::Dns.new(project_id: project)
end end
# name of the provider class # name of the provider class
@ -66,6 +68,10 @@ module Vmpooler
provider_config['network_name'] provider_config['network_name']
end end
def subnetwork_name(pool_name)
return pool_config(pool_name)['subnetwork_name'] if pool_config(pool_name)['subnetwork_name']
end
# main configuration options, overridable for each pool # main configuration options, overridable for each pool
def zone(pool_name) def zone(pool_name)
return pool_config(pool_name)['zone'] if pool_config(pool_name)['zone'] return pool_config(pool_name)['zone'] if pool_config(pool_name)['zone']
@ -77,6 +83,14 @@ module Vmpooler
return provider_config['machine_type'] if provider_config['machine_type'] return provider_config['machine_type'] if provider_config['machine_type']
end end
def dns_zone
provider_config['dns_zone']
end
def dns_zone_resource_name
provider_config['dns_zone_resource_name']
end
# Base methods that are implemented: # Base methods that are implemented:
# vms_in_pool lists all the VM names in a pool, which is based on the VMs # vms_in_pool lists all the VM names in a pool, which is based on the VMs
@ -162,6 +176,7 @@ module Vmpooler
network_interfaces = Google::Apis::ComputeV1::NetworkInterface.new( network_interfaces = Google::Apis::ComputeV1::NetworkInterface.new(
network: network_name network: network_name
) )
network_interfaces.subnetwork=subnetwork_name(pool_name) if subnetwork_name(pool_name)
init_params = { init_params = {
source_image: pool['template'], # The source image to create this disk. source_image: pool['template'], # The source image to create this disk.
labels: { 'vm' => new_vmname, 'pool' => pool_name }, labels: { 'vm' => new_vmname, 'pool' => pool_name },
@ -172,19 +187,23 @@ module Vmpooler
boot: true, boot: true,
initialize_params: Google::Apis::ComputeV1::AttachedDiskInitializeParams.new(init_params) initialize_params: Google::Apis::ComputeV1::AttachedDiskInitializeParams.new(init_params)
) )
# Assume all pool config is valid i.e. not missing # Assume all pool config is valid i.e. not missing
client = ::Google::Apis::ComputeV1::Instance.new( client = ::Google::Apis::ComputeV1::Instance.new(
name: new_vmname, name: new_vmname,
machine_type: pool['machine_type'], machine_type: pool['machine_type'],
disks: [disk], disks: [disk],
network_interfaces: [network_interfaces], network_interfaces: [network_interfaces],
labels: { 'vm' => new_vmname, 'pool' => pool_name } labels: { 'vm' => new_vmname, 'pool' => pool_name, project => nil }
) )
=begin TODO: Maybe this will be needed to set the hostname (usually internal DNS name but in opur case for some reason its nil)
given_hostname = "#{new_vmname}.#{dns_zone}"
client.hostname = given_hostname if given_hostname
=end
debug_logger('trigger insert_instance') debug_logger('trigger insert_instance')
result = connection.insert_instance(project, zone(pool_name), client) result = connection.insert_instance(project, zone(pool_name), client)
wait_for_operation(project, pool_name, result) wait_for_operation(project, pool_name, result)
get_vm(pool_name, new_vmname) created_instance = get_vm(pool_name, new_vmname)
dns_setup(created_instance)
end end
# create_disk creates an additional disk for an existing VM. It will name the new # create_disk creates an additional disk for an existing VM. It will name the new
@ -398,8 +417,10 @@ module Vmpooler
unless deleted unless deleted
debug_logger("trigger delete_instance #{vm_name}") debug_logger("trigger delete_instance #{vm_name}")
vm_hash = get_vm(pool_name, vm_name)
result = connection.delete_instance(project, zone(pool_name), vm_name) result = connection.delete_instance(project, zone(pool_name), vm_name)
wait_for_operation(project, pool_name, result, 10) wait_for_operation(project, pool_name, result, 10)
dns_teardown(vm_hash)
end end
# list and delete any leftover disk, for instance if they were detached from the instance # list and delete any leftover disk, for instance if they were detached from the instance
@ -437,7 +458,7 @@ module Vmpooler
def vm_ready?(_pool_name, vm_name) def vm_ready?(_pool_name, vm_name)
begin begin
# TODO: we could use a healthcheck resource attached to instance # TODO: we could use a healthcheck resource attached to instance
open_socket(vm_name, global_config[:config]['domain']) open_socket(vm_name, dns_zone || global_config[:config]['domain'])
rescue StandardError => _e rescue StandardError => _e
return false return false
end end
@ -469,6 +490,9 @@ module Vmpooler
debug_logger("trigger async delete_instance #{vm.name}") debug_logger("trigger async delete_instance #{vm.name}")
result = connection.delete_instance(project, zone, vm.name) result = connection.delete_instance(project, zone, vm.name)
vm_pool = vm.labels&.key?('pool') ? vm.labels['pool'] : nil
existing_vm = generate_vm_hash(vm, vm_pool)
dns_teardown(existing_vm)
result_list << result result_list << result
end end
# now check they are done # now check they are done
@ -529,6 +553,27 @@ module Vmpooler
# END BASE METHODS # END BASE METHODS
def dns_setup(created_instance)
zone = @dns.zone dns_zone_resource_name if dns_zone_resource_name
if zone && created_instance
name = created_instance['name']
change = zone.add name, "A", 60, [created_instance['ip']]
debug_logger("#{change.id} - #{change.started_at} - #{change.status}") if change
end
# TODO: should we catch Google::Cloud::AlreadyExistsError that is thrown when it already exist?
# and then delete and recreate?
# eg the error is Google::Cloud::AlreadyExistsError: alreadyExists: The resource 'entity.change.additions[0]' named 'instance-8.test.vmpooler.puppet.net. (A)' already exists
end
def dns_teardown(created_instance)
zone = @dns.zone dns_zone_resource_name if dns_zone_resource_name
if zone && created_instance
name = created_instance['name']
change = zone.remove name, "A"
debug_logger("#{change.id} - #{change.started_at} - #{change.status}") if change
end
end
def should_be_ignored(item, allowlist) def should_be_ignored(item, allowlist)
return false if allowlist.nil? return false if allowlist.nil?
@ -565,7 +610,7 @@ module Vmpooler
if result.error # unsure what kind of error can be stored here if result.error # unsure what kind of error can be stored here
error_message = '' error_message = ''
# array of errors, combine them all # array of errors, combine them all
result.error.each do |error| result.error.errors.each do |error|
error_message = "#{error_message} #{error.code}:#{error.message}" error_message = "#{error_message} #{error.code}:#{error.message}"
end end
raise "Operation: #{result.description} failed with error: #{error_message}" raise "Operation: #{result.description} failed with error: #{error_message}"
@ -606,7 +651,8 @@ module Vmpooler
'zone' => vm_object.zone, 'zone' => vm_object.zone,
'machine_type' => vm_object.machine_type, 'machine_type' => vm_object.machine_type,
'labels' => vm_object.labels, 'labels' => vm_object.labels,
'label_fingerprint' => vm_object.label_fingerprint 'label_fingerprint' => vm_object.label_fingerprint,
'ip' => vm_object.network_interfaces.first.network_ip
# 'powerstate' => powerstate # 'powerstate' => powerstate
} }
end end

View file

@ -0,0 +1,38 @@
title: Custom vmpooler provider
description: for the vmpooler provider
stage: GA
includedPermissions:
- compute.disks.create
- compute.disks.createSnapshot
- compute.disks.delete
- compute.disks.get
- compute.disks.list
- compute.disks.setLabels
- compute.disks.use
- compute.instances.attachDisk
- compute.instances.create
- compute.instances.delete
- compute.instances.detachDisk
- compute.instances.get
- compute.instances.list
- compute.instances.setLabels
- compute.instances.start
- compute.instances.stop
- compute.snapshots.create
- compute.snapshots.delete
- compute.snapshots.get
- compute.snapshots.list
- compute.snapshots.setLabels
- compute.snapshots.useReadOnly
- compute.subnetworks.use
- compute.zoneOperations.get
- dns.changes.create
- dns.changes.get
- dns.changes.list
- dns.managedZones.get
- dns.managedZones.list
- dns.resourceRecordSets.create
- dns.resourceRecordSets.update
- dns.resourceRecordSets.delete
- dns.resourceRecordSets.get
- dns.resourceRecordSets.list

View file

@ -0,0 +1,5 @@
#!/bin/bash
#set your project name in GCE here
project_id="vmpooler-test"
#this creates a custom role, that should then be applied to a service account used to run the API requests.
gcloud iam roles create Customvmpoolerprovider --project=$project-id --file=GCE_custom_role_for_SA.yaml

View file

@ -11,6 +11,7 @@ describe 'Vmpooler::PoolManager::Provider::Gce' do
let(:metrics) { Vmpooler::Metrics::DummyStatsd.new } let(:metrics) { Vmpooler::Metrics::DummyStatsd.new }
let(:poolname) { 'debian-9' } let(:poolname) { 'debian-9' }
let(:provider_options) { { 'param' => 'value' } } let(:provider_options) { { 'param' => 'value' } }
# let(:project) { 'vmpooler-test' }
let(:project) { 'dio-samuel-dev' } let(:project) { 'dio-samuel-dev' }
let(:zone) { 'us-west1-b' } let(:zone) { 'us-west1-b' }
let(:config) { YAML.load(<<-EOT let(:config) { YAML.load(<<-EOT
@ -23,7 +24,10 @@ describe 'Vmpooler::PoolManager::Provider::Gce' do
connection_pool_timeout: 1 connection_pool_timeout: 1
project: '#{project}' project: '#{project}'
zone: '#{zone}' zone: '#{zone}'
network_name: 'global/networks/default' network_name: global/networks/default
# network_name: 'projects/itsysopsnetworking/global/networks/shared1'
dns_zone_resource_name: 'example-com'
dns_zone: 'example.com'
:pools: :pools:
- name: '#{poolname}' - name: '#{poolname}'
alias: [ 'mockpool' ] alias: [ 'mockpool' ]
@ -32,13 +36,13 @@ describe 'Vmpooler::PoolManager::Provider::Gce' do
timeout: 10 timeout: 10
ready_ttl: 1440 ready_ttl: 1440
provider: 'gce' provider: 'gce'
network_name: 'default' # subnetwork_name: 'projects/itsysopsnetworking/regions/us-west1/subnetworks/vmpooler-test'
machine_type: 'zones/#{zone}/machineTypes/e2-micro' machine_type: 'zones/#{zone}/machineTypes/e2-micro'
EOT EOT
) )
} }
let(:vmname) { 'vm16' } let(:vmname) { 'vm17' }
let(:connection) { MockComputeServiceConnection.new } let(:connection) { MockComputeServiceConnection.new }
let(:redis_connection_pool) do let(:redis_connection_pool) do
Vmpooler::PoolManager::GenericConnectionPool.new( Vmpooler::PoolManager::GenericConnectionPool.new(
@ -80,11 +84,44 @@ EOT
# result = subject.create_snapshot(poolname, vmname, "sams") # result = subject.create_snapshot(poolname, vmname, "sams")
# result = subject.revert_snapshot(poolname, vmname, "sams") # result = subject.revert_snapshot(poolname, vmname, "sams")
# puts subject.get_vm(poolname, vmname) # puts subject.get_vm(poolname, vmname)
result = subject.create_vm(poolname, vmname)
result = subject.destroy_vm(poolname, vmname) result = subject.destroy_vm(poolname, vmname)
end end
skip 'debug' do context 'in itsysops' do
puts subject.purge_unconfigured_resources(['foo', '', 'blah']) let(:vmname) { "instance-10" }
let(:project) { 'vmpooler-test' }
let(:config) { YAML.load(<<-EOT
---
:config:
max_tries: 3
retry_factor: 10
:providers:
:gce:
connection_pool_timeout: 1
project: '#{project}'
zone: '#{zone}'
network_name: 'projects/itsysopsnetworking/global/networks/shared1'
dns_zone_resource_name: 'test-vmpooler-puppet-net'
dns_zone: 'test.vmpooler.puppet.net'
:pools:
- name: '#{poolname}'
alias: [ 'mockpool' ]
template: 'projects/debian-cloud/global/images/family/debian-9'
size: 5
timeout: 10
ready_ttl: 1440
provider: 'gce'
subnetwork_name: 'projects/itsysopsnetworking/regions/us-west1/subnetworks/vmpooler-test'
machine_type: 'zones/#{zone}/machineTypes/e2-micro'
EOT
) }
skip 'gets a vm' do
result = subject.create_vm(poolname, vmname)
#subject.get_vm(poolname, vmname)
#subject.dns_teardown({'name' => vmname})
# subject.dns_setup({'name' => vmname, 'ip' => '1.2.3.5'})
end
end end
end end

View file

@ -0,0 +1,9 @@
require 'rspec'
describe 'VmpoolerProviderGce' do
context 'when creating class ' do
it 'sets a version' do
expect(VmpoolerProviderGce::VERSION).not_to be_nil
end
end
end

View file

@ -17,6 +17,7 @@ Gem::Specification.new do |s|
s.require_paths = ["lib"] s.require_paths = ["lib"]
s.add_dependency "google-apis-compute_v1", "~> 0.14" s.add_dependency "google-apis-compute_v1", "~> 0.14"
s.add_dependency "googleauth", "~> 0.16.2" s.add_dependency "googleauth", "~> 0.16.2"
s.add_dependency "google-cloud-dns", "~>0.35.1"
s.add_development_dependency 'vmpooler', '~> 1.3', '>= 1.3.0' s.add_development_dependency 'vmpooler', '~> 1.3', '>= 1.3.0'

View file

@ -72,6 +72,13 @@
# - network_name # - network_name
# The GCE network_name to use # The GCE network_name to use
# (required) # (required)
# - dns_zone_resource_name
# The name given to the DNS zone ressource. This is not the domain, but the name identifier of a zone eg example-com
# (optional) when not set, the dns setup / teardown is skipped
# - dns_zone
# The dns zone domain set for the dns_zone_resource_name. This becomes the domain part of the FQDN ie $vm_name.$dns_zone
# When setting multiple providers at the same time, this value should be set for each GCE pools.
# default to: global config:domain. if dns_zone is set, it overwrites the top-level domain when checking vm_ready?
# Example: # Example:
:gce: :gce: