kitchen-vcenter 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/kitchen-vcenter.svg)](https://rubygems.org/gems/kitchen-vcenter)
|
4
|
+
[![Build Status](https://travis-ci.org/chef/kitchen-vcenter.svg?branch=master)](https://travis-ci.org/chef/kitchen-vcenter)
|
5
|
+
[![Dependency Status](https://gemnasium.com/chef/kitchen-vcenter.svg)](https://gemnasium.com/chef/kitchen-vcenter)
|
6
|
+
[![Inline docs](http://inch-ci.org/github/chef/kitchen-vcenter.svg?branch=master)](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: []
|