consul-ruby-client 0.0.2
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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +81 -0
- data/Rakefile +2 -0
- data/consul-ruby-client.gemspec +29 -0
- data/lib/consul/client.rb +18 -0
- data/lib/consul/client/agent.rb +322 -0
- data/lib/consul/client/base.rb +132 -0
- data/lib/consul/client/catalog.rb +99 -0
- data/lib/consul/client/key_value.rb +142 -0
- data/lib/consul/client/session.rb +135 -0
- data/lib/consul/client/status.rb +44 -0
- data/lib/consul/client/version.rb +5 -0
- data/lib/consul/model/health_check.rb +39 -0
- data/lib/consul/model/key_value.rb +28 -0
- data/lib/consul/model/node.rb +25 -0
- data/lib/consul/model/service.rb +30 -0
- data/lib/consul/model/session.rb +30 -0
- data/lib/consul/util/utils.rb +17 -0
- data/spec/base_client_spec.rb +25 -0
- data/spec/spec_helper.rb +13 -0
- metadata +182 -0
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'logger'
|
3
|
+
require 'rest-client'
|
4
|
+
require_relative '../util/utils'
|
5
|
+
|
6
|
+
module Consul
|
7
|
+
module Client
|
8
|
+
|
9
|
+
# Public API Base.
|
10
|
+
module Base
|
11
|
+
|
12
|
+
# Public: Creates an API Endpoint
|
13
|
+
#
|
14
|
+
# data_center - The data center to utilize, defaults to bootstrap 'dc1' datat center
|
15
|
+
# api_host - The host the Consul Agent is running on. Default: 127.0.0.1
|
16
|
+
# api_port - The port the Consul Agent is listening on. Default: 8500
|
17
|
+
# version - The version of the api to use.
|
18
|
+
# logger - Logging mechanism. Must conform to Ruby Logger interface
|
19
|
+
#
|
20
|
+
def initialize(data_center = 'dc1', api_host = '127.0.0.1', api_port = '8500', version = 'v1', logger = Logger.new(STDOUT))
|
21
|
+
@dc = data_center
|
22
|
+
@host = api_host
|
23
|
+
@port = api_port
|
24
|
+
@logger = logger
|
25
|
+
@version = version
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Test if this Consul Client is reachable.
|
29
|
+
def is_reachable
|
30
|
+
_get(base_url, nil, false) == 'Consul Agent'
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
# Protected: Generic get request. Wraps error handling and url validation.
|
36
|
+
#
|
37
|
+
# url - url endpoint to hit.
|
38
|
+
# params - Hash of key to value parameters
|
39
|
+
# json_only - Flag that annotates we should only be expecting json back. Default true, most consul endpoints return JSON.
|
40
|
+
#
|
41
|
+
# Returns:
|
42
|
+
# Throws:
|
43
|
+
# ArgumentError: the url is not valid.
|
44
|
+
# IOError: Unable to reach Consul Agent.
|
45
|
+
def _get(url, params = nil, json_only = true)
|
46
|
+
# Validation
|
47
|
+
validate_url(url)
|
48
|
+
|
49
|
+
opts = {}
|
50
|
+
opts[:params] = params unless params.nil?
|
51
|
+
opts[:accept] = :json if json_only
|
52
|
+
begin
|
53
|
+
return RestClient.get url, opts
|
54
|
+
rescue Exception => e
|
55
|
+
# Unable to communicate with consul agent.
|
56
|
+
logger.warn(e.message)
|
57
|
+
raise IOError.new "Unable to complete get request: #{e}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Protected: Generic put request. Wraps error and translates Rest response to success or failure.
|
62
|
+
#
|
63
|
+
# url - The url endpoint for the put request.
|
64
|
+
# value - The value to put at the url endpoint.
|
65
|
+
#
|
66
|
+
# Returns: true on success or false on failure and the body of the return message.
|
67
|
+
# Throws:
|
68
|
+
# ArgumentError: the url is not valid.
|
69
|
+
# IOError: Unable to reach Consul Agent.
|
70
|
+
def _put(url, value, params = nil)
|
71
|
+
# Validation
|
72
|
+
validate_url(url)
|
73
|
+
|
74
|
+
p = {}
|
75
|
+
p[:params] = params unless params.nil?
|
76
|
+
begin
|
77
|
+
if Consul::Utils.valid_json?(value)
|
78
|
+
resp = RestClient.put(url, value, :content_type => :json) {|response, req, res| response }
|
79
|
+
else
|
80
|
+
resp = RestClient.put(url, value) {|response, req, res| response }
|
81
|
+
end
|
82
|
+
success = (resp.code == 200 or resp.code == 201)
|
83
|
+
logger.warn("Unable to send #{value} to endpoint #{url} returned code: #{resp.code}") unless success
|
84
|
+
return success, resp.body
|
85
|
+
rescue Exception => e
|
86
|
+
logger.error('RestClient.put Error: Unable to reach consul agent')
|
87
|
+
raise IOError.new "Unable to complete put request: #{e}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def data_center
|
92
|
+
@data_center ||= 'dc1'
|
93
|
+
end
|
94
|
+
|
95
|
+
def host
|
96
|
+
@host ||= '127.0.0.1'
|
97
|
+
end
|
98
|
+
|
99
|
+
def port
|
100
|
+
@port ||= '8500'
|
101
|
+
end
|
102
|
+
|
103
|
+
def version
|
104
|
+
@version ||= 'v1'
|
105
|
+
end
|
106
|
+
|
107
|
+
def logger
|
108
|
+
@logger ||= Logger.new(STDOUT)
|
109
|
+
end
|
110
|
+
|
111
|
+
def https
|
112
|
+
@https = false
|
113
|
+
end
|
114
|
+
|
115
|
+
def base_versioned_url
|
116
|
+
"#{base_url}/#{version}"
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def base_url
|
122
|
+
"#{(https ? 'https': 'http')}://#{host}:#{port}"
|
123
|
+
end
|
124
|
+
|
125
|
+
# Private: Validates the url
|
126
|
+
def validate_url(url)
|
127
|
+
raise ArgumentError.new 'URL cannot be blank' if url.to_s == ''
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require_relative '../model/node'
|
3
|
+
|
4
|
+
# Consul Catalog End Point.
|
5
|
+
module Consul
|
6
|
+
module Client
|
7
|
+
class Catalog
|
8
|
+
include Consul::Client::Base
|
9
|
+
|
10
|
+
# Public: Returns a list of all the nodes on this client
|
11
|
+
#
|
12
|
+
# dc - Data Center to look for services in, defaults to the agents data center
|
13
|
+
#
|
14
|
+
def nodes(dc = nil)
|
15
|
+
params = {}
|
16
|
+
params[:dc] = dc unless dc.nil?
|
17
|
+
JSON.parse(_get build_url('nodes'), params).map {|n| Consul::Model::Node.new.extend(Consul::Model::Node::Representer).from_hash(n)}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Returns a list of services that are within the supplied or agent data center
|
21
|
+
#
|
22
|
+
# dc - Data Center to look for services in, defaults to the agents data center
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
# Consul::Client::Catalog.new('dc1').services =>
|
26
|
+
# {
|
27
|
+
# "consul": [],
|
28
|
+
# "redis": [],
|
29
|
+
# "postgresql": [
|
30
|
+
# "master",
|
31
|
+
# "slave"
|
32
|
+
# ]
|
33
|
+
# }
|
34
|
+
#
|
35
|
+
# Returns: List of services ids.
|
36
|
+
def services(dc = nil)
|
37
|
+
params = {}
|
38
|
+
params[:dc] = dc unless dc.nil?
|
39
|
+
JSON.parse(_get build_url('services'), params)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Returns all the nodes within a data center that have the service specified.
|
43
|
+
#
|
44
|
+
# dc - Data center, default: agent current data center
|
45
|
+
# tag - Tag, filter for tags
|
46
|
+
#
|
47
|
+
# Example:
|
48
|
+
# ConsulCatalog.new('dc1').service('my_service_id') =>
|
49
|
+
# [ConsulNode<@service_id=my_service_id ...>,
|
50
|
+
# ConsulNode<@service_id=my_service_id ...>,
|
51
|
+
# ...]
|
52
|
+
#
|
53
|
+
# Returns: List of nodes that have this service.
|
54
|
+
def service(id, dc = nil, tag = nil)
|
55
|
+
params = {}
|
56
|
+
params[:dc] = dc unless dc.nil?
|
57
|
+
params.add[:tag] = tag unless tag.nil?
|
58
|
+
JSON.parse(_get build_url("service/#{id}"), params).map {|n| Consul::Model::Node.new.extend(Consul::Model::Node::Representer).from_hash(n)}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Public: Returns all the nodes within a data center that have the service specified.
|
62
|
+
#
|
63
|
+
# dc - Data center, default: agent current data center
|
64
|
+
# tag - Tag, filter for tags
|
65
|
+
#
|
66
|
+
# Example:
|
67
|
+
# ConsulCatalog.new('dc1').node('my_node') =>
|
68
|
+
# ConsulNode<@service_id=my_service_id ...>
|
69
|
+
#
|
70
|
+
# Returns: Returns the node by the argument name.
|
71
|
+
def node(name, dc = nil)
|
72
|
+
params = {}
|
73
|
+
params[:dc] = dc unless dc.nil?
|
74
|
+
resp = JSON.parse(_get build_url("node/#{name}"), params)
|
75
|
+
n = Consul::Model::Node.new.extend(Consul::Model::Node::Representer).from_hash(resp['Node'])
|
76
|
+
unless resp[:Services].nil?
|
77
|
+
n.services = resp['Services'].keys.map{|k| Consul::Model::Service.new.extend(Consul::Model::Service::Representer).from_hash(resp[:Services][k])}
|
78
|
+
end
|
79
|
+
n
|
80
|
+
end
|
81
|
+
|
82
|
+
def data_centers
|
83
|
+
_get build_url('datacenters'), params = nil, json_only = false
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Public: Builds the base url
|
89
|
+
#
|
90
|
+
# Example:
|
91
|
+
#
|
92
|
+
# Returns: The base
|
93
|
+
def build_url(suffix)
|
94
|
+
"#{base_versioned_url}/catalog/#{suffix}"
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require_relative 'base'
|
3
|
+
require_relative '../model/key_value'
|
4
|
+
|
5
|
+
module Consul
|
6
|
+
module Client
|
7
|
+
class KeyValue
|
8
|
+
include Consul::Client::Base
|
9
|
+
|
10
|
+
# Public: Creates an API Endpoint
|
11
|
+
#
|
12
|
+
# data_center - The data center to utilize, defaults to bootstrap 'dc1' datat center
|
13
|
+
# api_host - The host the Consul Agent is running on. Default: 127.0.0.1
|
14
|
+
# api_port - The port the Consul Agent is listening on. Default: 8500
|
15
|
+
# version - The version of the api to use.
|
16
|
+
# logger - Logging mechanism. Must conform to Ruby Logger interface
|
17
|
+
#
|
18
|
+
def initialize(name_space = '', data_center = 'dc1', api_host = '127.0.0.1', api_port = '8500', version = 'v1', logger = Logger.new(STDOUT))
|
19
|
+
name_space = sanitize(name_space)
|
20
|
+
name_space = "#{name_space}/" unless name_space.nil? or name_space.empty?
|
21
|
+
@namespace = sanitize(name_space)
|
22
|
+
@dc = data_center
|
23
|
+
@host = api_host
|
24
|
+
@port = api_port
|
25
|
+
@logger = logger
|
26
|
+
@version = version
|
27
|
+
end
|
28
|
+
|
29
|
+
def name_space
|
30
|
+
@namespace ||= ''
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Gets the value associated with a given key.
|
34
|
+
#
|
35
|
+
# Reference: https://www.consul.io/docs/agent/http/kv.html
|
36
|
+
#
|
37
|
+
# key - Key to get value for, if recurse = true the key is treated by a prefix
|
38
|
+
# recurse - Flag to signify treating the key as a prefix
|
39
|
+
# index - Can be used to establish blocking queries by setting
|
40
|
+
# only_keys - Flag to return only keys
|
41
|
+
# separator - list only up to a given separator
|
42
|
+
#
|
43
|
+
# Returns: An array of Consul::Model::KeyValue objects, if only
|
44
|
+
def get(key,
|
45
|
+
recurse = false,
|
46
|
+
index = false,
|
47
|
+
only_keys = false,
|
48
|
+
separator = nil)
|
49
|
+
key = sanitize(key)
|
50
|
+
params = {}
|
51
|
+
params[:recurse] = nil if recurse
|
52
|
+
params[:index] = nil if index
|
53
|
+
params[:keys] = nil if only_keys
|
54
|
+
params[:separator] = separator unless separator.nil?
|
55
|
+
# begin
|
56
|
+
# resp = RestClient.get key_url(key), {:params => params}
|
57
|
+
# rescue
|
58
|
+
# # TODO need to pass more information back to the client.
|
59
|
+
# logger.warn("Unable to get value for #{key}")
|
60
|
+
# nil
|
61
|
+
# end
|
62
|
+
begin
|
63
|
+
resp = _get key_url(key), params
|
64
|
+
rescue Exception => e
|
65
|
+
logger.warn("Unable to get value for #{key} due to: #{e}")
|
66
|
+
return nil
|
67
|
+
end
|
68
|
+
return nil if resp.code == 404
|
69
|
+
json = JSON.parse(_get key_url(key), params)
|
70
|
+
return json if only_keys
|
71
|
+
json.map { |kv|
|
72
|
+
kv = Consul::Model::KeyValue.new.extend(Consul::Model::KeyValue::Representer).from_hash(kv)
|
73
|
+
kv.value = Base64.decode64(kv.value)
|
74
|
+
kv
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
# Public: Put the Key Value pair in consul.
|
79
|
+
#
|
80
|
+
# Low level put key value implementation.
|
81
|
+
#
|
82
|
+
# Reference: https://www.consul.io/docs/agent/http/kv.html
|
83
|
+
#
|
84
|
+
# key - Key
|
85
|
+
# value - Value to assign for Key
|
86
|
+
# flags - Client specified value [0, 2e64-1]
|
87
|
+
# cas - Check and Set operation
|
88
|
+
# acquire - Session id to acquire the lock with a valid session.
|
89
|
+
# release - Session id to release the lock with a valid session.
|
90
|
+
#
|
91
|
+
# Returns: True on success, False on failure
|
92
|
+
# Throws: IOError: Unable to contact Consul Agent.
|
93
|
+
def put(key,
|
94
|
+
value,
|
95
|
+
flags = nil,
|
96
|
+
cas = nil,
|
97
|
+
acquire_session = nil,
|
98
|
+
release_session = nil)
|
99
|
+
key = sanitize(key)
|
100
|
+
params = {}
|
101
|
+
params[:flags] = flags unless flags.nil?
|
102
|
+
params[:cas] = cas unless cas.nil?
|
103
|
+
params[:acquire] = acquire_session unless acquire_session.nil?
|
104
|
+
params[:release_session] = release_session unless release_session.nil?
|
105
|
+
begin
|
106
|
+
value = JSON.generate(value)
|
107
|
+
rescue JSON::GeneratorError
|
108
|
+
@logger.debug("Using non-JSON value for key #{key}")
|
109
|
+
end
|
110
|
+
_put build_url(key), value, {:params => params}
|
111
|
+
end
|
112
|
+
|
113
|
+
# Public: Delete the Key Value pair in consul.
|
114
|
+
#
|
115
|
+
# key - Key
|
116
|
+
# recurse - Delete all keys as the 'key' is a prefix for
|
117
|
+
# cas - Check and Set
|
118
|
+
def delete(key, recurse = false, cas = nil)
|
119
|
+
key = sanitize(key)
|
120
|
+
params = {}
|
121
|
+
params[:recurse] = nil if recurse
|
122
|
+
params[:cas] = cas unless cas.nil?
|
123
|
+
RestClient.delete build_url(key), {:params => params}
|
124
|
+
end
|
125
|
+
|
126
|
+
def build_url(suffix)
|
127
|
+
"#{base_versioned_url}/kv/#{suffix}"
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def sanitize(key)
|
133
|
+
key.gsub(/^\//,'').gsub(/\/$/,'')
|
134
|
+
end
|
135
|
+
|
136
|
+
def key_url(key)
|
137
|
+
build_url("#{name_space}#{key}")
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require_relative '../model/session'
|
3
|
+
require_relative '../util/utils'
|
4
|
+
|
5
|
+
module Consul
|
6
|
+
module Client
|
7
|
+
# Consul Session Client
|
8
|
+
class Session
|
9
|
+
include Consul::Client::Base
|
10
|
+
|
11
|
+
# Public: Creates an instance of Consul::Model::Session with as many preset
|
12
|
+
# defaults as possible.
|
13
|
+
#
|
14
|
+
# name - The name of the session.
|
15
|
+
# lock_delay - Allowance window for leaders to Valid values between '0s' and '60s'
|
16
|
+
# node - The name of the node, defaults to the node the agent is running on
|
17
|
+
# checks - Health Checks to associate to this session
|
18
|
+
# behaviour - 'release' or 'destroy' Behaviour when session is invalidated.
|
19
|
+
# ttl - When provided Must be between '10s' and '3600s'
|
20
|
+
#
|
21
|
+
# Returns: Consul::Model::Session instance.
|
22
|
+
def self.for_name(name,
|
23
|
+
lock_delay = '15s',
|
24
|
+
node = nil,
|
25
|
+
checks = ['serfHealth'],
|
26
|
+
behaviour = 'release',
|
27
|
+
ttl = nil)
|
28
|
+
raise ArgumentError.new "Illegal Name: #{name}" if name.nil?
|
29
|
+
session = Consul::Model::Session.new(name: name)
|
30
|
+
session[:lock_delay] = lock_delay unless lock_delay.nil?
|
31
|
+
session[:node] = node unless node.nil?
|
32
|
+
checks = [] if checks.nil?
|
33
|
+
checks += 'serfHealth' unless checks.include? 'serfHealth'
|
34
|
+
session[:checks] = checks
|
35
|
+
behaviour = 'release' if behaviour.nil? or behaviour != 'release' or behaviour != 'destroy'
|
36
|
+
session[:behaviour] = behaviour
|
37
|
+
session[:ttl] = ttl unless ttl.nil?
|
38
|
+
session
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Creates a new Consul Session.
|
42
|
+
#
|
43
|
+
# session - Session to create.
|
44
|
+
# dc - Consul data center
|
45
|
+
#
|
46
|
+
# Returns The Session ID a
|
47
|
+
def create(session, dc = nil)
|
48
|
+
raise TypeError, 'Session must be of type Consul::Model::Session' unless session.kind_of? Consul::Model::Session
|
49
|
+
params = {}
|
50
|
+
params[:dc] = dc unless dc.nil?
|
51
|
+
success, body = _put(build_url('create'), session.extend(Consul::Model::Session::Representer).to_json, params)
|
52
|
+
return Consul::Model::Session.new.extend(Consul::Model::Service::Representer).from_json(body) if success
|
53
|
+
logger.warn("Unable to create session with #{session}")
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: Destroys a given session
|
58
|
+
def destroy(session, dc = nil)
|
59
|
+
return false if session.nil?
|
60
|
+
session = extract_session_id(session)
|
61
|
+
params = nil
|
62
|
+
params = {:dc => dc} unless dc.nil?
|
63
|
+
success, _ = _put build_url("destroy/#{session}"), '', params
|
64
|
+
success
|
65
|
+
end
|
66
|
+
|
67
|
+
# Public: Return the session info for a given session name.
|
68
|
+
#ccs
|
69
|
+
def info(session, dc = nil)
|
70
|
+
return nil if session.nil?
|
71
|
+
session = extract_session_id(session)
|
72
|
+
params = {}
|
73
|
+
params[:dc] = dc unless dc.nil?
|
74
|
+
resp = _get build_url("info/#{session}"), params
|
75
|
+
JSON.parse(resp).map{|session_hash| session(session_hash)} unless resp.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
# Lists sessions belonging to a node
|
79
|
+
def node(session, dc = nil)
|
80
|
+
return nil if session.nil?
|
81
|
+
session = extract_session_id(session)
|
82
|
+
params = {}
|
83
|
+
params[:dc] = dc unless dc.nil?
|
84
|
+
resp = _get build_url("node/#{session}"), params
|
85
|
+
JSON.parse(resp).map{|session_hash| session(session_hash)} unless resp.nil?
|
86
|
+
end
|
87
|
+
|
88
|
+
# Lists all active sessions
|
89
|
+
def list(dc = nil)
|
90
|
+
params = {}
|
91
|
+
params[:dc] = dc unless dc.nil?
|
92
|
+
resp = _get build_url('list'), params
|
93
|
+
JSON.parse(resp).map{|session_hash| session(session_hash)} unless resp.nil?
|
94
|
+
end
|
95
|
+
|
96
|
+
# Renews a TTL-based session
|
97
|
+
def renew(session, dc = nil)
|
98
|
+
return nil if session.nil?
|
99
|
+
session = extract_session_id(session)
|
100
|
+
params = {}
|
101
|
+
params[:dc] = dc unless dc.nil?
|
102
|
+
success, _ = _put build_url("renew/#{session.name}"), session.to_json
|
103
|
+
success
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
# Private: Extracts the Session
|
109
|
+
def extract_session_id(session)
|
110
|
+
raise TypeError, 'Session cannot be null' if session.nil?
|
111
|
+
session = session.id if session.kind_of? Consul::Model::Session
|
112
|
+
session = session.to_str if session.respond_to?(:to_str)
|
113
|
+
session
|
114
|
+
end
|
115
|
+
|
116
|
+
def session(obj)
|
117
|
+
if Consul::Utils.valid_json?(obj)
|
118
|
+
Consul::Model::Session.new.extend(Consul::Model::Session::Representer).from_json(obj)
|
119
|
+
elsif obj.is_a?(Hash)
|
120
|
+
Consul::Model::Session.new.extend(Consul::Model::Session::Representer).from_hash(obj)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Private: Create the url for a session endpoint.
|
125
|
+
#
|
126
|
+
# suffix - Suffix of the url endpoint
|
127
|
+
#
|
128
|
+
# Return: The URL for a reachable endpoint
|
129
|
+
def build_url(suffix)
|
130
|
+
"#{base_versioned_url}/session/#{suffix}"
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|