vcloud-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +103 -0
- data/Rakefile +18 -0
- data/bin/vcloud-query +84 -0
- data/jenkins.sh +10 -0
- data/lib/vcloud/core.rb +23 -0
- data/lib/vcloud/core/compute_metadata.rb +13 -0
- data/lib/vcloud/core/edge_gateway.rb +46 -0
- data/lib/vcloud/core/entity.rb +23 -0
- data/lib/vcloud/core/metadata_helper.rb +29 -0
- data/lib/vcloud/core/org_vdc_network.rb +102 -0
- data/lib/vcloud/core/query.rb +142 -0
- data/lib/vcloud/core/vapp.rb +118 -0
- data/lib/vcloud/core/vapp_template.rb +39 -0
- data/lib/vcloud/core/vdc.rb +37 -0
- data/lib/vcloud/core/version.rb +5 -0
- data/lib/vcloud/core/vm.rb +162 -0
- data/lib/vcloud/fog.rb +5 -0
- data/lib/vcloud/fog/content_types.rb +20 -0
- data/lib/vcloud/fog/model_interface.rb +33 -0
- data/lib/vcloud/fog/relation.rb +8 -0
- data/lib/vcloud/fog/service_interface.rb +257 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/stub_fog_interface.rb +59 -0
- data/spec/vcloud/core/edge_gateway_spec.rb +79 -0
- data/spec/vcloud/core/metadata_helper_spec.rb +89 -0
- data/spec/vcloud/core/org_vdc_network_spec.rb +257 -0
- data/spec/vcloud/core/query_spec.rb +111 -0
- data/spec/vcloud/core/vapp_spec.rb +173 -0
- data/spec/vcloud/core/vapp_template_spec.rb +77 -0
- data/spec/vcloud/core/vdc_spec.rb +68 -0
- data/spec/vcloud/core/vm_spec.rb +290 -0
- data/spec/vcloud/data/basic_preamble_test.erb +8 -0
- data/spec/vcloud/data/basic_preamble_test.erb.OUT +8 -0
- data/spec/vcloud/fog/fog_model_interface_spec.rb +25 -0
- data/spec/vcloud/fog/service_interface_spec.rb +30 -0
- data/vcloud-core.gemspec +30 -0
- metadata +198 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 HM Government (Government Digital Service)
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# VCloud Core
|
2
|
+
|
3
|
+
VCloud Core is a gem that supports automatated provisioning of VMWare vCloud Director. It uses Fog under the hood. Primarily developed to support [VCloud Walker](https://github.com/alphagov/vcloud-walker) and [VCloud Tools](https://github.com/alphagov/vcloud-tools).
|
4
|
+
|
5
|
+
VCloud Core includes VCloud Query and a command-line wrapper for VCloud Query.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'vcloud-core'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install vcloud-core
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
1. Fork it
|
28
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
31
|
+
5. Create new Pull Request
|
32
|
+
|
33
|
+
## VCloud Query
|
34
|
+
|
35
|
+
### Get results from the vCloud Query API
|
36
|
+
|
37
|
+
VCloud Query is a light wrapper around the vCloud Query API.
|
38
|
+
|
39
|
+
Any, or all, records of a particular 'type' can be returned. These types map to
|
40
|
+
entities in the vCloud system itself, eg: 'vm', 'vApp', 'orgVdc', 'edgeGateway'.
|
41
|
+
|
42
|
+
Filters can be applied, using a simple query syntax. See below for basic usage and
|
43
|
+
examples.
|
44
|
+
|
45
|
+
Run with no arguments, it outputs a list of potential entity types to query, along
|
46
|
+
with the potential record types to display (default 'records')
|
47
|
+
|
48
|
+
#### Usage:
|
49
|
+
|
50
|
+
vcloud-query [options] [queriable type]
|
51
|
+
|
52
|
+
where [queriable type] maps to a vcloud entity type, eg: vApp, vm, orgVdc
|
53
|
+
|
54
|
+
#### Examples:
|
55
|
+
|
56
|
+
NB: examples assume FOG_CREDENTIAL has been set accordingly.
|
57
|
+
|
58
|
+
# Get a list of vApps, in YAML
|
59
|
+
vcloud-query -o yaml vApp
|
60
|
+
|
61
|
+
# Get general usage info
|
62
|
+
vcloud-query --help
|
63
|
+
|
64
|
+
# Get a list of all queriable types (left column)
|
65
|
+
vcloud-query
|
66
|
+
|
67
|
+
# Get all VMs with VMware Tools less than 9282, that are not a vApp Template:
|
68
|
+
vcloud-query --filter 'vmToolsVersion=lt=9282;isVAppTemplate==false' vm
|
69
|
+
|
70
|
+
#### Supports:
|
71
|
+
|
72
|
+
* Returning a list of queriable types (eg vm, vApp, edgeGateway) from the API
|
73
|
+
* Displaying all vCloud entities of a given type
|
74
|
+
* Filtering the results of the query based on common parameters such as:
|
75
|
+
* entity name
|
76
|
+
* metadata values
|
77
|
+
* key entity parameters
|
78
|
+
* Limiting the output to certain fields (eg: name, vmToolsVersion)
|
79
|
+
* Returning results in TSV, CSV, and YAML
|
80
|
+
|
81
|
+
#### Query Syntax:
|
82
|
+
|
83
|
+
Summary of filter query syntax:
|
84
|
+
|
85
|
+
attribute==value # == to check equality
|
86
|
+
attribute!=value # != to check inequality
|
87
|
+
attribute=lt=value # =lt= less than (=le= for <=)
|
88
|
+
attribute=gt=value # =gt= greater than (=ge= for >=)
|
89
|
+
attribute==value;attribute2==value2 # ; == AND
|
90
|
+
attribute==value,attribute2==value2 # , == OR
|
91
|
+
|
92
|
+
Parentheses can be used to group sub-queries.
|
93
|
+
|
94
|
+
**Do not use spaces in the query**
|
95
|
+
|
96
|
+
Entity metadata queries have their own subsyntax incorporating the value types:
|
97
|
+
|
98
|
+
metadata:key1==STRING:value1
|
99
|
+
metadata:key1=le=NUMBER:15
|
100
|
+
metadata:key1=gt=DATETIME:2012-06-18T12:00:00-05:00
|
101
|
+
|
102
|
+
See http://pubs.vmware.com/vcd-51/topic/com.vmware.vcloud.api.doc_51/GUID-4FD71B6D-6797-4B8E-B9F0-618F4ACBEFAC.html for details.
|
103
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
5
|
+
# Set a bogus Fog credential, otherwise it's possible for the unit
|
6
|
+
# tests to accidentially run (and succeed against!) an actual
|
7
|
+
# environment, if Fog connection is not stubbed correctly.
|
8
|
+
ENV['FOG_CREDENTIAL'] = 'random_nonsense_owiejfoweijf'
|
9
|
+
task.pattern = FileList['spec/vcloud/**/*_spec.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
task :default => [:spec]
|
13
|
+
|
14
|
+
require "gem_publisher"
|
15
|
+
task :publish_gem do |t|
|
16
|
+
gem = GemPublisher.publish_if_updated("vcloud-core.gemspec", :rubygems)
|
17
|
+
puts "Published #{gem}" if gem
|
18
|
+
end
|
data/bin/vcloud-query
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'optparse'
|
6
|
+
require 'methadone'
|
7
|
+
|
8
|
+
require 'vcloud/core'
|
9
|
+
|
10
|
+
class App
|
11
|
+
|
12
|
+
include Methadone::Main
|
13
|
+
include Methadone::CLILogging
|
14
|
+
include Vcloud
|
15
|
+
|
16
|
+
main do |type|
|
17
|
+
Query.new(type, options).run
|
18
|
+
end
|
19
|
+
|
20
|
+
on('-A', '--sort-asc', '=ATTRIBUTE', 'Sort ascending') do |v|
|
21
|
+
options[:sortAsc] = v
|
22
|
+
end
|
23
|
+
|
24
|
+
on('-D', '--sort-desc', '=ATTRIBUTE', 'Sort descending') do |v|
|
25
|
+
options[:sortDesc] = v
|
26
|
+
end
|
27
|
+
|
28
|
+
on('--fields', '=NAMES', 'Attribute or metadata key names') do |v|
|
29
|
+
options[:fields] = v
|
30
|
+
end
|
31
|
+
|
32
|
+
on('--format', '=ATTRIBUTE', 'Data format to retrieve: records, idrecords, references') do |v|
|
33
|
+
options[:format] = v
|
34
|
+
end
|
35
|
+
|
36
|
+
on('--filter', '=FILTER', 'Filter expression') do |v|
|
37
|
+
options[:filter] = v
|
38
|
+
end
|
39
|
+
|
40
|
+
on('-o', '--output-format', '=FORMAT', 'Output format: csv, tsv, yaml') do |v|
|
41
|
+
options[:output_format] = v.downcase
|
42
|
+
end
|
43
|
+
|
44
|
+
on("--mock", "Fog Mock mode")
|
45
|
+
on("--verbose", "Verbose output")
|
46
|
+
on("--debug", "Debugging output")
|
47
|
+
|
48
|
+
arg :type, :optional
|
49
|
+
|
50
|
+
description '
|
51
|
+
vcloud-query takes a query type and returns all vCloud entities of
|
52
|
+
that type, obeying supplied filter rules.
|
53
|
+
|
54
|
+
Query types map to vCloud entities, for example: vApp, vm, orgVdc, orgVdcNetwork.
|
55
|
+
|
56
|
+
Without a type argument, returns a list of available Entity Types to query.
|
57
|
+
|
58
|
+
See https://github.com/alphagov/vcloud-tools/blob/master/README.md for more info.
|
59
|
+
|
60
|
+
Example use:
|
61
|
+
|
62
|
+
# get a list of all vApps, returning all available parameters, in YAML
|
63
|
+
|
64
|
+
vcloud-query -o yaml vApp
|
65
|
+
|
66
|
+
# get a list of all powered off VMs return the name and containerName (vapp
|
67
|
+
# name)
|
68
|
+
|
69
|
+
vcloud-query --filter "status==POWERED_OFF" --fields name,containerName vm
|
70
|
+
|
71
|
+
# list all query types (types are left-most column, possible formats listed
|
72
|
+
# on the left (records is default, and most useful)
|
73
|
+
|
74
|
+
vcloud-query
|
75
|
+
|
76
|
+
'
|
77
|
+
|
78
|
+
|
79
|
+
version Vcloud::Core::VERSION
|
80
|
+
|
81
|
+
#use_log_level_option
|
82
|
+
|
83
|
+
go!
|
84
|
+
end
|
data/jenkins.sh
ADDED
data/lib/vcloud/core.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'vcloud/fog'
|
3
|
+
|
4
|
+
require 'vcloud/core/entity'
|
5
|
+
require 'vcloud/core/metadata_helper'
|
6
|
+
require 'vcloud/core/compute_metadata'
|
7
|
+
require 'vcloud/core/vdc'
|
8
|
+
require 'vcloud/core/edge_gateway'
|
9
|
+
require 'vcloud/core/vm'
|
10
|
+
require 'vcloud/core/vapp'
|
11
|
+
require 'vcloud/core/vapp_template'
|
12
|
+
require 'vcloud/core/org_vdc_network'
|
13
|
+
require 'vcloud/core/query'
|
14
|
+
|
15
|
+
module Vcloud
|
16
|
+
module Core
|
17
|
+
|
18
|
+
def self.logger
|
19
|
+
@logger ||=Logger.new(STDOUT)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Vcloud
|
2
|
+
module Core
|
3
|
+
module ComputeMetadata
|
4
|
+
|
5
|
+
def get_metadata id
|
6
|
+
vcloud_compute_metadata = Vcloud::Fog::ServiceInterface.new.get_vapp_metadata(id)
|
7
|
+
MetadataHelper.extract_metadata(vcloud_compute_metadata[:MetadataEntry])
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Vcloud
|
2
|
+
module Core
|
3
|
+
class EdgeGateway
|
4
|
+
|
5
|
+
attr_reader :id
|
6
|
+
|
7
|
+
def initialize(id)
|
8
|
+
unless id =~ /^[-0-9a-f]+$/
|
9
|
+
raise "EdgeGateway id : #{id} is not in correct format"
|
10
|
+
end
|
11
|
+
@id = id
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get_ids_by_name(name)
|
15
|
+
q = Query.new('edgeGateway', :filter => "name==#{name}")
|
16
|
+
unless res = q.get_all_results
|
17
|
+
raise "Error finding edgeGateway by name #{name}"
|
18
|
+
end
|
19
|
+
res.collect do |record|
|
20
|
+
record[:href].split('/').last if record.key?(:href)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.get_by_name(name)
|
25
|
+
ids = self.get_ids_by_name(name)
|
26
|
+
raise "edgeGateway #{name} not found" if ids.size == 0
|
27
|
+
raise "edgeGateway #{name} is not unique" if ids.size > 1
|
28
|
+
return self.new(ids.first)
|
29
|
+
end
|
30
|
+
|
31
|
+
def vcloud_attributes
|
32
|
+
fsi = Vcloud::Fog::ServiceInterface.new
|
33
|
+
fsi.get_edge_gateway(id)
|
34
|
+
end
|
35
|
+
|
36
|
+
def href
|
37
|
+
vcloud_attributes[:href]
|
38
|
+
end
|
39
|
+
|
40
|
+
def name
|
41
|
+
vcloud_attributes[:name]
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Vcloud
|
2
|
+
module Core
|
3
|
+
class Entity
|
4
|
+
|
5
|
+
def id_prefix;
|
6
|
+
raise 'id_prefix : method missing'
|
7
|
+
end
|
8
|
+
|
9
|
+
def id
|
10
|
+
raise 'id not found' unless @vcloud_attributes && @vcloud_attributes[:href]
|
11
|
+
extracted_id = @vcloud_attributes[:href].split('/').last
|
12
|
+
unless extracted_id =~ /^#{id_prefix}-[-0-9a-f]+$/
|
13
|
+
raise "#{id_prefix} id : #{extracted_id} is not in correct format"
|
14
|
+
end
|
15
|
+
extracted_id
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
@vcloud_attributes[:name]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Vcloud
|
2
|
+
module Core
|
3
|
+
module MetadataHelper
|
4
|
+
|
5
|
+
def extract_metadata vcloud_metadata_entries
|
6
|
+
metadata = {}
|
7
|
+
vcloud_metadata_entries.each do |entry|
|
8
|
+
next unless entry[:type] == Vcloud::Fog::ContentTypes::METADATA
|
9
|
+
key = entry[:Key].to_sym
|
10
|
+
val = entry[:TypedValue][:Value]
|
11
|
+
case entry[:TypedValue][:xsi_type]
|
12
|
+
when Fog::MetadataValueType::Number
|
13
|
+
val = val.to_i
|
14
|
+
when Fog::MetadataValueType::String
|
15
|
+
val = val.to_s
|
16
|
+
when Fog::MetadataValueType::DateTime
|
17
|
+
val = DateTime.parse(val)
|
18
|
+
when Fog::MetadataValueType::Boolean
|
19
|
+
val = val == 'true' ? true : false
|
20
|
+
end
|
21
|
+
metadata[key] = val
|
22
|
+
end
|
23
|
+
metadata
|
24
|
+
end
|
25
|
+
|
26
|
+
module_function :extract_metadata
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Vcloud
|
2
|
+
module Core
|
3
|
+
class OrgVdcNetwork
|
4
|
+
|
5
|
+
attr_reader :id
|
6
|
+
|
7
|
+
def initialize(id)
|
8
|
+
unless id =~ /^[-0-9a-f]+$/
|
9
|
+
raise "orgVdcNetwork id : #{id} is not in correct format"
|
10
|
+
end
|
11
|
+
@id = id
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.provision(config)
|
15
|
+
raise "Must specify a name" unless name = config[:name]
|
16
|
+
raise "Must specify a vdc_name" unless vdc_name = config[:vdc_name]
|
17
|
+
|
18
|
+
unless config[:fence_mode] == 'isolated' || config[:fence_mode] == 'natRouted'
|
19
|
+
raise "fence_mode #{config[:fence_mode]} not supported. Must be 'isolated' or 'natRouted'"
|
20
|
+
end
|
21
|
+
|
22
|
+
config[:is_shared] = false unless config[:is_shared]
|
23
|
+
|
24
|
+
if config[:fence_mode] == 'natRouted'
|
25
|
+
raise "Must specify an edge_gateway to connect to" unless config.key?(:edge_gateway)
|
26
|
+
edgegw = Vcloud::Core::EdgeGateway.get_by_name(config[:edge_gateway])
|
27
|
+
end
|
28
|
+
|
29
|
+
vdc = Vcloud::Core::Vdc.get_by_name(vdc_name)
|
30
|
+
|
31
|
+
options = construct_network_options(config)
|
32
|
+
options[:EdgeGateway] = { :href => edgegw.href } if edgegw
|
33
|
+
|
34
|
+
begin
|
35
|
+
Vcloud::Core.logger.info("Provisioning new OrgVdcNetwork #{name} in vDC '#{vdc_name}'")
|
36
|
+
attrs = Vcloud::Fog::ServiceInterface.new.post_create_org_vdc_network(vdc.id, name, options)
|
37
|
+
rescue RuntimeError => e
|
38
|
+
Vcloud::Core.logger.error("Could not provision orgVdcNetwork: #{e.message}")
|
39
|
+
end
|
40
|
+
|
41
|
+
raise "Did not successfully create orgVdcNetwork" unless attrs && attrs.key?(:href)
|
42
|
+
self.new(attrs[:href].split('/').last)
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
def vcloud_attributes
|
47
|
+
Vcloud::Fog::ServiceInterface.new.get_network(id)
|
48
|
+
end
|
49
|
+
|
50
|
+
def name
|
51
|
+
vcloud_attributes[:name]
|
52
|
+
end
|
53
|
+
|
54
|
+
def href
|
55
|
+
vcloud_attributes[:href]
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete
|
59
|
+
Vcloud::Fog::ServiceInterface.new.delete_network(id)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def self.construct_network_options(config)
|
65
|
+
opts = {}
|
66
|
+
opts[:Description] = config[:description] if config.key?(:description)
|
67
|
+
opts[:IsShared] = config[:is_shared]
|
68
|
+
|
69
|
+
ip_scope = {}
|
70
|
+
ip_scope[:IsInherited] = config[:is_inherited] || false
|
71
|
+
ip_scope[:Gateway] = config[:gateway] if config.key?(:gateway)
|
72
|
+
ip_scope[:Netmask] = config[:netmask] if config.key?(:netmask)
|
73
|
+
ip_scope[:Dns1] = config[:dns1] if config.key?(:dns1)
|
74
|
+
ip_scope[:Dns2] = config[:dns2] if config.key?(:dns2)
|
75
|
+
ip_scope[:DnsSuffix] = config[:dns_suffix] if config.key?(:dns_suffix)
|
76
|
+
ip_scope[:IsEnabled] = config[:is_enabled] || true
|
77
|
+
|
78
|
+
if config.key?(:ip_ranges) && config[:ip_ranges].size > 0
|
79
|
+
ip_scope[:IpRanges] = []
|
80
|
+
config[:ip_ranges].each do |range|
|
81
|
+
ip_scope[:IpRanges] << {
|
82
|
+
:IpRange => {
|
83
|
+
:StartAddress => range[:start_address],
|
84
|
+
:EndAddress => range[:end_address]
|
85
|
+
}
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
opts[:Configuration] = {
|
91
|
+
:FenceMode => config[:fence_mode],
|
92
|
+
:IpScopes => {
|
93
|
+
:IpScope => ip_scope
|
94
|
+
},
|
95
|
+
}
|
96
|
+
|
97
|
+
opts
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|