vm_shepherd 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dd16c5dc4a8f8276b0c59927637e27a635005091
4
+ data.tar.gz: a16f15c127599cdf280b3fc4fa21cecced662d8d
5
+ SHA512:
6
+ metadata.gz: 4ef0386504cbf50ec960bccf9e1eead05746441f7e4ff618249e43b0026da0fd6d1c9ffb17166a56ed203cc75b8726bd3a9214d5b0e27cfebc820e84b4e87899
7
+ data.tar.gz: 58ebe55c5ced17018c1a8666a8e0ff26b502870179c2cba965735ce67117d2bbde06ef17ec286ba1455bb62efea40892d9043daf28a3c7051f630a467f541751
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1 @@
1
+ 2.1.5
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'vsphere_clients', git: 'http://github.com/pivotal-cf-experimental/vsphere_clients'
4
+
5
+ gemspec
@@ -0,0 +1,31 @@
1
+ # VmShepherd
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'vm_shepherd'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install vm_shepherd
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it ( https://github.com/pivotal-cf-experimental/vm_shepherd/fork )
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create a new Pull Request
@@ -0,0 +1,11 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rubocop/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ RuboCop::RakeTask.new(:rubocop) do |t|
7
+ t.options << '--lint'
8
+ t.options << '--display-cop-names'
9
+ end
10
+
11
+ task default: [:rubocop, :spec]
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ set -ex
3
+
4
+ echo "-----> Running script: $0"
5
+
6
+ docker run \
7
+ --rm=true \
8
+ --volume=${PWD}:/vm_shepherd \
9
+ --workdir=/vm_shepherd \
10
+ ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME} \
11
+ /bin/sh -c 'bundle && bundle exec rspec --format documentation'
@@ -0,0 +1,81 @@
1
+ require 'aws-sdk-v1'
2
+
3
+ module VmShepherd
4
+ class AmiManager
5
+ class RetryLimitExceeded < StandardError
6
+ end
7
+
8
+ AWS_REGION = 'us-east-1'
9
+ OPS_MANAGER_PRIVATE_IP = '10.0.1.5'
10
+ OPS_MANAGER_INSTANCE_TYPE = 'm3.medium'
11
+ RETRY_LIMIT = 60
12
+ RETRY_INTERVAL = 5
13
+ DO_NOT_TERMINATE_TAG_KEY = 'do_not_terminate'
14
+
15
+ def initialize(aws_options)
16
+ AWS.config(
17
+ access_key_id: aws_options.fetch(:aws_access_key),
18
+ secret_access_key: aws_options.fetch(:aws_secret_key),
19
+ region: AWS_REGION
20
+ )
21
+ @aws_options = aws_options
22
+ end
23
+
24
+ def deploy(ami_file_path)
25
+ image_id = File.read(ami_file_path).strip
26
+
27
+ instance =
28
+ retry_ignoring_error_until(AWS::EC2::Errors::InvalidIPAddress::InUse) do
29
+ AWS.ec2.instances.create(
30
+ image_id: image_id,
31
+ key_name: aws_options.fetch(:ssh_key_name),
32
+ security_group_ids: [aws_options.fetch(:security_group_id)],
33
+ subnet: aws_options.fetch(:public_subnet_id),
34
+ private_ip_address: OPS_MANAGER_PRIVATE_IP,
35
+ instance_type: OPS_MANAGER_INSTANCE_TYPE
36
+ )
37
+ end
38
+
39
+ retry_ignoring_error_until(AWS::EC2::Errors::InvalidInstanceID::NotFound) do
40
+ instance.status == :running
41
+ end
42
+
43
+ instance.associate_elastic_ip(aws_options.fetch(:elastic_ip_id))
44
+ instance.add_tag('Name', value: aws_options.fetch(:vm_name))
45
+ end
46
+
47
+ def destroy
48
+ subnets = [
49
+ AWS.ec2.subnets[aws_options.fetch(:public_subnet_id)],
50
+ AWS.ec2.subnets[aws_options.fetch(:private_subnet_id)]
51
+ ]
52
+
53
+ subnets.each do |subnet|
54
+ subnet.instances.each do |instance|
55
+ instance.terminate unless instance.tags.to_h.fetch(DO_NOT_TERMINATE_TAG_KEY, false)
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+ attr_reader :aws_options
62
+
63
+
64
+ def retry_ignoring_error_until(exception_class, &block)
65
+ tries = 0
66
+ condition_reached = false
67
+ loop do
68
+ begin
69
+ tries += 1
70
+ raise(RetryLimitExceeded) if tries > RETRY_LIMIT
71
+ condition_reached = block.call
72
+ sleep RETRY_INTERVAL
73
+ rescue exception_class
74
+ retry
75
+ end
76
+ break if condition_reached
77
+ end
78
+ condition_reached
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ require 'rbvmomi'
2
+
3
+ module VmShepherd
4
+ module OvaManager
5
+ class Base
6
+ attr_reader :vcenter
7
+
8
+ def initialize(vcenter)
9
+ @vcenter = vcenter
10
+ end
11
+
12
+ def find_datacenter(name)
13
+ match = connection.searchIndex.FindByInventoryPath(inventoryPath: name)
14
+ return unless match and match.is_a?(RbVmomi::VIM::Datacenter)
15
+ match
16
+ end
17
+
18
+ private
19
+
20
+ def connection
21
+ @connection ||= RbVmomi::VIM.connect(
22
+ host: @vcenter.fetch(:host),
23
+ user: @vcenter.fetch(:user),
24
+ password: @vcenter.fetch(:password),
25
+ ssl: true,
26
+ insecure: true,
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,202 @@
1
+ require 'tmpdir'
2
+ require 'fileutils'
3
+ require 'rbvmomi'
4
+ require 'rbvmomi/utils/deploy'
5
+ require 'vsphere_clients'
6
+ require 'vm_shepherd/ova_manager/base'
7
+ require 'vm_shepherd/ova_manager/open_monkey_patch'
8
+
9
+ module VmShepherd
10
+ module OvaManager
11
+ class Deployer < Base
12
+ attr_reader :location
13
+
14
+ def initialize(vcenter, location)
15
+ super(vcenter)
16
+ @location = location
17
+ raise 'Target folder must be set' unless @location[:folder]
18
+ end
19
+
20
+ def deploy(name_prefix, ova_path, ova_config)
21
+ ova_path = File.expand_path(ova_path.strip)
22
+ check_vm_status(ova_config)
23
+
24
+ tmp_dir = untar_vbox_ova(ova_path)
25
+ ovf_path = obtain_ovf_path(tmp_dir)
26
+
27
+ deployer = build_deployer(@location)
28
+ template = deploy_ovf_template(name_prefix, deployer, ovf_path)
29
+ vm = create_vm_from_template(deployer, template)
30
+
31
+ reconfigure_vm(vm, ova_config)
32
+ power_on_vm(vm)
33
+ ensure
34
+ FileUtils.remove_entry_secure(tmp_dir, force: true)
35
+ end
36
+
37
+ private
38
+
39
+ def check_vm_status(ova_config)
40
+ log('Checking for existing VM') do # Bad idea to redeploy VM over existing running VM
41
+ ip = ova_config[:external_ip] || ova_config[:ip]
42
+ port = ova_config[:external_port] || 443
43
+ raise "VM exists at #{ip}" if system("nc -z -w 5 #{ip} #{port}")
44
+ end
45
+ end
46
+
47
+ def untar_vbox_ova(ova_path)
48
+ log("Untarring #{ova_path}") do
49
+ Dir.mktmpdir.tap do |dir|
50
+ system_or_exit("cd #{dir} && tar xfv '#{ova_path}'")
51
+ end
52
+ end
53
+ end
54
+
55
+ def obtain_ovf_path(dir)
56
+ raise 'Failed to find ovf' unless (file_path = Dir["#{dir}/*.ovf"].first)
57
+ "file://#{file_path}"
58
+ end
59
+
60
+ def deploy_ovf_template(name_prefix, deployer, ovf_path)
61
+ log('Uploading template') do
62
+ deployer.upload_ovf_as_template(
63
+ ovf_path,
64
+ Time.new.strftime("#{name_prefix}-%F-%H-%M"),
65
+ run_without_interruptions: true,
66
+ )
67
+ end
68
+ end
69
+
70
+ def create_vm_from_template(deployer, template)
71
+ log('Cloning template') do
72
+ deployer.linked_clone(template, "#{template.name}-vm", {
73
+ :numCPUs => 2,
74
+ :memoryMB => 2048,
75
+ })
76
+ end
77
+ end
78
+
79
+ def reconfigure_vm(vm, ova_config)
80
+ ip_configuration = {
81
+ 'ip0' => ova_config[:ip],
82
+ 'netmask0' => ova_config[:netmask],
83
+ 'gateway' => ova_config[:gateway],
84
+ 'DNS' => ova_config[:dns],
85
+ 'ntp_servers' => ova_config[:ntp_servers],
86
+ }
87
+
88
+ log("Reconfiguring VM using #{ip_configuration.inspect}") do
89
+ property_specs = []
90
+
91
+ # Order of ip configuration keys must match
92
+ # order of OVF template properties.
93
+ ip_configuration.each_with_index do |(key, value), i|
94
+ property_specs << RbVmomi::VIM::VAppPropertySpec.new.tap do |spec|
95
+ spec.operation = 'edit'
96
+ spec.info = RbVmomi::VIM::VAppPropertyInfo.new.tap do |p|
97
+ p.key = i
98
+ p.label = key
99
+ p.value = value
100
+ end
101
+ end
102
+ end
103
+
104
+ property_specs << RbVmomi::VIM::VAppPropertySpec.new.tap do |spec|
105
+ spec.operation = 'edit'
106
+ spec.info = RbVmomi::VIM::VAppPropertyInfo.new.tap do |p|
107
+ p.key = ip_configuration.length
108
+ p.label = 'admin_password'
109
+ p.value = ova_config[:vm_password]
110
+ end
111
+ end
112
+
113
+ vm_config_spec = RbVmomi::VIM::VmConfigSpec.new
114
+ vm_config_spec.ovfEnvironmentTransport = ['com.vmware.guestInfo']
115
+ vm_config_spec.property = property_specs
116
+
117
+ vmachine_spec = RbVmomi::VIM::VirtualMachineConfigSpec.new
118
+ vmachine_spec.vAppConfig = vm_config_spec
119
+ vm.ReconfigVM_Task(spec: vmachine_spec).wait_for_completion
120
+ end
121
+ end
122
+
123
+ def power_on_vm(vm)
124
+ log('Powering on VM') do
125
+ vm.PowerOnVM_Task.wait_for_completion
126
+ wait_for('VM IP') { vm.guest_ip }
127
+ end
128
+ end
129
+
130
+ def build_deployer(location)
131
+ unless (datacenter = find_datacenter(location[:datacenter]))
132
+ raise "Failed to find datacenter '#{location[:datacenter]}'"
133
+ end
134
+
135
+ unless (cluster = datacenter.find_compute_resource(location[:cluster]))
136
+ raise "Failed to find cluster '#{location[:cluster]}'"
137
+ end
138
+
139
+ unless (datastore = datacenter.find_datastore(location[:datastore]))
140
+ raise "Failed to find datastore '#{location[:datastore]}'"
141
+ end
142
+
143
+ unless (network = datacenter.networkFolder.traverse(location[:network]))
144
+ raise "Failed to find network '#{location[:network]}'"
145
+ end
146
+
147
+ resource_pool_name = location[:resource_pool] || location[:resource_pool_name]
148
+ unless (resource_pool = find_resource_pool(cluster, resource_pool_name))
149
+ raise "Failed to find resource pool '#{resource_pool_name}'"
150
+ end
151
+
152
+ target_folder = datacenter.vmFolder.traverse(location[:folder], RbVmomi::VIM::Folder, true)
153
+
154
+ VsphereClients::CachedOvfDeployer.new(
155
+ logged_connection,
156
+ network,
157
+ cluster,
158
+ resource_pool,
159
+ target_folder, # template
160
+ target_folder, # vm
161
+ datastore,
162
+ )
163
+ end
164
+
165
+ def find_resource_pool(cluster, resource_pool_name)
166
+ if resource_pool_name
167
+ cluster.resourcePool.resourcePool.find { |rp| rp.name == resource_pool_name }
168
+ else
169
+ cluster.resourcePool
170
+ end
171
+ end
172
+
173
+ def logged_connection
174
+ log("connecting to #{@vcenter[:user]}@#{@vcenter[:host]}") { connection }
175
+ end
176
+
177
+ def log(title, &blk)
178
+ puts "--- Running: #{title} @ #{DateTime.now}"
179
+ blk.call
180
+ end
181
+
182
+ def system_or_exit(*args)
183
+ puts "--- Running: #{args} @ #{DateTime.now}"
184
+ system(*args) || fail('FAILED')
185
+ end
186
+
187
+ def wait_for(title, &blk)
188
+ Timeout.timeout(7*60) do
189
+ until (value = blk.call)
190
+ puts '--- Waiting for 30 secs'
191
+ sleep 30
192
+ end
193
+ puts "--- Value obtained for #{title} is #{value}"
194
+ value
195
+ end
196
+ rescue Timeout::Error
197
+ puts "--- Timed out waiting for #{title}"
198
+ raise
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,29 @@
1
+ require 'rbvmomi'
2
+ require 'logger'
3
+ require 'vsphere_clients/vm_folder_client'
4
+ require 'vm_shepherd/ova_manager/base'
5
+
6
+ module VmShepherd
7
+ module OvaManager
8
+ class Destroyer < Base
9
+ def initialize(datacenter_name, vcenter)
10
+ @datacenter_name = datacenter_name
11
+ @vcenter = vcenter
12
+ end
13
+
14
+ def clean_folder(folder_name)
15
+ vm_folder_client.delete_folder(folder_name)
16
+ vm_folder_client.create_folder(folder_name)
17
+ end
18
+
19
+ private
20
+
21
+ def vm_folder_client
22
+ @vm_folder_client ||= VsphereClients::VmFolderClient.new(
23
+ find_datacenter(@datacenter_name),
24
+ Logger.new(STDERR)
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ # open is used by RbvMomi when trying to upload OVA file.
2
+ # It does not support local file upload without following patch.
3
+ module Kernel
4
+ private
5
+ alias open_without_file open
6
+ class << self
7
+ alias open_without_file open
8
+ end
9
+
10
+ def open(name, *rest, &blk)
11
+ name = name[7..-1] if name.start_with?('file://')
12
+ open_without_file(name, *rest, &blk)
13
+ end
14
+ end