vcloud-core 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|