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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -0
- data/README.md +31 -0
- data/Rakefile +11 -0
- data/ci/run_specs.sh +11 -0
- data/lib/vm_shepherd/ami_manager.rb +81 -0
- data/lib/vm_shepherd/ova_manager/base.rb +31 -0
- data/lib/vm_shepherd/ova_manager/deployer.rb +202 -0
- data/lib/vm_shepherd/ova_manager/destroyer.rb +29 -0
- data/lib/vm_shepherd/ova_manager/open_monkey_patch.rb +14 -0
- data/lib/vm_shepherd/shepherd.rb +166 -0
- data/lib/vm_shepherd/vapp_manager/deployer.rb +151 -0
- data/lib/vm_shepherd/vapp_manager/destroyer.rb +46 -0
- data/lib/vm_shepherd/version.rb +3 -0
- data/spec/fixtures/ova_manager/foo.ova +0 -0
- data/spec/fixtures/shepherd/aws.yml +11 -0
- data/spec/fixtures/shepherd/unknown.yml +1 -0
- data/spec/fixtures/shepherd/vcloud.yml +19 -0
- data/spec/fixtures/shepherd/vsphere.yml +20 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/vm_shepherd/ami_manager_spec.rb +146 -0
- data/spec/vm_shepherd/ova_manager/base_spec.rb +56 -0
- data/spec/vm_shepherd/ova_manager/deployer_spec.rb +134 -0
- data/spec/vm_shepherd/ova_manager/destroyer_spec.rb +42 -0
- data/spec/vm_shepherd/shepherd_spec.rb +213 -0
- data/spec/vm_shepherd/vapp_manager/deployer_spec.rb +287 -0
- data/spec/vm_shepherd/vapp_manager/destroyer_spec.rb +104 -0
- data/vm_shepherd.gemspec +31 -0
- metadata +198 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.5
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
data/ci/run_specs.sh
ADDED
@@ -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
|