vm_shepherd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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