train-vsphere-gom 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +15 -0
- data/README.md +88 -0
- data/lib/train-vsphere-gom.rb +3 -0
- data/lib/train-vsphere-gom/connection.rb +191 -0
- data/lib/train-vsphere-gom/guest_operations.rb +261 -0
- data/lib/train-vsphere-gom/transport.rb +29 -0
- data/lib/train-vsphere-gom/version.rb +5 -0
- data/train-vsphere-gom.gemspec +30 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4836ba3be5bd955bdb834325a38d79d6dd05c0f9fee8e735cd7a2de1de9c7c94
|
4
|
+
data.tar.gz: a76b3ec3cbcb66e743989c03a072095a6fd191c7e1d9f487530b4b00ba62c03c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c6b9c54a5e7939c40a1c20cf18e6fe6a92f3904968f3f0361dd0300b4ed9e1b86a7f6505ae4f85f6308dd05f5592da3bab6c0d28df1e823a78a098038de23df3
|
7
|
+
data.tar.gz: 13430dd8a4d44f36e076cb54c2c8de94f05bd4bd7facd1ea09f985d85c4c66697936b376079cb7a14694541e8a60185cb982ccc1e37c2784c2386aa25624a028
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
group :development do
|
6
|
+
gem "bump", "~> 0.10"
|
7
|
+
gem "bundler-audit", "~> 0.7"
|
8
|
+
gem "chefstyle", "~> 1.7"
|
9
|
+
gem "guard", "~> 2.16"
|
10
|
+
gem "mdl", "~> 0.11"
|
11
|
+
gem "pry", "~> 0.14"
|
12
|
+
gem "rake", "~> 13.0"
|
13
|
+
gem "train", "~> 3.5"
|
14
|
+
gem "yard", "~> 0.9"
|
15
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# Train-vsphere-gom
|
2
|
+
|
3
|
+
`train-vsphere-gom` is a Train plugin and is used as a Train OS Transport to
|
4
|
+
connect to virtual machines via VMware Tools.
|
5
|
+
|
6
|
+
This allows working with machines in different network segments, without
|
7
|
+
adding routes, NAT or firewall routes.
|
8
|
+
|
9
|
+
vSphere Guest Operations Manager covers various use cases within a target VM,
|
10
|
+
including file transfers, Windows registry access and command execution. It is
|
11
|
+
used by various 3rd party tools to monitor and manage VMs.
|
12
|
+
|
13
|
+
## Requirements
|
14
|
+
|
15
|
+
- VMware vSphere 5.0 or higher
|
16
|
+
|
17
|
+
## Permissions
|
18
|
+
|
19
|
+
Needs login credentials for vCenter (API) as well as the target machine (OS).
|
20
|
+
|
21
|
+
Mandatory privileges in vCenter:
|
22
|
+
|
23
|
+
- VirtualMachine.GuestOperations.Query
|
24
|
+
- VirtualMachine.GuestOperations.Execute
|
25
|
+
- VirtualMachine.GuestOperations.Modify (for file uploads)
|
26
|
+
|
27
|
+
## Installation
|
28
|
+
|
29
|
+
Install this Gem from rubygems.org:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
gem install train-vsphere-gom
|
33
|
+
```
|
34
|
+
|
35
|
+
## Transport parameters
|
36
|
+
|
37
|
+
| Option | Explanation | Default | Env. Vars |
|
38
|
+
|--------------------|--------------------------------------------------------|------------|---------------|
|
39
|
+
| `host` | VM to connect (`host` for Train compatibility) | _required_ | `VI_VM` |
|
40
|
+
| `user` | VM username for login | _required_ | |
|
41
|
+
| `password` | VM password for login | _required_ | |
|
42
|
+
| `vcenter_server` | VCenter server | _required_ | `VI_SERVER` |
|
43
|
+
| `vcenter_username` | VCenter username | _required_ | `VI_USERNAME` |
|
44
|
+
| `vcenter_password` | VCenter password | _required_ | `VI_PASSWORD` |
|
45
|
+
| `vcenter_insecure` | Allow connections when SSL certificate is not matching | `false` | |
|
46
|
+
| `logger` | Logger instance | $stdout, info | |
|
47
|
+
| `quick` | Enable quick mode | `false` | |
|
48
|
+
| `shell_type` | Shell type ("auto", "linux", "cmd", "powershell") | `auto` | |
|
49
|
+
| `timeout` | Timeout in seconds | `60` | |
|
50
|
+
|
51
|
+
By design, Train VSphere GOM requires two authentication steps: One to get access to the VCenter API and one
|
52
|
+
to get access to the guest VM with local credentials.
|
53
|
+
|
54
|
+
VMs can be searched by their IP address, their UUID, their VMware inventory path or by DNS name.
|
55
|
+
|
56
|
+
The environment variables are aligned to those from VMware SDK and ESXCLI.
|
57
|
+
|
58
|
+
## Quick Mode
|
59
|
+
|
60
|
+
In quick mode, non-essential operations are omitted to save time over slow connections.
|
61
|
+
|
62
|
+
- no deletion of temporary files (in system temp directory)
|
63
|
+
- no checking for file existance before trying to read
|
64
|
+
- only reading standard error, if exit code was not zero
|
65
|
+
|
66
|
+
## Limitations
|
67
|
+
|
68
|
+
- SSPI based guest VM logins are not supported yet
|
69
|
+
- using PowerShell via GOM is very slow (7-10 seconds roundtrip)
|
70
|
+
|
71
|
+
## Example use
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
require 'train-vsphere-gom'
|
75
|
+
|
76
|
+
train = Train.create('vsphere-gom', {
|
77
|
+
# Relying on VI_* variables for VCenter configuration
|
78
|
+
host: '10.20.30.40'
|
79
|
+
username: 'Administrator',
|
80
|
+
password: 'Password'
|
81
|
+
})
|
82
|
+
conn = train.connection
|
83
|
+
conn.run_command("Write-Host 'Inside the VM'")
|
84
|
+
```
|
85
|
+
|
86
|
+
## References
|
87
|
+
|
88
|
+
- [vSphere Web Services: GOM](https://code.vmware.com/docs/5722/vsphere-web-services-api-reference/doc/vim.vm.guest.GuestOperationsManager.html)
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require "resolv" unless defined?(Resolv)
|
2
|
+
require "rbvmomi" unless defined?(RbVmomi)
|
3
|
+
|
4
|
+
require_relative "guest_operations"
|
5
|
+
|
6
|
+
module TrainPlugins
|
7
|
+
module VsphereGom
|
8
|
+
class Connection < Train::Plugins::Transport::BaseConnection
|
9
|
+
attr_reader :options, :config
|
10
|
+
|
11
|
+
attr_writer :logger, :vim, :vm, :vm_guest
|
12
|
+
|
13
|
+
def initialize(config = {})
|
14
|
+
@config = config
|
15
|
+
|
16
|
+
unless vm
|
17
|
+
logger.error format("[VSphere-GOM] Could not find VM for '%<id>s'. Check power status, if searched via IP", id: config[:host])
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
logger.debug format("[VSphere-GOM] Found %<id>s for %<search_type>s %<search>s",
|
22
|
+
id: @vm._ref,
|
23
|
+
search: config[:host],
|
24
|
+
search_type: search_type(config[:host]))
|
25
|
+
|
26
|
+
super(config)
|
27
|
+
end
|
28
|
+
|
29
|
+
def close
|
30
|
+
return unless vim
|
31
|
+
|
32
|
+
vim.close
|
33
|
+
logger.info format("[VSphere-GOM] Closed connection to %<vm>s (VCenter %<vcenter>s)",
|
34
|
+
vm: options[:host],
|
35
|
+
vcenter: options[:vcenter_server])
|
36
|
+
end
|
37
|
+
|
38
|
+
def uri
|
39
|
+
"vsphere-gom://#{options[:user]}@#{options[:vcenter_server]}/#{options[:host]}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def file_via_connection(path, *args)
|
43
|
+
if windows?
|
44
|
+
Train::File::Remote::Windows.new(self, path, *args)
|
45
|
+
else
|
46
|
+
Train::File::Remote::Unix.new(self, path, *args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def upload(locals, remote)
|
51
|
+
logger.debug format("[VSphere-GOM] Copy %<locals>s to %<remote>s",
|
52
|
+
locals: Array(locals).join(", "),
|
53
|
+
remote: remote)
|
54
|
+
|
55
|
+
# Collect recursive list of directories and files
|
56
|
+
all = Array(locals).map do |local|
|
57
|
+
File.directory?(local) ? Dir.glob("#{local}/**/*") : local
|
58
|
+
end.flatten
|
59
|
+
|
60
|
+
dirs = all.select { |obj| File.directory? obj }
|
61
|
+
dirs.each { |dir| tools.create_dir(dir) }
|
62
|
+
|
63
|
+
# Then upload all files
|
64
|
+
files = all.select { |obj| File.file? obj }
|
65
|
+
files.each do |local|
|
66
|
+
remotefile = path(remote, File.basename(local))
|
67
|
+
vm_guest.upload_file(local, remotefile)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def download(remotes, local)
|
72
|
+
logger.debug format("[VSphere-GOM] Download %<remotes>s to %<local>s",
|
73
|
+
remotes: Array(Remotes).join(","),
|
74
|
+
local: local)
|
75
|
+
|
76
|
+
Array(remotes).each do |remote|
|
77
|
+
localfile = File.join(local, File.basename(remote))
|
78
|
+
vm_guest.download_file(remote, localfile)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def run_command_via_connection(command)
|
83
|
+
logger.debug format("[VSphere-GOM] Sending command to %<vm>s (VCenter %<vcenter>s)",
|
84
|
+
vm: options[:host],
|
85
|
+
vcenter: options[:vcenter_server])
|
86
|
+
|
87
|
+
result = vm_guest.run(command, shell_type: config[:shell_type].to_sym, timeout: config[:timeout].to_i)
|
88
|
+
|
89
|
+
if windows? && result.exit_status != 0
|
90
|
+
logger.debug format("[VSphere-GOM] Received Windows exit code: %<hexcode>s",
|
91
|
+
hexcode: hexit_code(result.exit_status))
|
92
|
+
end
|
93
|
+
|
94
|
+
result
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def vim
|
100
|
+
@vim ||= RbVmomi::VIM.connect(
|
101
|
+
user: config[:vcenter_username],
|
102
|
+
password: config[:vcenter_password],
|
103
|
+
insecure: config[:vcenter_insecure],
|
104
|
+
host: config[:vcenter_server]
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def vm
|
109
|
+
@vm ||= find_vm(config[:host])
|
110
|
+
end
|
111
|
+
|
112
|
+
def logger
|
113
|
+
return @logger if @logger
|
114
|
+
|
115
|
+
@logger = config[:logger] || Logger.new($stdout, level: :info)
|
116
|
+
end
|
117
|
+
|
118
|
+
def vm_guest
|
119
|
+
return @vm_guest if @vm_guest
|
120
|
+
|
121
|
+
username = config[:user] || config[:username]
|
122
|
+
password = config[:password]
|
123
|
+
ssl_verify = !config[:vcenter_insecure]
|
124
|
+
quick = config[:quick]
|
125
|
+
|
126
|
+
@vm_guest = Support::GuestOperations.new(vim, vm, username, password, ssl_verify: ssl_verify, quick: quick)
|
127
|
+
@vm_guest.logger = logger
|
128
|
+
@vm_guest
|
129
|
+
end
|
130
|
+
|
131
|
+
def os_family
|
132
|
+
return vm.guest.guestFamily == "windowsGuest" ? :windows : :linux if vm.guest.guestFamily
|
133
|
+
|
134
|
+
# VMware tools are not initialized or missing, infer from Guest Id
|
135
|
+
vm.config&.guestId =~ /^[Ww]in/ ? :windows : :linux
|
136
|
+
end
|
137
|
+
|
138
|
+
def linux?
|
139
|
+
os_family == :linux
|
140
|
+
end
|
141
|
+
|
142
|
+
def windows?
|
143
|
+
os_family == :windows
|
144
|
+
end
|
145
|
+
|
146
|
+
def hexit_code(exit_code)
|
147
|
+
exit_code += 2.pow(32) if exit_code < 0
|
148
|
+
|
149
|
+
"0x" + exit_code.to_s(16).upcase
|
150
|
+
end
|
151
|
+
|
152
|
+
def find_vm(needle)
|
153
|
+
root_folder = vim.serviceInstance.content.rootFolder
|
154
|
+
|
155
|
+
if ip?(needle)
|
156
|
+
root_folder.findByIp(config[:host])
|
157
|
+
elsif uuid?(needle)
|
158
|
+
root_folder.findByUuid(config[:host])
|
159
|
+
elsif inventory_path?(needle)
|
160
|
+
root_folder.findByInventoryPath(config[:host])
|
161
|
+
else
|
162
|
+
root_folder.findByDnsName(config[:host])
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def ip?(ip_string)
|
167
|
+
ip_string.match? Resolv::IPv4::Regex
|
168
|
+
end
|
169
|
+
|
170
|
+
def uuid?(uuid)
|
171
|
+
uuid.downcase.match?(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
|
172
|
+
end
|
173
|
+
|
174
|
+
def inventory_path?(path)
|
175
|
+
path.include? "/"
|
176
|
+
end
|
177
|
+
|
178
|
+
def moref?(ref)
|
179
|
+
ref.match?(/vm-[0-9]*/)
|
180
|
+
end
|
181
|
+
|
182
|
+
def search_type(needle)
|
183
|
+
return "IP" if ip?(needle)
|
184
|
+
return "UUID" if uuid?(needle)
|
185
|
+
return "PATH" if inventory_path?(needle)
|
186
|
+
|
187
|
+
"DNS"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
require "rbvmomi" unless defined?(RbVmomi)
|
2
|
+
require "net/http" unless defined?(Net::HTTP)
|
3
|
+
|
4
|
+
class Support
|
5
|
+
# Encapsulate VMware Tools GOM interaction, originally inspired by github:dnuffer/raidopt
|
6
|
+
class GuestOperations
|
7
|
+
SHELL_TYPES = {
|
8
|
+
linux: {
|
9
|
+
suffix: ".sh",
|
10
|
+
cmd: "/bin/sh",
|
11
|
+
args: '-c ". %<cmdfile>s" > %<outfile>s 2> %<errfile>s',
|
12
|
+
},
|
13
|
+
|
14
|
+
# BUG: Includes prompt
|
15
|
+
cmd: {
|
16
|
+
suffix: ".cmd",
|
17
|
+
cmd: "cmd.exe",
|
18
|
+
args: '/s /c "%<cmdfile>s" > %<outfile>s 2> %<errfile>s',
|
19
|
+
},
|
20
|
+
|
21
|
+
# Invoking PS via cmd seems the only way to get this to work
|
22
|
+
powershell: {
|
23
|
+
suffix: ".ps1",
|
24
|
+
cmd: "cmd.exe",
|
25
|
+
args: "/C powershell -NonInteractive -ExecutionPolicy Bypass -File %<cmdfile>s >%<outfile>s 2>%<errfile>s",
|
26
|
+
},
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
attr_writer :gom, :logger
|
30
|
+
|
31
|
+
def initialize(vim, vm, username, password, ssl_verify: true, logger: nil, quick: false)
|
32
|
+
@vim = vim
|
33
|
+
@vm = vm
|
34
|
+
|
35
|
+
@guest_auth = RbVmomi::VIM::NamePasswordAuthentication(interactiveSession: false, username: username, password: password)
|
36
|
+
|
37
|
+
@ssl_verify = ssl_verify
|
38
|
+
@quick = quick
|
39
|
+
end
|
40
|
+
|
41
|
+
# Required privileges: VirtualMachine.GuestOperations.Execute, VirtualMachine.GuestOperations.Modify
|
42
|
+
def run(command, shell_type: :auto, timeout: 60.0)
|
43
|
+
logger.debug format("Running `%s` remotely", command)
|
44
|
+
|
45
|
+
if shell_type == :auto
|
46
|
+
shell_type = :linux if linux?
|
47
|
+
shell_type = :powershell if windows?
|
48
|
+
end
|
49
|
+
|
50
|
+
shell = SHELL_TYPES[shell_type]
|
51
|
+
raise "Unsupported shell type #{shell_type}" unless shell
|
52
|
+
|
53
|
+
logger.warn "Command execution on Windows is very slow" unless @warned || linux?
|
54
|
+
@warned = true
|
55
|
+
|
56
|
+
temp_file = write_temp_file(command, suffix: shell[:suffix] || "")
|
57
|
+
temp_out = "#{temp_file}-out.txt"
|
58
|
+
temp_err = "#{temp_file}-err.txt"
|
59
|
+
|
60
|
+
begin
|
61
|
+
args = format(shell[:args], cmdfile: temp_file, outfile: temp_out, errfile: temp_err)
|
62
|
+
exit_code = run_program(shell[:cmd], args, timeout)
|
63
|
+
rescue StandardError
|
64
|
+
proc_err = read_file(temp_err)
|
65
|
+
raise format("Error executing command %s. Exit code: %d. StdErr %s", command, exit_code || -1, proc_err)
|
66
|
+
end
|
67
|
+
|
68
|
+
stdout = read_file(temp_out)
|
69
|
+
stdout = ascii_only(stdout) if bom?(stdout)
|
70
|
+
|
71
|
+
stderr = read_file(temp_err) unless exit_code == 0 && @quick
|
72
|
+
|
73
|
+
unless @quick
|
74
|
+
delete_file(temp_file)
|
75
|
+
delete_file(temp_out)
|
76
|
+
delete_file(temp_err)
|
77
|
+
end
|
78
|
+
|
79
|
+
::Train::Extras::CommandResult.new(stdout, stderr || "", exit_code)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Required privilege: VirtualMachine.GuestOperations.Query
|
83
|
+
def exist?(remote_file)
|
84
|
+
logger.debug format("Checking for remote file %s", remote_file)
|
85
|
+
|
86
|
+
gom.fileManager.ListFilesInGuest(vm: @vm, auth: @guest_auth, filePath: remote_file)
|
87
|
+
|
88
|
+
true
|
89
|
+
rescue RbVmomi::Fault
|
90
|
+
false
|
91
|
+
end
|
92
|
+
|
93
|
+
def read_file(remote_file)
|
94
|
+
return "" unless @quick || exist?(remote_file)
|
95
|
+
|
96
|
+
download_file(remote_file, nil) || ""
|
97
|
+
end
|
98
|
+
|
99
|
+
# Required privilege: VirtualMachine.GuestOperations.Modify
|
100
|
+
def write_file(remote_file, contents)
|
101
|
+
logger.debug format("Writing to remote file %s", remote_file)
|
102
|
+
|
103
|
+
put_url = gom.fileManager.InitiateFileTransferToGuest(
|
104
|
+
vm: @vm,
|
105
|
+
auth: @guest_auth,
|
106
|
+
guestFilePath: remote_file,
|
107
|
+
fileAttributes: RbVmomi::VIM::GuestFileAttributes(),
|
108
|
+
fileSize: contents.size,
|
109
|
+
overwrite: true
|
110
|
+
)
|
111
|
+
|
112
|
+
# VCenter internal name might mismatch the external, so fix it
|
113
|
+
put_url = put_url.gsub(%r{^https://\*:}, format("https://%s:%s", @vm._connection.host, put_url))
|
114
|
+
uri = URI.parse(put_url)
|
115
|
+
|
116
|
+
request = Net::HTTP::Put.new(uri.request_uri)
|
117
|
+
request["Transfer-Encoding"] = "chunked"
|
118
|
+
request["Content-Length"] = contents.size
|
119
|
+
request.body = contents
|
120
|
+
|
121
|
+
http_request(put_url, request)
|
122
|
+
rescue RbVmomi::Fault => e
|
123
|
+
logger.error "Error during upload, check permissions on remote system: '" + e.message + "'"
|
124
|
+
end
|
125
|
+
|
126
|
+
# Required privilege: VirtualMachine.GuestOperations.Modify
|
127
|
+
def write_temp_file(contents, prefix: "", suffix: "")
|
128
|
+
logger.debug format("Writing to temporary remote file")
|
129
|
+
|
130
|
+
temp_name = gom.fileManager.CreateTemporaryFileInGuest(vm: @vm, auth: @guest_auth, prefix: prefix, suffix: suffix)
|
131
|
+
write_file(temp_name, contents)
|
132
|
+
|
133
|
+
temp_name
|
134
|
+
end
|
135
|
+
|
136
|
+
# Required privilege: VirtualMachine.GuestOperations.Modify
|
137
|
+
def upload_file(local_file, remote_file)
|
138
|
+
logger.debug format("Uploading %s to remote file %s", local_file, remote_file)
|
139
|
+
|
140
|
+
write_file(remote_file, File.open(local_file, "rb").read)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Required privilege: VirtualMachine.GuestOperations.Modify
|
144
|
+
def download_file(remote_file, local_file)
|
145
|
+
logger.debug format("Downloading remote file %s to %s", local_file, remote_file)
|
146
|
+
|
147
|
+
info = gom.fileManager.InitiateFileTransferFromGuest(vm: @vm, auth: @guest_auth, guestFilePath: remote_file)
|
148
|
+
uri = URI.parse(info.url)
|
149
|
+
|
150
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
151
|
+
response = http_request(info.url, request)
|
152
|
+
|
153
|
+
if response.body.size != info.size
|
154
|
+
raise format("Downloaded file has different size than reported: %s (%d bytes instead of %d bytes)", remote_file, response.body.size, info.size)
|
155
|
+
end
|
156
|
+
|
157
|
+
local_file.nil? ? response.body : File.open(local_file, "w") { |file| file.write(response.body) }
|
158
|
+
end
|
159
|
+
|
160
|
+
# Required privilege: VirtualMachine.GuestOperations.Modify
|
161
|
+
def delete_file(remote_file)
|
162
|
+
logger.debug format("Deleting remote file %s", remote_file)
|
163
|
+
|
164
|
+
gom.fileManager.DeleteFileInGuest(vm: @vm, auth: @guest_auth, filePath: remote_file)
|
165
|
+
|
166
|
+
true
|
167
|
+
rescue RbVmomi::Fault => e
|
168
|
+
raise unless e.message.start_with? "FileNotFound:"
|
169
|
+
|
170
|
+
false
|
171
|
+
end
|
172
|
+
|
173
|
+
# Required privilege: VirtualMachine.GuestOperations.Modify
|
174
|
+
def delete_directory(remote_dir, recursive: true)
|
175
|
+
logger.debug format("Deleting remote directory %s", remote_dir)
|
176
|
+
|
177
|
+
gom.fileManager.DeleteDirectoryInGuest(vm: @vm, auth: @guest_auth, directoryPath: remote_dir, recursive: recursive)
|
178
|
+
|
179
|
+
true
|
180
|
+
rescue RbVmomi::Fault => e
|
181
|
+
raise if e.message.start_with? "NotADirectory:"
|
182
|
+
|
183
|
+
false
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def gom
|
189
|
+
@gom ||= @vim.serviceContent.guestOperationsManager
|
190
|
+
end
|
191
|
+
|
192
|
+
def logger
|
193
|
+
@logger ||= Logger.new($stdout, level: :info)
|
194
|
+
end
|
195
|
+
|
196
|
+
def ascii_only(string)
|
197
|
+
string.bytes[2..].map(&:chr).join.delete("\000")
|
198
|
+
end
|
199
|
+
|
200
|
+
def bom?(string)
|
201
|
+
string.bytes[0..1] == [0xFF, 0xFE]
|
202
|
+
end
|
203
|
+
|
204
|
+
def http_request(url, request_data)
|
205
|
+
uri = URI.parse(url)
|
206
|
+
|
207
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
208
|
+
http.use_ssl = (uri.scheme == "https")
|
209
|
+
http.verify_mode = @ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
210
|
+
|
211
|
+
http.request(request_data)
|
212
|
+
end
|
213
|
+
|
214
|
+
def linux?
|
215
|
+
os_family == :linux
|
216
|
+
end
|
217
|
+
|
218
|
+
def windows?
|
219
|
+
os_family == :windows
|
220
|
+
end
|
221
|
+
|
222
|
+
def os_family
|
223
|
+
return @vm.guest.guestFamily == "windowsGuest" ? :windows : :linux if @vm.guest.guestFamily
|
224
|
+
|
225
|
+
# VMware tools are not initialized or missing, infer from Guest ID
|
226
|
+
@vm.config&.guestId&.match(/^win/) ? :windows : :linux
|
227
|
+
end
|
228
|
+
|
229
|
+
def run_program(path, args = "", timeout = 60.0)
|
230
|
+
logger.debug format("Running %s %s", path, args)
|
231
|
+
|
232
|
+
gp_spec = RbVmomi::VIM::GuestProgramSpec.new(programPath: path, arguments: args)
|
233
|
+
pid = gom.processManager.StartProgramInGuest(vm: @vm, auth: @guest_auth, spec: gp_spec)
|
234
|
+
|
235
|
+
wait_for_process_exit(pid, timeout)
|
236
|
+
process_exit_code(pid)
|
237
|
+
end
|
238
|
+
|
239
|
+
def wait_for_process_exit(pid, timeout = 60.0, interval = 1.0)
|
240
|
+
start = Time.new
|
241
|
+
|
242
|
+
loop do
|
243
|
+
return unless process_running?(pid)
|
244
|
+
break if (Time.new - start) >= timeout
|
245
|
+
|
246
|
+
sleep interval
|
247
|
+
end
|
248
|
+
|
249
|
+
raise format("Timeout waiting for process %d to exit after %d seconds", pid, timeout) if (Time.new - start) >= timeout
|
250
|
+
end
|
251
|
+
|
252
|
+
def process_running?(pid)
|
253
|
+
procs = gom.processManager.ListProcessesInGuest(vm: @vm, auth: @guest_auth, pids: [pid])
|
254
|
+
procs.empty? || procs.any? { |gpi| gpi.exitCode.nil? }
|
255
|
+
end
|
256
|
+
|
257
|
+
def process_exit_code(pid)
|
258
|
+
gom.processManager.ListProcessesInGuest(vm: @vm, auth: @guest_auth, pids: [pid])&.first&.exitCode
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "train"
|
2
|
+
require "train/plugins"
|
3
|
+
require "train-vsphere-gom/connection"
|
4
|
+
|
5
|
+
module TrainPlugins
|
6
|
+
module VsphereGom
|
7
|
+
class Transport < Train.plugin(1)
|
8
|
+
name "vsphere-gom"
|
9
|
+
|
10
|
+
option :vcenter_server, required: true, default: ENV["VI_SERVER"]
|
11
|
+
option :vcenter_username, required: true, default: ENV["VI_USERNAME"]
|
12
|
+
option :vcenter_password, required: true, default: ENV["VI_PASSWORD"]
|
13
|
+
option :vcenter_insecure, required: false, default: true
|
14
|
+
|
15
|
+
option :host, default: ENV["VI_VM"], required: true
|
16
|
+
option :user, required: true
|
17
|
+
option :password, required: true
|
18
|
+
|
19
|
+
option :quick, default: false
|
20
|
+
option :shell_type, default: :auto
|
21
|
+
option :timeout, default: 60
|
22
|
+
|
23
|
+
# inspec -t vsphere-gom://
|
24
|
+
def connection(_instance_opts = nil)
|
25
|
+
@connection ||= TrainPlugins::VsphereGom::Connection.new(@options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "train-vsphere-gom"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "train-vsphere-gom"
|
7
|
+
spec.version = TrainPlugins::VsphereGom::VERSION
|
8
|
+
spec.authors = ["Thomas Heinen"]
|
9
|
+
spec.email = ["theinen@tecracer.de"]
|
10
|
+
spec.summary = "Train transport for vSphere GOM"
|
11
|
+
spec.description = "Execute commands via VMware Tools (without need for network)"
|
12
|
+
spec.homepage = "https://github.com/tecracer-chef/train-vsphere-gom"
|
13
|
+
spec.license = "Apache-2.0"
|
14
|
+
|
15
|
+
spec.files = %w{
|
16
|
+
README.md train-vsphere-gom.gemspec Gemfile
|
17
|
+
} + Dir.glob(
|
18
|
+
"lib/**/*", File::FNM_DOTMATCH
|
19
|
+
).reject { |f| File.directory?(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.required_ruby_version = "~> 2.6"
|
23
|
+
|
24
|
+
# If you only need certain gems during development or testing, list
|
25
|
+
# them in Gemfile, not here.
|
26
|
+
|
27
|
+
# Do not list inspec as a dependency of a train plugin.
|
28
|
+
# Do not list train or train-core as a dependency of a train plugin.
|
29
|
+
spec.add_dependency "rbvmomi", "~> 3.0"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: train-vsphere-gom
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas Heinen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rbvmomi
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
description: Execute commands via VMware Tools (without need for network)
|
28
|
+
email:
|
29
|
+
- theinen@tecracer.de
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- Gemfile
|
35
|
+
- README.md
|
36
|
+
- lib/train-vsphere-gom.rb
|
37
|
+
- lib/train-vsphere-gom/connection.rb
|
38
|
+
- lib/train-vsphere-gom/guest_operations.rb
|
39
|
+
- lib/train-vsphere-gom/transport.rb
|
40
|
+
- lib/train-vsphere-gom/version.rb
|
41
|
+
- train-vsphere-gom.gemspec
|
42
|
+
homepage: https://github.com/tecracer-chef/train-vsphere-gom
|
43
|
+
licenses:
|
44
|
+
- Apache-2.0
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.6'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.0.3
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Train transport for vSphere GOM
|
65
|
+
test_files: []
|