train-vsphere-gom 0.1.0
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/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: []
|