kitchen-vcenter 1.0.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/CHANGELOG.md +13 -0
- data/README.md +127 -0
- data/lib/base.rb +35 -0
- data/lib/kitchen-vcenter/version.rb +22 -0
- data/lib/kitchen/driver/vcenter.rb +171 -0
- data/lib/lookup_service_helper.rb +463 -0
- data/lib/sso.rb +269 -0
- data/lib/support/clone_vm.rb +53 -0
- metadata +164 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 3d3bbb3c6bad4d1c38ef1fc4049a0363083e1bd7
|
|
4
|
+
data.tar.gz: e6e3e44a085fdb28159d4562130b73a860948e42
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8b96a21adbe30109d9991564f81eded1b0c647366df9631145c678aec30c73d5dd64c45428b4d715845c77ea07184f3b76d83c8cf3185812eb8a0b0e7b116830
|
|
7
|
+
data.tar.gz: 705e798243b0763f288a7d6938a5b3e1ca9461500c97ccc21c836d48f4ea99d30fe4d3b9fb21eb9aacf02abf699b0e29c3b58995e7c5dd94dd689ede96976b1d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
## [1.0.0](https://github.com/chef/kitchen-vcenter/tree/1.0.0) (2017-08-10)
|
|
4
|
+
**Merged pull requests:**
|
|
5
|
+
|
|
6
|
+
- 1.0.0 release [\#4](https://github.com/chef/kitchen-vcenter/pull/4) ([jjasghar](https://github.com/jjasghar))
|
|
7
|
+
- Second walk through [\#3](https://github.com/chef/kitchen-vcenter/pull/3) ([jjasghar](https://github.com/jjasghar))
|
|
8
|
+
- Updated so that the vm\_name is set according to suite and platform. [\#2](https://github.com/chef/kitchen-vcenter/pull/2) ([russellseymour](https://github.com/russellseymour))
|
|
9
|
+
- Prep-for-public release [\#1](https://github.com/chef/kitchen-vcenter/pull/1) ([jjasghar](https://github.com/jjasghar))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
|
data/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# kitchen-vcenter
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/kitchen-vcenter)
|
|
4
|
+
[](https://travis-ci.org/chef/kitchen-vcenter)
|
|
5
|
+
[](https://gemnasium.com/chef/kitchen-vcenter)
|
|
6
|
+
[](http://inch-ci.org/github/chef/kitchen-vcenter)
|
|
7
|
+
|
|
8
|
+
This is the official Chef test-kitchen plugin for VMware REST API. This plugin gives kitchen the ability to create, bootstrap, and test VMware vms.
|
|
9
|
+
- Documentation: [https://github.com/chef/kitchen-vcenter/blob/master/README.md](https://github.com/chef/kitchen-vcenter/blob/master/README.md)
|
|
10
|
+
- Source: [https://github.com/chef/kitchen-vcenter/tree/master](https://github.com/chef/kitchen-vcenter/tree/master)
|
|
11
|
+
- Issues: [https://github.com/chef/kitchen-vcenter/issues](https://github.com/chef/knife-vcenter/issues)
|
|
12
|
+
- Slack: sign up: https://code.vmware.com/slack/ slack channel: #chef
|
|
13
|
+
- Mailing list: [https://discourse.chef.io/](https://discourse.chef.io/)
|
|
14
|
+
|
|
15
|
+
This is a `test-kitchen` plugin that allows interaction with vSphere using the vSphere Automation SDK.
|
|
16
|
+
|
|
17
|
+
Please refer to the [CHANGELOG](CHANGELOG.md) for version history and known issues.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Chef 13.0 higher
|
|
22
|
+
- Ruby 2.3.3 or higher
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Using [ChefDK](https://downloads.chef.io/chef-dk/), simply install the Gem:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
chef gem install kitchen-vcenter
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If you're using bundler, simply add Chef and kitchen-vcenter to your Gemfile:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
gem 'chef'
|
|
36
|
+
gem 'kitchen-vcenter'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
A sample `.kitchen.yml` file, details are below.
|
|
42
|
+
|
|
43
|
+
```yml
|
|
44
|
+
driver:
|
|
45
|
+
name: vcenter
|
|
46
|
+
vcenter_username: <%= ENV['VCENTER_USER'] || "administrator@vsphere.local" %>
|
|
47
|
+
vcenter_password: <%= ENV['VCENTER_PASSWORD'] || "P@ssw0rd!" %>
|
|
48
|
+
vcenter_host: vcenter.chef.io
|
|
49
|
+
vcenter_disable_ssl_verify: true
|
|
50
|
+
driver_config:
|
|
51
|
+
targethost: 172.16.20.41
|
|
52
|
+
datacenter: "Datacenter"
|
|
53
|
+
|
|
54
|
+
platforms:
|
|
55
|
+
- name: ubuntu-1604
|
|
56
|
+
driver_config:
|
|
57
|
+
template: ubuntu16-template
|
|
58
|
+
- name: centos-7
|
|
59
|
+
driver_config:
|
|
60
|
+
template: centos7-template
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Required parameters:
|
|
65
|
+
|
|
66
|
+
The following parameters should be set in the main `driver_config` section as they are common to all platforms.
|
|
67
|
+
|
|
68
|
+
- `vcenter_username` - Name to use when connecting to the vSphere environment
|
|
69
|
+
- `vcenter_password` - Password associated with the specified user
|
|
70
|
+
- `vcenter_host` - Host against which logins should be attempted
|
|
71
|
+
- `vcenter_disable_ssl_verify` - Whether or not to disable SSL verification checks. Good when using self signed certificates. Default: false
|
|
72
|
+
|
|
73
|
+
The following parameters should be set in the `driver_config` for the individual platform.
|
|
74
|
+
|
|
75
|
+
- `targethost` - Host on which the new virtual machine should be created
|
|
76
|
+
- `template` - Template or virtual machine to use when cloning the new machine
|
|
77
|
+
- `datacenter` - Name of the datacenter to use to deploy into
|
|
78
|
+
|
|
79
|
+
### Optional Parameters
|
|
80
|
+
|
|
81
|
+
The following optional parameters should be used in the `driver_config` for the platform.
|
|
82
|
+
|
|
83
|
+
- `folder` - Folder into which the new machine should be stored. If specified the folder _must_ already exist
|
|
84
|
+
|
|
85
|
+
## Contributing
|
|
86
|
+
|
|
87
|
+
For information on contributing to this project see <https://github.com/chef/chef/blob/master/CONTRIBUTING.md>
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
* Report issues/questions/feature requests on [GitHub Issues][issues]
|
|
92
|
+
|
|
93
|
+
Pull requests are very welcome! Make sure your patches are well tested.
|
|
94
|
+
Ideally create a topic branch for every separate change you make. For
|
|
95
|
+
example:
|
|
96
|
+
|
|
97
|
+
1. Fork the repo
|
|
98
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
99
|
+
3. Run the tests and rubocop, `bundle exec rake spec` and `bundle exec rake rubocop`
|
|
100
|
+
4. Commit your changes (`git commit -am 'Added some feature'`)
|
|
101
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
|
102
|
+
6. Create new Pull Request
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
Author:: Russell Seymour ([rseymour@chef.io](mailto:rseymour@chef.io))
|
|
108
|
+
|
|
109
|
+
Author:: JJ Asghar ([jj@chef.io](mailto:jj@chef.io))
|
|
110
|
+
|
|
111
|
+
Copyright:: Copyright (c) 2017 Chef Software, Inc.
|
|
112
|
+
|
|
113
|
+
License:: Apache License, Version 2.0
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
117
|
+
you may not use this file except in compliance with the License.
|
|
118
|
+
You may obtain a copy of the License at
|
|
119
|
+
|
|
120
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
121
|
+
|
|
122
|
+
Unless required by applicable law or agreed to in writing, software
|
|
123
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
124
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
125
|
+
See the License for the specific language governing permissions and
|
|
126
|
+
limitations under the License.
|
|
127
|
+
```
|
data/lib/base.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# Author:: Chef Partner Engineering (<partnereng@chef.io>)
|
|
4
|
+
# Copyright:: Copyright (c) 2017 Chef Software, Inc.
|
|
5
|
+
# License:: Apache License, Version 2.0
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
#
|
|
19
|
+
|
|
20
|
+
require 'logger'
|
|
21
|
+
|
|
22
|
+
module Base
|
|
23
|
+
attr_accessor :log
|
|
24
|
+
|
|
25
|
+
def self.log
|
|
26
|
+
@log ||= init_logger
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.init_logger
|
|
30
|
+
log = Logger.new(STDOUT)
|
|
31
|
+
log.progname = 'Knife VCenter'
|
|
32
|
+
log.level = Logger::INFO
|
|
33
|
+
log
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# Author:: Chef Partner Engineering (<partnereng@chef.io>)
|
|
4
|
+
# Copyright:: Copyright (c) 2017 Chef Software, Inc.
|
|
5
|
+
# License:: Apache License, Version 2.0
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
#
|
|
19
|
+
|
|
20
|
+
module KitchenVcenter
|
|
21
|
+
VERSION = '1.0.0'.freeze
|
|
22
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# Author:: Chef Partner Engineering (<partnereng@chef.io>)
|
|
4
|
+
# Copyright:: Copyright (c) 2017 Chef Software, Inc.
|
|
5
|
+
# License:: Apache License, Version 2.0
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
#
|
|
19
|
+
|
|
20
|
+
require 'kitchen'
|
|
21
|
+
require 'rbvmomi'
|
|
22
|
+
require 'sso'
|
|
23
|
+
require 'base'
|
|
24
|
+
require 'lookup_service_helper'
|
|
25
|
+
require 'vapi'
|
|
26
|
+
require 'com/vmware/cis'
|
|
27
|
+
require 'com/vmware/vcenter'
|
|
28
|
+
require 'com/vmware/vcenter/vm'
|
|
29
|
+
require 'support/clone_vm'
|
|
30
|
+
require 'securerandom'
|
|
31
|
+
|
|
32
|
+
module Kitchen
|
|
33
|
+
module Driver
|
|
34
|
+
#
|
|
35
|
+
# Vcenter
|
|
36
|
+
#
|
|
37
|
+
class Vcenter < Kitchen::Driver::Base
|
|
38
|
+
attr_accessor :connection_options, :ipaddress, :vapi_config, :session_svc, :session_id
|
|
39
|
+
|
|
40
|
+
default_config :targethost
|
|
41
|
+
default_config :folder
|
|
42
|
+
default_config :template
|
|
43
|
+
default_config :datacenter
|
|
44
|
+
default_config :vcenter_username
|
|
45
|
+
default_config :vcenter_password
|
|
46
|
+
default_config :vcenter_host
|
|
47
|
+
default_config :vcenter_disable_ssl_verify, false
|
|
48
|
+
default_config :poweron, true
|
|
49
|
+
default_config :vm_name, nil
|
|
50
|
+
|
|
51
|
+
def create(state)
|
|
52
|
+
# If the vm_name has not been set then set it now based on the suite, platform and a random number
|
|
53
|
+
if config[:vm_name].nil?
|
|
54
|
+
config[:vm_name] = format('%s-%s-%s', instance.suite.name, instance.platform.name, SecureRandom.hex(4))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
connect
|
|
58
|
+
|
|
59
|
+
# Using the clone class, create a machine for TK
|
|
60
|
+
# Find the identifier for the targethost to pass to rbvmomi
|
|
61
|
+
config[:targethost] = get_host(config[:targethost])
|
|
62
|
+
|
|
63
|
+
# Same thing needs to happen with the folder name if it has been set
|
|
64
|
+
config[:folder] = {
|
|
65
|
+
name: config[:folder],
|
|
66
|
+
id: get_folder(config[:folder])
|
|
67
|
+
} unless config[:folder].nil?
|
|
68
|
+
|
|
69
|
+
# Create a hash of options that the clone requires
|
|
70
|
+
options = {
|
|
71
|
+
name: config[:vm_name],
|
|
72
|
+
targethost: config[:targethost],
|
|
73
|
+
poweron: config[:poweron],
|
|
74
|
+
template: config[:template],
|
|
75
|
+
datacenter: config[:datacenter],
|
|
76
|
+
folder: config[:folder]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Create an object from which the clone operation can be called
|
|
80
|
+
clone_obj = Support::CloneVm.new(connection_options, options)
|
|
81
|
+
state[:hostname] = clone_obj.clone()
|
|
82
|
+
state[:vm_name] = config[:vm_name]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def destroy(state)
|
|
86
|
+
return if state[:vm_name].nil?
|
|
87
|
+
connect
|
|
88
|
+
vm = get_vm(state[:vm_name])
|
|
89
|
+
|
|
90
|
+
vm_obj = Com::Vmware::Vcenter::VM.new(vapi_config)
|
|
91
|
+
|
|
92
|
+
# shut the machine down if it is running
|
|
93
|
+
if vm.power_state.value == "POWERED_ON"
|
|
94
|
+
power = Com::Vmware::Vcenter::Vm::Power.new(vapi_config)
|
|
95
|
+
power.stop(vm.vm)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# delete the vm
|
|
99
|
+
vm_obj.delete(vm.vm)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def validate_state(state = {})
|
|
105
|
+
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def existing_state_value?(state, property)
|
|
109
|
+
state.key?(property) && !state[property].nil?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def get_host(name)
|
|
113
|
+
filter = Com::Vmware::Vcenter::Host::FilterSpec.new({names: Set.new([name])})
|
|
114
|
+
host_obj = Com::Vmware::Vcenter::Host.new(vapi_config)
|
|
115
|
+
host = host_obj.list
|
|
116
|
+
host[0].host
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def get_folder(name)
|
|
120
|
+
# Create a filter to ensure that only the named folder is returned
|
|
121
|
+
filter = Com::Vmware::Vcenter::Folder::FilterSpec.new({names: Set.new([name])})
|
|
122
|
+
# filter.names = name
|
|
123
|
+
folder_obj = Com::Vmware::Vcenter::Folder.new(vapi_config)
|
|
124
|
+
folder = folder_obj.list(filter)
|
|
125
|
+
|
|
126
|
+
folder[0].folder
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def get_vm(name)
|
|
130
|
+
filter = Com::Vmware::Vcenter::VM::FilterSpec.new({names: Set.new([name])})
|
|
131
|
+
vm_obj = Com::Vmware::Vcenter::VM.new(vapi_config)
|
|
132
|
+
vm_obj.list(filter)[0]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def connect
|
|
136
|
+
# Configure the connection to vCenter
|
|
137
|
+
lookup_service_helper = LookupServiceHelper.new(config[:vcenter_host])
|
|
138
|
+
vapi_urls = lookup_service_helper.find_vapi_urls()
|
|
139
|
+
vapi_url = vapi_urls.values[0]
|
|
140
|
+
|
|
141
|
+
# Create the VAPI config object
|
|
142
|
+
ssl_options = {}
|
|
143
|
+
ssl_options[:verify] = config[:vcenter_disable_ssl_verify] ? :none : :peer
|
|
144
|
+
@vapi_config = VAPI::Bindings::VapiConfig.new(vapi_url, ssl_options)
|
|
145
|
+
|
|
146
|
+
# get the SSO url
|
|
147
|
+
sso_url = lookup_service_helper.find_sso_url()
|
|
148
|
+
sso = SSO::Connection.new(sso_url).login(config[:vcenter_username], config[:vcenter_password])
|
|
149
|
+
token = sso.request_bearer_token()
|
|
150
|
+
vapi_config.set_security_context(
|
|
151
|
+
VAPI::Security.create_saml_bearer_security_context(token.to_s)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Login and get the session information
|
|
155
|
+
@session_svc = Com::Vmware::Cis::Session.new(vapi_config)
|
|
156
|
+
@session_id = session_svc.create()
|
|
157
|
+
vapi_config.set_security_context(
|
|
158
|
+
VAPI::Security.create_session_security_context(session_id)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Configure the hash for use when connecting for cloning a machine
|
|
162
|
+
@connection_options = {
|
|
163
|
+
user: config[:vcenter_username],
|
|
164
|
+
password: config[:vcenter_password],
|
|
165
|
+
insecure: config[:vcenter_disable_ssl_verify] ? true : false,
|
|
166
|
+
host: config[:vcenter_host],
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# Copyright 2014-2017 VMware, Inc. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
require 'savon'
|
|
5
|
+
require 'nokogiri'
|
|
6
|
+
require 'base'
|
|
7
|
+
# require 'sample/framework/sample_base'
|
|
8
|
+
|
|
9
|
+
# Utility class that helps use the lookup service.
|
|
10
|
+
class LookupServiceHelper
|
|
11
|
+
|
|
12
|
+
attr_reader :sample, :wsdl_url, :soap_url
|
|
13
|
+
attr_reader :serviceRegistration
|
|
14
|
+
|
|
15
|
+
# Constructs a new instance.
|
|
16
|
+
# @param sample [SampleBase] the associated sample, which provides access
|
|
17
|
+
# to the configuration properties of the sample
|
|
18
|
+
def initialize(host)
|
|
19
|
+
|
|
20
|
+
@soap_url = format("https://%s/lookupservice/sdk", host)
|
|
21
|
+
@wsdl_url = format("https://%s/lookupservice/wsdl/lookup.wsdl", host)
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Connects to the lookup service.
|
|
26
|
+
def connect
|
|
27
|
+
rsc = RetrieveServiceContent.new(client).invoke()
|
|
28
|
+
@serviceRegistration = rsc.get_service_registration()
|
|
29
|
+
Base.log.info "service registration = #{serviceRegistration}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Finds the SSO service URL.
|
|
33
|
+
# In a MxN setup where there are more than one PSC nodes;
|
|
34
|
+
# This method returns the first SSO service endpoint URL
|
|
35
|
+
# as returned by the lookup service.
|
|
36
|
+
#
|
|
37
|
+
# @return [String] SSO Service endpoint URL.
|
|
38
|
+
def find_sso_url
|
|
39
|
+
result = find_service_url(product='com.vmware.cis',
|
|
40
|
+
service='cs.identity',
|
|
41
|
+
endpoint='com.vmware.cis.cs.identity.sso',
|
|
42
|
+
protocol='wsTrust')
|
|
43
|
+
raise 'SSO URL not found' unless result && result.size > 0
|
|
44
|
+
return result.values[0]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Finds all the vAPI service endpoint URLs.
|
|
48
|
+
# In a MxN setup where there are more than one management node;
|
|
49
|
+
# this method returns more than one URL
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash] vapi service endpoint URLs in a dictionary
|
|
52
|
+
# where the key is the node_id and the value is the service URL.
|
|
53
|
+
def find_vapi_urls
|
|
54
|
+
return find_service_url(product='com.vmware.cis',
|
|
55
|
+
service='cs.vapi',
|
|
56
|
+
endpoint='com.vmware.vapi.endpoint',
|
|
57
|
+
protocol='vapi.json.https.public')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Finds the vapi service endpoint URL of a management node.
|
|
61
|
+
#
|
|
62
|
+
# @param node_id [String] The UUID of the management node.
|
|
63
|
+
# @return [String] vapi service endpoint URL of a management node or
|
|
64
|
+
# nil if no vapi endpoint is found.
|
|
65
|
+
def find_vapi_url(node_id)
|
|
66
|
+
raise 'node_id is required' if node_id.nil?
|
|
67
|
+
result = find_vapi_urls()
|
|
68
|
+
raise 'VAPI URLs not found' unless result && result.size > 0
|
|
69
|
+
return result[node_id]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Finds all the vim service endpoint URLs
|
|
73
|
+
# In a MxN setup where there are more than one management node;
|
|
74
|
+
# this method returns more than one URL
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash] vim service endpoint URLs in a dictionary where
|
|
77
|
+
# the key is the node_id and the value is the service URL.
|
|
78
|
+
def find_vim_urls
|
|
79
|
+
return find_service_url(product='com.vmware.cis',
|
|
80
|
+
service='vcenterserver',
|
|
81
|
+
endpoint='com.vmware.vim',
|
|
82
|
+
protocol='vmomi')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Finds the vim service endpoint URL of a management node
|
|
86
|
+
#
|
|
87
|
+
# @param node_id [String] The UUID of the management node.
|
|
88
|
+
# @return [String] vim service endpoint URL of a management node or
|
|
89
|
+
# nil if no vim endpoint is found.
|
|
90
|
+
def find_vim_url(node_id)
|
|
91
|
+
raise 'node_id is required' if node_id.nil?
|
|
92
|
+
result = find_vim_urls()
|
|
93
|
+
raise 'VIM URLs not found' unless result && result.size > 0
|
|
94
|
+
return result[node_id]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Finds all the spbm service endpoint URLs
|
|
98
|
+
# In a MxN setup where there are more than one management node;
|
|
99
|
+
# this method returns more than one URL
|
|
100
|
+
#
|
|
101
|
+
# @return [Hash] spbm service endpoint URLs in a dictionary where
|
|
102
|
+
# the key is the node_id and the value is the service URL.
|
|
103
|
+
def find_vim_pbm_urls
|
|
104
|
+
return find_service_url(product='com.vmware.vim.sms',
|
|
105
|
+
service='sms',
|
|
106
|
+
endpoint='com.vmware.vim.pbm',
|
|
107
|
+
protocol='https')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Finds the spbm service endpoint URL of a management node
|
|
111
|
+
#
|
|
112
|
+
# @param node_id [String] The UUID of the management node.
|
|
113
|
+
# @return [String] spbm service endpoint URL of a management node or
|
|
114
|
+
# nil if no spbm endpoint is found.
|
|
115
|
+
def find_vim_pbm_url(node_id)
|
|
116
|
+
raise 'node_id is required' if node_id.nil?
|
|
117
|
+
result = find_vim_pbm_urls()
|
|
118
|
+
raise 'PBM URLs not found' unless result && result.size > 0
|
|
119
|
+
return result[node_id]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get the management node id from the instance name
|
|
123
|
+
#
|
|
124
|
+
# @param instance_name [String] The instance name of the management node
|
|
125
|
+
# @return [String] The UUID of the management node or
|
|
126
|
+
# nil is no management node is found by the given instance name
|
|
127
|
+
def get_mgmt_node_id(instance_name)
|
|
128
|
+
raise 'instance_name is required' if instance_name.nil?
|
|
129
|
+
result = find_mgmt_nodes()
|
|
130
|
+
raise 'Management nodes not found' unless result && result.size > 0
|
|
131
|
+
return result[instance_name]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def get_mgmt_node_instance_name(node_id)
|
|
135
|
+
raise 'node_id is required' if node_id.nil?
|
|
136
|
+
result = find_mgmt_nodes()
|
|
137
|
+
raise 'Management nodes not found' unless result && result.size > 0
|
|
138
|
+
result.each { |k, v| return k if v == node_id }
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Finds the instance name and UUID of the management node for M1xN1 or
|
|
143
|
+
# when the PSC and management services all reside on a single node.
|
|
144
|
+
def get_default_mgmt_node
|
|
145
|
+
result = find_mgmt_nodes()
|
|
146
|
+
raise 'Management nodes not found' unless result && result.size > 0
|
|
147
|
+
#WHY: raise MultipleManagementNodeException.new if result.size > 1
|
|
148
|
+
return [result.keys[0], result.values[0]]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Finds all the management nodes
|
|
152
|
+
#
|
|
153
|
+
# @return [Hash] management node instance name and node id (UUID) in a dictionary.
|
|
154
|
+
def find_mgmt_nodes
|
|
155
|
+
#assert self.serviceRegistration is not None
|
|
156
|
+
list = List.new(client, 'com.vmware.cis', 'vcenterserver',
|
|
157
|
+
'vmomi', 'com.vmware.vim')
|
|
158
|
+
|
|
159
|
+
list.invoke()
|
|
160
|
+
list.get_instance_names()
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
# Finds a service URL with the given attributes.
|
|
166
|
+
def find_service_url(product, service, endpoint, protocol)
|
|
167
|
+
#assert serviceRegistration is not None
|
|
168
|
+
list = List.new(client, product, service, protocol, endpoint)
|
|
169
|
+
|
|
170
|
+
list.invoke()
|
|
171
|
+
list.get_service_endpoints()
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Gets or creates the Savon client instance.
|
|
175
|
+
def client
|
|
176
|
+
@client ||= Savon.client do |globals|
|
|
177
|
+
# see: http://savonrb.com/version2/globals.html
|
|
178
|
+
globals.wsdl wsdl_url
|
|
179
|
+
globals.endpoint soap_url
|
|
180
|
+
|
|
181
|
+
globals.strip_namespaces false
|
|
182
|
+
globals.env_namespace :S
|
|
183
|
+
|
|
184
|
+
# set like this so https connection does not fail
|
|
185
|
+
# TODO: find an acceptable solution for production
|
|
186
|
+
globals.ssl_verify_mode :none
|
|
187
|
+
|
|
188
|
+
# dev/debug settings
|
|
189
|
+
# globals.pretty_print_xml ENV['DEBUG_SOAP']
|
|
190
|
+
# globals.log ENV['DEBUG_SOAP']
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# @abstract Base class for invocable service calls.
|
|
197
|
+
class Invocable
|
|
198
|
+
|
|
199
|
+
attr_reader :operation, :client, :response
|
|
200
|
+
|
|
201
|
+
# Constructs a new instance.
|
|
202
|
+
# @param operation [Symbol] the operation name
|
|
203
|
+
# @param client [Savon::Client] the client
|
|
204
|
+
def initialize(operation, client)
|
|
205
|
+
@operation = operation
|
|
206
|
+
@client = client
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Invokes the service call represented by this type.
|
|
210
|
+
def invoke
|
|
211
|
+
request = request_xml.to_s
|
|
212
|
+
Base.log.debug(request)
|
|
213
|
+
@response = client.call(operation, xml:request)
|
|
214
|
+
Base.log.debug(response)
|
|
215
|
+
self # for chaining with new
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Builds the request XML content.
|
|
219
|
+
def request_xml
|
|
220
|
+
builder = Builder::XmlMarkup.new()
|
|
221
|
+
builder.instruct!(:xml, encoding: "UTF-8")
|
|
222
|
+
|
|
223
|
+
builder.tag!("S:Envelope",
|
|
224
|
+
"xmlns:S" => "http://schemas.xmlsoap.org/soap/envelope/") do |envelope|
|
|
225
|
+
envelope.tag!("S:Body") do |body|
|
|
226
|
+
body_xml(body)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
builder.target!
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Builds the body portion of the request XML content.
|
|
233
|
+
# Specific service operations must override this method.
|
|
234
|
+
def body_xml
|
|
235
|
+
raise 'abstract method not implemented!'
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Gets the response XML content.
|
|
239
|
+
def response_xml
|
|
240
|
+
raise 'illegal state: response not set yet' if response.nil?
|
|
241
|
+
@response_xml ||= Nokogiri::XML(response.to_xml)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def response_hash
|
|
245
|
+
@response_hash ||= response.to_hash
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Encapsulates the list operation of the lookup service.
|
|
250
|
+
class List < Invocable
|
|
251
|
+
|
|
252
|
+
# Constructs a new instance.
|
|
253
|
+
def initialize(client, product, service, protocol, endpoint)
|
|
254
|
+
super(:list, client)
|
|
255
|
+
|
|
256
|
+
@product = product
|
|
257
|
+
@service = service
|
|
258
|
+
@protocol = protocol
|
|
259
|
+
@endpoint = endpoint
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
=begin
|
|
263
|
+
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
|
|
264
|
+
<S:Body>
|
|
265
|
+
<List xmlns="urn:lookup">
|
|
266
|
+
<_this type="LookupServiceRegistration">ServiceRegistration</_this>
|
|
267
|
+
<filterCriteria>
|
|
268
|
+
<serviceType>
|
|
269
|
+
<product>com.vmware.cis</product>
|
|
270
|
+
<type>cs.identity</type>
|
|
271
|
+
</serviceType>
|
|
272
|
+
<endpointType>
|
|
273
|
+
<protocol>wsTrust</protocol>
|
|
274
|
+
<type>com.vmware.cis.cs.identity.sso</type>
|
|
275
|
+
</endpointType>
|
|
276
|
+
</filterCriteria>
|
|
277
|
+
</List>
|
|
278
|
+
</S:Body>
|
|
279
|
+
</S:Envelope>
|
|
280
|
+
=end
|
|
281
|
+
def body_xml(body)
|
|
282
|
+
body.tag!("List", "xmlns" => "urn:lookup") do |list|
|
|
283
|
+
#TODO: use the copy that was retrieved on startup?
|
|
284
|
+
list.tag!("_this",
|
|
285
|
+
"type" => "LookupServiceRegistration") do |this|
|
|
286
|
+
this << "ServiceRegistration"
|
|
287
|
+
end
|
|
288
|
+
list.tag!("filterCriteria") do |criteria|
|
|
289
|
+
criteria.tag!("serviceType") do |stype|
|
|
290
|
+
stype.tag!("product") do |p|
|
|
291
|
+
p << @product
|
|
292
|
+
end
|
|
293
|
+
stype.tag!("type") do |t|
|
|
294
|
+
t << @service
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
criteria.tag!("endpointType") do |etype|
|
|
298
|
+
etype.tag!("protocol") do |p|
|
|
299
|
+
p << @protocol
|
|
300
|
+
end
|
|
301
|
+
etype.tag!("type") do |t|
|
|
302
|
+
t << @endpoint
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Gets the service endpoint information from the response.
|
|
310
|
+
# Support for MxN.
|
|
311
|
+
# @return [Hash] a hash where the key is NodeId and the Value is a Service URL
|
|
312
|
+
def get_service_endpoints
|
|
313
|
+
result = {}
|
|
314
|
+
=begin
|
|
315
|
+
<ListResponse xmlns="urn:lookup">
|
|
316
|
+
<returnval>
|
|
317
|
+
<serviceVersion>2.0</serviceVersion>
|
|
318
|
+
<vendorNameResourceKey/>
|
|
319
|
+
<vendorNameDefault/>
|
|
320
|
+
<vendorProductInfoResourceKey/>
|
|
321
|
+
<vendorProductInfoDefault/>
|
|
322
|
+
<serviceEndpoints>
|
|
323
|
+
<url>https://pa-rdinfra3-vm7-dhcp5583.eng.vmware.com/sts/STSService/vsphere.local</url>
|
|
324
|
+
<endpointType>
|
|
325
|
+
<protocol>wsTrust</protocol>
|
|
326
|
+
<type>com.vmware.cis.cs.identity.sso</type>
|
|
327
|
+
</endpointType>
|
|
328
|
+
<sslTrust>
|
|
329
|
+
...
|
|
330
|
+
</sslTrust>
|
|
331
|
+
</serviceEndpoints>
|
|
332
|
+
<serviceNameResourceKey/>
|
|
333
|
+
<serviceNameDefault/>
|
|
334
|
+
<serviceDescriptionResourceKey/>
|
|
335
|
+
<serviceDescriptionDefault/>
|
|
336
|
+
<ownerId>pa-rdinfra3-vm7-dhcp5583.eng.vmware.com@vsphere.local</ownerId>
|
|
337
|
+
<serviceType>
|
|
338
|
+
<product>com.vmware.cis</product>
|
|
339
|
+
<type>cs.identity</type>
|
|
340
|
+
</serviceType>
|
|
341
|
+
<nodeId/>
|
|
342
|
+
<serviceId>6a8a5058-5d3d-4d42-bb5e-383b91c8732e</serviceId>
|
|
343
|
+
<siteId>default-first-site</siteId>
|
|
344
|
+
</returnval>
|
|
345
|
+
</ListResponse>
|
|
346
|
+
=end
|
|
347
|
+
Base.log.debug "List: response_hash = #{response_hash}"
|
|
348
|
+
return_val = response_hash[:list_response][:returnval]
|
|
349
|
+
return_val = [return_val] if return_val.is_a? Hash
|
|
350
|
+
return_val.each { |entry|
|
|
351
|
+
#FYI: the node_id is sometimes null, so use the service_id in this case
|
|
352
|
+
node_id = entry[:node_id] || entry[:service_id]
|
|
353
|
+
result[node_id] = entry[:service_endpoints][:url]
|
|
354
|
+
}
|
|
355
|
+
Base.log.debug "List: result = #{result}"
|
|
356
|
+
return result
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def get_instance_names
|
|
360
|
+
result = {}
|
|
361
|
+
=begin
|
|
362
|
+
<serviceAttributes>
|
|
363
|
+
<key>com.vmware.cis.cm.GroupInternalId</key>
|
|
364
|
+
<value>com.vmware.vim.vcenter</value>
|
|
365
|
+
</serviceAttributes>
|
|
366
|
+
<serviceAttributes>
|
|
367
|
+
<key>com.vmware.cis.cm.ControlScript</key>
|
|
368
|
+
<value>vmware-vpxd.sh</value>
|
|
369
|
+
</serviceAttributes>
|
|
370
|
+
<serviceAttributes>
|
|
371
|
+
<key>com.vmware.cis.cm.HostId</key>
|
|
372
|
+
<value>906477a1-24c6-4d48-9e99-55ef962878f7</value>
|
|
373
|
+
</serviceAttributes>
|
|
374
|
+
<serviceAttributes>
|
|
375
|
+
<key>com.vmware.vim.vcenter.instanceName</key>
|
|
376
|
+
<value>pa-rdinfra3-vm7-dhcp5583.eng.vmware.com</value>
|
|
377
|
+
</serviceAttributes>
|
|
378
|
+
=end
|
|
379
|
+
Base.log.debug "List: response_hash = #{response_hash}"
|
|
380
|
+
return_val = response_hash[:list_response][:returnval]
|
|
381
|
+
return_val = [return_val] if return_val.is_a? Hash
|
|
382
|
+
return_val.each { |entry|
|
|
383
|
+
node_id = entry[:node_id]
|
|
384
|
+
#TODO: is it possible there be 0 or 1 attrs? if so, deal with it.
|
|
385
|
+
attrs = entry[:service_attributes]
|
|
386
|
+
Base.log.debug "List: attrs=#{attrs}"
|
|
387
|
+
attrs.each { |attr|
|
|
388
|
+
if attr[:key] == 'com.vmware.vim.vcenter.instanceName'
|
|
389
|
+
result[attr[:value]] = node_id
|
|
390
|
+
end
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
Base.log.debug "List: result = #{result}"
|
|
394
|
+
return result
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Encapsulates the RetrieveServiceContent operation of the lookup service.
|
|
399
|
+
class RetrieveServiceContent < Invocable
|
|
400
|
+
|
|
401
|
+
# Constructs a new instance.
|
|
402
|
+
def initialize(client)
|
|
403
|
+
super(:retrieve_service_content, client)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
=begin
|
|
407
|
+
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
|
|
408
|
+
<S:Body>
|
|
409
|
+
<RetrieveServiceContent xmlns="urn:lookup">
|
|
410
|
+
<_this type="LookupServiceInstance">ServiceInstance</_this>
|
|
411
|
+
</RetrieveServiceContent>
|
|
412
|
+
</S:Body>
|
|
413
|
+
</S:Envelope>
|
|
414
|
+
=end
|
|
415
|
+
def body_xml(body)
|
|
416
|
+
body.tag!("RetrieveServiceContent", "xmlns" => "urn:lookup") do |rsc|
|
|
417
|
+
rsc.tag!("_this", "type" => "LookupServiceInstance") do |this|
|
|
418
|
+
this << "ServiceInstance"
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
=begin
|
|
424
|
+
...
|
|
425
|
+
<RetrieveServiceContentResponse xmlns="urn:lookup">
|
|
426
|
+
<returnval>
|
|
427
|
+
<lookupService type="LookupLookupService">lookupService</lookupService>
|
|
428
|
+
<serviceRegistration type="LookupServiceRegistration">ServiceRegistration</serviceRegistration>
|
|
429
|
+
<deploymentInformationService type="LookupDeploymentInformationService">deploymentInformationService</deploymentInformationService>
|
|
430
|
+
<l10n type="LookupL10n">l10n</l10n>
|
|
431
|
+
</returnval>
|
|
432
|
+
</RetrieveServiceContentResponse>
|
|
433
|
+
...
|
|
434
|
+
=end
|
|
435
|
+
def get_service_registration
|
|
436
|
+
Base.log.debug "RetrieveServiceContent: response_hash = #{response_hash}"
|
|
437
|
+
return_val = response_hash[:retrieve_service_content_response][:returnval]
|
|
438
|
+
result = return_val[:service_registration]
|
|
439
|
+
Base.log.debug "RetrieveServiceContent: result = #{result}"
|
|
440
|
+
result
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
class MultipleManagementNodeException < Exception
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# main: quick self tester
|
|
448
|
+
if __FILE__ == $0
|
|
449
|
+
Base.log.level = Logger::DEBUG if ENV['DEBUG']
|
|
450
|
+
sample = SelfTestSample.new
|
|
451
|
+
sample.ls_ip = ARGV[0] || '10.67.245.207'
|
|
452
|
+
#MXN: sample.ls_ip = '10.160.42.83'
|
|
453
|
+
#MXN: sample.ls_ip = '10.160.35.191'
|
|
454
|
+
#MAYBE: sample.main() # for arg parsing
|
|
455
|
+
ls_helper = LookupServiceHelper.new(sample)
|
|
456
|
+
ls_helper.connect()
|
|
457
|
+
puts '***************************************'
|
|
458
|
+
puts "SSO URL: #{ls_helper.find_sso_url()}"
|
|
459
|
+
puts "VAPI URL: #{ls_helper.find_vapi_urls()}"
|
|
460
|
+
puts "VIM URL: #{ls_helper.find_vim_urls()}"
|
|
461
|
+
puts "PBM URL: #{ls_helper.find_vim_pbm_urls()}"
|
|
462
|
+
puts "Mgmt Nodes: #{ls_helper.find_mgmt_nodes()}"
|
|
463
|
+
end
|
data/lib/sso.rb
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Copyright 2014-2017 VMware, Inc. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
require 'savon'
|
|
5
|
+
require 'nokogiri'
|
|
6
|
+
require 'date'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
|
|
9
|
+
# A little utility library for VMware SSO.
|
|
10
|
+
# For now, this is not a general purpose library that covers all
|
|
11
|
+
# the interfaces of the SSO service.
|
|
12
|
+
# Specifically, the support is limited to the following:
|
|
13
|
+
# * request bearer token.
|
|
14
|
+
module SSO
|
|
15
|
+
|
|
16
|
+
# The XML date format.
|
|
17
|
+
DATE_FORMAT = "%FT%T.%LZ"
|
|
18
|
+
|
|
19
|
+
# The XML namespaces that are required: SOAP, WSDL, et al.
|
|
20
|
+
NAMESPACES = {
|
|
21
|
+
"xmlns:S" => "http://schemas.xmlsoap.org/soap/envelope/",
|
|
22
|
+
"xmlns:wst" => "http://docs.oasis-open.org/ws-sx/ws-trust/200512",
|
|
23
|
+
"xmlns:u" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
|
|
24
|
+
"xmlns:x" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Provides the connection details for the SSO service.
|
|
28
|
+
class Connection
|
|
29
|
+
|
|
30
|
+
attr_accessor :sso_url, :wsdl_url, :username, :password
|
|
31
|
+
|
|
32
|
+
# Creates a new instance.
|
|
33
|
+
def initialize(sso_url, wsdl_url=nil)
|
|
34
|
+
self.sso_url = sso_url
|
|
35
|
+
self.wsdl_url = wsdl_url || "#{sso_url}?wsdl"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Login with the given credentials.
|
|
39
|
+
# Note: this does not invoke a login action, but rather stores the
|
|
40
|
+
# credentials for use later.
|
|
41
|
+
def login(username, password)
|
|
42
|
+
self.username = username
|
|
43
|
+
self.password = password
|
|
44
|
+
self # enable builder pattern
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Gets (or creates) the Savon client instance.
|
|
48
|
+
def client
|
|
49
|
+
# construct and init the client proxy
|
|
50
|
+
@client ||= Savon.client do |globals|
|
|
51
|
+
# see: http://savonrb.com/version2/globals.html
|
|
52
|
+
globals.wsdl wsdl_url
|
|
53
|
+
globals.endpoint sso_url
|
|
54
|
+
|
|
55
|
+
globals.strip_namespaces false
|
|
56
|
+
globals.env_namespace :S
|
|
57
|
+
|
|
58
|
+
# set like this so https connection does not fail
|
|
59
|
+
# TODO: find an acceptable solution for production
|
|
60
|
+
globals.ssl_verify_mode :none
|
|
61
|
+
|
|
62
|
+
# dev/debug settings
|
|
63
|
+
# globals.pretty_print_xml ENV['DEBUG_SOAP']
|
|
64
|
+
# globals.log ENV['DEBUG_SOAP']
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Invokes the request bearer token operation.
|
|
69
|
+
# @return [SamlToken]
|
|
70
|
+
def request_bearer_token()
|
|
71
|
+
rst = RequestSecurityToken.new(client, username, password)
|
|
72
|
+
rst.invoke()
|
|
73
|
+
rst.saml_token
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @abstract Base class for invocable service calls.
|
|
78
|
+
class SoapInvocable
|
|
79
|
+
|
|
80
|
+
attr_reader :operation, :client, :response
|
|
81
|
+
|
|
82
|
+
# Constructs a new instance.
|
|
83
|
+
# @param operation [Symbol] the SOAP operation name (in Symbol form)
|
|
84
|
+
# @param client [Savon::Client] the client
|
|
85
|
+
def initialize(operation, client)
|
|
86
|
+
@operation = operation
|
|
87
|
+
@client = client
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Invokes the service call represented by this type.
|
|
91
|
+
def invoke
|
|
92
|
+
request = request_xml.to_s
|
|
93
|
+
puts "request = #{request}" if ENV['DEBUG']
|
|
94
|
+
@response = client.call(operation, xml:request)
|
|
95
|
+
puts "response = #{response}" if ENV['DEBUG']
|
|
96
|
+
self # for chaining with new
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Builds the request XML content.
|
|
100
|
+
def request_xml
|
|
101
|
+
builder = Builder::XmlMarkup.new()
|
|
102
|
+
builder.instruct!(:xml, encoding: "UTF-8")
|
|
103
|
+
|
|
104
|
+
builder.tag!("S:Envelope", NAMESPACES) do |envelope|
|
|
105
|
+
if has_header?
|
|
106
|
+
envelope.tag!("S:Header") do |header|
|
|
107
|
+
header_xml(header)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
envelope.tag!("S:Body") do |body|
|
|
111
|
+
body_xml(body)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
builder.target!
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def has_header?
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Builds the header portion of the SOAP request.
|
|
122
|
+
# Specific service operations must override this method.
|
|
123
|
+
def header_xml(header)
|
|
124
|
+
raise 'abstract method not implemented!'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Builds the body portion of the SOAP request.
|
|
128
|
+
# Specific service operations must override this method.
|
|
129
|
+
def body_xml(body)
|
|
130
|
+
raise 'abstract method not implemented!'
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Gets the response XML content.
|
|
134
|
+
def response_xml
|
|
135
|
+
raise 'illegal state: response not set yet' if response.nil?
|
|
136
|
+
@response_xml ||= Nokogiri::XML(response.to_xml)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def response_hash
|
|
140
|
+
@response_hash ||= response.to_hash
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Encapsulates an issue operation that requests a security token
|
|
145
|
+
# from the SSO service.
|
|
146
|
+
class RequestSecurityToken < SoapInvocable
|
|
147
|
+
|
|
148
|
+
attr_accessor :request_type, :delegatable
|
|
149
|
+
|
|
150
|
+
# Constructs a new instance.
|
|
151
|
+
def initialize(client, username, password, hours=2)
|
|
152
|
+
super(:issue, client)
|
|
153
|
+
|
|
154
|
+
@username = username
|
|
155
|
+
@password = password
|
|
156
|
+
@hours = hours
|
|
157
|
+
|
|
158
|
+
#TODO: these things should be configurable, so we can get
|
|
159
|
+
#non-delegatable tokens, HoK tokens, etc.
|
|
160
|
+
@request_type = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue"
|
|
161
|
+
@delegatable = true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def now
|
|
165
|
+
@now ||= Time.now.utc.to_datetime
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def created
|
|
169
|
+
@created ||= now.strftime(DATE_FORMAT)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def future
|
|
173
|
+
@future ||= now + (2/24.0) #days (for DateTime math)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def expires
|
|
177
|
+
@expires ||= future.strftime(DATE_FORMAT)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Builds the header XML for the SOAP request.
|
|
181
|
+
def header_xml(header)
|
|
182
|
+
id = "uuid-" + SecureRandom.uuid
|
|
183
|
+
|
|
184
|
+
#header.tag!("x:Security", "x:mustUnderstand" => "1") do |security|
|
|
185
|
+
header.tag!("x:Security") do |security|
|
|
186
|
+
security.tag!("u:Timestamp", "u:Id" => "_0") do |timestamp|
|
|
187
|
+
timestamp.tag!("u:Created") do |element|
|
|
188
|
+
element << created
|
|
189
|
+
end
|
|
190
|
+
timestamp.tag!("u:Expires") do |element|
|
|
191
|
+
element << expires
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
security.tag!("x:UsernameToken", "u:Id" => id) do |utoken|
|
|
195
|
+
utoken.tag!("x:Username") do |element|
|
|
196
|
+
element << @username
|
|
197
|
+
end
|
|
198
|
+
utoken.tag!("x:Password") do |element|
|
|
199
|
+
element << @password
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Builds the body XML for the SOAP request.
|
|
206
|
+
def body_xml(body)
|
|
207
|
+
body.tag!("wst:RequestSecurityToken") do |rst|
|
|
208
|
+
rst.tag!("wst:RequestType") do |element|
|
|
209
|
+
element << request_type
|
|
210
|
+
end
|
|
211
|
+
rst.tag!("wst:Delegatable") do |element|
|
|
212
|
+
element << delegatable.to_s
|
|
213
|
+
end
|
|
214
|
+
=begin
|
|
215
|
+
#TODO: we don't seem to need this, but I'm leaving this
|
|
216
|
+
#here for now as a reminder.
|
|
217
|
+
rst.tag!("wst:Lifetime") do |lifetime|
|
|
218
|
+
lifetime.tag!("u:Created") do |element|
|
|
219
|
+
element << created
|
|
220
|
+
end
|
|
221
|
+
lifetime.tag!("u:Expires") do |element|
|
|
222
|
+
element << expires
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
=end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Gets the saml_token from the SOAP response body.
|
|
230
|
+
# @return [SamlToken] the requested SAML token
|
|
231
|
+
def saml_token
|
|
232
|
+
assertion = response_xml.at_xpath('//saml2:Assertion',
|
|
233
|
+
'saml2' => 'urn:oasis:names:tc:SAML:2.0:assertion')
|
|
234
|
+
SamlToken.new(assertion)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Holds a SAML token.
|
|
239
|
+
class SamlToken
|
|
240
|
+
attr_reader :xml
|
|
241
|
+
|
|
242
|
+
# Creates a new instance.
|
|
243
|
+
def initialize(xml)
|
|
244
|
+
@xml = xml
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
#TODO: add some getters for interesting content
|
|
248
|
+
|
|
249
|
+
def to_s
|
|
250
|
+
esc_token = xml.to_xml(:indent => 0, :encoding => 'UTF-8')
|
|
251
|
+
esc_token = esc_token.gsub(/\n/, '')
|
|
252
|
+
esc_token
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# main: quick self tester
|
|
258
|
+
if __FILE__ == $0
|
|
259
|
+
cloudvm_ip = ARGV[0]
|
|
260
|
+
cloudvm_ip ||= "10.20.17.0"
|
|
261
|
+
#cloudvm_ip ||= "10.67.245.207"
|
|
262
|
+
sso_url = "https://#{cloudvm_ip}/sts/STSService/vsphere.local"
|
|
263
|
+
wsdl_url = "#{sso_url}?wsdl"
|
|
264
|
+
sso = SSO::Connection.new(sso_url, wsdl_url)
|
|
265
|
+
#sso.login("administrator@vsphere.local", "Admin!23")
|
|
266
|
+
sso.login("root", "vmware")
|
|
267
|
+
token = sso.request_bearer_token
|
|
268
|
+
puts token.to_s
|
|
269
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require 'rbvmomi'
|
|
2
|
+
|
|
3
|
+
class Support
|
|
4
|
+
class CloneVm
|
|
5
|
+
attr_reader :vim, :options
|
|
6
|
+
|
|
7
|
+
def initialize(conn_opts, options)
|
|
8
|
+
@options = options
|
|
9
|
+
|
|
10
|
+
# Connect to vSphere
|
|
11
|
+
@vim ||= RbVmomi::VIM.connect conn_opts
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def clone
|
|
15
|
+
|
|
16
|
+
# set the datacenter name
|
|
17
|
+
dc = vim.serviceInstance.find_datacenter(options[:datacenter])
|
|
18
|
+
src_vm = dc.find_vm(options[:template])
|
|
19
|
+
hosts = dc.hostFolder.children
|
|
20
|
+
|
|
21
|
+
# Specify where the machine is going to be created
|
|
22
|
+
relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec
|
|
23
|
+
relocate_spec.host = options[:targethost]
|
|
24
|
+
relocate_spec.pool = hosts.first.resourcePool
|
|
25
|
+
|
|
26
|
+
clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(location: relocate_spec,
|
|
27
|
+
powerOn: options[:poweron],
|
|
28
|
+
template: false)
|
|
29
|
+
|
|
30
|
+
# Set the folder to use
|
|
31
|
+
dest_folder = options[:folder].nil? ? src_vm.parent : options[:folder][:id]
|
|
32
|
+
|
|
33
|
+
puts "Cloning the template #{options[:template]} to create the VM..."
|
|
34
|
+
task = src_vm.CloneVM_Task(folder: dest_folder, name: options[:name], spec: clone_spec)
|
|
35
|
+
task.wait_for_completion
|
|
36
|
+
|
|
37
|
+
# get the IP address of the machine for bootstrapping
|
|
38
|
+
# machine name is based on the path, e.g. that includes the folder
|
|
39
|
+
name = options[:folder].nil? ? options[:name] : format("%s/%s", options[:folder][:name], options[:name])
|
|
40
|
+
new_vm = dc.find_vm(name)
|
|
41
|
+
|
|
42
|
+
if new_vm.nil?
|
|
43
|
+
puts format("Unable to find machine: %s", name)
|
|
44
|
+
else
|
|
45
|
+
puts 'Waiting for network interfaces to become available...'
|
|
46
|
+
sleep 2 while new_vm.guest.net.empty? || !new_vm.guest.ipAddress
|
|
47
|
+
new_vm.guest.net[0].ipConfig.ipAddress.detect do |addr|
|
|
48
|
+
addr.origin != 'linklayer'
|
|
49
|
+
end.ipAddress
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kitchen-vcenter
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Russell Seymour
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-08-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: test-kitchen
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.16'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.16'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: vsphere-automation-sdk
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.5'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.5'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rbvmomi
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.11'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.11'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: savon
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '2.11'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '2.11'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: bundler
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.7'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.7'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: github_changelog_generator
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rake
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '10.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '10.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rubocop
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0.49'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0.49'
|
|
125
|
+
description: Test Kitchen driver for VMware vCenter using SDK
|
|
126
|
+
email:
|
|
127
|
+
- russell@chef.io
|
|
128
|
+
executables: []
|
|
129
|
+
extensions: []
|
|
130
|
+
extra_rdoc_files: []
|
|
131
|
+
files:
|
|
132
|
+
- CHANGELOG.md
|
|
133
|
+
- README.md
|
|
134
|
+
- lib/base.rb
|
|
135
|
+
- lib/kitchen-vcenter/version.rb
|
|
136
|
+
- lib/kitchen/driver/vcenter.rb
|
|
137
|
+
- lib/lookup_service_helper.rb
|
|
138
|
+
- lib/sso.rb
|
|
139
|
+
- lib/support/clone_vm.rb
|
|
140
|
+
homepage: https://github.com/chef/kitchen-vcenter
|
|
141
|
+
licenses:
|
|
142
|
+
- Apache-2.0
|
|
143
|
+
metadata: {}
|
|
144
|
+
post_install_message:
|
|
145
|
+
rdoc_options: []
|
|
146
|
+
require_paths:
|
|
147
|
+
- lib
|
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0'
|
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - ">="
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '0'
|
|
158
|
+
requirements: []
|
|
159
|
+
rubyforge_project:
|
|
160
|
+
rubygems_version: 2.6.11
|
|
161
|
+
signing_key:
|
|
162
|
+
specification_version: 4
|
|
163
|
+
summary: Test Kitchen driver for VMare vCenter
|
|
164
|
+
test_files: []
|